mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
feat(ntfs): correlate USN entries by full MFT identity
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
2be301f894
commit
a44d457745
3 files changed
+132
-1
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createUsnMftCorrelator } from "./ntfsJournalCorrelation.js";
|
||||
|
||||
const baseRecord = {
|
||||
index: 7,
|
||||
sequenceNumber: 2,
|
||||
fileOffset: 7168,
|
||||
totalBytes: 1024,
|
||||
baseRecordReference: null,
|
||||
declaredRecordNumber: 7,
|
||||
fixups: {
|
||||
sequenceNumber: 5,
|
||||
entries: 3,
|
||||
verifiedSectors: 2,
|
||||
coveredSectors: 2,
|
||||
mismatchSector: null,
|
||||
malformed: false,
|
||||
},
|
||||
};
|
||||
|
||||
it("keeps a reused record number unresolved while resolving a full parent reference", () => {
|
||||
const correlate = createUsnMftCorrelator([baseRecord]);
|
||||
expect(
|
||||
correlate({
|
||||
fileReference: 0x0001_0000_0000_0007n,
|
||||
parentReference: 0x0002_0000_0000_0007n,
|
||||
}),
|
||||
).toEqual({
|
||||
file: { kind: "unresolved" },
|
||||
parent: {
|
||||
kind: "matched",
|
||||
record: { index: 7, sequenceNumber: 2, fileOffset: 7168 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps duplicates ambiguous and skips extensions and unverified records", () => {
|
||||
const correlate = createUsnMftCorrelator([
|
||||
baseRecord,
|
||||
{ ...baseRecord, fileOffset: 8192 },
|
||||
{
|
||||
...baseRecord,
|
||||
index: 9,
|
||||
declaredRecordNumber: 9,
|
||||
baseRecordReference: { recordNumber: 7, sequenceNumber: 2 },
|
||||
},
|
||||
{
|
||||
...baseRecord,
|
||||
index: 9,
|
||||
declaredRecordNumber: 9,
|
||||
fixups: { ...baseRecord.fixups, verifiedSectors: 1, mismatchSector: 1 },
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
correlate({
|
||||
fileReference: 0x0002_0000_0000_0007n,
|
||||
parentReference: 0x0002_0000_0000_0009n,
|
||||
}),
|
||||
).toEqual({ file: { kind: "ambiguous" }, parent: { kind: "unresolved" } });
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { FIXUP_SECTOR_BYTES, type MftRecord } from "./ntfsMft.js";
|
||||
import type { UsnRecordV2 } from "./ntfsUsnJournal.js";
|
||||
|
||||
type RecordIdentity = Pick<
|
||||
MftRecord,
|
||||
"index" | "sequenceNumber" | "fileOffset"
|
||||
>;
|
||||
|
||||
type CorrelatableRecord = RecordIdentity &
|
||||
Pick<
|
||||
MftRecord,
|
||||
"baseRecordReference" | "declaredRecordNumber" | "fixups" | "totalBytes"
|
||||
>;
|
||||
type JournalReferences = Pick<UsnRecordV2, "fileReference" | "parentReference">;
|
||||
|
||||
export type MftJournalMatch =
|
||||
| { readonly kind: "matched"; readonly record: RecordIdentity }
|
||||
| { readonly kind: "unresolved" | "ambiguous" };
|
||||
|
||||
export interface UsnMftMatches {
|
||||
readonly file: MftJournalMatch;
|
||||
readonly parent: MftJournalMatch;
|
||||
}
|
||||
|
||||
/** Build one lookup for a selected MFT, then reuse it for each journal entry. */
|
||||
export const createUsnMftCorrelator = (
|
||||
records: Iterable<CorrelatableRecord>,
|
||||
): ((entry: JournalReferences) => UsnMftMatches) => {
|
||||
const byReference = new Map<bigint, RecordIdentity | null>();
|
||||
for (const record of records) {
|
||||
// The stream ordinal is the segment number in an acquisition of the
|
||||
// logical MFT stream. Extension records do not identify separate files.
|
||||
if (
|
||||
record.baseRecordReference !== null ||
|
||||
record.fixups.verifiedSectors * FIXUP_SECTOR_BYTES !== record.totalBytes
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
record.declaredRecordNumber !== null &&
|
||||
record.declaredRecordNumber !== 0 &&
|
||||
record.declaredRecordNumber !== record.index
|
||||
)
|
||||
continue;
|
||||
|
||||
const reference =
|
||||
(BigInt(record.sequenceNumber) << 48n) | BigInt(record.index);
|
||||
byReference.set(
|
||||
reference,
|
||||
byReference.has(reference)
|
||||
? null
|
||||
: {
|
||||
index: record.index,
|
||||
sequenceNumber: record.sequenceNumber,
|
||||
fileOffset: record.fileOffset,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const lookup = (reference: bigint): MftJournalMatch => {
|
||||
const record = byReference.get(reference);
|
||||
if (record === undefined) return { kind: "unresolved" };
|
||||
if (record === null) return { kind: "ambiguous" };
|
||||
return { kind: "matched", record };
|
||||
};
|
||||
|
||||
return (entry) => ({
|
||||
file: lookup(entry.fileReference),
|
||||
parent: lookup(entry.parentReference),
|
||||
});
|
||||
};
|
||||
@@ -22,7 +22,7 @@ const MFT_MAX_RECORD_BYTES = 65536;
|
||||
/** An update sequence array holds at least the sequence number and one fixup word. */
|
||||
const USA_MIN_ENTRIES = 2;
|
||||
/** NTFS fixes up one 512-byte sector at a time. */
|
||||
const FIXUP_SECTOR_BYTES = 512;
|
||||
export const FIXUP_SECTOR_BYTES = 512;
|
||||
/**
|
||||
* Fixed whole-file ceiling. Records are inventoried from their headers only;
|
||||
* attributes and their payloads are not decoded, so this is deliberately
|
||||
|
||||
Reference in new issue
Block a user