feat(ntfs): page correlated journal observations by source offset

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 13:03:32 +08:00
1 parent afed309f0f
commit c0b3c18b8a
2 files changed
+133

No files matched your search

@@ -0,0 +1,60 @@
import { expect, it } from "vitest";
import { composeNtfsJournalTimeline } from "./ntfsJournalTimeline.js";
import { parseMftImage } from "./ntfsMft.js";
it("keeps exact USN values and coverage rows on bounded source-order pages", () => {
const record = Buffer.alloc(1024);
record.write("FILE");
record.writeUInt16LE(48, 4);
record.writeUInt16LE(3, 6);
record.writeUInt16LE(2, 16);
record.writeUInt16LE(56, 20);
record.writeUInt32LE(64, 24);
record.writeUInt32LE(1024, 28);
record.writeUInt32LE(0xffff_ffff, 56);
const mft = parseMftImage(record);
if (!mft.ok) throw new Error(mft.error.message);
const journal = Buffer.alloc(72);
journal.writeUInt32LE(64, 8);
journal.writeUInt16LE(2, 12);
journal.writeBigUInt64LE(0x0002_0000_0000_0000n, 16);
journal.writeBigInt64LE(9007199254740993n, 32);
journal.writeBigUInt64LE(133000000000000001n, 40);
journal.writeUInt16LE(60, 66);
const first = composeNtfsJournalTimeline(mft.value, journal, {
startFileOffset: 0,
maxRows: 1,
});
expect(first).toEqual({
rows: [
{ kind: "coverage", issue: { kind: "padding", fileOffset: 0, bytes: 8 } },
],
nextFileOffset: 8,
mftLimitations: [],
});
const second = composeNtfsJournalTimeline(mft.value, journal, {
startFileOffset: first.nextFileOffset ?? 0,
maxRows: 1,
});
expect(second).toMatchObject({
rows: [
{
kind: "event",
event: {
fileOffset: 8,
usn: "9007199254740993",
timestampFileTime: "133000000000000001",
mft: {
file: { kind: "matched", record: { index: 0, sequenceNumber: 2 } },
parent: { kind: "unresolved" },
},
},
},
],
nextFileOffset: null,
});
});
+73
View File
@@ -0,0 +1,73 @@
import type { MftImage } from "./ntfsMft.js";
import {
createUsnMftCorrelator,
type UsnMftMatches,
} from "./ntfsJournalCorrelation.js";
import {
readUsnJournal,
type UsnRecordIssue,
type UsnRecordV2,
} from "./ntfsUsnJournal.js";
type JournalEvent = Omit<
UsnRecordV2,
"fileReference" | "parentReference" | "usn" | "timestampFileTime"
> & {
readonly fileReference: string;
readonly parentReference: string;
readonly usn: string;
readonly timestampFileTime: string;
readonly mft: UsnMftMatches;
};
type TimelineRow =
| { readonly kind: "event"; readonly event: JournalEvent }
| { readonly kind: "coverage"; readonly issue: UsnRecordIssue };
export interface NtfsJournalTimeline {
readonly rows: readonly TimelineRow[];
/** First source offset left out of the delivery window. */
readonly nextFileOffset: number | null;
readonly mftLimitations: readonly string[];
}
/** Compose a bounded source-order page for an explicitly paired acquisition. */
export const composeNtfsJournalTimeline = (
mft: MftImage,
journal: Buffer,
window: { readonly startFileOffset: number; readonly maxRows: number },
): NtfsJournalTimeline => {
const rows: TimelineRow[] = [];
const correlate = createUsnMftCorrelator(mft.records);
let nextFileOffset: number | null = null;
for (const result of readUsnJournal(journal)) {
const fileOffset = result.ok
? result.value.fileOffset
: result.error.fileOffset;
if (fileOffset < window.startFileOffset) continue;
if (rows.length >= window.maxRows) {
nextFileOffset = fileOffset;
break;
}
if (!result.ok) {
rows.push({ kind: "coverage", issue: result.error });
continue;
}
const entry = result.value;
rows.push({
kind: "event",
event: {
...entry,
fileReference: entry.fileReference.toString(),
parentReference: entry.parentReference.toString(),
usn: entry.usn.toString(),
timestampFileTime: entry.timestampFileTime.toString(),
mft: correlate(entry),
},
});
}
return { rows, nextFileOffset, mftLimitations: mft.limitations };
};