// Tests der Laufwerks-Schicht — komplett ohne Windows und ohne koffi: // disc.ts arbeitet gegen die LaufwerkApi-Schnittstelle, hier nachgebildet. // Die Testfälle wandern sinngemäß aus dem bewiesenen Docker-Zweig mit // (test_win_ioctl.py, test_windows.py, test_cdrom.py) — ein Treiber, der // dieselben Fälle besteht, kennt dieselben Fallen (KONZEPT § 10). import { describe, expect, it, beforeEach } from 'vitest' import { CDROM_DISK_AUDIO_TRACK, CDROM_DISK_DATA_TRACK, ERROR_ACCESS_DENIED, ERROR_INVALID_FUNCTION, ERROR_NOT_READY, IOCTL_CDROM_DISK_TYPE, IOCTL_DISK_GET_LENGTH_INFO, IOCTL_STORAGE_CHECK_VERIFY2, IOCTL_STORAGE_EJECT_MEDIA, IOCTL_STORAGE_MEDIA_REMOVAL, IOCTL_STORAGE_QUERY_PROPERTY, DRIVE_CDROM, Win32Fehler, type LaufwerkApi, } from '../src/kern/laufwerk/codes' import { angabenSpeicherLeeren, auswerfenMitGrund, classify, geraeteInfo, geraetepfad, kennung, listOptischeLaufwerke, textBei, BLURAY_MIN_BYTES, UHD_MIN_BYTES, } from '../src/kern/laufwerk/disc' describe('Steuercodes — gegen die Microsoft-Dokumentation gerechnet', () => { // Sollwerte aus der Doku (dieselbe Gegenprobe wie test_win_ioctl.py): // ein Zahlendreher sähe sonst aus wie „das Laufwerk kann das nicht". it.each([ ['IOCTL_STORAGE_CHECK_VERIFY2', IOCTL_STORAGE_CHECK_VERIFY2, 0x002d0800], ['IOCTL_STORAGE_MEDIA_REMOVAL', IOCTL_STORAGE_MEDIA_REMOVAL, 0x002d4804], ['IOCTL_STORAGE_EJECT_MEDIA', IOCTL_STORAGE_EJECT_MEDIA, 0x002d4808], ['IOCTL_STORAGE_QUERY_PROPERTY', IOCTL_STORAGE_QUERY_PROPERTY, 0x002d1400], ['IOCTL_CDROM_DISK_TYPE', IOCTL_CDROM_DISK_TYPE, 0x00020040], ['IOCTL_DISK_GET_LENGTH_INFO', IOCTL_DISK_GET_LENGTH_INFO, 0x0007405c], ])('%s stimmt mit der Doku überein', (_name, wert, erwartet) => { expect(wert).toBe(erwartet) }) }) describe('classify — Typ aus Spur-Flags und Größe', () => { it('Audio-Spur ist eine CD, egal wie groß', () => { expect(classify(CDROM_DISK_AUDIO_TRACK, 700 * 1e6)).toBe('cd') expect(classify(CDROM_DISK_AUDIO_TRACK | CDROM_DISK_DATA_TRACK, 0)).toBe('cd') }) it('Datenspur wird nach Größe eingeordnet', () => { expect(classify(CDROM_DISK_DATA_TRACK, 4.7e9)).toBe('dvd') // Der gemessene Fall vom 28.08.2026: BD-50 mit 48.149.364.736 Bytes // (44,84 GiB) liegt unter der 55-GiB-Schwelle und ist „bluray". expect(classify(CDROM_DISK_DATA_TRACK, 48_149_364_736)).toBe('bluray') expect(classify(CDROM_DISK_DATA_TRACK, 66 * 1024 ** 3)).toBe('uhd') }) it('die Schwellen selbst', () => { expect(classify(CDROM_DISK_DATA_TRACK, BLURAY_MIN_BYTES)).toBe('bluray') expect(classify(CDROM_DISK_DATA_TRACK, BLURAY_MIN_BYTES - 1)).toBe('dvd') expect(classify(CDROM_DISK_DATA_TRACK, UHD_MIN_BYTES)).toBe('uhd') }) it('keine Spur-Info heißt unknown, nicht dvd', () => { expect(classify(0, 33e9)).toBe('unknown') }) }) describe('geraetepfad und kennung — hin und zurück', () => { it('baut den Gerätepfad OHNE abschließenden Backslash', () => { // Mit Backslash öffnet CreateFileW das Wurzelverzeichnis statt des // Geräts — die Steuercodes scheitern dann mit ERROR_INVALID_FUNCTION. expect(geraetepfad('G')).toBe('\\\\.\\G:') expect(geraetepfad('g:')).toBe('\\\\.\\G:') expect(geraetepfad('G:\\')).toBe('\\\\.\\G:') }) it('wirft bei leerer Angabe', () => { expect(() => geraetepfad('')).toThrow() }) it('kennung löst den Pfad wieder auf (der /dev/G-404-Fund)', () => { expect(kennung('\\\\.\\G:')).toBe('G') expect(kennung(geraetepfad('e'))).toBe('E') }) }) describe('textBei — STORAGE_DEVICE_DESCRIPTOR auswerten', () => { it('liest die Zeichenkette am Offset', () => { const puffer = Buffer.alloc(40) puffer.writeUInt32LE(32, 12) // Offset an Position 12 zeigt auf Byte 32 puffer.write('HL-DT-ST\x00', 32, 'latin1') expect(textBei(puffer, 12)).toBe('HL-DT-ST') }) it('Offset 0 heißt „gibt es nicht"', () => { expect(textBei(Buffer.alloc(40), 12)).toBe('') }) it('Offset außerhalb des Puffers ist leer, kein Absturz', () => { const puffer = Buffer.alloc(16) puffer.writeUInt32LE(999, 12) expect(textBei(puffer, 12)).toBe('') expect(textBei(Buffer.alloc(2), 12)).toBe('') }) }) // ── Die Win32-Nachbildung (Muster aus test_windows.py) ────────────────── interface FakeVerhalten { buchstaben?: string[] typen?: Record oeffnenFehler?: number antworten?: Map // Buffer = Antwort, number = Win32-Fehlercode } function fakeApi(verhalten: FakeVerhalten): LaufwerkApi & { protokoll: string[] } { const protokoll: string[] = [] return { protokoll, laufwerksbuchstaben: () => verhalten.buchstaben ?? ['C', 'G'], laufwerkstyp: (b) => verhalten.typen?.[b] ?? (b === 'G' ? DRIVE_CDROM : 3), oeffnen(pfad, zugriff) { protokoll.push(`oeffnen ${pfad} zugriff=${zugriff}`) if (verhalten.oeffnenFehler !== undefined) throw new Win32Fehler('CreateFileW', verhalten.oeffnenFehler) return { pfad } }, schliessen() { protokoll.push('schliessen') }, steuern(_h, code, eingabe) { protokoll.push(`steuern 0x${code.toString(16)}${eingabe ? ` ein=${eingabe.toString('hex')}` : ''}`) const antwort = verhalten.antworten?.get(code) if (antwort === undefined) return Buffer.alloc(0) if (typeof antwort === 'number') throw new Win32Fehler(`DeviceIoControl(0x${code.toString(16)})`, antwort) return antwort }, } } function u32(wert: number): Buffer { const b = Buffer.alloc(4) b.writeUInt32LE(wert) return b } function u64(wert: bigint): Buffer { const b = Buffer.alloc(8) b.writeBigUInt64LE(wert) return b } const sofort = async (): Promise => {} beforeEach(() => angabenSpeicherLeeren()) describe('listOptischeLaufwerke', () => { it('nimmt nur DRIVE_CDROM und baut Gerätepfade', () => { const api = fakeApi({ buchstaben: ['C', 'E', 'G'], typen: { C: 3, E: DRIVE_CDROM, G: DRIVE_CDROM } }) expect(listOptischeLaufwerke(api)).toEqual(['\\\\.\\E:', '\\\\.\\G:']) }) }) describe('auswerfenMitGrund — entriegeln, auswerfen, NACHSEHEN (R3)', () => { it('meldet ok erst, wenn das Medium wirklich weg ist', async () => { let verifyRunde = 0 const api = fakeApi({}) api.steuern = (_h, code, eingabe) => { api.protokoll.push(`steuern 0x${code.toString(16)}${eingabe ? ` ein=${eingabe.toString('hex')}` : ''}`) if (code === IOCTL_STORAGE_CHECK_VERIFY2) { verifyRunde += 1 // Erst beim zweiten Nachsehen ist die Disc draußen. if (verifyRunde >= 2) throw new Win32Fehler('DeviceIoControl', ERROR_NOT_READY) return Buffer.alloc(0) } return Buffer.alloc(0) } const { grund, fehler } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort) expect(grund).toBe('ok') expect(fehler).toBeNull() // Reihenfolge: erst entriegeln (MEDIA_REMOVAL mit 0), dann auswerfen. const reihenfolge = api.protokoll.filter((z) => z.startsWith('steuern')) expect(reihenfolge[0]).toContain(IOCTL_STORAGE_MEDIA_REMOVAL.toString(16)) expect(reihenfolge[0]).toContain('ein=00') expect(reihenfolge[1]).toContain(IOCTL_STORAGE_EJECT_MEDIA.toString(16)) }) it('Erfolg des Auswurf-Befehls allein ist KEIN ok — bleibt-drin', async () => { // Der v1-Fehler: CDROMEJECT quittiert Erfolg auf verriegeltem Laufwerk // und tut nichts. CHECK_VERIFY2 sagt hier weiter „Medium da". const api = fakeApi({ antworten: new Map() }) const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort) expect(grund).toBe('bleibt-drin') }) it('kein Zugriff aufs Gerät heißt kein-zugriff, wirft nicht', async () => { const api = fakeApi({ oeffnenFehler: ERROR_ACCESS_DENIED }) const { grund, fehler } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort) expect(grund).toBe('kein-zugriff') expect(fehler).toContain('Win32-Fehler 5') }) it('abgelehnter Auswurf-Befehl heißt abgelehnt', async () => { const api = fakeApi({ antworten: new Map([[IOCTL_STORAGE_EJECT_MEDIA, ERROR_INVALID_FUNCTION]]) }) const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort) expect(grund).toBe('abgelehnt') }) it('gescheitertes Entriegeln verhindert den Auswurf-Versuch nicht', async () => { const api = fakeApi({ antworten: new Map([ [IOCTL_STORAGE_MEDIA_REMOVAL, ERROR_INVALID_FUNCTION], [IOCTL_STORAGE_CHECK_VERIFY2, ERROR_NOT_READY], ]), }) const { grund } = await auswerfenMitGrund(api, '\\\\.\\G:', sofort) expect(grund).toBe('ok') }) }) describe('geraeteInfo — ready/empty/unknown, nie eine falsche Aussage', () => { const beschreibung = (() => { // STORAGE_DEVICE_DESCRIPTOR: Offsets bei 12/16/20/24 → Strings ab 40. const b = Buffer.alloc(96) b.writeUInt32LE(40, 12) b.write('HL-DT-ST\x00', 40, 'latin1') b.writeUInt32LE(56, 16) b.write('BD-RE BU40N\x00', 56, 'latin1') b.writeUInt32LE(72, 20) b.write('1.03\x00', 72, 'latin1') b.writeUInt32LE(80, 24) b.write('0025114C0149\x00', 80, 'latin1') return b })() it('Disc drin: ready mit Typ, Größe, Modell, Seriennummer', () => { const api = fakeApi({ antworten: new Map([ [IOCTL_STORAGE_CHECK_VERIFY2, Buffer.alloc(0)], [IOCTL_CDROM_DISK_TYPE, u32(CDROM_DISK_DATA_TRACK)], [IOCTL_DISK_GET_LENGTH_INFO, u64(33_759_690_752n)], [IOCTL_STORAGE_QUERY_PROPERTY, beschreibung], ]), }) const info = geraeteInfo(api, '\\\\.\\G:') expect(info).toMatchObject({ id: 'G', status: 'ready', typ: 'bluray', groesseBytes: 33_759_690_752, modell: 'HL-DT-ST BD-RE BU40N 1.03', seriennummer: '0025114C0149', grund: '', }) }) it('leeres Laufwerk: empty, kein Fehler', () => { const api = fakeApi({ antworten: new Map([[IOCTL_STORAGE_CHECK_VERIFY2, ERROR_NOT_READY]]), }) expect(geraeteInfo(api, '\\\\.\\G:').status).toBe('empty') }) it('Gerät nicht ansprechbar: unknown MIT Grund — nicht empty (R2)', () => { const api = fakeApi({ oeffnenFehler: ERROR_ACCESS_DENIED }) const info = geraeteInfo(api, '\\\\.\\G:') expect(info.status).toBe('unknown') expect(info.grund).toContain('hält das Laufwerk fest') }) it('der 30.08.-Fall: Medium da, aber Typ-Abfrage bricht — ready mit Grund', () => { const api = fakeApi({ antworten: new Map([ [IOCTL_STORAGE_CHECK_VERIFY2, Buffer.alloc(0)], [IOCTL_DISK_GET_LENGTH_INFO, ERROR_INVALID_FUNCTION], ]), }) const info = geraeteInfo(api, '\\\\.\\G:') expect(info.status).toBe('ready') expect(info.typ).toBe('unknown') expect(info.grund).toContain('beantwortet keine Medien-Abfragen mehr') }) })