mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
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:
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,
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in new issue
Block a user