/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
import { OneStore } from "../onestore";
import { OneStoreFileType } from "../onestore/header";
import { ObjectSpaceStore } from "../onestore/object-space";
import { GUID } from "../utils/guid";
import { FileNodeID } from "../onestore/file-node-types";
import { parseNotebookToc, parseSection } from "./parse";
import {
parseEncryptionXml,
verifyPassword,
decryptDataKey
} from "../crypto";
import type { Color, Notebook, Section, SectionEntry } from "./types";
export { parseNotebookToc };
export * from "./types";
export type OneNoteFileType = "section" | "notebook" | "unknown";
/**
* Thrown when a section is encrypted and cannot be imported without a password.
*/
export class OneNoteEncryptedError extends Error {
constructor(
public readonly filename: string,
public readonly encryptionXml: string,
message?: string
) {
super(
message ||
`The section "${filename}" is password-protected. Provide the password to import it.`
);
this.name = "OneNoteEncryptedError";
}
}
const SECTION_FILE_TYPE_GUID = "7B5C52E4-D88C-4DA7-AEB1-5378D02996D3";
const TOC_FILE_TYPE_GUID = "43FF2FA1-EFD9-4C76-9EE2-10EA5722765F";
/**
* Sniffs the type of a OneNote file by reading the file type GUID from the
* file header without fully parsing it.
*/
export function sniffOneNoteFileType(data: Uint8Array): OneNoteFileType {
try {
if (data.length < 64) return "unknown";
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
// The file type GUID is the first GUID in the OneStore header.
const guid = GUID.fromBuffer(new Uint8Array(data.slice(0, 16)));
const fileType = guid.toString();
if (fileType === SECTION_FILE_TYPE_GUID) return "section";
if (fileType === TOC_FILE_TYPE_GUID) return "notebook";
return "unknown";
} catch {
return "unknown";
}
}
/**
* Parses a single OneNote section (.one) file.
*
* @param data the raw bytes of the .one file
* @param filename the name of the file, used as fallback for the display name
* @param password optional password for encrypted sections
*/
export async function parseOneNoteSection(
data: Uint8Array,
filename: string,
password?: string
): Promise {
const store = new OneStore(data);
if (store.header.fileType !== OneStoreFileType.STORE) {
throw new Error(`Not a valid .one file: ${filename}`);
}
// Pre-check: scan the root file node list for encryption markers. If
// the file is encrypted and no password was provided, fail early rather
// than letting the parser crash on garbage property set data.
const hasEncryption = containsEncryptionMarker(store);
if (hasEncryption && !password) {
throw new OneNoteEncryptedError(filename, "");
}
const spaceStore = ObjectSpaceStore.parse(store);
// Handle encryption: decrypt the root object space if a password is provided.
const rootSpace = spaceStore.dataRoot;
if (rootSpace.isEncrypted) {
if (!password) {
throw new OneNoteEncryptedError(filename, rootSpace.encryptionXml!);
}
const encryptionInfo = parseEncryptionXml(rootSpace.encryptionXml!);
const valid = await verifyPassword(password, encryptionInfo);
if (!valid) {
throw new OneNoteEncryptedError(
filename,
rootSpace.encryptionXml!,
"Incorrect password."
);
}
const dataKey = await decryptDataKey(password, encryptionInfo);
await rootSpace.decryptObjects(dataKey, store.reader);
}
return parseSection(spaceStore, filename);
}
/**
* Scans the entire file node tree for ObjectDataEncryptionKeyV2FNDX markers
* without fully parsing the object space. This is used as a cheap pre-check
* to detect encrypted files before the parser encounters garbage data.
*/
function containsEncryptionMarker(store: OneStore): boolean {
return walkForEncryption(store.fileNodeList);
}
function walkForEncryption(list: {
fragments: { rgFileNodes: { FileNodeID: number; children: any[] }[] }[];
}): boolean {
for (const fragment of list.fragments) {
for (const node of fragment.rgFileNodes) {
if (node.FileNodeID === FileNodeID.ObjectDataEncryptionKeyV2FNDX)
return true;
for (const child of node.children) {
if (walkForEncryption(child)) return true;
}
}
}
return false;
}
export type ResolveFile = (name: string) => Uint8Array | undefined;
/**
* Parses the table of contents of a OneNote notebook (.onetoc2) file.
* Returns the section/group entry names in the order they appear in the
* notebook along with the notebook's color.
*/
export function parseOneNoteNotebookToc(data: Uint8Array): {
entries: string[];
color?: { alpha: number; r: number; g: number; b: number };
} {
const store = new OneStore(data);
if (store.header.fileType !== OneStoreFileType.TABLE_OF_CONTENTS) {
throw new Error("Not a valid .onetoc2 file");
}
const spaceStore = ObjectSpaceStore.parse(store);
return parseNotebookToc(spaceStore.dataRoot);
}
/**
* Parses a OneNote notebook table of contents (.onetoc2) file.
*
* @param data the raw bytes of the .onetoc2 file
* @param resolveFile a function that resolves the bytes of a section file by
* its (sanitized) name; returns undefined when a file cannot be resolved in
* which case the entry is skipped
*/
export async function parseOneNoteNotebook(
data: Uint8Array,
resolveFile: ResolveFile,
password?: string
): Promise {
const store = new OneStore(data);
if (store.header.fileType !== OneStoreFileType.TABLE_OF_CONTENTS) {
throw new Error("Not a valid .onetoc2 file");
}
const spaceStore = ObjectSpaceStore.parse(store);
const { entries, color } = parseNotebookToc(spaceStore.dataRoot);
const sections: SectionEntry[] = [];
for (const name of entries) {
const sectionData = resolveFile(name);
if (!sectionData) continue;
try {
sections.push({
type: "section",
section: await parseOneNoteSection(sectionData, name, password)
});
} catch (e) {
// A single bad section shouldn't take down the whole notebook.
console.warn(`failed to import section ${name}:`, e);
}
}
return { entries: sections, color };
}