diff --git a/docs/specs/native/ntfs-mft/plan.md b/docs/specs/native/ntfs-mft/plan.md index 916a946520..ea1784c7c5 100644 --- a/docs/specs/native/ntfs-mft/plan.md +++ b/docs/specs/native/ntfs-mft/plan.md @@ -63,3 +63,8 @@ entries and explicit discovered omissions. This adds no volume reads, path reconstruction, nonresident payload recovery or target execution. Real extension captures and Windows-host execution remain unverified; source-built fixtures exercise reference ownership, malformed prefixes, cycles and resource bounds. + + +Journal and extension joins share `hasVerifiedMftRecordIdentity` at the MFT +owner. It consumes parsed sequence, slot verdict, extent and fixup facts; each +consumer keeps its separate base-versus-extension ownership rule. diff --git a/src/domain/windows/ntfsJournalCorrelation.ts b/src/domain/windows/ntfsJournalCorrelation.ts index 8f918b3e16..eb0b4fa972 100644 --- a/src/domain/windows/ntfsJournalCorrelation.ts +++ b/src/domain/windows/ntfsJournalCorrelation.ts @@ -1,4 +1,4 @@ -import { FIXUP_SECTOR_BYTES, type MftRecord } from "./ntfsMft.js"; +import { hasVerifiedMftRecordIdentity, type MftRecord } from "./ntfsMft.js"; import type { UsnRecord } from "./ntfsUsnJournal.js"; type RecordIdentity = Pick< @@ -40,12 +40,9 @@ export const createUsnMftCorrelator = ( // logical MFT stream. Extension records do not identify separate files. if ( record.baseRecordReference !== null || - record.sequenceNumber === 0 || - !record.extentValid || - record.fixups.verifiedSectors * FIXUP_SECTOR_BYTES !== record.totalBytes + !hasVerifiedMftRecordIdentity(record) ) continue; - if (record.recordNumberMatchesSlot === false) continue; const reference = (BigInt(record.sequenceNumber) << 48n) | BigInt(record.index); diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 4a84e75ec0..36eaba78f1 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -118,6 +118,22 @@ export interface MftRecord extends MftAttributeScan { readonly fixups: MftRecordFixups; } +/** Producer-owned identity validity; base/extension ownership is a separate relation. */ +export const hasVerifiedMftRecordIdentity = ( + record: Pick< + MftRecord, + | "sequenceNumber" + | "extentValid" + | "recordNumberMatchesSlot" + | "fixups" + | "totalBytes" + >, +): boolean => + record.sequenceNumber !== 0 && + record.extentValid && + record.recordNumberMatchesSlot !== false && + record.fixups.verifiedSectors * FIXUP_SECTOR_BYTES === record.totalBytes; + export interface MftInventory { readonly metadata: MftMetadata; /** The record size the stream declared, or the fixed default when it did not. */ @@ -463,11 +479,6 @@ const resolveMftAttributeLists = ( decoded.set(index, record); return ok(record); }; - const verifiedIdentity = (record: MftRecord): boolean => - record.sequenceNumber !== 0 && - record.extentValid && - record.recordNumberMatchesSlot !== false && - record.fixups.verifiedSectors * FIXUP_SECTOR_BYTES === record.totalBytes; return records.map((base): MftRecord => { if ( @@ -484,7 +495,7 @@ const resolveMftAttributeLists = ( seenEntries = new Set(), seenAttributes = new Set(); const queue: MftAttributeListEntry[] = []; - const baseVerified = verifiedIdentity(base); + const baseVerified = hasVerifiedMftRecordIdentity(base); let complete = baseVerified && base.attributeCoverage === "complete"; if (!complete) incompleteRecords.add(base.index); const enqueueList = (attribute: MftAttribute): void => { @@ -512,7 +523,8 @@ const resolveMftAttributeLists = ( const selected = selectRecord(entry.reference.recordNumber); if (!selected.ok) return selected.error; const target = selected.value; - if (!verifiedIdentity(target)) return unresolved("unverified-record"); + if (!hasVerifiedMftRecordIdentity(target)) + return unresolved("unverified-record"); if (target.sequenceNumber !== entry.reference.sequenceNumber) return unresolved("sequence-mismatch"); if (