diff --git a/src/domain/windows/ntfsMft.test.ts b/src/domain/windows/ntfsMft.test.ts index fee133b7b6..9f28a097eb 100644 --- a/src/domain/windows/ntfsMft.test.ts +++ b/src/domain/windows/ntfsMft.test.ts @@ -17,6 +17,7 @@ interface RecordOptions { readonly corruptSector?: number; /** Truncate the update sequence array to this many entries; 0 omits it. */ readonly usaEntries?: number; + readonly usaOffset?: number; } /** One FILE record with a zeroed body and fixups applied the way NTFS writes them. */ @@ -30,7 +31,7 @@ const recordOf = (options: RecordOptions = {}): Uint8Array => { const usaEntries = options.usaEntries ?? sectors + 1; if (usaEntries > 0) { - const usaOffset = 48; + const usaOffset = options.usaOffset ?? 48; view.setUint16(4, usaOffset, true); view.setUint16(6, usaEntries, true); view.setUint16(usaOffset, sequenceNumber, true); @@ -110,6 +111,26 @@ describe("parseMftImage", () => { expect(parsed.value.limitations).toEqual([]); }); + it("verifies the older FILE header without reading its fixup words as a record number", () => { + const oldRecord = recordOf({ usaOffset: 42 }); + const newRecord = recordOf(); + new DataView(newRecord.buffer).setUint32(44, 1, true); + + const parsed = parseMftImage(streamOf(oldRecord, newRecord)); + if (!parsed.ok) throw new Error(parsed.error.message); + expect(parsed.value.records).toMatchObject([ + { + declaredRecordNumber: null, + fixups: { malformed: false, verifiedSectors: 2 }, + }, + { + declaredRecordNumber: 1, + fixups: { malformed: false, verifiedSectors: 2 }, + }, + ]); + expect(parsed.value.limitations).toEqual([]); + }); + it("keeps a record whose sector trailer is not the sequence number, and reports it", () => { const parsed = parseMftImage( streamOf(recordOf(), recordOf({ sequenceNumber: 9, corruptSector: 1 })), @@ -278,7 +299,7 @@ describe("parseMftImage refusals and limits", () => { expect(parsed.ok).toBe(false); if (parsed.ok) return; expect(parsed.error.message).toMatch( - /shorter than one 48-byte record header/u, + /shorter than one 42-byte record header/u, ); }); diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 028647c5c2..74ce98a9ec 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -8,7 +8,7 @@ import type { export const MFT_FILE_RECORD_MAGIC = "FILE"; export const MFT_FILE_RECORD_MAGIC_BYTES = 4; /** The shortest header worth reading: a FILE record header ends at `nextAttributeId`. */ -export const NTFS_MFT_MIN_RECORD_BYTES = 48; +const NTFS_MFT_MIN_RECORD_BYTES = 42; /** * The `FILE` signature at file offset 0. An MFT stream is a run of fixed-size @@ -50,7 +50,7 @@ const RECORD_HEADER = { nextAttributeId: 40, recordNumber: 44, } as const; -const RECORD_HEADER_BYTES = NTFS_MFT_MIN_RECORD_BYTES; +const RECORD_HEADER_WITH_NUMBER_BYTES = 48; const FILE_RECORD_IN_USE = 0x0001; const FILE_RECORD_IS_DIRECTORY = 0x0002; @@ -282,6 +282,9 @@ const readRecord = ( true, ); + const usaOffset = view.getUint16(RECORD_HEADER.updateSequenceOffset, true); + const fixups = verifyFixups(record, view, usaOffset); + return { index, fileOffset, @@ -297,7 +300,7 @@ const readRecord = ( ? null : decodeMftFileReference(baseRecordReference), declaredRecordNumber: - recordBytes >= RECORD_HEADER.recordNumber + 4 + !fixups.malformed && usaOffset >= RECORD_HEADER_WITH_NUMBER_BYTES ? view.getUint32(RECORD_HEADER.recordNumber, true) : null, logFileSequenceNumber: Number( @@ -308,7 +311,7 @@ const readRecord = ( true, ), nextAttributeId: view.getUint16(RECORD_HEADER.nextAttributeId, true), - fixups: verifyFixups(record, view), + fixups, }; }; @@ -318,8 +321,11 @@ const readRecord = ( * trailer words. A sector whose trailer is not the sequence number was not * cleanly written, so the record's attributes cannot be trusted. */ -const verifyFixups = (record: Uint8Array, view: DataView): MftRecordFixups => { - const usaOffset = view.getUint16(RECORD_HEADER.updateSequenceOffset, true); +const verifyFixups = ( + record: Uint8Array, + view: DataView, + usaOffset: number, +): MftRecordFixups => { const usaCount = view.getUint16(RECORD_HEADER.updateSequenceCount, true); const sectors = Math.floor(record.length / FIXUP_SECTOR_BYTES); // Entry 0 holds the sequence number, so sector n's saved trailer is entry n+1. @@ -328,7 +334,7 @@ const verifyFixups = (record: Uint8Array, view: DataView): MftRecordFixups => { if ( usaCount < USA_MIN_ENTRIES || coveredSectors > sectors || - usaOffset < RECORD_HEADER_BYTES || + usaOffset < NTFS_MFT_MIN_RECORD_BYTES || usaOffset % 2 !== 0 || usaOffset + usaCount * 2 > FIXUP_SECTOR_BYTES - 2 )