diff --git a/src/domain/artifactFormatMetadata.ts b/src/domain/artifactFormatMetadata.ts index 49ba459ccc..61e83f863d 100644 --- a/src/domain/artifactFormatMetadata.ts +++ b/src/domain/artifactFormatMetadata.ts @@ -3,6 +3,7 @@ import { ntfsFileNameSchema, ntfsAttributeSchema, ntfsAttributeIssueSchema, + ntfsAttributeListResolutionSchema, } from "./windows/ntfsMftAttributes.js"; import { z } from "zod"; import { MFT_INLINE_HEADER_LIMIT } from "./windows/ntfsMft.js"; @@ -332,6 +333,7 @@ const ntfsRecordHeaderSchema = z.strictObject({ attributeCoverage: z.enum(["complete", "incomplete"]).exactOptional(), attributeIssue: ntfsAttributeIssueSchema.nullable().exactOptional(), fileNameCoverage: z.enum(["complete", "incomplete"]).exactOptional(), + attributeListResolution: ntfsAttributeListResolutionSchema.exactOptional(), fixups: z.strictObject({ sequenceNumber: offset, entries: offset, diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index b8466bf22f..8c80c0ed87 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -2,6 +2,7 @@ import { decodeMftFileReference, readMftAttributes, type MftAttributeScan, + type MftAttributeListResolution, type MftFileReference, } from "./ntfsMftAttributes.js"; export type { MftFileReference, MftFileName } from "./ntfsMftAttributes.js"; @@ -80,6 +81,8 @@ export interface MftRecordFixups { } export interface MftRecord extends MftAttributeScan { + /** Derived links from a base record's attribute lists into this same acquisition. */ + readonly attributeListResolution?: MftAttributeListResolution; /** Zero-based ordinal of this record in the stream. */ readonly index: number; /** File offset where the record starts. */ @@ -120,7 +123,10 @@ export interface MftInventory { } /** Journal joins need identity headers, without retaining every attribute payload. */ -export type MftRecordHeader = Omit; +export type MftRecordHeader = Omit< + MftRecord, + keyof MftAttributeScan | "attributeListResolution" +>; export interface MftIndex extends MftInventory { readonly records: readonly MftRecordHeader[]; } @@ -228,6 +234,7 @@ export const parseMftIndex = ( attributes: _attributes, attributeCoverage: _coverage, attributeIssue: _issue, + attributeListResolution: _resolution, ...header }) => records.push(header), { startRecord, recordLimit }, diff --git a/src/domain/windows/ntfsMftAttributes.ts b/src/domain/windows/ntfsMftAttributes.ts index f1f7223af5..0a5be70e53 100644 --- a/src/domain/windows/ntfsMftAttributes.ts +++ b/src/domain/windows/ntfsMftAttributes.ts @@ -97,6 +97,56 @@ export const ntfsAttributeSchema = z.strictObject({ ]), }); export type MftAttribute = z.infer; + +/** Per selected record; each additional decoded record is bounded to 64 KiB. */ +export const MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT = 32; +export const ntfsAttributeListResolutionSchema = z.strictObject({ + coverage: z.enum(["complete", "incomplete"]), + knownEntryCount: offset, + omittedKnownEntries: offset, + unreadListAttributeOffsets: z.array(offset), + incompleteRecordIndices: z.array(offset), + references: z + .array( + z.strictObject({ + listEntryFileOffset: offset, + result: z.discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("resolved"), + attributeFileOffset: offset, + }), + z.strictObject({ + kind: z.literal("ambiguous"), + candidateCount: offset, + }), + z.strictObject({ + kind: z.literal("unresolved"), + reason: z.enum([ + "unverified-base", + "record-unavailable", + "unverified-record", + "sequence-mismatch", + "base-reference-mismatch", + "attribute-unavailable", + "capacity", + ]), + }), + ]), + }), + ) + .max(MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT), + /** Additional facts only; local physical-record attributes remain unchanged. */ + linkedAttributes: z + .array(ntfsAttributeSchema) + .max(MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT), + linkedFileNames: z + .array(ntfsFileNameSchema) + .max(MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT), +}); +export type MftAttributeListResolution = z.infer< + typeof ntfsAttributeListResolutionSchema +>; + export interface MftAttributeScan { readonly attributeIssue: AttributeIssue | null; readonly fileNames: readonly MftFileName[];