From 14030e2f111c8d35333ef7e4fc037b228ba9e391 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:06:54 +0000 Subject: [PATCH] fix(ntfs): expose unrecognized FILE record flag bits Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- src/domain/artifactFormatMetadata.ts | 6 +++++- src/domain/windows/ntfsMft.test.ts | 10 ++++++++++ src/domain/windows/ntfsMft.ts | 17 ++++++++++++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/domain/artifactFormatMetadata.ts b/src/domain/artifactFormatMetadata.ts index 4a2a3bd745..0c3fc627ed 100644 --- a/src/domain/artifactFormatMetadata.ts +++ b/src/domain/artifactFormatMetadata.ts @@ -309,7 +309,11 @@ const ntfsRecordHeaderSchema = z.strictObject({ fileOffset: offset, sequenceNumber: offset, hardLinkCount: offset, - flags: z.strictObject({ raw: offset, names: z.array(z.string()) }), + flags: z.strictObject({ + raw: offset, + names: z.array(z.string()), + unknownMask: offset, + }), inUse: z.boolean(), directory: z.boolean(), usedBytes: offset, diff --git a/src/domain/windows/ntfsMft.test.ts b/src/domain/windows/ntfsMft.test.ts index f27aebe51f..ddf0eeea97 100644 --- a/src/domain/windows/ntfsMft.test.ts +++ b/src/domain/windows/ntfsMft.test.ts @@ -111,6 +111,16 @@ describe("MFT header facts", () => { expect(parsed.value.limitations).toEqual([]); }); + it("identifies unrecognized flags without losing their source bits", () => { + const parsed = parseMftImage(recordOf({ flags: 0x8001 })); + if (!parsed.ok) throw new Error(parsed.error.message); + expect(parsed.value.metadata.record_headers[0]?.flags).toEqual({ + raw: 0x8001, + names: ["in-use"], + unknownMask: 0x8000, + }); + }); + it("keeps the inline header window separate from the parsed record count", () => { const parsed = parseMftImage( streamOf(...Array.from({ length: 33 }, () => recordOf())), diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 2188d6f82f..9dfff8aa56 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -90,7 +90,11 @@ export interface MftRecord { readonly sequenceNumber: number; /** Hard link count as recorded; a zero count marks a record with no names. */ readonly hardLinkCount: number; - readonly flags: { readonly raw: number; readonly names: readonly string[] }; + readonly flags: { + readonly raw: number; + readonly names: readonly string[]; + readonly unknownMask: number; + }; readonly inUse: boolean; readonly directory: boolean; /** Used and allocated byte counts as declared by the FILE record. */ @@ -156,11 +160,14 @@ const limit = ( ...(boundary === undefined ? {} : { boundary }), }); -const namesOf = (raw: number): readonly string[] => { +const parseRecordFlags = (raw: number): MftRecord["flags"] => { const names: string[] = []; - for (const [bit, name] of FILE_RECORD_NAMES) + let unknownMask = raw; + for (const [bit, name] of FILE_RECORD_NAMES) { if ((raw & bit) !== 0) names.push(name); - return names; + unknownMask &= ~bit; + } + return { raw, names, unknownMask }; }; const isRecordSize = (bytes: number): boolean => @@ -355,7 +362,7 @@ const readRecord = ( fileOffset, sequenceNumber: view.getUint16(RECORD_HEADER.sequenceNumber, true), hardLinkCount: view.getUint16(RECORD_HEADER.hardLinkCount, true), - flags: { raw: flags, names: namesOf(flags) }, + flags: parseRecordFlags(flags), inUse: (flags & FILE_RECORD_IN_USE) !== 0, directory: (flags & FILE_RECORD_IS_DIRECTORY) !== 0, usedBytes,