feat: add local OneNote and ONEPKG reader

This commit is contained in:
2026-07-22 17:06:03 +02:00
commit f581cbdced
93 changed files with 14949 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { OneNoteParserError } from '../parser/error.js';
/** A bounded, little-endian reader. Every child view remains inside its range. */
export class BinaryReader {
readonly bytes: Uint8Array;
readonly start: number;
readonly end: number;
readonly structure: string;
private cursor: number;
constructor(
bytes: Uint8Array,
start = 0,
length = bytes.byteLength - start,
structure = 'binary data'
) {
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(length) ||
start < 0 ||
length < 0 ||
start + length > bytes.byteLength
) {
throw new OneNoteParserError(
'BOUNDS_EXCEEDED',
`Invalid ${structure} range (${start}, ${length}) for ${bytes.byteLength} bytes`,
{ offset: start, structure }
);
}
this.bytes = bytes;
this.start = start;
this.end = start + length;
this.cursor = start;
this.structure = structure;
}
get offset(): number {
return this.cursor;
}
get relativeOffset(): number {
return this.cursor - this.start;
}
get remaining(): number {
return this.end - this.cursor;
}
seek(relativeOffset: number): void {
if (
!Number.isSafeInteger(relativeOffset) ||
relativeOffset < 0 ||
relativeOffset > this.end - this.start
) {
this.fail(`Invalid seek to relative offset ${relativeOffset}`);
}
this.cursor = this.start + relativeOffset;
}
skip(length: number): void {
this.ensure(length);
this.cursor += length;
}
u8(): number {
this.ensure(1);
return this.bytes[this.cursor++]!;
}
u16(): number {
this.ensure(2);
const value = this.dataView(2).getUint16(0, true);
this.cursor += 2;
return value;
}
u32(): number {
this.ensure(4);
const value = this.dataView(4).getUint32(0, true);
this.cursor += 4;
return value;
}
u64(): bigint {
this.ensure(8);
const value = this.dataView(8).getBigUint64(0, true);
this.cursor += 8;
return value;
}
read(length: number): Uint8Array {
this.ensure(length);
const value = this.bytes.subarray(this.cursor, this.cursor + length);
this.cursor += length;
return value;
}
viewAt(
absoluteOffset: number,
length: number,
structure = this.structure
): BinaryReader {
if (
!Number.isSafeInteger(absoluteOffset) ||
!Number.isSafeInteger(length) ||
absoluteOffset < 0 ||
length < 0 ||
absoluteOffset + length > this.bytes.byteLength
) {
throw new OneNoteParserError(
'BOUNDS_EXCEEDED',
`${structure} range (${absoluteOffset}, ${length}) exceeds ${this.bytes.byteLength} bytes`,
{ offset: absoluteOffset, structure }
);
}
return new BinaryReader(this.bytes, absoluteOffset, length, structure);
}
subview(length: number, structure = this.structure): BinaryReader {
this.ensure(length);
const view = new BinaryReader(this.bytes, this.cursor, length, structure);
this.cursor += length;
return view;
}
private ensure(length: number): void {
if (
!Number.isSafeInteger(length) ||
length < 0 ||
length > this.remaining
) {
this.fail(
`Tried to read ${String(length)} bytes with ${this.remaining} remaining`
);
}
}
private dataView(length: number): DataView {
return new DataView(
this.bytes.buffer,
this.bytes.byteOffset + this.cursor,
length
);
}
private fail(message: string): never {
throw new OneNoteParserError('BOUNDS_EXCEEDED', message, {
offset: this.cursor,
structure: this.structure,
});
}
}

View File

@@ -0,0 +1,28 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import type { BinaryReader } from './BinaryReader.js';
const HEX = Array.from({ length: 256 }, (_, value) =>
value.toString(16).padStart(2, '0').toUpperCase()
);
/** Read the mixed-endian GUID layout used by OneStore. */
export function readGuid(reader: BinaryReader): string {
const bytes = reader.read(16);
const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15] as const;
const h = order.map((index) => HEX[bytes[index]!]!);
return `{${h.slice(0, 4).join('')}-${h.slice(4, 6).join('')}-${h.slice(6, 8).join('')}-${h.slice(8, 10).join('')}-${h.slice(10).join('')}}`;
}
export function normalizeGuid(value: string): string {
const plain = value.replace(/[{}-]/g, '').toUpperCase();
if (!/^[0-9A-F]{32}$/.test(plain)) {
throw new TypeError(`Invalid GUID: ${value}`);
}
return `{${plain.slice(0, 8)}-${plain.slice(8, 12)}-${plain.slice(12, 16)}-${plain.slice(16, 20)}-${plain.slice(20)}}`;
}
export const NIL_GUID = '{00000000-0000-0000-0000-000000000000}';

View File

@@ -0,0 +1,35 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import { OneNoteParserError } from '../parser/error.js';
const FILETIME_UNIX_EPOCH_TICKS = 116_444_736_000_000_000n;
const TICKS_PER_MILLISECOND = 10_000n;
const SECONDS_1980_TO_UNIX = 315_532_800;
export function filetimeToIso(value: bigint, structure: string): string {
const unixMilliseconds =
(value - FILETIME_UNIX_EPOCH_TICKS) / TICKS_PER_MILLISECOND;
const numeric = Number(unixMilliseconds);
if (!Number.isSafeInteger(numeric)) {
throw new OneNoteParserError(
'INVALID_STRUCTURE',
`FILETIME is outside the supported date range: ${value}`,
{ structure }
);
}
const date = new Date(numeric);
if (Number.isNaN(date.getTime())) {
throw new OneNoteParserError(
'INVALID_STRUCTURE',
`Invalid FILETIME: ${value}`,
{ structure }
);
}
return date.toISOString();
}
export function oneNoteTimeToIso(value: number): string {
return new Date((value + SECONDS_1980_TO_UNIX) * 1000).toISOString();
}