feat: add local OneNote and ONEPKG reader
This commit is contained in:
158
src/onenote/binary/BinaryReader.ts
Normal file
158
src/onenote/binary/BinaryReader.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
28
src/onenote/binary/guid.ts
Normal file
28
src/onenote/binary/guid.ts
Normal 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}';
|
||||
35
src/onenote/binary/time.ts
Normal file
35
src/onenote/binary/time.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user