fix(ntfs): accept older FILE record headers

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
N0zoM1z0andgensokyo-dev authored and gensokyo-dev committed 2026-11-11 10:03:35 +08:00
1 parent d7338876d6
commit 29fd6a7d03
2 files changed
+36 -9

No files matched your search

+23 -2
View File
@@ -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,
);
});
+13 -7
View File
@@ -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
)