1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-19 20:31:46 +02:00

Desktop: Add OneNote Importer (#10642)

This commit is contained in:
pedr 2024-11-09 09:47:01 -03:00 committed by GitHub
parent 5aa29d38ef
commit b248700e28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
194 changed files with 15886 additions and 7 deletions

View File

@ -85,6 +85,7 @@ plugin_types/
readme/
packages/react-native-vosk/lib/
packages/lib/countable/Countable.js
packages/onenote-converter/pkg/onenote_converter.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-cli/app/LinkSelector.js
@ -1172,6 +1173,8 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js

View File

@ -67,6 +67,7 @@ echo "IS_MACOS=$IS_MACOS"
echo "Node $( node -v )"
echo "Npm $( npm -v )"
echo "Yarn $( yarn -v )"
echo "Rust $( rustc --version )"
# =============================================================================
# Install packages

View File

@ -26,6 +26,8 @@ jobs:
node-version: '18'
cache: 'yarn'
- uses: dtolnay/rust-toolchain@stable
- name: Install Yarn
run: |
corepack enable

View File

@ -69,6 +69,7 @@ jobs:
- uses: actions/checkout@v4
- uses: olegtarasov/get-tag@v2.1.3
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
# We need to pin the version to 18.15, because 18.16+ fails with this error:

2
.gitignore vendored
View File

@ -1149,6 +1149,8 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js

View File

@ -35,6 +35,9 @@ COPY packages/utils ./packages/utils
COPY packages/lib ./packages/lib
COPY packages/server ./packages/server
# We don't want to build onenote-converter since it is not used by the server
RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
# For some reason there's both a .yarn/cache and .yarn/berry/cache that are
# being generated, and both have the same content. Not clear why it does this
# but we can delete it anyway. We can delete the cache because we use

View File

@ -5,6 +5,9 @@
},
],
"settings": {
"rust-analyzer.linkedProjects": [
"./packages/onenote-converter/Cargo.toml",
],
"files.exclude": {
"_mydocs/mdtest/": true,
"_releases/": true,

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<body>
<div class="container-outline" style="left: 48px;position: absolute;top: 107px;width: 624px;">
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<style>
.small {
font: italic 13px sans-serif;
}
.heavy {
font: bold 30px sans-serif;
}
/* Note that the color of the text is set with the *
* fill property, the color property is for HTML only */
.Rrrrr {
font: italic 40px serif;
fill: red;
}
</style>
<text x="20" y="35" class="small">My</text>
<text x="40" y="35" class="heavy">cat</text>
<text x="55" y="55" class="small">is</text>
<text x="65" y="55" class="Rrrrr">Grumpy!</text>
</svg>
</div>
</body>
</html>

View File

@ -25,6 +25,7 @@ const { ResourceScreen } = require('./ResourceScreen.js');
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
import InteropService from '@joplin/lib/services/interop/InteropService';
import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
import bridge from '../services/bridge';
@ -91,6 +92,9 @@ async function initialize() {
type: 'NOTE_VISIBLE_PANES_SET',
panes: Setting.value('noteVisiblePanes'),
});
InteropService.instance().document = document;
InteropService.instance().xmlSerializer = new XMLSerializer();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@ -1,11 +1,11 @@
import paginationToSql from './models/utils/paginationToSql';
import Database from './database';
import uuid from './uuid';
import time from './time';
import JoplinDatabase, { TableField } from './JoplinDatabase';
import { LoadOptions, SaveOptions } from './models/utils/types';
import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger';
import { BaseItemEntity, SqlQuery } from './services/database/types';
import uuid from './uuid';
const Mutex = require('async-mutex').Mutex;
// New code should make use of this enum
@ -80,6 +80,8 @@ class BaseModel {
['TYPE_COMMAND', ModelType.Command],
];
private static uuidGenerator: ()=> string = uuid.create;
public static TYPE_NOTE = ModelType.Note;
public static TYPE_FOLDER = ModelType.Folder;
public static TYPE_SETTING = ModelType.Setting;
@ -576,7 +578,7 @@ class BaseModel {
if (options.isNew) {
if (this.useUuid() && !o.id) {
modelId = uuid.create();
modelId = this.generateUuid();
o.id = modelId;
}
@ -757,6 +759,15 @@ class BaseModel {
return this.db_;
}
public static generateUuid() {
return this.uuidGenerator();
}
public static setIdGenerator(generator: ()=> string) {
const previous = this.uuidGenerator;
this.uuidGenerator = generator;
return previous;
}
// static isReady() {
// return !!this.db_;
// }

View File

@ -17,9 +17,11 @@
},
"devDependencies": {
"@testing-library/react-hooks": "8.0.1",
"@types/adm-zip": "0.5.5",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6",
"@types/markdown-it": "13.0.9",
"@types/mustache": "4.2.5",
"@types/node": "18.19.42",
@ -29,6 +31,7 @@
"canvas": "2.11.2",
"clean-html": "1.5.0",
"jest": "29.7.0",
"jsdom": "23.2.0",
"pdfjs-dist": "3.11.174",
"react": "18.3.1",
"react-test-renderer": "18.3.1",
@ -44,11 +47,13 @@
"@joplin/fork-sax": "^1.2.56",
"@joplin/fork-uslug": "^1.0.17",
"@joplin/htmlpack": "~3.2",
"@joplin/onenote-converter": "0.0.1",
"@joplin/renderer": "~3.2",
"@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56",
"@joplin/utils": "~3.2",
"@types/nanoid": "3.0.0",
"adm-zip": "0.5.12",
"async-mutex": "0.5.0",
"base-64": "1.0.0",
"base64-stream": "1.0.0",

View File

@ -30,6 +30,8 @@ export default class InteropService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private eventEmitter_: any = null;
private static instance_: InteropService;
private document_: Document;
private xmlSerializer_: XMLSerializer;
public static instance(): InteropService {
if (!this.instance_) this.instance_ = new InteropService();
@ -133,6 +135,14 @@ export default class InteropService {
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('Text document'),
}, () => new InteropService_Importer_Md()),
makeImportModule({
format: 'zip',
fileExtensions: ['zip'],
sources: [FileSystemItem.File],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('OneNote Notebook'),
}, dynamicRequireModuleFactory('./InteropService_Importer_OneNote')),
];
const exportModules = [
@ -189,6 +199,22 @@ export default class InteropService {
this.eventEmitter_.emit('modulesChanged');
}
public set xmlSerializer(xmlSerializer: XMLSerializer) {
this.xmlSerializer_ = xmlSerializer;
}
public get xmlSerializer() {
return this.xmlSerializer_;
}
public set document(document: Document) {
this.document_ = document;
}
public get document() {
return this.document_;
}
// Find the module that matches the given type ("importer" or "exporter")
// and the given format. Some formats can have multiple associated importers
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
@ -273,6 +299,8 @@ export default class InteropService {
format: 'auto',
destinationFolderId: null,
destinationFolder: null,
xmlSerializer: this.xmlSerializer,
document: this.document,
...options,
};

View File

@ -0,0 +1,176 @@
import Note from '../../models/Note';
import Folder from '../../models/Folder';
import { remove, readFile } from 'fs-extra';
import { createTempDir, setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils';
import { NoteEntity } from '../database/types';
import { MarkupToHtml } from '@joplin/renderer';
import BaseModel from '../../BaseModel';
import InteropService from './InteropService';
import InteropService_Importer_OneNote from './InteropService_Importer_OneNote';
import { JSDOM } from 'jsdom';
import { ImportModuleOutputFormat } from './types';
describe('InteropService_Importer_OneNote', () => {
let tempDir: string;
async function importNote(path: string) {
const newFolder = await Folder.save({ title: 'folder' });
const service = InteropService.instance();
await service.import({
outputFormat: ImportModuleOutputFormat.Markdown,
path,
destinationFolder: newFolder,
destinationFolderId: newFolder.id,
});
const allNotes: NoteEntity[] = await Note.all();
return allNotes;
}
beforeAll(() => {
const jsdom = new JSDOM('<div></div>');
InteropService.instance().document = jsdom.window.document;
InteropService.instance().xmlSerializer = new jsdom.window.XMLSerializer();
});
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
tempDir = await createTempDir();
});
afterEach(async () => {
await remove(tempDir);
});
it('should import a simple OneNote notebook', async () => {
const notes = await importNote(`${supportDir}/onenote/simple_notebook.zip`);
const folders = await Folder.all();
expect(notes.length).toBe(2);
const mainNote = notes[0];
expect(folders.length).toBe(3);
const parentFolder = folders.find(f => f.id === mainNote.parent_id);
expect(parentFolder.title).toBe('Section title');
expect(folders.find(f => f.id === parentFolder.parent_id).title).toBe('Simple notebook');
expect(mainNote.title).toBe('Page title');
expect(mainNote.markup_language).toBe(MarkupToHtml.MARKUP_LANGUAGE_HTML);
expect(mainNote.body).toMatchSnapshot(mainNote.title);
});
it('should preserve indentation of subpages in Section page', async () => {
const notes = await importNote(`${supportDir}/onenote/subpages.zip`);
const sectionPage = notes.find(n => n.title === 'Section');
const menuHtml = sectionPage.body.split('<ul>')[1].split('</ul>')[0];
const menuLines = menuHtml.split('</li>');
const pageTwo = notes.find(n => n.title === 'Page 2');
expect(menuLines[3].trim()).toBe(`<li class="l1"><a href=":/${pageTwo.id}" target="content" title="Page 2">${pageTwo.title}</a>`);
const pageTwoA = notes.find(n => n.title === 'Page 2-a');
expect(menuLines[4].trim()).toBe(`<li class="l2"><a href=":/${pageTwoA.id}" target="content" title="Page 2-a">${pageTwoA.title}</a>`);
const pageTwoAA = notes.find(n => n.title === 'Page 2-a-a');
expect(menuLines[5].trim()).toBe(`<li class="l3"><a href=":/${pageTwoAA.id}" target="content" title="Page 2-a-a">${pageTwoAA.title}</a>`);
const pageTwoB = notes.find(n => n.title === 'Page 2-b');
expect(menuLines[7].trim()).toBe(`<li class="l2"><a href=":/${pageTwoB.id}" target="content" title="Page 2-b">${pageTwoB.title}</a>`);
});
it('should created subsections', async () => {
const notes = await importNote(`${supportDir}/onenote/subsections.zip`);
const folders = await Folder.all();
const parentSection = folders.find(f => f.title === 'Group Section 1');
const subSection = folders.find(f => f.title === 'Group Section 1-a');
const subSection1 = folders.find(f => f.title === 'Subsection 1');
const subSection2 = folders.find(f => f.title === 'Subsection 2');
const notesFromParentSection = notes.filter(n => n.parent_id === parentSection.id);
expect(parentSection.id).toBe(subSection1.parent_id);
expect(parentSection.id).toBe(subSection2.parent_id);
expect(parentSection.id).toBe(subSection.parent_id);
expect(folders.length).toBe(7);
expect(notes.length).toBe(6);
expect(notesFromParentSection.length).toBe(2);
});
it('should expect notes to be rendered the same', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/complex_notes.zip`);
const folders = await Folder.all();
const parentSection = folders.find(f => f.title === 'Quick Notes');
expect(folders.length).toBe(3);
expect(notes.length).toBe(7);
expect(notes.filter(n => n.parent_id === parentSection.id).length).toBe(6);
for (const note of notes) {
expect(note.body).toMatchSnapshot(note.title);
}
BaseModel.setIdGenerator(originalIdGenerator);
});
it('should render the proper tree for notebook with group sections', async () => {
const notes = await importNote(`${supportDir}/onenote/group_sections.zip`);
const folders = await Folder.all();
const mainFolder = folders.find(f => f.title === 'Notebook created on OneNote App');
const section = folders.find(f => f.title === 'Section');
const sectionA1 = folders.find(f => f.title === 'Section A1');
const sectionA = folders.find(f => f.title === 'Section A');
const sectionB1 = folders.find(f => f.title === 'Section B1');
const sectionB = folders.find(f => f.title === 'Section B');
const sectionD1 = folders.find(f => f.title === 'Section D1');
const sectionD = folders.find(f => f.title === 'Section D');
expect(section.parent_id).toBe(mainFolder.id);
expect(sectionA.parent_id).toBe(mainFolder.id);
expect(sectionD.parent_id).toBe(mainFolder.id);
expect(sectionA1.parent_id).toBe(sectionA.id);
expect(sectionB.parent_id).toBe(sectionA.id);
expect(sectionB1.parent_id).toBe(sectionB.id);
expect(sectionD1.parent_id).toBe(sectionD.id);
expect(notes.filter(n => n.parent_id === sectionA1.id).length).toBe(2);
expect(notes.filter(n => n.parent_id === sectionB1.id).length).toBe(2);
expect(notes.filter(n => n.parent_id === sectionD1.id).length).toBe(1);
});
it.each([
'svg_with_text_and_style.html',
'many_svgs.html',
])('should extract svgs', async (filename: string) => {
const titleGenerator = () => {
let id = 0;
return () => {
id += 1;
return `id${id}`;
};
};
const filepath = `${supportDir}/onenote/${filename}`;
const content = await readFile(filepath, 'utf-8');
const jsdom = new JSDOM('<div></div>');
InteropService.instance().document = jsdom.window.document;
InteropService.instance().xmlSerializer = new jsdom.window.XMLSerializer();
const importer = new InteropService_Importer_OneNote();
await importer.init('asdf', {
document: jsdom.window.document,
xmlSerializer: new jsdom.window.XMLSerializer(),
});
expect(importer.extractSvgs(content, titleGenerator())).toMatchSnapshot();
});
it('should ignore broken characters at the start of paragraph', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/bug_broken_character.zip`);
expect(notes.find(n => n.title === 'Action research - Wikipedia').body).toMatchSnapshot();
BaseModel.setIdGenerator(originalIdGenerator);
});
});

View File

@ -0,0 +1,157 @@
import { ImportExportResult, ImportModuleOutputFormat, ImportOptions } from './types';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { NoteEntity } from '../database/types';
import { rtrimSlashes } from '../../path-utils';
import * as AdmZip from 'adm-zip';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import { join, resolve, normalize, sep, dirname } from 'path';
import Logger from '@joplin/utils/Logger';
import { uuidgen } from '../../uuid';
import shim from '../../shim';
const logger = Logger.create('InteropService_Importer_OneNote');
export type SvgXml = {
title: string;
content: string;
};
type ExtractSvgsReturn = {
svgs: SvgXml[];
html: string;
};
// See onenote-converter README.md for more information
export default class InteropService_Importer_OneNote extends InteropService_Importer_Base {
protected importedNotes: Record<string, NoteEntity> = {};
private document: Document = null;
private xmlSerializer: XMLSerializer = null;
public async init(sourcePath: string, options: ImportOptions) {
await super.init(sourcePath, options);
if (!options.document || !options.xmlSerializer) {
throw new Error('OneNote importer requires document and XMLSerializer to be able to extract SVG from HTML.');
}
this.document = options.document;
this.xmlSerializer = options.xmlSerializer;
}
private getEntryDirectory(unzippedPath: string, entryName: string) {
const withoutBasePath = entryName.replace(unzippedPath, '');
return normalize(withoutBasePath).split(sep)[0];
}
public async exec(result: ImportExportResult) {
const sourcePath = rtrimSlashes(this.sourcePath_);
const unzipTempDirectory = await this.temporaryDirectory_(true);
const zip = new AdmZip(sourcePath);
logger.info('Unzipping files...');
zip.extractAllTo(unzipTempDirectory, false);
const files = zip.getEntries();
if (files.length === 0) {
result.warnings.push('Zip file has no files.');
return result;
}
const tempOutputDirectory = await this.temporaryDirectory_(true);
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].entryName);
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
const outputDirectory2 = join(tempOutputDirectory, baseFolder);
const notebookFiles = zip.getEntries().filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2');
const { oneNoteConverter } = shim.requireDynamic('../../../onenote-converter/pkg/onenote_converter');
logger.info('Extracting OneNote to HTML');
for (const notebookFile of notebookFiles) {
const notebookFilePath = join(unzipTempDirectory, notebookFile.entryName);
try {
await oneNoteConverter(notebookFilePath, resolve(outputDirectory2), notebookBaseDir);
} catch (error) {
console.error(error);
}
}
logger.info('Extracting SVGs into files');
await this.moveSvgToLocalFile(tempOutputDirectory);
logger.info('Importing HTML into Joplin');
const importer = new InteropService_Importer_Md();
importer.setMetadata({ fileExtensions: ['html'] });
await importer.init(tempOutputDirectory, {
...this.options_,
format: 'html',
outputFormat: ImportModuleOutputFormat.Html,
});
logger.info('Finished');
result = await importer.exec(result);
return result;
}
private async moveSvgToLocalFile(baseFolder: string) {
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));
for (const file of htmlFiles) {
const fileLocation = join(baseFolder, file.path);
const originalHtml = await shim.fsDriver().readFile(fileLocation);
const { svgs, html: updatedHtml } = this.extractSvgs(originalHtml, () => uuidgen(10));
if (!svgs || !svgs.length) continue;
await shim.fsDriver().writeFile(fileLocation, updatedHtml, 'utf8');
await this.createSvgFiles(svgs, join(baseFolder, dirname(file.path)));
}
}
private async getValidHtmlFiles(baseFolder: string) {
const files = await shim.fsDriver().readDirStats(baseFolder, { recursive: true });
const htmlFiles = files.filter(f => !f.isDirectory() && f.path.endsWith('.html'));
return htmlFiles;
}
private async createSvgFiles(svgs: SvgXml[], svgBaseFolder: string) {
for (const svg of svgs) {
await shim.fsDriver().writeFile(join(svgBaseFolder, svg.title), svg.content, 'utf8');
}
}
public extractSvgs(html: string, titleGenerator: ()=> string): ExtractSvgsReturn {
const htmlDocument = this.document.implementation.createHTMLDocument('htmlDocument');
const root = htmlDocument.createElement('html');
const body = htmlDocument.createElement('body');
root.appendChild(body);
root.innerHTML = html;
// get all "top-level" SVGS (ignore nested)
const svgNodeList = root.querySelectorAll('svg');
if (!svgNodeList || !svgNodeList.length) {
return { svgs: [], html };
}
const svgs: SvgXml[] = [];
for (const svgNode of svgNodeList) {
const title = `${titleGenerator()}.svg`;
const img = htmlDocument.createElement('img');
img.setAttribute('style', svgNode.getAttribute('style'));
img.setAttribute('src', `./${title}`);
svgNode.removeAttribute('style');
svgs.push({
title,
content: this.xmlSerializer.serializeToString(svgNode),
});
svgNode.parentElement.replaceChild(img, svgNode);
}
return {
svgs,
html: this.xmlSerializer.serializeToString(root),
};
}
}

File diff suppressed because one or more lines are too long

View File

@ -51,6 +51,8 @@ export interface ImportOptions {
onProgress?: (progressState: any, progress?: any)=> void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onError?: (error: any)=> void;
document?: Document;
xmlSerializer?: XMLSerializer;
defaultFolderTitle?: string;
}

View File

@ -17,6 +17,7 @@ import crypto from './services/e2ee/crypto';
import FileApiDriverLocal from './file-api-driver-local';
import * as mimeUtils from './mime-utils';
import BaseItem from './models/BaseItem';
const { _ } = require('./locale');
const http = require('http');
const https = require('https');
@ -309,13 +310,11 @@ function shimInit(options: ShimInitOptions = null) {
const isUpdate = !!options.destinationResourceId;
const uuid = require('./uuid').default;
if (!(await fs.pathExists(filePath))) throw new Error(_('Cannot access %s', filePath));
defaultProps = defaultProps ? defaultProps : {};
let resourceId = defaultProps.id ? defaultProps.id : uuid.create();
let resourceId = defaultProps.id ? defaultProps.id : BaseItem.generateUuid();
if (isUpdate) resourceId = options.destinationResourceId;
let resource = isUpdate ? {} : Resource.new();

7
packages/onenote-converter/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/target
/output
/.idea
*.iml
/pkg

View File

@ -0,0 +1,5 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}

1040
packages/onenote-converter/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
[package]
name = "onenote-converter"
version = "0.0.1"
authors = ["Pedro Luiz <pedrlz.frn@gmail.com>"]
edition = "2018"
description = "Convert Microsoft OneNote® notebooks to HTML"
license = "MIT"
repository = "https://github.com/laurent22/joplin"
keywords = ["onenote"]
[dependencies]
askama = "0.10"
color-eyre = "0.5"
log = "0.4.11"
mime_guess = "2.0.3"
once_cell = "1.4.1"
palette = "0.5.0"
percent-encoding = "2.1.0"
regex = "1"
sanitize-filename = "0.3.0"
structopt = "0.3"
console_error_panic_hook = "0.1.7"
bytes = "1.2.0"
encoding_rs = "0.8.31"
enum-primitive-derive = "0.2.2"
itertools = "0.10.3"
num-traits = "0.2"
paste = "1.0"
thiserror = "1.0"
uuid = "1.1.2"
widestring = "1.0.2"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"console"
]
[lib]
crate-type = ["cdylib"]

View File

@ -0,0 +1,66 @@
# OneNote Converter
This package is used to process OneNote backup files and output HTML that Joplin can import.
The code is based on the projects created by https://github.com/msiemens
We adapted it to target WebAssembly, adding Node.js functions that could interface with the host machine. For that to happen we are using custom-made functions (see `node_functions.js`) and the Node.js standard library (see `src/utils.rs`).
## How the OneNote Importer Process Works
The requirement for this project was to simplify the migration process from OneNote to Joplin. The starting point of this migration is to export the notebook from OneNote as a `zip` file containing files in the binary format used by OneNote.
The process looks like this:
1. Unzip the backup file.
2. Use `onenote-converter` to read and convert the binary files to HTML (this project).
3. Extract the SVG nodes from the HTML to resources:
1. Find all SVG nodes in the HTML file.
2. Create SVG files from the nodes.
3. Update the HTML file with references to the SVGs.
4. Use the Importer HTML service to create the Joplin notes and resources.
See the `InteropService_Importer_OneNote` class in the `lib` project for details.
### SVG Extraction
The OneNote drawing feature uses `<svg>` tags to save user drawings. Joplin doesn't support SVG rendering due to security concerns, so we added a step to extract the `<svg>` elements as SVG images, replacing them with `<img>` tags.
For each HTML file, we:
- Mount the HTML in the document.
- Find all the `svg` nodes.
- Replace each `svg` node with an `img` node that has a unique title, which will be used as the resource name.
- After editing the entire document, update the HTML.
- Create the SVG images on the local disk with the title used in the replaced `img` tags.
After this, the HTML should look the same and is ready to be imported by the Importer HTML service.
## Project structure:
```
- onenote-converter
- package.json -> where the project is built
- node_functions.js -> where the custom-made functions used inside rust goes
...
- pkg -> artifact folder generated in the build step
- onenote_converter.js -> main file
...
- src
- lib.rs -> starting point
```
## Development requirements:
To work with the project you will need:
- Rust https://www.rust-lang.org/learn/get-started
When working with the Rust code you will probably rather run `yarn buildDev` since it is faster and it has more logging messages (they can be disabled in the macro `log!()`)
During development, it will be easier to test it where this library is called. `InteropService_Importer_Onenote.ts` is the code that depends on this and already has some tests.
## Security concerns
We are using WebAssembly with Node.js calls to the file system, reading and writing files and directories, which means
it is not isolated (no more than Node.js is, for that matter).

View File

@ -0,0 +1,2 @@
[general]
dirs = ["src/templates"]

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M16.172 11l-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M17 15.245v6.872a.5.5 0 0 1-.757.429L12 20l-4.243 2.546a.5.5 0 0 1-.757-.43v-6.87a8 8 0 1 1 10 0zm-8 1.173v3.05l3-1.8 3 1.8v-3.05A7.978 7.978 0 0 1 12 17a7.978 7.978 0 0 1-3-.582zM12 15a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M13 21v2h-2v-2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a3.99 3.99 0 0 1 3 1.354A3.99 3.99 0 0 1 15 3h6a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-8zm7-2V5h-5a2 2 0 0 0-2 2v12h7zm-9 0V7a2 2 0 0 0-2-2H4v14h7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M5.763 17H20V5H4v13.385L5.763 17zm.692 2L2 22.5V4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6.455z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 172 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2v14h14V5H5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm7.003 13l7.07-7.071-1.414-1.414-5.656 5.657-2.829-2.829-1.414 1.414L11.003 16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 7h5v2h-5V7zm-2 5h7v2h-7v-2zm3 5h4v2h-4v-2zM2 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-5h2v2h-2v-2zm0-8h2v6h-2V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1zm-1-2V4H5v16h14zM8 7h8v2H8V7zm0 4h8v2H8v-2zm0 4h5v2H8v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 5v14h8V5H8zM4 5v2h2V5H4zm14 0v2h2V5h-2zM4 9v2h2V9H4zm14 0v2h2V9h-2zM4 13v2h2v-2H4zm14 0v2h2v-2h-2zM4 17v2h2v-2H4zm14 0v2h2v-2h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 3h9.382a1 1 0 0 1 .894.553L14 5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-6.382a1 1 0 0 1-.894-.553L12 16H5v6H3V3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zm-6-2h5V9.157l-6-5.454-6 5.454V19h5v-6h2v6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9.973 18H11v-5h2v5h1.027c.132-1.202.745-2.194 1.74-3.277.113-.122.832-.867.917-.973a6 6 0 1 0-9.37-.002c.086.107.807.853.918.974.996 1.084 1.609 2.076 1.741 3.278zM10 20v1h4v-1h-4zm-4.246-5a8 8 0 1 1 12.49.002C17.624 15.774 16 17 16 18.5V21a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.5C8 17 6.375 15.774 5.754 15z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M18.364 15.536L16.95 14.12l1.414-1.414a5 5 0 1 0-7.071-7.071L9.879 7.05 8.464 5.636 9.88 4.222a7 7 0 0 1 9.9 9.9l-1.415 1.414zm-2.828 2.828l-1.415 1.414a7 7 0 0 1-9.9-9.9l1.415-1.414L7.05 9.88l-1.414 1.414a5 5 0 1 0 7.071 7.071l1.414-1.414 1.415 1.414zm-.708-10.607l1.415 1.415-7.071 7.07-1.415-1.414 7.071-7.07z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 1 1 14 0v1zM5 12v8h14v-8H5zm6 2h2v4h-2v-4zm6-4V9A5 5 0 0 0 7 9v1h10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M15.243 4.515l-6.738 6.737-.707 2.121-1.04 1.041 2.828 2.829 1.04-1.041 2.122-.707 6.737-6.738-4.242-4.242zm6.364 3.535a1 1 0 0 1 0 1.414l-7.779 7.779-2.12.707-1.415 1.414a1 1 0 0 1-1.414 0l-4.243-4.243a1 1 0 0 1 0-1.414l1.414-1.414.707-2.121 7.779-7.779a1 1 0 0 1 1.414 0l5.657 5.657zm-6.364-.707l1.414 1.414-4.95 4.95-1.414-1.414 4.95-4.95zM4.283 16.89l2.828 2.829-1.414 1.414-4.243-1.414 2.828-2.829z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 13.535V3h8v3h-6v11a4 4 0 1 1-2-3.465z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 193 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M9.366 10.682a10.556 10.556 0 0 0 3.952 3.952l.884-1.238a1 1 0 0 1 1.294-.296 11.422 11.422 0 0 0 4.583 1.364 1 1 0 0 1 .921.997v4.462a1 1 0 0 1-.898.995c-.53.055-1.064.082-1.602.082C9.94 21 3 14.06 3 5.5c0-.538.027-1.072.082-1.602A1 1 0 0 1 4.077 3h4.462a1 1 0 0 1 .997.921A11.422 11.422 0 0 0 10.9 8.504a1 1 0 0 1-.296 1.294l-1.238.884zm-2.522-.657l1.9-1.357A13.41 13.41 0 0 1 7.647 5H5.01c-.006.166-.009.333-.009.5C5 12.956 11.044 19 18.5 19c.167 0 .334-.003.5-.01v-2.637a13.41 13.41 0 0 1-3.668-1.097l-1.357 1.9a12.442 12.442 0 0 1-1.588-.75l-.058-.033a12.556 12.556 0 0 1-4.702-4.702l-.033-.058a12.442 12.442 0 0 1-.75-1.588z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 802 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0H24V24H0z"/>
<path d="M12 19c.828 0 1.5.672 1.5 1.5S12.828 22 12 22s-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm0-17c3.314 0 6 2.686 6 6 0 2.165-.753 3.29-2.674 4.923C13.399 14.56 13 15.297 13 17h-2c0-2.474.787-3.695 3.031-5.601C15.548 10.11 16 9.434 16 8c0-2.21-1.79-4-4-4S8 5.79 8 8v1H6V8c0-3.314 2.686-6 6-6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3.741 1.408l18.462 10.154a.5.5 0 0 1 0 .876L3.741 22.592A.5.5 0 0 1 3 22.154V1.846a.5.5 0 0 1 .741-.438zM5 13v6.617L18.85 12 5 4.383V11h5v2H5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18.26l-7.053 3.948 1.575-7.928L.587 8.792l8.027-.952L12 .5l3.386 7.34 8.027.952-5.935 5.488 1.575 7.928z"/></svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@ -0,0 +1,23 @@
const { execCommand } = require('@joplin/utils');
const yargs = require('yargs');
async function main() {
const argv = yargs.argv;
if (!argv.profile) throw new Error('OneNote build: profile value is missing');
if (!['release', 'dev'].includes(argv.profile)) throw new Error('OneNote build: profile value is invalid');
const buildCommand = `wasm-pack build --target nodejs --${argv.profile}`;
await execCommand(buildCommand);
}
// eslint-disable-next-line promise/prefer-await-to-then
main().catch((error) => {
console.error('Fatal error');
if (error.stderr.includes('No such file or directory (os error 2)')) {
console.error('----------------------------------------------------------------');
console.error('Rust toolchain is missing, please install it: https://rustup.rs/');
console.error('----------------------------------------------------------------');
}
process.exit(1);
});

View File

@ -0,0 +1,22 @@
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "warn"
notice = "deny"
[licenses]
unlicensed = "deny"
allow-osi-fsf-free = "either"
copyleft = "allow"
default = "deny"
[bans]
multiple-versions = "deny"
wildcards = "warn"
skip = [
{ name = "cfg-if" },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"

View File

@ -0,0 +1,49 @@
const fs = require('node:fs');
const path = require('node:path');
function mkdirSyncRecursive(filepath) {
if (!fs.existsSync(filepath)) {
mkdirSyncRecursive(filepath.substring(0, filepath.lastIndexOf(path.sep)));
fs.mkdirSync(filepath);
}
}
function isDirectory(filepath) {
if (!fs.existsSync(filepath)) return false;
return fs.lstatSync(filepath).isDirectory();
}
function readDir(filepath) {
const dirContents = fs.readdirSync(filepath, { withFileTypes: true });
return dirContents.map(entry => filepath + path.sep + entry.name).join('\n');
}
function removePrefix(basePath, prefix) {
return basePath.replace(prefix, '');
}
function getOutputPath(inputDir, outputDir, filePath) {
const basePathFromInputFolder = filePath.replace(inputDir, '');
const newOutput = path.join(outputDir, basePathFromInputFolder);
return path.dirname(newOutput);
}
function getParentDir(filePath) {
return path.basename(path.dirname(filePath));
}
function normalizeAndWriteFile(filePath, data) {
filePath = path.normalize(filePath);
fs.writeFileSync(filePath, data);
}
module.exports = {
mkdirSyncRecursive,
isDirectory,
readDir,
removePrefix,
getOutputPath,
getParentDir,
normalizeAndWriteFile,
};

View File

@ -0,0 +1,28 @@
{
"name": "@joplin/onenote-converter",
"collaborators": [
"Pedro Luiz <pedrlz.frn@gmail.com>"
],
"description": "This package file only exists to build the @joplin/onenote-converter",
"version": "0.0.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/laurent22/joplin"
},
"files": [
"./pkg/onenote_converter_bg.wasm",
"./pkg/onenote_converter.js",
"./pkg/onenote_converter.d.ts"
],
"main": "./pkg/onenote_converter.js",
"types": "./pkg/onenote_converter.d.ts",
"scripts": {
"build": "node ./build.js --profile=release",
"buildDev": "node ./build.js --profile=dev"
},
"devDependencies": {
"wasm-pack": "0.13.0",
"yargs": "17.7.2"
}
}

View File

@ -0,0 +1,87 @@
pub use crate::parser::Parser;
use color_eyre::eyre::eyre;
use color_eyre::eyre::Result;
use std::panic;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::utils::utils::{log, log_warn};
use crate::utils::{get_file_extension, get_file_name, get_output_path, get_parent_dir};
mod notebook;
mod page;
mod parser;
mod section;
mod templates;
mod utils;
extern crate console_error_panic_hook;
extern crate web_sys;
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn oneNoteConverter(input: &str, output: &str, base_path: &str) {
panic::set_hook(Box::new(console_error_panic_hook::hook));
if let Err(e) = _main(input, output, base_path) {
log_warn!("{:?}", e);
}
}
fn _main(input_path: &str, output_dir: &str, base_path: &str) -> Result<()> {
log!("Starting parsing of the file: {:?}", input_path);
convert(&input_path, &output_dir, base_path)?;
Ok(())
}
pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
let mut parser = Parser::new();
let extension: String = unsafe { get_file_extension(path) }
.unwrap()
.as_string()
.unwrap();
match extension.as_str() {
".one" => {
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap();
log!("Parsing .one file: {}", _name);
if path.contains("OneNote_RecycleBin") {
return Ok(());
}
let section = parser.parse_section(path.to_owned())?;
let section_output_dir = unsafe { get_output_path(base_path, output_dir, path) }
.unwrap()
.as_string()
.unwrap();
section::Renderer::new().render(&section, section_output_dir.to_owned())?;
}
".onetoc2" => {
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap();
log!("Parsing .onetoc2 file: {}", _name);
let notebook = parser.parse_notebook(path.to_owned())?;
let notebook_name = unsafe { get_parent_dir(path) }
.expect("Input file has no parent folder")
.as_string()
.expect("Parent folder has no name");
log!("notebook name: {:?}", notebook_name);
let notebook_output_dir = unsafe { get_output_path(base_path, output_dir, path) }
.unwrap()
.as_string()
.unwrap();
log!("Notebok directory: {:?}", notebook_output_dir);
notebook::Renderer::new().render(&notebook, &notebook_name, &notebook_output_dir)?;
}
ext => return Err(eyre!("Invalid file extension: {}, file: {}", ext, path)),
}
Ok(())
}

View File

@ -0,0 +1,114 @@
use crate::parser::notebook::Notebook;
use crate::parser::property::common::Color;
use crate::parser::section::{Section, SectionEntry};
use crate::templates::notebook::Toc;
use crate::utils::utils::log;
use crate::utils::{join_path, make_dir, remove_prefix};
use crate::{section, templates};
use color_eyre::eyre::Result;
use palette::rgb::Rgb;
use palette::{Alpha, ConvertFrom, Hsl, Saturate, Shade, Srgb};
pub(crate) type RgbColor = Alpha<Rgb<palette::encoding::Srgb, u8>, f32>;
pub(crate) struct Renderer;
impl Renderer {
pub fn new() -> Self {
Renderer
}
pub fn render(&mut self, notebook: &Notebook, name: &str, output_dir: &str) -> Result<()> {
log!("Notebook name: {:?} {:?}", name, output_dir);
let _ = unsafe { make_dir(output_dir) };
// let notebook_dir = unsafe { join_path(output_dir, sanitize_filename::sanitize(name).as_str()) }.unwrap().as_string().unwrap();
let notebook_dir = output_dir.to_owned();
let _ = unsafe { make_dir(&notebook_dir) };
let mut toc = Vec::new();
for entry in notebook.entries() {
match entry {
SectionEntry::Section(section) => {
toc.push(Toc::Section(self.render_section(
section,
notebook_dir.clone(),
output_dir.into(),
)?));
}
SectionEntry::SectionGroup(group) => {
let dir_name = sanitize_filename::sanitize(group.display_name());
let section_group_dir =
unsafe { join_path(notebook_dir.as_str(), dir_name.as_str()) }
.unwrap()
.as_string()
.unwrap();
log!("Section group directory: {:?}", section_group_dir);
let _ = unsafe { make_dir(section_group_dir.as_str()) };
let mut entries = Vec::new();
for entry in group.entries() {
if let SectionEntry::Section(section) = entry {
entries.push(self.render_section(
section,
section_group_dir.clone(),
output_dir.to_owned(),
)?);
}
}
toc.push(templates::notebook::Toc::SectionGroup(
group.display_name().to_string(),
entries,
))
}
}
}
templates::notebook::render(name, &toc)?;
Ok(())
}
fn render_section(
&mut self,
section: &Section,
notebook_dir: String,
base_dir: String,
) -> Result<templates::notebook::Section> {
let mut renderer = section::Renderer::new();
let section_path = renderer.render(section, notebook_dir)?;
log!("section_path: {:?}", section_path);
let path_from_base_dir = unsafe { remove_prefix(section_path.as_str(), base_dir.as_str()) }
.unwrap()
.as_string()
.unwrap();
log!("path_from_base_dir: {:?}", path_from_base_dir);
Ok(templates::notebook::Section {
name: section.display_name().to_string(),
path: path_from_base_dir,
color: section.color().map(prepare_color),
})
}
}
fn prepare_color(color: Color) -> RgbColor {
Alpha {
alpha: color.alpha() as f32 / 255.0,
color: Srgb::convert_from(
Hsl::convert_from(Srgb::new(
color.r() as f32 / 255.0,
color.g() as f32 / 255.0,
color.b() as f32 / 255.0,
))
.darken(0.2)
.saturate(1.0),
)
.into_format(),
}
}

View File

@ -0,0 +1,22 @@
use crate::page::Renderer;
use color_eyre::Result;
use log::warn;
// use crate::something_else::contents::Content;
use crate::parser::contents::Content;
impl<'a> Renderer<'a> {
pub(crate) fn render_content(&mut self, content: &Content) -> Result<String> {
match content {
Content::RichText(text) => self.render_rich_text(text),
Content::Image(image) => self.render_image(image),
Content::EmbeddedFile(file) => self.render_embedded_file(file),
Content::Table(table) => self.render_table(table),
Content::Ink(ink) => Ok(self.render_ink(ink, None, false)),
Content::Unknown => {
warn!("Page with unknown content");
Ok(String::new())
}
}
}
}

View File

@ -0,0 +1,89 @@
use crate::page::Renderer;
use crate::parser::contents::EmbeddedFile;
use crate::parser::property::embedded_file::FileType;
use crate::utils::utils::log;
use crate::utils::{join_path, write_file};
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use std::path::PathBuf;
impl<'a> Renderer<'a> {
pub(crate) fn render_embedded_file(&mut self, file: &EmbeddedFile) -> Result<String> {
let content;
let filename = self.determine_filename(file.filename())?;
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) }
.unwrap()
.as_string()
.unwrap();
log!("Rendering embedded file: {:?}", path);
let _ = unsafe { write_file(path.as_str(), file.data()) };
let file_type = Self::guess_type(file);
match file_type {
FileType::Audio => content = format!("<audio controls src=\"{}\"></audio>", filename),
FileType::Video => content = format!("<video controls src=\"{}\"></video>", filename),
FileType::Unknown => {
content = format!(
"<p style=\"font-size: 11pt; line-height: 17px;\"><a href=\"{}\">{}</a></p>",
filename, filename
)
}
};
Ok(self.render_with_note_tags(file.note_tags(), content))
}
fn guess_type(file: &EmbeddedFile) -> FileType {
match file.file_type() {
FileType::Audio => return FileType::Audio,
FileType::Video => return FileType::Video,
_ => {}
};
let filename = file.filename();
if let Some(mime) = mime_guess::from_path(filename).first() {
if mime.type_() == "audio" {
return FileType::Audio;
}
if mime.type_() == "video" {
return FileType::Video;
}
}
FileType::Unknown
}
pub(crate) fn determine_filename(&mut self, filename: &str) -> Result<String> {
let mut i = 0;
let mut current_filename = filename.to_string();
loop {
if !self.section.files.contains(&current_filename) {
self.section.files.insert(current_filename.clone());
return Ok(current_filename);
}
let path = PathBuf::from(filename);
let ext = path
.extension()
.wrap_err("Embedded file has no extension")?
.to_str()
.wrap_err("Embedded file name is non utf-8")?;
let base = path
.as_os_str()
.to_str()
.wrap_err("Embedded file name is non utf-8")?
.strip_suffix(ext)
.wrap_err("Failed to strip extension from file name")?
.trim_matches('.');
current_filename = format!("{}-{}.{}", base, i, ext);
i += 1;
}
}
}

View File

@ -0,0 +1,82 @@
use crate::page::Renderer;
use crate::parser::contents::Image;
use crate::utils::utils::log;
use crate::utils::{join_path, px, write_file, AttributeSet, StyleSet};
use color_eyre::Result;
impl<'a> Renderer<'a> {
pub(crate) fn render_image(&mut self, image: &Image) -> Result<String> {
let mut content = String::new();
if let Some(data) = image.data() {
let filename = self.determine_image_filename(image)?;
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) }
.unwrap()
.as_string()
.unwrap();
log!("Rendering image: {:?}", path);
let _ = unsafe { write_file(path.as_str(), data) };
let mut attrs = AttributeSet::new();
let mut styles = StyleSet::new();
attrs.set("src", filename);
if let Some(text) = image.alt_text() {
attrs.set("alt", text.to_string().replace('"', "&quot;"));
}
if let Some(width) = image.layout_max_width() {
styles.set("max-width", px(width));
}
if let Some(height) = image.layout_max_height() {
styles.set("max-height", px(height));
}
if image.offset_horizontal().is_some() || image.offset_vertical().is_some() {
styles.set("position", "absolute".to_string());
}
if let Some(offset) = image.offset_horizontal() {
styles.set("left", px(offset));
}
if let Some(offset) = image.offset_vertical() {
styles.set("top", px(offset));
}
if styles.len() > 0 {
attrs.set("style", styles.to_string());
}
content.push_str(&format!("<img {} />", attrs.to_string()));
}
Ok(self.render_with_note_tags(image.note_tags(), content))
}
fn determine_image_filename(&mut self, image: &Image) -> Result<String> {
if let Some(name) = image.image_filename() {
return self.determine_filename(name);
}
if let Some(ext) = image.extension() {
let mut i = 0;
loop {
let filename = format!("image{}{}", i, ext);
if !self.section.files.contains(&filename) {
self.section.files.insert(filename.clone());
return Ok(filename);
}
i += 1;
}
}
unimplemented!()
}
}

View File

@ -0,0 +1,210 @@
use crate::page::Renderer;
use crate::parser::contents::{Ink, InkBoundingBox, InkPoint, InkStroke};
use crate::utils::{px, AttributeSet, StyleSet};
use itertools::Itertools;
impl<'a> Renderer<'a> {
const SVG_SCALING_FACTOR: f32 = 2540.0 / 96.0;
pub(crate) fn render_ink(
&mut self,
ink: &Ink,
display_bounding_box: Option<&InkBoundingBox>,
embedded: bool,
) -> String {
if ink.ink_strokes().is_empty() {
return String::new();
}
let mut attrs = AttributeSet::new();
let mut styles = StyleSet::new();
styles.set("overflow", "visible".to_string());
styles.set("position", "absolute".to_string());
let path = self.render_ink_path(ink.ink_strokes());
let offset_horizontal = ink
.offset_horizontal()
.filter(|_| !embedded)
.unwrap_or_default();
let offset_vertical = ink
.offset_vertical()
.filter(|_| !embedded)
.unwrap_or_default();
let display_bounding_box = ink
.bounding_box()
.or_else(|| display_bounding_box.map(|bb| bb.scale(Self::SVG_SCALING_FACTOR)))
.filter(|_| embedded);
let (x_min, width) = get_boundary(ink.ink_strokes(), |p| p.x());
let (y_min, height) = get_boundary(ink.ink_strokes(), |p| p.y());
let stroke_strength = ink.ink_strokes()[0]
.width()
.max(ink.ink_strokes()[0].height())
.max(140.0);
let x_min = x_min as f32 - stroke_strength / 2.0;
let y_min = y_min as f32 - stroke_strength / 2.0;
let width = width as f32 + stroke_strength + Self::SVG_SCALING_FACTOR;
let height = height as f32 + stroke_strength + Self::SVG_SCALING_FACTOR;
styles.set(
"height",
format!(
"{}px",
((height as f32) / (Self::SVG_SCALING_FACTOR)).round()
),
);
styles.set(
"width",
format!(
"{}px",
((width as f32) / (Self::SVG_SCALING_FACTOR)).round()
),
);
let display_y_min = display_bounding_box.map(|bb| bb.y()).unwrap_or_default();
let display_x_min = display_bounding_box.map(|bb| bb.x()).unwrap_or_default();
styles.set(
"top",
format!(
"{}px",
((y_min - display_y_min) / Self::SVG_SCALING_FACTOR + offset_vertical * 48.0)
.round()
),
);
styles.set(
"left",
format!(
"{}px",
((x_min - display_x_min) / Self::SVG_SCALING_FACTOR + offset_horizontal * 48.0)
.round()
),
);
attrs.set(
"viewBox",
format!(
"{} {} {} {}",
x_min.round(),
y_min.round(),
width.round(),
height.round()
),
);
if styles.len() > 0 {
attrs.set("style", styles.to_string());
}
if embedded {
let mut span_styles = StyleSet::new();
if let Some(bb) = display_bounding_box {
span_styles.set("width", px(bb.width() / Self::SVG_SCALING_FACTOR / 48.0));
span_styles.set("height", px(bb.height() / Self::SVG_SCALING_FACTOR / 48.0));
}
format!(
"<span style=\"{}\" class=\"ink-text\"><svg {}>{}</svg></span>",
span_styles.to_string(),
attrs.to_string(),
path
)
} else {
format!("<svg {}>{}</svg>", attrs.to_string(), path)
}
}
fn render_ink_path(&mut self, strokes: &[InkStroke]) -> String {
let mut attrs = AttributeSet::new();
attrs.set(
"d",
strokes
.iter()
.map(|stroke| self.render_ink_path_points(stroke))
.collect_vec()
.join(" "),
);
let stroke = &strokes[0];
let opacity = (255 - stroke.transparency().unwrap_or_default()) as f32 / 256.0;
attrs.set("opacity", format!("{:.2}", opacity));
let color = if let Some(value) = stroke.color() {
let r = value % 256;
let rem = (value - r) / 256;
let g = rem % 256;
let rem = (rem - g) / 256;
let b = rem % 256;
format!("rgb({}, {}, {})", r, g, b)
} else {
"WindowText".to_string()
};
attrs.set("stroke", color);
attrs.set("stroke-width", stroke.width().round().to_string());
let pen_type = stroke.pen_tip().unwrap_or_default();
attrs.set(
"stroke-linejoin",
if pen_type == 0 { "round" } else { "bevel" }.to_string(),
);
attrs.set(
"stroke-linecap",
if pen_type == 0 { "round" } else { "square" }.to_string(),
);
attrs.set("fill", "none".to_string());
format!("<path {} />", attrs.to_string())
}
fn render_ink_path_points(&self, stroke: &InkStroke) -> String {
let start = &stroke.path()[0];
let mut path = stroke.path()[1..].iter().map(display_point).collect_vec();
if path.is_empty() {
path.push("0 0".to_string());
}
format!("M {} l {}", display_point(start), path.join(" "))
}
}
fn get_boundary<F: Fn(&InkPoint) -> f32>(strokes: &[InkStroke], coord: F) -> (f32, f32) {
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for stroke in strokes {
let start = coord(&stroke.path()[0]);
let mut pos = start;
for point in stroke.path()[1..].iter() {
pos += coord(point);
if pos < min {
min = pos;
}
if pos > max {
max = pos;
}
}
}
(min, max - min)
}
fn display_point(p: &InkPoint) -> String {
format!("{} {}", p.x().floor(), p.y().round())
}

View File

@ -0,0 +1,182 @@
use crate::page::Renderer;
use crate::parser::contents::{List, OutlineElement};
use crate::parser::property::common::ColorRef;
use crate::utils::{px, AttributeSet, StyleSet};
use color_eyre::Result;
const FORMAT_NUMBERED_LIST: char = '\u{fffd}';
impl<'a> Renderer<'a> {
pub(crate) fn render_list<'b>(
&mut self,
elements: impl Iterator<Item = (&'b OutlineElement, u8, u8)>,
indents: &[f32],
) -> Result<String> {
let mut contents = String::new();
let mut in_list = false;
let mut list_end = None;
for (element, parent_level, current_level) in elements {
if !in_list && self.is_list(element) {
let tags = self.list_tags(element);
let list_start = tags.0;
list_end = Some(tags.1);
contents.push_str(&list_start);
in_list = true;
}
if in_list && !self.is_list(element) {
contents.push_str(&list_end.take().expect("no list end tag defined"));
in_list = false;
}
contents.push_str(&self.render_outline_element(
element,
parent_level,
current_level,
indents,
)?);
}
if in_list {
contents.push_str(&list_end.expect("no list end tag defined"));
}
Ok(contents)
}
pub(crate) fn list_tags(&mut self, element: &OutlineElement) -> (String, String) {
let list = element
.list_contents()
.first()
.expect("no list contents defined");
let tag = if self.is_numbered_list(list) {
"ol"
} else {
"ul"
};
let attrs = self.list_attrs(list, element.list_spacing());
(format!("<{} {}>", tag, attrs), format!("</{}>", tag))
}
fn list_attrs(&mut self, list: &List, spacing: Option<f32>) -> AttributeSet {
let mut attrs = AttributeSet::new();
let mut container_style = StyleSet::new();
let mut item_style = StyleSet::new();
let mut marker_style = StyleSet::new();
let mut list_font = list.list_font();
let mut list_format = list.list_format();
let mut font_size = list.font_size();
self.fix_wingdings(&mut list_font, &mut list_format, &mut font_size);
match list_format {
[FORMAT_NUMBERED_LIST, '\u{0}', ..] => {}
[FORMAT_NUMBERED_LIST, '\u{1}', ..] => {
container_style.set("list-style-type", "upper-roman".to_string())
}
[FORMAT_NUMBERED_LIST, '\u{2}', ..] => {
container_style.set("list-style-type", "lower-roman".to_string())
}
[FORMAT_NUMBERED_LIST, '\u{3}', ..] => {
container_style.set("list-style-type", "upper-latin".to_string())
}
[FORMAT_NUMBERED_LIST, '\u{4}', ..] => {
container_style.set("list-style-type", "lower-latin".to_string())
}
[FORMAT_NUMBERED_LIST, c, ..] => {
dbg!(c);
unimplemented!();
}
[c] => marker_style.set("content", format!("'{}'", c)),
_ => {}
}
let bullet_spacing = spacing.unwrap_or(0.2);
item_style.set("padding-left", px(bullet_spacing));
container_style.set("position", "relative".to_string());
container_style.set("left", px(-bullet_spacing));
if let Some(font) = list_font {
marker_style.set("font-family", font.to_string());
}
if let Some(font) = list.font() {
marker_style.set("font-family", font.to_string());
}
if let Some(ColorRef::Manual { r, g, b }) = list.font_color() {
marker_style.set("color", format!("rgb({},{},{})", r, g, b));
}
if let Some(size) = font_size {
marker_style.set("font-size", ((size as f32) / 2.0).to_string() + "pt");
}
if let Some(restart) = list.list_restart() {
attrs.set("start", restart.to_string())
}
if container_style.len() > 0 {
attrs.set("style", container_style.to_string());
}
let class = self.gen_class("list");
if marker_style.len() > 0 {
attrs.set("class", class.clone());
self.global_styles
.insert(format!(".{} li::marker", class), marker_style);
}
self.global_styles
.insert(format!(".{} li", class), item_style);
attrs
}
fn fix_wingdings(
&self,
list_font: &mut Option<&str>,
list_format: &mut &[char],
font_size: &mut Option<u16>,
) {
match list_font.zip(list_format.first()) {
// See http://www.alanwood.net/demos/wingdings.html
Some(("Wingdings", '\u{a7}')) => *list_format = &['\u{25aa}'],
Some(("Wingdings", '\u{a8}')) => *list_format = &['\u{25fb}'],
Some(("Wingdings", '\u{77}')) => *list_format = &['\u{2b25}'],
// See http://www.alanwood.net/demos/wingdings-2.html
Some(("Wingdings 2", '\u{ae}')) => *list_format = &['\u{25c6}'],
// See http://www.alanwood.net/demos/wingdings-3.html
Some(("Wingdings 3", '\u{7d}')) => {
*list_format = &['\u{25b6}'];
*font_size = Some(18);
}
_ => return,
}
*list_font = Some("Calibri");
}
fn is_numbered_list(&self, list: &List) -> bool {
list.list_format()
.first()
.map(|c| *c == FORMAT_NUMBERED_LIST)
.unwrap_or_default()
}
pub(crate) fn is_list(&self, element: &OutlineElement) -> bool {
element.list_contents().first().is_some()
}
}

View File

@ -0,0 +1,100 @@
use crate::parser::page::{Page, PageContent};
use crate::section;
use crate::utils::StyleSet;
use color_eyre::Result;
use std::collections::{HashMap, HashSet};
pub(crate) mod content;
pub(crate) mod embedded_file;
pub(crate) mod image;
pub(crate) mod ink;
pub(crate) mod list;
pub(crate) mod note_tag;
pub(crate) mod outline;
pub(crate) mod rich_text;
pub(crate) mod table;
pub(crate) struct Renderer<'a> {
output: String,
section: &'a mut section::Renderer,
in_list: bool,
global_styles: HashMap<String, StyleSet>,
global_classes: HashSet<String>,
}
impl<'a> Renderer<'a> {
pub(crate) fn new(output: String, section: &'a mut section::Renderer) -> Self {
Self {
output,
section,
in_list: false,
global_styles: HashMap::new(),
global_classes: HashSet::new(),
}
}
pub(crate) fn render_page(&mut self, page: &Page) -> Result<String> {
let title_text = page.title_text().unwrap_or("Untitled Page");
let mut content = String::new();
if let Some(title) = page.title() {
let mut styles = StyleSet::new();
styles.set("position", "absolute".to_string());
styles.set(
"top",
format!("{}px", (title.offset_vertical() * 48.0 + 24.0).round()),
);
styles.set(
"left",
format!("{}px", (title.offset_horizontal() * 48.0 + 48.0).round()),
);
let mut title_field = format!("<div class=\"title\" style=\"{}\">", styles.to_string());
for outline in title.contents() {
title_field.push_str(&self.render_outline(outline)?)
}
title_field.push_str("</div>");
content.push_str(&title_field);
}
let page_content = page
.contents()
.iter()
.map(|content| self.render_page_content(content))
.collect::<Result<String>>()?;
content.push_str(&page_content);
crate::templates::page::render(title_text, &content, &self.global_styles)
}
pub(crate) fn gen_class(&mut self, prefix: &str) -> String {
let mut i = 0;
loop {
let class = format!("{}-{}", prefix, i);
if !self.global_classes.contains(&class) {
self.global_classes.insert(class.clone());
return class;
}
i += 1;
}
}
fn render_page_content(&mut self, content: &PageContent) -> Result<String> {
match content {
PageContent::Outline(outline) => self.render_outline(outline),
PageContent::Image(image) => self.render_image(image),
PageContent::EmbeddedFile(file) => self.render_embedded_file(file),
PageContent::Ink(ink) => Ok(self.render_ink(ink, None, false)),
PageContent::Unknown => Ok(String::new()),
}
}
}

View File

@ -0,0 +1,539 @@
use crate::page::Renderer;
use crate::parser::contents::{NoteTag, OutlineElement};
use crate::parser::property::common::ColorRef;
use crate::parser::property::note_tag::{ActionItemStatus, NoteTagShape};
use crate::utils::StyleSet;
use std::borrow::Cow;
const COLOR_BLUE: &str = "#4673b7";
const COLOR_GREEN: &str = "#369950";
const COLOR_ORANGE: &str = "#dba24d";
const COLOR_PINK: &str = "#f78b9d";
const COLOR_RED: &str = "#db5b4d";
const COLOR_YELLOW: &str = "#ffd678";
const ICON_ARROW_RIGHT: &str = include_str!("../../assets/icons/arrow-right-line.svg");
const ICON_AWARD: &str = include_str!("../../assets/icons/award-line.svg");
const ICON_BOOK: &str = include_str!("../../assets/icons/book-open-line.svg");
const ICON_BUBBLE: &str = include_str!("../../assets/icons/chat-4-line.svg");
const ICON_CHECKBOX_COMPLETE: &str = include_str!("../../assets/icons/checkbox-fill.svg");
const ICON_CHECKBOX_EMPTY: &str = include_str!("../../assets/icons/checkbox-blank-line.svg");
const ICON_CHECK_MARK: &str = include_str!("../../assets/icons/check-line.svg");
const ICON_CIRCLE: &str = include_str!("../../assets/icons/checkbox-blank-circle-fill.svg");
const ICON_CONTACT: &str = include_str!("../../assets/icons/contacts-line.svg");
const ICON_EMAIL: &str = include_str!("../../assets/icons/send-plane-2-line.svg");
const ICON_ERROR: &str = include_str!("../../assets/icons/error-warning-line.svg");
const ICON_FILM: &str = include_str!("../../assets/icons/film-line.svg");
const ICON_FLAG: &str = include_str!("../../assets/icons/flag-fill.svg");
const ICON_HOME: &str = include_str!("../../assets/icons/home-4-line.svg");
const ICON_LIGHT_BULB: &str = include_str!("../../assets/icons/lightbulb-line.svg");
const ICON_LINK: &str = include_str!("../../assets/icons/link.svg");
const ICON_LOCK: &str = include_str!("../../assets/icons/lock-line.svg");
const ICON_MUSIC: &str = include_str!("../../assets/icons/music-fill.svg");
const ICON_PAPER: &str = include_str!("../../assets/icons/file-list-2-line.svg");
const ICON_PEN: &str = include_str!("../../assets/icons/mark-pen-line.svg");
const ICON_PERSON: &str = include_str!("../../assets/icons/user-line.svg");
const ICON_PHONE: &str = include_str!("../../assets/icons/phone-line.svg");
const ICON_QUESTION_MARK: &str = include_str!("../../assets/icons/question-mark.svg");
const ICON_SQUARE: &str = include_str!("../../assets/icons/checkbox-blank-fill.svg");
const ICON_STAR: &str = include_str!("../../assets/icons/star-fill.svg");
#[derive(Debug, Copy, Clone, PartialEq)]
enum IconSize {
Normal,
Large,
}
impl<'a> Renderer<'a> {
pub(crate) fn render_with_note_tags(
&mut self,
note_tags: &[NoteTag],
content: String,
) -> String {
if let Some((markup, styles)) = self.render_note_tags(note_tags) {
let mut contents = String::new();
contents.push_str(&format!("<div style=\"{}\">{}", styles, markup));
contents.push_str(&content);
contents.push_str("</div>");
contents
} else {
content
}
}
pub(crate) fn render_note_tags(&mut self, note_tags: &[NoteTag]) -> Option<(String, StyleSet)> {
let mut markup = String::new();
let mut styles = StyleSet::new();
if note_tags.is_empty() {
return None;
}
for note_tag in note_tags {
if let Some(def) = note_tag.definition() {
if let Some(ColorRef::Manual { r, g, b }) = def.highlight_color() {
styles.set("background-color", format!("rgb({},{},{})", r, g, b));
}
if let Some(ColorRef::Manual { r, g, b }) = def.text_color() {
styles.set("color", format!("rgb({},{},{})", r, g, b));
}
if def.shape() != NoteTagShape::NoIcon {
let (icon, icon_style) =
self.note_tag_icon(def.shape(), note_tag.item_status());
let mut icon_classes = vec!["note-tag-icon".to_string()];
if icon_style.len() > 0 {
let class = self.gen_class("icon");
icon_classes.push(class.to_string());
self.global_styles
.insert(format!(".{} > svg", class), icon_style);
}
markup.push_str(&format!(
"<span class=\"{}\">{}</span>",
icon_classes.join(" "),
icon
));
}
}
}
Some((markup, styles))
}
pub(crate) fn has_note_tag(&self, element: &OutlineElement) -> bool {
element
.contents()
.iter()
.flat_map(|element| element.rich_text())
.any(|text| !text.note_tags().is_empty())
}
fn note_tag_icon(
&self,
shape: NoteTagShape,
status: ActionItemStatus,
) -> (Cow<'static, str>, StyleSet) {
let mut style = StyleSet::new();
match shape {
NoteTagShape::NoIcon => unimplemented!(),
NoteTagShape::GreenCheckBox => self.icon_checkbox(status, style, COLOR_GREEN),
NoteTagShape::YellowCheckBox => self.icon_checkbox(status, style, COLOR_YELLOW),
NoteTagShape::BlueCheckBox => self.icon_checkbox(status, style, COLOR_BLUE),
NoteTagShape::GreenStarCheckBox => {
self.icon_checkbox_with_star(status, style, COLOR_GREEN)
}
NoteTagShape::YellowStarCheckBox => {
self.icon_checkbox_with_star(status, style, COLOR_YELLOW)
}
NoteTagShape::BlueStarCheckBox => {
self.icon_checkbox_with_star(status, style, COLOR_BLUE)
}
NoteTagShape::GreenExclamationCheckBox => {
self.icon_checkbox_with_exclamation(status, style, COLOR_GREEN)
}
NoteTagShape::YellowExclamationCheckBox => {
self.icon_checkbox_with_exclamation(status, style, COLOR_YELLOW)
}
NoteTagShape::BlueExclamationCheckBox => {
self.icon_checkbox_with_exclamation(status, style, COLOR_BLUE)
}
NoteTagShape::GreenRightArrowCheckBox => {
self.icon_checkbox_with_right_arrow(status, style, COLOR_GREEN)
}
NoteTagShape::YellowRightArrowCheckBox => {
self.icon_checkbox_with_right_arrow(status, style, COLOR_YELLOW)
}
NoteTagShape::BlueRightArrowCheckBox => {
self.icon_checkbox_with_right_arrow(status, style, COLOR_BLUE)
}
NoteTagShape::YellowStar => {
style.set("fill", COLOR_YELLOW.to_string());
(
Cow::from(ICON_STAR),
self.icon_style(IconSize::Normal, style),
)
}
NoteTagShape::BlueFollowUpFlag => unimplemented!(),
NoteTagShape::QuestionMark => (
Cow::from(ICON_QUESTION_MARK),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::BlueRightArrow => unimplemented!(),
NoteTagShape::HighPriority => (
Cow::from(ICON_ERROR),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::ContactInformation => (
Cow::from(ICON_PHONE),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::Meeting => unimplemented!(),
NoteTagShape::TimeSensitive => unimplemented!(),
NoteTagShape::LightBulb => (
Cow::from(ICON_LIGHT_BULB),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::Pushpin => unimplemented!(),
NoteTagShape::Home => (
Cow::from(ICON_HOME),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::CommentBubble => (
Cow::from(ICON_BUBBLE),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::SmilingFace => unimplemented!(),
NoteTagShape::AwardRibbon => (
Cow::from(ICON_AWARD),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::YellowKey => unimplemented!(),
NoteTagShape::BlueCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_BLUE),
NoteTagShape::BlueCircle1 => unimplemented!(),
NoteTagShape::BlueCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_BLUE),
NoteTagShape::BlueCircle2 => unimplemented!(),
NoteTagShape::BlueCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_BLUE),
NoteTagShape::BlueCircle3 => unimplemented!(),
NoteTagShape::BlueEightPointStar => unimplemented!(),
NoteTagShape::BlueCheckMark => self.icon_checkmark(style, COLOR_BLUE),
NoteTagShape::BlueCircle => self.icon_circle(style, COLOR_BLUE),
NoteTagShape::BlueDownArrow => unimplemented!(),
NoteTagShape::BlueLeftArrow => unimplemented!(),
NoteTagShape::BlueSolidTarget => unimplemented!(),
NoteTagShape::BlueStar => unimplemented!(),
NoteTagShape::BlueSun => unimplemented!(),
NoteTagShape::BlueTarget => unimplemented!(),
NoteTagShape::BlueTriangle => unimplemented!(),
NoteTagShape::BlueUmbrella => unimplemented!(),
NoteTagShape::BlueUpArrow => unimplemented!(),
NoteTagShape::BlueXWithDots => unimplemented!(),
NoteTagShape::BlueX => unimplemented!(),
NoteTagShape::GreenCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_GREEN),
NoteTagShape::GreenCircle1 => unimplemented!(),
NoteTagShape::GreenCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_GREEN),
NoteTagShape::GreenCircle2 => unimplemented!(),
NoteTagShape::GreenCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_GREEN),
NoteTagShape::GreenCircle3 => unimplemented!(),
NoteTagShape::GreenEightPointStar => unimplemented!(),
NoteTagShape::GreenCheckMark => self.icon_checkmark(style, COLOR_GREEN),
NoteTagShape::GreenCircle => self.icon_circle(style, COLOR_GREEN),
NoteTagShape::GreenDownArrow => unimplemented!(),
NoteTagShape::GreenLeftArrow => unimplemented!(),
NoteTagShape::GreenRightArrow => unimplemented!(),
NoteTagShape::GreenSolidArrow => unimplemented!(),
NoteTagShape::GreenStar => unimplemented!(),
NoteTagShape::GreenSun => unimplemented!(),
NoteTagShape::GreenTarget => unimplemented!(),
NoteTagShape::GreenTriangle => unimplemented!(),
NoteTagShape::GreenUmbrella => unimplemented!(),
NoteTagShape::GreenUpArrow => unimplemented!(),
NoteTagShape::GreenXWithDots => unimplemented!(),
NoteTagShape::GreenX => unimplemented!(),
NoteTagShape::YellowCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_YELLOW),
NoteTagShape::YellowCircle1 => unimplemented!(),
NoteTagShape::YellowCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_YELLOW),
NoteTagShape::YellowCircle2 => unimplemented!(),
NoteTagShape::YellowCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_YELLOW),
NoteTagShape::YellowCircle3 => unimplemented!(),
NoteTagShape::YellowEightPointStar => unimplemented!(),
NoteTagShape::YellowCheckMark => self.icon_checkmark(style, COLOR_YELLOW),
NoteTagShape::YellowCircle => self.icon_circle(style, COLOR_YELLOW),
NoteTagShape::YellowDownArrow => unimplemented!(),
NoteTagShape::YellowLeftArrow => unimplemented!(),
NoteTagShape::YellowRightArrow => unimplemented!(),
NoteTagShape::YellowSolidTarget => unimplemented!(),
NoteTagShape::YellowSun => unimplemented!(),
NoteTagShape::YellowTarget => unimplemented!(),
NoteTagShape::YellowTriangle => unimplemented!(),
NoteTagShape::YellowUmbrella => unimplemented!(),
NoteTagShape::YellowUpArrow => unimplemented!(),
NoteTagShape::YellowXWithDots => unimplemented!(),
NoteTagShape::YellowX => unimplemented!(),
NoteTagShape::FollowUpTodayFlag => unimplemented!(),
NoteTagShape::FollowUpTomorrowFlag => unimplemented!(),
NoteTagShape::FollowUpThisWeekFlag => unimplemented!(),
NoteTagShape::FollowUpNextWeekFlag => unimplemented!(),
NoteTagShape::NoFollowUpDateFlag => unimplemented!(),
NoteTagShape::BluePersonCheckBox => {
self.icon_checkbox_with_person(status, style, COLOR_BLUE)
}
NoteTagShape::YellowPersonCheckBox => {
self.icon_checkbox_with_person(status, style, COLOR_YELLOW)
}
NoteTagShape::GreenPersonCheckBox => {
self.icon_checkbox_with_person(status, style, COLOR_GREEN)
}
NoteTagShape::BlueFlagCheckBox => {
self.icon_checkbox_with_flag(status, style, COLOR_BLUE)
}
NoteTagShape::RedFlagCheckBox => self.icon_checkbox_with_flag(status, style, COLOR_RED),
NoteTagShape::GreenFlagCheckBox => {
self.icon_checkbox_with_flag(status, style, COLOR_GREEN)
}
NoteTagShape::RedSquare => self.icon_square(style, COLOR_RED),
NoteTagShape::YellowSquare => self.icon_square(style, COLOR_YELLOW),
NoteTagShape::BlueSquare => self.icon_square(style, COLOR_BLUE),
NoteTagShape::GreenSquare => self.icon_square(style, COLOR_GREEN),
NoteTagShape::OrangeSquare => self.icon_square(style, COLOR_ORANGE),
NoteTagShape::PinkSquare => self.icon_square(style, COLOR_PINK),
NoteTagShape::EMailMessage => (
Cow::from(ICON_EMAIL),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::ClosedEnvelope => unimplemented!(),
NoteTagShape::OpenEnvelope => unimplemented!(),
NoteTagShape::MobilePhone => unimplemented!(),
NoteTagShape::TelephoneWithClock => unimplemented!(),
NoteTagShape::QuestionBalloon => unimplemented!(),
NoteTagShape::PaperClip => unimplemented!(),
NoteTagShape::FrowningFace => unimplemented!(),
NoteTagShape::InstantMessagingContactPerson => unimplemented!(),
NoteTagShape::PersonWithExclamationMark => unimplemented!(),
NoteTagShape::TwoPeople => unimplemented!(),
NoteTagShape::ReminderBell => unimplemented!(),
NoteTagShape::Contact => (
Cow::from(ICON_CONTACT),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::RoseOnAStem => unimplemented!(),
NoteTagShape::CalendarDateWithClock => unimplemented!(),
NoteTagShape::MusicalNote => (
Cow::from(ICON_MUSIC),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::MovieClip => (
Cow::from(ICON_FILM),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::QuotationMark => unimplemented!(),
NoteTagShape::Globe => unimplemented!(),
NoteTagShape::HyperlinkGlobe => (
Cow::from(ICON_LINK),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::Laptop => unimplemented!(),
NoteTagShape::Plane => unimplemented!(),
NoteTagShape::Car => unimplemented!(),
NoteTagShape::Binoculars => unimplemented!(),
NoteTagShape::PresentationSlide => unimplemented!(),
NoteTagShape::Padlock => (
Cow::from(ICON_LOCK),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::OpenBook => (
Cow::from(ICON_BOOK),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::NotebookWithClock => unimplemented!(),
NoteTagShape::BlankPaperWithLines => (
Cow::from(ICON_PAPER),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::Research => unimplemented!(),
NoteTagShape::Pen => (
Cow::from(ICON_PEN),
self.icon_style(IconSize::Normal, style),
),
NoteTagShape::DollarSign => unimplemented!(),
NoteTagShape::CoinsWithAWindowBackdrop => unimplemented!(),
NoteTagShape::ScheduledTask => unimplemented!(),
NoteTagShape::LightningBolt => unimplemented!(),
NoteTagShape::Cloud => unimplemented!(),
NoteTagShape::Heart => unimplemented!(),
NoteTagShape::Sunflower => unimplemented!(),
}
}
fn icon_checkbox(
&self,
status: ActionItemStatus,
mut style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
style.set("fill", color.to_string());
if status.completed() {
(
Cow::from(ICON_CHECKBOX_COMPLETE),
self.icon_style(IconSize::Large, style),
)
} else {
(
Cow::from(ICON_CHECKBOX_EMPTY),
self.icon_style(IconSize::Large, style),
)
}
}
fn icon_checkbox_with_person(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, ICON_PERSON)
}
fn icon_checkbox_with_right_arrow(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, ICON_ARROW_RIGHT)
}
fn icon_checkbox_with_star(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, ICON_STAR)
}
fn icon_checkbox_with_flag(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, ICON_FLAG)
}
fn icon_checkbox_with_1(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, "<span class=\"content\">1</span>")
}
fn icon_checkbox_with_2(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, "<span class=\"content\">2</span>")
}
fn icon_checkbox_with_3(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, "<span class=\"content\">3</span>")
}
fn icon_checkbox_with_exclamation(
&self,
status: ActionItemStatus,
style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
self.icon_checkbox_with(status, style, color, "<span class=\"content\">!</span>")
}
fn icon_checkbox_with(
&self,
status: ActionItemStatus,
mut style: StyleSet,
color: &'static str,
secondary_icon: &'static str,
) -> (Cow<'static, str>, StyleSet) {
style.set("fill", color.to_string());
let mut content = String::new();
content.push_str(if status.completed() {
ICON_CHECKBOX_COMPLETE
} else {
ICON_CHECKBOX_EMPTY
});
content.push_str(&format!(
"<span class=\"icon-secondary\">{}</span>",
secondary_icon
));
(Cow::from(content), self.icon_style(IconSize::Large, style))
}
fn icon_checkmark(
&self,
mut style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
style.set("fill", color.to_string());
(
Cow::from(ICON_CHECK_MARK),
self.icon_style(IconSize::Large, style),
)
}
fn icon_circle(
&self,
mut style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
style.set("fill", color.to_string());
(
Cow::from(ICON_CIRCLE),
self.icon_style(IconSize::Normal, style),
)
}
fn icon_square(
&self,
mut style: StyleSet,
color: &'static str,
) -> (Cow<'static, str>, StyleSet) {
style.set("fill", color.to_string());
(
Cow::from(ICON_SQUARE),
self.icon_style(IconSize::Large, style),
)
}
fn icon_style(&self, size: IconSize, mut style: StyleSet) -> StyleSet {
match size {
IconSize::Normal => {
style.set("height", "16px".to_string());
style.set("width", "16px".to_string());
}
IconSize::Large => {
style.set("height", "20px".to_string());
style.set("width", "20px".to_string());
}
}
match (self.in_list, size) {
(false, IconSize::Normal) => {
style.set("left", "-23px".to_string());
}
(false, IconSize::Large) => {
style.set("left", "-25px".to_string());
}
(true, IconSize::Normal) => {
style.set("left", "-38px".to_string());
}
(true, IconSize::Large) => {
style.set("left", "-40px".to_string());
}
};
style
}
}

View File

@ -0,0 +1,146 @@
use crate::page::Renderer;
use crate::parser::contents::{Outline, OutlineElement, OutlineItem};
use crate::utils::{px, AttributeSet, StyleSet};
use color_eyre::Result;
impl<'a> Renderer<'a> {
pub(crate) fn render_outline(&mut self, outline: &Outline) -> Result<String> {
let mut attrs = AttributeSet::new();
let mut styles = StyleSet::new();
let mut contents = String::new();
attrs.set("class", "container-outline".to_string());
if let Some(width) = outline.layout_max_width() {
let outline_width = if outline.is_layout_size_set_by_user() {
width
} else {
width.max(13.0)
};
styles.set("width", px(outline_width));
};
if outline.offset_horizontal().is_some() || outline.offset_vertical().is_some() {
styles.set("position", "absolute".to_string());
}
if let Some(offset) = outline.offset_horizontal() {
styles.set("left", px(offset));
}
if let Some(offset) = outline.offset_vertical() {
styles.set("top", px(offset));
}
if styles.len() > 0 {
attrs.set("style", styles.to_string());
}
contents.push_str(&format!("<div {}>", attrs));
contents.push_str(&self.render_outline_items(
outline.items(),
0,
outline.child_level(),
outline.indents(),
)?);
contents.push_str("</div>");
Ok(contents)
}
pub(crate) fn render_outline_items(
&mut self,
items: &[OutlineItem],
parent_level: u8,
current_level: u8,
indents: &[f32],
) -> Result<String> {
self.render_list(
flatten_outline_items(items, parent_level, current_level),
indents,
)
}
pub(crate) fn render_outline_element(
&mut self,
element: &OutlineElement,
parent_level: u8,
current_level: u8,
indents: &[f32],
) -> Result<String> {
let mut indent_width = 0.0;
for i in (parent_level + 1)..=current_level {
indent_width += indents.get(i as usize).copied().unwrap_or(0.75);
}
let mut contents = String::new();
let is_list = self.is_list(element);
let mut attrs = AttributeSet::new();
attrs.set("class", "outline-element".to_string());
let mut styles = StyleSet::new();
styles.set("margin-left", px(indent_width as f32));
attrs.set("style", styles.to_string());
if is_list {
contents.push_str(&format!("<li {}>", attrs));
} else {
contents.push_str(&format!("<div {}>", attrs));
}
self.in_list = is_list;
contents.extend(
element
.contents()
.iter()
.map(|content| self.render_content(content))
.collect::<Result<Vec<_>, _>>()?
.into_iter(),
);
self.in_list = false;
if !is_list {
contents.push_str("</div>");
}
let children = element.children();
if !children.is_empty() {
contents.push_str(&self.render_outline_items(
children,
current_level,
current_level + element.child_level(),
indents,
)?);
}
if is_list {
contents.push_str("</li>");
}
contents.push('\n');
Ok(contents)
}
}
fn flatten_outline_items<'a>(
items: &'a [OutlineItem],
parent_level: u8,
current_level: u8,
) -> Box<dyn Iterator<Item = (&'a OutlineElement, u8, u8)> + 'a> {
Box::new(items.iter().flat_map(move |item| match item {
OutlineItem::Element(element) => {
Box::new(Some((element, parent_level, current_level)).into_iter())
}
OutlineItem::Group(group) => flatten_outline_items(
group.outlines(),
parent_level,
current_level + group.child_level(),
),
}))
}

View File

@ -0,0 +1,306 @@
use crate::page::Renderer;
use crate::parser::contents::{EmbeddedObject, RichText};
use crate::parser::property::common::ColorRef;
use crate::parser::property::rich_text::{ParagraphAlignment, ParagraphStyling};
use crate::utils::{px, AttributeSet, StyleSet};
use color_eyre::eyre::ContextCompat;
use color_eyre::Result;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
impl<'a> Renderer<'a> {
pub(crate) fn render_rich_text(&mut self, text: &RichText) -> Result<String> {
let mut content = String::new();
let mut attrs = AttributeSet::new();
let mut style = self.parse_paragraph_styles(text);
if let Some((note_tag_html, note_tag_styles)) = self.render_note_tags(text.note_tags()) {
content.push_str(&note_tag_html);
style.extend(note_tag_styles);
}
content.push_str(&self.parse_content(text)?);
if content.starts_with("http://") || content.starts_with("https://") {
content = format!("<a href=\"{}\">{}</a>", content, content);
}
if style.len() > 0 {
attrs.set("style", style.to_string());
}
match text.paragraph_style().style_id() {
Some(t) if !self.in_list && is_tag(t) => {
Ok(format!("<{} {}>{}</{}>", t, attrs, content, t))
}
_ if style.len() > 0 => Ok(format!("<span style=\"{}\">{}</span>", style, content)),
_ => Ok(content),
}
}
fn parse_content(&mut self, data: &RichText) -> Result<String> {
if !data.embedded_objects().is_empty() {
return Ok(data
.embedded_objects()
.iter()
.map(|object| match object {
EmbeddedObject::Ink(container) => {
self.render_ink(container.ink(), container.bounding_box(), true)
}
EmbeddedObject::InkSpace(space) => {
format!("<span class=\"ink-space\" style=\"padding-left: {}; padding-top: {};\"></span>",
px(space.width()), px(space.height()))
}
EmbeddedObject::InkLineBreak => {
"<span class=\"ink-linebreak\"><br></span>".to_string()
}
})
.collect_vec()
.join(""));
}
let mut indices = data.text_run_indices().to_vec();
let mut styles = data.text_run_formatting().to_vec();
let mut text = data.text().to_string();
if text.is_empty() {
text = "&nbsp;".to_string();
}
// TODO: Maybe this shouldn't be here
// When the this character is at the start of the paragraph it makes
// all the styles to be shifted by minus one.
// A better solution would be to look if there isn't anything wrong with the parser,
// but I haven't found what could be causing this yet.
if text.starts_with("\u{000B}") && !indices.is_empty(){
indices.remove(0);
styles.pop();
}
if indices.is_empty() {
return Ok(fix_newlines(&text));
}
assert!(indices.len() + 1 >= styles.len());
// Split text into parts specified by indices
let mut parts: Vec<String> = vec![];
for i in indices.iter().copied().rev() {
let part = text.chars().skip(i as usize).collect();
text = text.chars().take(i as usize).collect();
parts.push(part);
}
if !indices.is_empty() {
parts.push(text);
}
let mut in_hyperlink = false;
let content = parts
.into_iter()
.rev()
.zip(styles.iter())
.map(|(text, style)| {
if style.hyperlink() {
let text = self.render_hyperlink(text, style, in_hyperlink);
in_hyperlink = true;
text
} else {
in_hyperlink = false;
let style = self.parse_style(style);
if style.len() > 0 {
Ok(format!("<span style=\"{}\">{}</span>", style, text))
} else {
Ok(text)
}
}
})
.collect::<Result<String>>()?;
Ok(fix_newlines(&content))
}
fn render_hyperlink(
&self,
text: String,
style: &ParagraphStyling,
in_hyperlink: bool,
) -> Result<String> {
const HYPERLINK_MARKER: &str = "\u{fddf}HYPERLINK \"";
let style = self.parse_style(style);
if text.starts_with(HYPERLINK_MARKER) {
let url = text
.strip_prefix(HYPERLINK_MARKER)
.wrap_err("Hyperlink has no start marker")?
.strip_suffix('"')
.wrap_err("Hyperlink has no end marker")?;
Ok(format!("<a href=\"{}\" style=\"{}\">", url, style))
} else if in_hyperlink {
Ok(text + "</a>")
} else {
Ok(format!(
"<a href=\"{}\" style=\"{}\">{}</a>",
text, style, text
))
}
}
fn parse_paragraph_styles(&self, text: &RichText) -> StyleSet {
if !text.embedded_objects().is_empty() {
assert_eq!(
text.text(),
"",
"paragraph with text and embedded objects is not supported"
);
return StyleSet::new();
}
let mut styles = self.parse_style(text.paragraph_style());
if let [style] = text.text_run_formatting() {
styles.extend(self.parse_style(style))
}
if text.paragraph_space_before() > 0.0 {
styles.set("padding-top", px(text.paragraph_space_before()))
}
if text.paragraph_space_after() > 0.0 {
styles.set("padding-bottom", px(text.paragraph_space_after()))
}
if let Some(line_spacing) = text.paragraph_line_spacing_exact() {
styles.set(
"line-height",
((line_spacing as f32) * 50.0).floor().to_string() + "pt",
);
// TODO: why not implemented?
// if line_spacing > 0.0 {
// dbg!(text);
// unimplemented!();
// }
}
match text.paragraph_alignment() {
ParagraphAlignment::Center => styles.set("text-align", "center".to_string()),
ParagraphAlignment::Right => styles.set("text-align", "right".to_string()),
_ => {}
}
styles
}
fn parse_style(&self, style: &ParagraphStyling) -> StyleSet {
let mut styles = StyleSet::new();
if style.bold() {
styles.set("font-weight", "bold".to_string());
}
if style.italic() {
styles.set("font-style", "italic".to_string());
}
if style.underline() {
styles.set("text-decoration", "underline".to_string());
}
if style.superscript() {
styles.set("vertical-align", "super".to_string());
}
if style.subscript() {
styles.set("vertical-align", "sub".to_string());
}
if style.strikethrough() {
styles.set("text-decoration", "line-through".to_string());
}
if let Some(font) = style.font() {
styles.set("font-family", font.to_string());
}
if let Some(size) = style.font_size() {
styles.set("font-size", ((size as f32) / 2.0).to_string() + "pt");
}
if let Some(ColorRef::Manual { r, g, b }) = style.font_color() {
styles.set("color", format!("rgb({},{},{})", r, g, b));
}
if let Some(ColorRef::Manual { r, g, b }) = style.highlight() {
styles.set("background-color", format!("rgb({},{},{})", r, g, b));
}
if style.paragraph_alignment().is_some() {
unimplemented!()
}
if let Some(space) = style.paragraph_space_before() {
if space != 0.0 {
unimplemented!()
}
}
if let Some(space) = style.paragraph_space_after() {
if space != 0.0 {
unimplemented!()
}
}
if let Some(space) = style.paragraph_line_spacing_exact() {
if space != 0.0 {
unimplemented!()
}
if let Some(size) = style.font_size() {
styles.set(
"line-height",
format!("{}px", (size as f32 * 1.2 / 72.0 * 48.0).floor()),
)
}
}
if style.math_formatting() {
// FIXME: Handle math formatting
// See https://docs.microsoft.com/en-us/windows/win32/api/richedit/ns-richedit-gettextex
// for unicode chars used
// unimplemented!()
}
styles
}
}
fn is_tag(tag: &str) -> bool {
!matches!(tag, "PageDateTime" | "PageTitle")
}
fn fix_newlines(text: &str) -> String {
static REGEX_LEADING_SPACES: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<br>(\s+)").expect("failed to compile regex"));
let text = text
.replace("\u{000b}", "<br>")
.replace("\n", "<br>")
.replace("\r", "<br>");
REGEX_LEADING_SPACES
.replace_all(&text, |captures: &Captures| {
"<br>".to_string() + &"&nbsp;".repeat(captures[1].len())
})
.to_string()
}

View File

@ -0,0 +1,121 @@
use crate::page::Renderer;
use crate::parser::contents::{OutlineElement, Table, TableCell};
use crate::utils::{px, AttributeSet, StyleSet};
use color_eyre::Result;
impl<'a> Renderer<'a> {
pub(crate) fn render_table(&mut self, table: &Table) -> Result<String> {
let mut content = String::new();
let mut styles = StyleSet::new();
styles.set("border-collapse", "collapse".to_string());
if table.borders_visible() {
styles.set("border", "1pt solid #A3A3A3".to_string());
}
let mut attributes = AttributeSet::new();
attributes.set("style", styles.to_string());
attributes.set("cellspacing", "0".to_string());
attributes.set("cellpadding", "0".to_string());
if table.borders_visible() {
attributes.set("border", "1".to_string());
}
content.push_str(&format!("<table {}>", attributes.to_string()));
let locked_cols = calc_locked_cols(table.cols_locked(), table.cols());
let mut col_widths = table.col_widths().to_vec();
col_widths.extend(vec![0.0; table.cols() as usize - col_widths.len()].into_iter());
let col_widths = &*col_widths;
for row in table.contents() {
content.push_str("<tr>");
assert_eq!(row.contents().len(), col_widths.len());
let cells = row
.contents()
.iter()
.zip(col_widths.iter().copied())
.zip(locked_cols.iter().copied())
.map(|((cell, width), locked)| {
if locked {
(cell, Some(width))
} else {
(cell, None)
}
});
for (cell, width) in cells {
self.render_table_cell(&mut content, cell, width)?;
}
content.push_str("</tr>");
}
content.push_str("</table>");
Ok(self.render_with_note_tags(table.note_tags(), content))
}
fn render_table_cell(
&mut self,
contents: &mut String,
cell: &TableCell,
width: Option<f32>,
) -> Result<()> {
let mut styles = StyleSet::new();
styles.set("padding", "2pt".to_string());
styles.set("vertical-align", "top".to_string());
styles.set("min-width", px(1.0));
if let Some(width) = width {
styles.set("width", px(width));
}
if let Some(color) = cell.background_color() {
styles.set(
"background",
format!("rgb({}, {}, {})", color.r(), color.g(), color.b()),
)
}
let mut attrs = AttributeSet::new();
attrs.set("style", styles.to_string());
contents.push_str(&format!("<td {}>", attrs.to_string()));
let cell_level = self.table_cell_level(cell.contents());
let elements = cell.contents().iter().map(|el| (el, 0, cell_level));
contents.push_str(&self.render_list(elements, cell.outline_indent_distance().value())?);
contents.push_str("</td>");
Ok(())
}
fn table_cell_level(&self, elements: &[OutlineElement]) -> u8 {
let needs_nesting = elements
.iter()
.any(|element| self.is_list(element) || self.has_note_tag(element));
if needs_nesting {
2
} else {
1
}
}
}
fn calc_locked_cols(data: &[u8], count: u32) -> Vec<bool> {
if data.is_empty() {
return vec![false; count as usize];
}
(0..count)
.map(|i| data[i as usize / 8] & (1 << (i % 8)) == 1)
.collect()
}

View File

@ -0,0 +1,123 @@
//! OneNote parsing error handling.
use std::borrow::Cow;
use std::{io, string};
use thiserror::Error;
/// The result of parsing a OneNote file.
pub type Result<T> = std::result::Result<T, Error>;
/// A parsing error.
///
/// If the crate is compiled with the `backtrace` feature enabled, the
/// parsing error struct will contain a backtrace of the location where
/// the error occured. The backtrace can be accessed using
/// [`std::error::Error::backtrace()`].
#[derive(Error, Debug)]
#[error("{kind}")]
pub struct Error {
kind: ErrorKind,
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Self {
Error { kind }
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
ErrorKind::from(err).into()
}
}
impl From<std::string::FromUtf16Error> for Error {
fn from(err: std::string::FromUtf16Error) -> Self {
ErrorKind::from(err).into()
}
}
impl From<widestring::error::MissingNulTerminator> for Error {
fn from(err: widestring::error::MissingNulTerminator) -> Self {
ErrorKind::from(err).into()
}
}
impl From<uuid::Error> for Error {
fn from(err: uuid::Error) -> Self {
ErrorKind::from(err).into()
}
}
/// Details about a parsing error
#[allow(missing_docs)]
#[derive(Error, Debug)]
pub enum ErrorKind {
/// Hit the end of the OneNote file before it was expected.
#[error("Unexpected end of file")]
UnexpectedEof,
/// The parser was asked to process a table-of-contents file that turned out not to be one.
#[error("Not a table of contents file: {file}")]
NotATocFile { file: String },
/// The parser was asked to process a section file that turned out not to be one.
#[error("Not a section file: {file}")]
NotASectionFile { file: String },
/// When parsing a section group the table-of-contents file for this group was found to be missing.
#[error("Table of contents file is missing in dir {dir}")]
TocFileMissing { dir: String },
/// Malformed data was encountered when parsing the OneNote file.
#[error("Malformed data: {0}")]
MalformedData(Cow<'static, str>),
/// Malformed data was encountered when parsing the OneNote data.
#[error("Malformed OneNote data: {0}")]
MalformedOneNoteData(Cow<'static, str>),
/// Malformed data was encountered when parsing the OneNote file contents.
#[error("Malformed OneNote file data: {0}")]
MalformedOneNoteFileData(Cow<'static, str>),
/// Malformed data was encountered when parsing the OneNote file contents.
#[error("Malformed OneNote incorrect type: {0}")]
MalformedOneNoteIncorrectType(String),
/// Malformed data was encountered when parsing the OneStore data.
#[error("Malformed OneStore data: {0}")]
MalformedOneStoreData(Cow<'static, str>),
/// Malformed data was encountered when parsing the FSSHTTPB data.
#[error("Malformed FSSHTTPB data: {0}")]
MalformedFssHttpBData(Cow<'static, str>),
/// A malformed UUID was encountered
#[error("Invalid UUID: {err}")]
InvalidUuid {
#[from]
err: uuid::Error,
},
/// An I/O failure was encountered during parsing.
#[error("I/O failure: {err}")]
IO {
#[from]
err: io::Error,
},
/// A malformed UTF-16 string was encountered during parsing.
#[error("Malformed UTF-16 string: {err}")]
Utf16Error {
#[from]
err: string::FromUtf16Error,
},
/// A UTF-16 string without a null terminator was encountered during parsing.
#[error("UTF-16 string is missing null terminator: {err}")]
Utf16MissingNull {
#[from]
err: widestring::error::MissingNulTerminator,
},
}

View File

@ -0,0 +1,23 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::Reader;
/// A byte array with the length determined by a `CompactU64`.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.3].
///
/// [\[MS-FSSHTTPB\] 2.2.1.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/6bdda105-af7f-4757-8dbe-0c7f3100647e
pub(crate) struct BinaryItem(Vec<u8>);
impl BinaryItem {
pub(crate) fn parse(reader: Reader) -> Result<BinaryItem> {
let size = CompactU64::parse(reader)?.value();
let data = reader.read(size as usize)?.to_vec();
Ok(BinaryItem(data))
}
pub(crate) fn value(self) -> Vec<u8> {
self.0
}
}

View File

@ -0,0 +1,33 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::Reader;
/// A FSSHTTP cell identifier.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.10] and [\[MS-FSSHTTPB\] 2.2.1.11].
///
/// [\[MS-FSSHTTPB\] 2.2.1.10]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/75bf8297-ef9c-458a-95a3-ad6265bfa864
/// [\[MS-FSSHTTPB\] 2.2.1.11]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d3f4d22d-6fb4-4032-8587-f3eb9c256e45
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CellId(pub ExGuid, pub ExGuid);
impl CellId {
pub(crate) fn parse(reader: Reader) -> Result<CellId> {
let first = ExGuid::parse(reader)?;
let second = ExGuid::parse(reader)?;
Ok(CellId(first, second))
}
pub(crate) fn parse_array(reader: Reader) -> Result<Vec<CellId>> {
let mut values = vec![];
let count = CompactU64::parse(reader)?.value();
for _ in 0..count {
values.push(CellId::parse(reader)?);
}
Ok(values)
}
}

View File

@ -0,0 +1,195 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::Reader;
/// A compact unsigned 64-bit integer.
///
/// The first byte encodes the total width of the integer. If the first byte is zero, there is no
/// further data and the integer value is zero. Otherwise the index of the lowest bit with value 1
/// of the first byte indicates the width of the remaining integer data:
/// If the lowest bit is set, the integer data is 1 byte wide; if the second bit is set, the
/// integer data is 2 bytes wide etc.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.1].
///
/// [\[MS-FSSHTTPB\] 2.2.1.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/8eb74ebe-81d1-4569-a29a-308a6128a52f
#[derive(Debug)]
pub(crate) struct CompactU64(u64);
impl CompactU64 {
pub(crate) fn value(&self) -> u64 {
self.0
}
pub(crate) fn parse(reader: Reader) -> Result<CompactU64> {
let bytes = reader.bytes();
let first_byte = bytes.first().copied().ok_or(ErrorKind::UnexpectedEof)?;
if first_byte == 0 {
reader.advance(1)?;
return Ok(CompactU64(0));
}
if first_byte & 1 != 0 {
return Ok(CompactU64((reader.get_u8()? >> 1) as u64));
}
if first_byte & 2 != 0 {
return Ok(CompactU64((reader.get_u16()? >> 2) as u64));
}
if first_byte & 4 != 0 {
if reader.remaining() < 3 {
return Err(ErrorKind::UnexpectedEof.into());
}
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], 0]);
reader.advance(3)?;
return Ok(CompactU64((value >> 3) as u64));
}
if first_byte & 8 != 0 {
if reader.remaining() < 4 {
return Err(ErrorKind::UnexpectedEof.into());
}
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
reader.advance(4)?;
return Ok(CompactU64((value >> 4) as u64));
}
if first_byte & 16 != 0 {
if reader.remaining() < 5 {
return Err(ErrorKind::UnexpectedEof.into());
}
let value =
u64::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], 0, 0, 0]);
reader.advance(5)?;
return Ok(CompactU64(value >> 5));
}
if first_byte & 32 != 0 {
if reader.remaining() < 6 {
return Err(ErrorKind::UnexpectedEof.into());
}
let value = u64::from_le_bytes([
first_byte, bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], 0, 0,
]);
reader.advance(6)?;
return Ok(CompactU64(value >> 6));
}
if first_byte & 64 != 0 {
if reader.remaining() < 7 {
return Err(ErrorKind::UnexpectedEof.into());
}
let value = u64::from_le_bytes([
first_byte, bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], 0,
]);
reader.advance(7)?;
return Ok(CompactU64(value >> 7));
}
if first_byte & 128 != 0 {
reader.advance(1)?;
return Ok(CompactU64(reader.get_u64()?));
}
panic!("unexpected compact u64 type: {:x}", first_byte)
}
}
#[cfg(test)]
mod test {
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::reader::Reader;
#[test]
fn test_zero() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_7_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_14_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_21_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0xd4u8, 0x8b, 0x10]))
.unwrap()
.value(),
135546
);
}
#[test]
fn test_28_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_35_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_42_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_49_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
#[test]
fn test_64_bit() {
assert_eq!(
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
0
);
}
}

View File

@ -0,0 +1,118 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::shared::guid::Guid;
use crate::parser::Reader;
use std::fmt;
/// A variable-width encoding of an extended GUID (GUID + 32 bit value)
///
/// See [\[MS-FSSHTTPB\] 2.2.1.7].
///
/// [\[MS-FSSHTTPB\] 2.2.1.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/bff58e9f-8222-4fbb-b112-5826d5febedd
#[derive(Clone, Copy, PartialEq, Hash, Eq)]
pub struct ExGuid {
pub guid: Guid,
pub value: u32,
}
impl ExGuid {
pub fn fallback() -> ExGuid {
return ExGuid {
guid: Guid::nil(),
value: 0,
};
}
pub(crate) fn is_nil(&self) -> bool {
self.guid.is_nil() && self.value == 0
}
pub(crate) fn as_option(&self) -> Option<ExGuid> {
if self.is_nil() {
None
} else {
Some(*self)
}
}
pub(crate) fn from_guid(guid: Guid, value: u32) -> ExGuid {
ExGuid { guid, value }
}
pub(crate) fn parse(reader: Reader) -> Result<ExGuid> {
let data = reader.get_u8()?;
// A null ExGuid ([FSSHTTPB] 2.2.1.7.1)
if data == 0 {
return Ok(ExGuid {
guid: Guid::nil(),
value: 0,
});
}
// A ExGuid with a 5 bit value ([FSSHTTPB] 2.2.1.7.2)
if data & 0b111 == 4 {
return Ok(ExGuid {
guid: Guid::parse(reader)?,
value: (data >> 3) as u32,
});
}
// A ExGuid with a 10 bit value ([FSSHTTPB] 2.2.1.7.3)
if data & 0b111111 == 32 {
let value = (reader.get_u8()? as u16) << 2 | (data >> 6) as u16;
return Ok(ExGuid {
guid: Guid::parse(reader)?,
value: value as u32,
});
}
// A ExGuid with a 17 bit value ([FSSHTTPB] 2.2.1.7.4)
if data & 0b1111111 == 64 {
let value = (reader.get_u16()? as u32) << 1 | (data >> 7) as u32;
return Ok(ExGuid {
guid: Guid::parse(reader)?,
value,
});
}
// A ExGuid with a 32 bit value ([FSSHTTPB] 2.2.1.7.5)
if data == 128 {
let value = reader.get_u32()?;
return Ok(ExGuid {
guid: Guid::parse(reader)?,
value,
});
}
Err(
ErrorKind::MalformedData(format!("unexpected ExGuid first byte: {:b}", data).into())
.into(),
)
}
/// Parse an array of `ExGuid` values.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.8]
///
/// [\[MS-FSSHTTPB\] 2.2.1.8]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/10d6fb35-d630-4ae3-b530-b9e877fc27d3
pub(crate) fn parse_array(reader: Reader) -> Result<Vec<ExGuid>> {
let mut values = vec![];
let count = CompactU64::parse(reader)?.value();
for _ in 0..count {
values.push(ExGuid::parse(reader)?);
}
Ok(values)
}
}
impl fmt::Debug for ExGuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ExGuid {{{}, {}}}", self.guid, self.value)
}
}

View File

@ -0,0 +1,7 @@
pub(crate) mod binary_item;
pub(crate) mod cell_id;
pub(crate) mod compact_u64;
pub(crate) mod exguid;
pub(crate) mod object_types;
pub(crate) mod serial_number;
pub(crate) mod stream_object;

View File

@ -0,0 +1,51 @@
use enum_primitive_derive::Primitive;
use num_traits::ToPrimitive;
use std::fmt;
/// Stream object types.
///
/// While the FSSHTTPB protocol specified more object types than listed here, we only need a limited
/// number of them to parse OneNote files stored in FSSHTTPB format.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5.1] and [\[MS-FSSHTTPB\] 2.2.1.5.2].
///
/// [\[MS-FSSHTTPB\] 2.2.1.5.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a1017f48-a888-49ff-b71d-cc3c707f753a
/// [\[MS-FSSHTTPB\] 2.2.1.5.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ac629d63-60a1-49b2-9db2-fa3c19971cc9
#[derive(Debug, Primitive, PartialEq)]
pub enum ObjectType {
CellManifest = 0x0B,
DataElement = 0x01,
DataElementFragment = 0x06A,
DataElementPackage = 0x15,
ObjectDataBlob = 0x02,
ObjectGroupBlobReference = 0x1C,
ObjectGroupData = 0x1E,
ObjectGroupDataBlob = 0x05,
ObjectGroupDataExcluded = 0x03,
ObjectGroupDataObject = 0x16,
ObjectGroupDeclaration = 0x1D,
ObjectGroupMetadata = 0x078,
ObjectGroupMetadataBlock = 0x79,
ObjectGroupObject = 0x18,
/// An indicator that the object contains a OneNote packing object.
///
/// See [\[MS-ONESTORE\] 2.8.1] (look for _Packaging Start_)
///
/// [\[MS-ONESTORE\] 2.8.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/a2f046ea-109a-49c4-912d-dc2888cf0565
OneNotePackaging = 0x7a,
RevisionManifest = 0x1A,
RevisionManifestGroupReference = 0x19,
RevisionManifestRoot = 0x0A,
StorageIndexCellMapping = 0x0E,
StorageIndexManifestMapping = 0x11,
StorageIndexRevisionMapping = 0x0D,
StorageManifest = 0x0C,
StorageManifestRoot = 0x07,
}
impl fmt::LowerHex for ObjectType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.to_u64().unwrap();
fmt::LowerHex::fmt(&value, f)
}
}

View File

@ -0,0 +1,35 @@
use crate::parser::errors::Result;
use crate::parser::shared::guid::Guid;
use crate::parser::Reader;
/// A variable-width serial number.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.9].
///
/// [\[MS-FSSHTTPB\] 2.2.1.9]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9db15fa4-0dc2-4b17-b091-d33886d8a0f6
#[derive(Debug)]
#[allow(dead_code)]
pub struct SerialNumber {
pub guid: Guid,
pub serial: u64,
}
impl SerialNumber {
pub(crate) fn parse(reader: Reader) -> Result<SerialNumber> {
let serial_type = reader.get_u8()?;
// A null-value ([FSSHTTPB] 2.2.1.9.1)
if serial_type == 0 {
return Ok(SerialNumber {
guid: Guid::nil(),
serial: 0,
});
}
// A serial number with a 64 bit value ([FSSHTTPB] 2.2.1.9.2)
let guid = Guid::parse(reader)?;
let serial = reader.get_u64()?;
Ok(SerialNumber { guid, serial })
}
}

View File

@ -0,0 +1,234 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::Reader;
use num_traits::{FromPrimitive, ToPrimitive};
/// A FSSHTTPB stream object header.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5].
///
/// [\[MS-FSSHTTPB\] 2.2.1.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/5faee10f-8e55-43f8-935a-d6e4294856fc
#[derive(Debug)]
#[allow(dead_code)]
pub struct ObjectHeader {
pub compound: bool,
pub object_type: ObjectType,
pub length: u64,
}
impl ObjectHeader {
pub(crate) fn try_parse(reader: Reader, object_type: ObjectType) -> Result<()> {
Self::try_parse_start(reader, object_type, Self::parse)
}
/// Parse a 16-bit or 32-bit stream object header.
pub(crate) fn parse(reader: Reader) -> Result<ObjectHeader> {
let header_type = reader.bytes().first().ok_or(ErrorKind::UnexpectedEof)?;
match header_type & 0b11 {
0x0 => Self::parse_16(reader),
0x2 => Self::parse_32(reader),
_ => Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object header type: {:x}", header_type).into(),
)
.into()),
}
}
pub(crate) fn try_parse_16(reader: Reader, object_type: ObjectType) -> Result<()> {
Self::try_parse_start(reader, object_type, Self::parse_16)
}
/// Parse a 16 bit stream object header.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5.1]
///
/// [\[MS-FSSHTTPB\] 2.2.1.5.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a1017f48-a888-49ff-b71d-cc3c707f753a
pub(crate) fn parse_16(reader: Reader) -> Result<ObjectHeader> {
let data = reader.get_u16()?;
let header_type = data & 0b11;
if header_type != 0x0 {
return Err(ErrorKind::MalformedFssHttpBData(
format!(
"unexpected object header type for 16 bit header: 0x{:x}",
header_type
)
.into(),
)
.into());
}
let compound = data & 0x4 == 0x4;
let object_type_value = (data >> 3) & 0x3f;
let object_type = if let Some(object_type) = ObjectType::from_u16(object_type_value) {
object_type
} else {
return Err(ErrorKind::MalformedFssHttpBData(
format!("invalid object type: 0x{:x}", object_type_value).into(),
)
.into());
};
let length = (data >> 9) as u64;
Ok(ObjectHeader {
compound,
object_type,
length,
})
}
pub(crate) fn try_parse_32(reader: Reader, object_type: ObjectType) -> Result<()> {
Self::try_parse_start(reader, object_type, Self::parse_32)
}
/// Parse a 32 bit stream object header.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5.2]
///
/// [\[MS-FSSHTTPB\] 2.2.1.5.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ac629d63-60a1-49b2-9db2-fa3c19971cc9
fn parse_32(reader: Reader) -> Result<ObjectHeader> {
let data = reader.get_u32()?;
let header_type = data & 0b11;
if header_type != 0x2 {
return Err(ErrorKind::MalformedFssHttpBData(
format!(
"unexpected object header type for 32 bit header: 0x{:x}",
header_type
)
.into(),
)
.into());
}
let compound = data & 0x4 == 0x4;
let object_type_value = (data >> 3) & 0x3fff;
let object_type = if let Some(object_type) = ObjectType::from_u32(object_type_value) {
object_type
} else {
return Err(ErrorKind::MalformedFssHttpBData(
format!("invalid object type: 0x{:x}", object_type_value).into(),
)
.into());
};
let mut length = (data >> 17) as u64;
if length == 0x7fff {
length = CompactU64::parse(reader)?.value();
}
Ok(ObjectHeader {
compound,
object_type,
length,
})
}
pub(crate) fn try_parse_end_16(reader: Reader, object_type: ObjectType) -> Result<()> {
Self::try_parse_end(reader, object_type, Self::parse_end_16)
}
/// Parse a 16-bit stream object header end.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5.4]
///
/// [\[MS-FSSHTTPB\] 2.2.1.5.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d8cedbb8-073b-4711-8867-f88b887ab0a9
fn parse_end_16(reader: Reader) -> Result<ObjectType> {
let data = reader.get_u16()?;
let header_type = data & 0b11;
if header_type != 0x3 {
return Err(ErrorKind::MalformedFssHttpBData(
format!(
"unexpected object header type for 16 bit end header: {:x}",
header_type
)
.into(),
)
.into());
}
let object_type_value = data >> 2;
if let Some(object_type) = ObjectType::from_u16(object_type_value) {
Ok(object_type)
} else {
Err(ErrorKind::MalformedFssHttpBData(
format!("invalid object type: 0x{:x}", object_type_value).into(),
)
.into())
}
}
pub(crate) fn try_parse_end_8(reader: Reader, object_type: ObjectType) -> Result<()> {
Self::try_parse_end(reader, object_type, Self::parse_end_8)
}
/// Parse a 8-bit stream object header end.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.5.3]
///
/// [\[MS-FSSHTTPB\] 2.2.1.5.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/544ce81a-44e3-48ff-b094-0e51c7207aa1
fn parse_end_8(reader: Reader) -> Result<ObjectType> {
let data = reader.get_u8()?;
let header_type = data & 0b11;
if header_type != 0x1 {
return Err(ErrorKind::MalformedFssHttpBData(
format!(
"unexpected object header type for 8 bit end header: {:x}",
header_type
)
.into(),
)
.into());
}
let object_type_value = data >> 2;
if let Some(object_type) = ObjectType::from_u8(object_type_value) {
Ok(object_type)
} else {
Err(ErrorKind::MalformedFssHttpBData(
format!("invalid object type: 0x{:x}", object_type_value).into(),
)
.into())
}
}
pub(crate) fn has_end_8(reader: Reader, object_type: ObjectType) -> Result<bool> {
let data = reader.bytes().first().ok_or(ErrorKind::UnexpectedEof)?;
Ok(data & 0b11 == 0x1 && data >> 2 == object_type.to_u8().unwrap())
}
fn try_parse_start(
reader: Reader,
object_type: ObjectType,
parse: fn(Reader) -> Result<ObjectHeader>,
) -> Result<()> {
match parse(reader) {
Ok(header) if header.object_type == object_type => Ok(()),
Ok(header) => Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", header.object_type).into(),
)
.into()),
Err(e) => Err(e),
}
}
fn try_parse_end(
reader: Reader,
object_type: ObjectType,
parse: fn(Reader) -> Result<ObjectType>,
) -> Result<()> {
match parse(reader) {
Ok(header) if header == object_type => Ok(()),
Ok(header) => Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", header).into(),
)
.into()),
Err(e) => Err(e),
}
}
}

View File

@ -0,0 +1,23 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
impl DataElement {
/// Parse a cell manifest.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.4]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/541f7f92-ee5d-407e-9ece-fb1b35832a10
pub(crate) fn parse_cell_manifest(reader: Reader) -> Result<ExGuid> {
ObjectHeader::try_parse_16(reader, ObjectType::CellManifest)?;
let id = ExGuid::parse(reader)?;
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
Ok(id)
}
}

View File

@ -0,0 +1,56 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
/// A data element fragment.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.7].
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9a860e3b-cf61-484b-8ee3-d875afaf7a05
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct DataElementFragment {
pub(crate) id: ExGuid,
pub(crate) size: u64,
pub(crate) chunk_reference: DataElementFragmentChunkReference,
pub(crate) data: Vec<u8>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct DataElementFragmentChunkReference {
pub(crate) offset: u64,
pub(crate) length: u64,
}
impl DataElement {
/// Parse a data element fragment.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.7]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9a860e3b-cf61-484b-8ee3-d875afaf7a05
pub(crate) fn parse_data_element_fragment(reader: Reader) -> Result<DataElementFragment> {
ObjectHeader::try_parse(reader, ObjectType::DataElementFragment)?;
let id = ExGuid::parse(reader)?;
let size = CompactU64::parse(reader)?.value();
let offset = CompactU64::parse(reader)?.value();
let length = CompactU64::parse(reader)?.value();
let data = reader.read(size as usize)?.to_vec();
let chunk_reference = DataElementFragmentChunkReference { offset, length };
let fragment = DataElementFragment {
id,
size,
chunk_reference,
data,
};
Ok(fragment)
}
}

View File

@ -0,0 +1,196 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::serial_number::SerialNumber;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::data_element_fragment::DataElementFragment;
use crate::parser::fsshttpb::data_element::object_data_blob::ObjectDataBlob;
use crate::parser::fsshttpb::data_element::object_group::ObjectGroup;
use crate::parser::fsshttpb::data_element::revision_manifest::RevisionManifest;
use crate::parser::fsshttpb::data_element::storage_index::StorageIndex;
use crate::parser::fsshttpb::data_element::storage_manifest::StorageManifest;
use crate::parser::Reader;
use std::collections::HashMap;
use std::fmt::Debug;
pub(crate) mod cell_manifest;
pub(crate) mod data_element_fragment;
pub(crate) mod object_data_blob;
pub(crate) mod object_group;
pub(crate) mod revision_manifest;
pub(crate) mod storage_index;
pub(crate) mod storage_manifest;
/// A FSSHTTPB data element package.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12].
///
/// [\[MS-FSSHTTPB\] 2.2.1.12]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/99a25464-99b5-4262-a964-baabed2170eb
#[derive(Debug)]
pub(crate) struct DataElementPackage {
pub(crate) storage_indexes: HashMap<ExGuid, StorageIndex>,
pub(crate) storage_manifests: HashMap<ExGuid, StorageManifest>,
pub(crate) cell_manifests: HashMap<ExGuid, ExGuid>,
pub(crate) revision_manifests: HashMap<ExGuid, RevisionManifest>,
pub(crate) object_groups: HashMap<ExGuid, ObjectGroup>,
pub(crate) data_element_fragments: HashMap<ExGuid, DataElementFragment>,
pub(crate) object_data_blobs: HashMap<ExGuid, ObjectDataBlob>,
}
impl DataElementPackage {
pub(crate) fn parse(reader: Reader) -> Result<DataElementPackage> {
ObjectHeader::try_parse_16(reader, ObjectType::DataElementPackage)?;
if reader.get_u8()? != 0 {
return Err(ErrorKind::MalformedFssHttpBData("invalid padding byte".into()).into());
}
let mut package = DataElementPackage {
storage_indexes: Default::default(),
storage_manifests: Default::default(),
cell_manifests: Default::default(),
revision_manifests: Default::default(),
object_groups: Default::default(),
data_element_fragments: Default::default(),
object_data_blobs: Default::default(),
};
loop {
if ObjectHeader::has_end_8(reader, ObjectType::DataElementPackage)? {
break;
}
DataElement::parse(reader, &mut package)?
}
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElementPackage)?;
Ok(package)
}
/// Look up the object groups referenced by a cell.
pub(crate) fn find_objects(
&self,
cell: ExGuid,
storage_index: &StorageIndex,
) -> Result<Vec<&ObjectGroup>> {
let revision_id = self
.find_cell_revision_id(cell)
.ok_or_else(|| ErrorKind::MalformedFssHttpBData("cell revision id not found".into()))?;
let revision_mapping_id = storage_index
.find_revision_mapping_id(revision_id)
.ok_or_else(|| {
ErrorKind::MalformedFssHttpBData("revision mapping id not found".into())
})?;
let revision_manifest = self
.find_revision_manifest(revision_mapping_id)
.ok_or_else(|| {
ErrorKind::MalformedFssHttpBData("revision manifest not found".into())
})?;
revision_manifest
.group_references
.iter()
.map(|reference| {
self.find_object_group(*reference).ok_or_else(|| {
ErrorKind::MalformedFssHttpBData("object group not found".into()).into()
})
})
.collect::<Result<_>>()
}
/// Look up a blob by its ID.
pub(crate) fn find_blob(&self, id: ExGuid) -> Option<&[u8]> {
self.object_data_blobs.get(&id).map(|blob| blob.value())
}
/// Find the first storage index.
pub(crate) fn find_storage_index(&self) -> Option<&StorageIndex> {
self.storage_indexes.values().next()
}
/// Find the first storage manifest.
pub(crate) fn find_storage_manifest(&self) -> Option<&StorageManifest> {
self.storage_manifests.values().next()
}
/// Look up a cell revision ID by the cell's manifest ID.
pub(crate) fn find_cell_revision_id(&self, id: ExGuid) -> Option<ExGuid> {
self.cell_manifests.get(&id).copied()
}
/// Look up a revision manifest by its ID.
pub(crate) fn find_revision_manifest(&self, id: ExGuid) -> Option<&RevisionManifest> {
self.revision_manifests.get(&id)
}
/// Look up an object group by its ID.
pub(crate) fn find_object_group(&self, id: ExGuid) -> Option<&ObjectGroup> {
self.object_groups.get(&id)
}
}
/// A parser for a single data element.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.1]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f0901ac0-4f26-413f-805b-a6830781f64c
#[derive(Debug)]
pub(crate) struct DataElement;
impl DataElement {
pub(crate) fn parse(reader: Reader, package: &mut DataElementPackage) -> Result<()> {
ObjectHeader::try_parse_16(reader, ObjectType::DataElement)?;
let id = ExGuid::parse(reader)?;
let _serial = SerialNumber::parse(reader)?;
let element_type = CompactU64::parse(reader)?;
match element_type.value() {
0x01 => {
package
.storage_indexes
.insert(id, Self::parse_storage_index(reader)?);
}
0x02 => {
package
.storage_manifests
.insert(id, Self::parse_storage_manifest(reader)?);
}
0x03 => {
package
.cell_manifests
.insert(id, Self::parse_cell_manifest(reader)?);
}
0x04 => {
package
.revision_manifests
.insert(id, Self::parse_revision_manifest(reader)?);
}
0x05 => {
package
.object_groups
.insert(id, Self::parse_object_group(reader)?);
}
0x06 => {
package
.data_element_fragments
.insert(id, Self::parse_data_element_fragment(reader)?);
}
0x0A => {
package
.object_data_blobs
.insert(id, Self::parse_object_data_blob(reader)?);
}
x => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("invalid element type: 0x{:X}", x).into(),
)
.into())
}
}
Ok(())
}
}

View File

@ -0,0 +1,38 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::binary_item::BinaryItem;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
use std::fmt;
/// An object data blob.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.8]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.8]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d36dd2b4-bad1-441b-93c7-adbe3069152c
pub(crate) struct ObjectDataBlob(Vec<u8>);
impl ObjectDataBlob {
pub(crate) fn value(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for ObjectDataBlob {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ObjectDataBlob({} bytes)", self.0.len())
}
}
impl DataElement {
pub(crate) fn parse_object_data_blob(reader: Reader) -> Result<ObjectDataBlob> {
ObjectHeader::try_parse(reader, ObjectType::ObjectDataBlob)?;
let data = BinaryItem::parse(reader)?;
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
Ok(ObjectDataBlob(data.value()))
}
}

View File

@ -0,0 +1,336 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::binary_item::BinaryItem;
use crate::parser::fsshttpb::data::cell_id::CellId;
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
use std::fmt;
/// An object group.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/21404be6-0334-490e-80b5-82fccb9c04af
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct ObjectGroup {
pub(crate) declarations: Vec<ObjectGroupDeclaration>,
pub(crate) metadata: Vec<ObjectGroupMetadata>,
pub(crate) objects: Vec<ObjectGroupData>,
}
/// An object group declaration.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.1]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ef660e4b-a099-4e76-81f7-ed5c04a70caa
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum ObjectGroupDeclaration {
Object {
object_id: ExGuid,
partition_id: u64,
data_size: u64,
object_reference_count: u64,
cell_reference_count: u64,
},
Blob {
object_id: ExGuid,
blob_id: ExGuid,
partition_id: u64,
object_reference_count: u64,
cell_reference_count: u64,
},
}
impl ObjectGroupDeclaration {
pub(crate) fn partition_id(&self) -> u64 {
match self {
ObjectGroupDeclaration::Object { partition_id, .. } => *partition_id,
ObjectGroupDeclaration::Blob { partition_id, .. } => *partition_id,
}
}
pub(crate) fn object_id(&self) -> ExGuid {
match self {
ObjectGroupDeclaration::Object { object_id, .. } => *object_id,
ObjectGroupDeclaration::Blob { object_id, .. } => *object_id,
}
}
}
/// An object group's metadata.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.3] and [\[MS-FSSHTTPB\] 2.2.1.12.6.3.1]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d35a8e21-e139-455c-a20b-3f47a5d9fb89
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.3.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/507c6b42-2772-4319-b530-8fbbf4d34afd
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct ObjectGroupMetadata {
pub(crate) change_frequency: ObjectChangeFrequency,
}
#[derive(Debug)]
pub(crate) enum ObjectChangeFrequency {
Unknown = 0,
Frequent = 1,
Infrequent = 2,
Independent = 3,
Custom = 4,
}
impl ObjectChangeFrequency {
fn parse(value: u64) -> ObjectChangeFrequency {
match value {
x if x == ObjectChangeFrequency::Unknown as u64 => ObjectChangeFrequency::Unknown,
x if x == ObjectChangeFrequency::Frequent as u64 => ObjectChangeFrequency::Frequent,
x if x == ObjectChangeFrequency::Infrequent as u64 => ObjectChangeFrequency::Infrequent,
x if x == ObjectChangeFrequency::Independent as u64 => {
ObjectChangeFrequency::Independent
}
x if x == ObjectChangeFrequency::Custom as u64 => ObjectChangeFrequency::Custom,
x => panic!("unexpected change frequency: {}", x),
}
}
}
/// An object group's data.
pub(crate) enum ObjectGroupData {
/// An object.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.4]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d542b89c-9e81-4af6-885a-47b2f9c1ce53
Object {
group: Vec<ExGuid>,
cells: Vec<CellId>,
data: Vec<u8>,
},
/// An excluded object.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.4]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d542b89c-9e81-4af6-885a-47b2f9c1ce53
ObjectExcluded {
group: Vec<ExGuid>,
cells: Vec<CellId>,
size: u64,
},
/// A blob reference.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.5]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9f73af5e-bd70-4703-8ec6-1866338f1b91
BlobReference {
objects: Vec<ExGuid>,
cells: Vec<CellId>,
blob: ExGuid,
},
}
struct DebugSize(usize);
impl fmt::Debug for ObjectGroupData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ObjectGroupData::Object { group, cells, data } => f
.debug_struct("Object")
.field("group", group)
.field("cells", cells)
.field("data", &DebugSize(data.len()))
.finish(),
ObjectGroupData::ObjectExcluded { group, cells, size } => f
.debug_struct("ObjectExcluded")
.field("group", group)
.field("cells", cells)
.field("size", size)
.finish(),
ObjectGroupData::BlobReference {
objects,
cells,
blob,
} => f
.debug_struct("ObjectExcluded")
.field("objects", objects)
.field("cells", cells)
.field("blob", blob)
.finish(),
}
}
}
impl fmt::Debug for DebugSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} bytes", self.0)
}
}
impl DataElement {
pub(crate) fn parse_object_group(reader: Reader) -> Result<ObjectGroup> {
let declarations = DataElement::parse_object_group_declarations(reader)?;
let mut metadata = vec![];
let object_header = ObjectHeader::parse(reader)?;
match object_header.object_type {
ObjectType::ObjectGroupMetadataBlock => {
metadata = DataElement::parse_object_group_metadata(reader)?;
// Parse object header for the group data section
let object_header = ObjectHeader::parse(reader)?;
if object_header.object_type != ObjectType::ObjectGroupData {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into());
}
}
ObjectType::ObjectGroupData => {} // Skip, will be parsed below
_ => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into())
}
}
let objects = DataElement::parse_object_group_data(reader)?;
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
Ok(ObjectGroup {
declarations,
metadata,
objects,
})
}
fn parse_object_group_declarations(reader: Reader) -> Result<Vec<ObjectGroupDeclaration>> {
ObjectHeader::try_parse(reader, ObjectType::ObjectGroupDeclaration)?;
let mut declarations = vec![];
loop {
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupDeclaration)? {
break;
}
let object_header = ObjectHeader::parse(reader)?;
match object_header.object_type {
ObjectType::ObjectGroupObject => {
let object_id = ExGuid::parse(reader)?;
let partition_id = CompactU64::parse(reader)?.value();
let data_size = CompactU64::parse(reader)?.value();
let object_reference_count = CompactU64::parse(reader)?.value();
let cell_reference_count = CompactU64::parse(reader)?.value();
declarations.push(ObjectGroupDeclaration::Object {
object_id,
partition_id,
data_size,
object_reference_count,
cell_reference_count,
})
}
ObjectType::ObjectGroupDataBlob => {
let object_id = ExGuid::parse(reader)?;
let blob_id = ExGuid::parse(reader)?;
let partition_id = CompactU64::parse(reader)?.value();
let object_reference_count = CompactU64::parse(reader)?.value();
let cell_reference_count = CompactU64::parse(reader)?.value();
declarations.push(ObjectGroupDeclaration::Blob {
object_id,
blob_id,
partition_id,
object_reference_count,
cell_reference_count,
})
}
_ => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into())
}
}
}
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupDeclaration)?;
Ok(declarations)
}
fn parse_object_group_metadata(reader: Reader) -> Result<Vec<ObjectGroupMetadata>> {
let mut declarations = vec![];
loop {
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupMetadataBlock)? {
break;
}
ObjectHeader::try_parse_32(reader, ObjectType::ObjectGroupMetadata)?;
let frequency = CompactU64::parse(reader)?;
declarations.push(ObjectGroupMetadata {
change_frequency: ObjectChangeFrequency::parse(frequency.value()),
})
}
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupMetadataBlock)?;
Ok(declarations)
}
fn parse_object_group_data(reader: Reader) -> Result<Vec<ObjectGroupData>> {
let mut objects = vec![];
loop {
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupData)? {
break;
}
let object_header = ObjectHeader::parse(reader)?;
match object_header.object_type {
ObjectType::ObjectGroupDataExcluded => {
let group = ExGuid::parse_array(reader)?;
let cells = CellId::parse_array(reader)?;
let size = CompactU64::parse(reader)?.value();
objects.push(ObjectGroupData::ObjectExcluded { group, cells, size })
}
ObjectType::ObjectGroupDataObject => {
let group = ExGuid::parse_array(reader)?;
let cells = CellId::parse_array(reader)?;
let data = BinaryItem::parse(reader)?.value();
objects.push(ObjectGroupData::Object { group, cells, data })
}
ObjectType::ObjectGroupBlobReference => {
let references = ExGuid::parse_array(reader)?;
let cells = CellId::parse_array(reader)?;
let blob = ExGuid::parse(reader)?;
objects.push(ObjectGroupData::BlobReference {
objects: references,
cells,
blob,
})
}
_ => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into())
}
}
}
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupData)?;
Ok(objects)
}
}

View File

@ -0,0 +1,81 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
/// A revision manifest.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.5]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/eb3351db-8626-4804-a35b-f3eeda13c74d
#[derive(Debug)]
pub(crate) struct RevisionManifest {
pub(crate) rev_id: ExGuid,
pub(crate) base_rev_id: ExGuid,
pub(crate) root_declare: Vec<RevisionManifestRootDeclare>,
pub(crate) group_references: Vec<ExGuid>,
}
/// A revision manifest root declaration.
#[derive(Debug)]
pub(crate) struct RevisionManifestRootDeclare {
pub(crate) root_id: ExGuid,
pub(crate) object_id: ExGuid,
}
impl RevisionManifestRootDeclare {
fn parse(reader: Reader) -> Result<RevisionManifestRootDeclare> {
let root_id = ExGuid::parse(reader)?;
let object_id = ExGuid::parse(reader)?;
Ok(RevisionManifestRootDeclare { root_id, object_id })
}
}
impl DataElement {
pub(crate) fn parse_revision_manifest(reader: Reader) -> Result<RevisionManifest> {
ObjectHeader::try_parse_16(reader, ObjectType::RevisionManifest)?;
let rev_id = ExGuid::parse(reader)?;
let base_rev_id = ExGuid::parse(reader)?;
let mut root_declare = vec![];
let mut group_references = vec![];
loop {
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
break;
}
let object_header = ObjectHeader::parse_16(reader)?;
match object_header.object_type {
ObjectType::RevisionManifestRoot => {
root_declare.push(RevisionManifestRootDeclare::parse(reader)?)
}
ObjectType::RevisionManifestGroupReference => {
group_references.push(ExGuid::parse(reader)?)
}
_ => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into())
}
}
}
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
let manifest = RevisionManifest {
rev_id,
base_rev_id,
root_declare,
group_references,
};
Ok(manifest)
}
}

View File

@ -0,0 +1,142 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::cell_id::CellId;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::serial_number::SerialNumber;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::Reader;
use std::collections::HashMap;
/// A storage index.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.2]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f5724986-bd0f-488d-9b85-7d5f954d8e9a
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct StorageIndex {
pub(crate) manifest_mappings: Vec<StorageIndexManifestMapping>,
pub(crate) cell_mappings: HashMap<CellId, StorageIndexCellMapping>,
pub(crate) revision_mappings: HashMap<ExGuid, StorageIndexRevisionMapping>,
}
impl StorageIndex {
pub(crate) fn find_cell_mapping_id(&self, cell_id: CellId) -> Option<ExGuid> {
self.cell_mappings.get(&cell_id).map(|mapping| mapping.id)
}
pub(crate) fn find_revision_mapping_id(&self, id: ExGuid) -> Option<ExGuid> {
self.revision_mappings
.get(&id)
.map(|mapping| mapping.revision_mapping)
}
}
/// A storage indexes manifest mapping.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct StorageIndexManifestMapping {
pub(crate) mapping_id: ExGuid,
pub(crate) serial: SerialNumber,
}
/// A storage indexes cell mapping.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct StorageIndexCellMapping {
pub(crate) cell_id: CellId,
pub(crate) id: ExGuid,
pub(crate) serial: SerialNumber,
}
/// A storage indexes revision mapping.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct StorageIndexRevisionMapping {
pub(crate) revision_mapping: ExGuid,
pub(crate) serial: SerialNumber,
}
impl DataElement {
pub(crate) fn parse_storage_index(reader: Reader) -> Result<StorageIndex> {
let mut manifest_mappings = vec![];
let mut cell_mappings = HashMap::new();
let mut revision_mappings = HashMap::new();
loop {
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
break;
}
let object_header = ObjectHeader::parse_16(reader)?;
match object_header.object_type {
ObjectType::StorageIndexManifestMapping => {
manifest_mappings.push(Self::parse_storage_index_manifest_mapping(reader)?)
}
ObjectType::StorageIndexCellMapping => {
let (id, mapping) = Self::parse_storage_index_cell_mapping(reader)?;
cell_mappings.insert(id, mapping);
}
ObjectType::StorageIndexRevisionMapping => {
let (id, mapping) = Self::parse_storage_index_revision_mapping(reader)?;
revision_mappings.insert(id, mapping);
}
_ => {
return Err(ErrorKind::MalformedFssHttpBData(
format!("unexpected object type: {:x}", object_header.object_type).into(),
)
.into())
}
}
}
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
Ok(StorageIndex {
manifest_mappings,
cell_mappings,
revision_mappings,
})
}
fn parse_storage_index_manifest_mapping(reader: Reader) -> Result<StorageIndexManifestMapping> {
let mapping_id = ExGuid::parse(reader)?;
let serial = SerialNumber::parse(reader)?;
Ok(StorageIndexManifestMapping { mapping_id, serial })
}
fn parse_storage_index_cell_mapping(
reader: Reader,
) -> Result<(CellId, StorageIndexCellMapping)> {
let cell_id = CellId::parse(reader)?;
let id = ExGuid::parse(reader)?;
let serial = SerialNumber::parse(reader)?;
let mapping = StorageIndexCellMapping {
cell_id,
id,
serial,
};
Ok((cell_id, mapping))
}
fn parse_storage_index_revision_mapping(
reader: Reader,
) -> Result<(ExGuid, StorageIndexRevisionMapping)> {
let id = ExGuid::parse(reader)?;
let revision_mapping = ExGuid::parse(reader)?;
let serial = SerialNumber::parse(reader)?;
let mapping = StorageIndexRevisionMapping {
revision_mapping,
serial,
};
Ok((id, mapping))
}
}

View File

@ -0,0 +1,47 @@
use crate::parser::errors::Result;
use crate::parser::fsshttpb::data::cell_id::CellId;
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElement;
use crate::parser::shared::guid::Guid;
use crate::parser::Reader;
use std::collections::HashMap;
/// A storage manifest.
///
/// See [\[MS-FSSHTTPB\] 2.2.1.12.3]
///
/// [\[MS-FSSHTTPB\] 2.2.1.12.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a681199b-45f3-4378-b929-fb13e674ac5c
#[derive(Debug)]
pub(crate) struct StorageManifest {
pub(crate) id: Guid,
pub(crate) roots: HashMap<ExGuid, CellId>,
}
impl DataElement {
pub(crate) fn parse_storage_manifest(reader: Reader) -> Result<StorageManifest> {
ObjectHeader::try_parse_16(reader, ObjectType::StorageManifest)?;
let id = Guid::parse(reader)?;
let mut roots = HashMap::new();
loop {
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
break;
}
ObjectHeader::try_parse_16(reader, ObjectType::StorageManifestRoot)?;
let root_manifest = ExGuid::parse(reader)?;
let cell = CellId::parse(reader)?;
roots.insert(root_manifest, cell);
}
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
Ok(StorageManifest { id, roots })
}
}

View File

@ -0,0 +1,12 @@
//! The FSSHTTP binary packaging format.
//!
//! This is the lowest level of the OneNote file format as the FSSHTTPB format specifies how
//! objects and revisions are stored in a binary file.
//!
//! See [\[MS-FSSHTTPB\]]
//!
//! [\[MS-FSSHTTPB\]]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f59fc37d-2232-4b14-baac-25f98e9e7b5a
pub(crate) mod data;
pub(crate) mod data_element;
pub(crate) mod packaging;

View File

@ -0,0 +1,62 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::fsshttpb::data::object_types::ObjectType;
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
use crate::parser::fsshttpb::data_element::DataElementPackage;
use crate::parser::shared::guid::Guid;
use crate::parser::Reader;
/// A OneNote file packaged in FSSHTTPB format.
///
/// See [\[MS-ONESTORE\] 2.8.1]
///
/// [\[MS-ONESTORE\] 2.8.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/a2f046ea-109a-49c4-912d-dc2888cf0565
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct OneStorePackaging {
pub(crate) file_type: Guid,
pub(crate) file: Guid,
pub(crate) legacy_file_version: Guid,
pub(crate) file_format: Guid,
pub(crate) storage_index: ExGuid,
pub(crate) cell_schema: Guid,
pub(crate) data_element_package: DataElementPackage,
}
impl OneStorePackaging {
pub(crate) fn parse(reader: Reader) -> Result<OneStorePackaging> {
let file_type = Guid::parse(reader)?;
let file = Guid::parse(reader)?;
let legacy_file_version = Guid::parse(reader)?;
let file_format = Guid::parse(reader)?;
if file != legacy_file_version {
return Err(
ErrorKind::MalformedOneStoreData("not a legacy OneStore file".into()).into(),
);
}
if reader.get_u32()? != 0 {
return Err(ErrorKind::MalformedFssHttpBData("invalid padding data".into()).into());
}
ObjectHeader::try_parse_32(reader, ObjectType::OneNotePackaging)?;
let storage_index = ExGuid::parse(reader)?;
let cell_schema = Guid::parse(reader)?;
let data_element_package = DataElementPackage::parse(reader)?;
ObjectHeader::try_parse_end_16(reader, ObjectType::OneNotePackaging)?;
Ok(OneStorePackaging {
file_type,
file,
legacy_file_version,
file_format,
storage_index,
cell_schema,
data_element_package,
})
}
}

View File

@ -0,0 +1,51 @@
macro_rules! guid {
({ $p0:tt - $p1:tt - $p2:tt - $p3:tt - $p4:tt }) => {
crate::parser::shared::guid::Guid::from_str(concat!(
stringify!($p0),
'-',
stringify!($p1),
'-',
stringify!($p2),
'-',
stringify!($p3),
'-',
stringify!($p4),
))
.unwrap()
};
}
macro_rules! exguid {
({$guid:tt , $n:literal}) => {
crate::parser::fsshttpb::data::exguid::ExGuid::from_guid(guid!($guid), $n)
};
}
#[cfg(test)]
mod test {
use crate::parser::fsshttpb::data::exguid::ExGuid;
use crate::parser::shared::guid::Guid;
#[test]
fn parse_guid() {
let guid = guid!({ 1A5A319C - C26B - 41AA - B9C5 - 9BD8C44E07D4 });
assert_eq!(
guid,
Guid::from_str("1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4").unwrap()
);
}
#[test]
fn parse_exguid() {
let guid = exguid!({{1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4}, 1});
assert_eq!(
guid,
ExGuid::from_guid(
Guid::from_str("1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4").unwrap(),
1
)
);
}
}

View File

@ -0,0 +1,73 @@
//! A OneNote file parser.
#![warn(missing_docs)]
#![deny(unused_must_use)]
pub mod errors;
mod fsshttpb;
#[macro_use]
mod macros;
mod one;
mod onenote;
mod onestore;
mod reader;
mod shared;
mod utils;
pub(crate) type Reader<'a, 'b> = &'b mut crate::parser::reader::Reader<'a>;
pub use onenote::Parser;
/// The data that represents a OneNote notebook.
pub mod notebook {
pub use crate::parser::onenote::notebook::Notebook;
}
/// The data that represents a OneNote section.
pub mod section {
pub use crate::parser::onenote::section::{Section, SectionEntry};
}
/// The data that represents a OneNote page.
pub mod page {
pub use crate::parser::onenote::page::Page;
pub use crate::parser::onenote::page_content::PageContent;
}
/// The data that represents the contents of a OneNote section.
pub mod contents {
pub use crate::parser::onenote::content::Content;
pub use crate::parser::onenote::embedded_file::EmbeddedFile;
pub use crate::parser::onenote::image::Image;
pub use crate::parser::onenote::ink::{Ink, InkBoundingBox, InkPoint, InkStroke};
pub use crate::parser::onenote::list::List;
pub use crate::parser::onenote::note_tag::NoteTag;
pub use crate::parser::onenote::outline::{Outline, OutlineElement, OutlineItem};
pub use crate::parser::onenote::rich_text::{EmbeddedObject, RichText};
pub use crate::parser::onenote::table::{Table, TableCell};
}
/// Collection of properties used by the OneNote file format.
pub mod property {
/// Properties related to multiple types of objects.
pub mod common {
pub use crate::parser::one::property::color::Color;
pub use crate::parser::one::property::color_ref::ColorRef;
}
/// Properties related to embedded files.
pub mod embedded_file {
pub use crate::parser::one::property::file_type::FileType;
}
/// Properties related to note tags.
pub mod note_tag {
pub use crate::parser::one::property::note_tag::ActionItemStatus;
pub use crate::parser::one::property::note_tag_shape::NoteTagShape;
}
/// Properties related to rich-text content.
pub mod rich_text {
pub use crate::parser::one::property::paragraph_alignment::ParagraphAlignment;
pub use crate::parser::onenote::rich_text::ParagraphStyling;
}
}

View File

@ -0,0 +1,11 @@
//! The OneNote file format.
//!
//! This module implements parsing OneNote objects from a OneNote revision store (see `onestore/`).
//! It defines the types of objects we can parse along with their properties.
//!
//! See [\[MS-ONE\]]
//!
//! [\[MS-ONE\]]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/73d22548-a613-4350-8c23-07d15576be50
pub(crate) mod property;
pub(crate) mod property_set;

View File

@ -0,0 +1,21 @@
use crate::parser::errors::Result;
use crate::parser::one::property::{simple, PropertyType};
use crate::parser::onestore::object::Object;
/// The author of an object.
///
/// See [\[MS-ONE\] 2.2.67]
///
/// [\[MS-ONE\] 2.2.67]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/db06251b-b672-4c9b-8ba5-d948caaa3edd
#[derive(Debug)]
pub(crate) struct Author(String);
impl Author {
pub(crate) fn into_value(self) -> String {
self.0
}
pub(crate) fn parse(object: &Object) -> Result<Option<Author>> {
Ok(simple::parse_string(PropertyType::Author, object)?.map(Author))
}
}

View File

@ -0,0 +1,73 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::one::property::PropertyType;
use crate::parser::onestore::object::Object;
/// A charset representation.
///
/// See [\[MS-ONE\] 2.3.55].
///
/// [\[MS-ONE\] 2.3.55]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/64e2db6e-6eeb-443c-9ccf-0f72b37ba411
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone)]
pub enum Charset {
Ansi,
Default,
Symbol,
Mac,
ShiftJis,
Hangul,
Johab,
Gb2312,
ChineseBig5,
Greek,
Turkish,
Vietnamese,
Hebrew,
Arabic,
Baltic,
Russian,
Thai,
EastEurope,
Oem,
}
impl Charset {
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<Charset>> {
let value = match object.props().get(prop_type) {
Some(value) => value
.to_u8()
.ok_or_else(|| ErrorKind::MalformedOneNoteFileData("charset is not a u8".into()))?,
None => return Ok(None),
};
let charset = match value {
0 => Charset::Ansi,
1 => Charset::Default,
2 => Charset::Symbol,
77 => Charset::Mac,
128 => Charset::ShiftJis,
129 => Charset::Hangul,
130 => Charset::Johab,
134 => Charset::Gb2312,
136 => Charset::ChineseBig5,
161 => Charset::Greek,
162 => Charset::Turkish,
163 => Charset::Vietnamese,
177 => Charset::Hebrew,
178 => Charset::Arabic,
186 => Charset::Baltic,
204 => Charset::Russian,
222 => Charset::Thai,
238 => Charset::EastEurope,
255 => Charset::Oem,
_ => {
return Err(ErrorKind::MalformedOneNoteFileData(
format!("invalid charset: {}", value).into(),
)
.into())
}
};
Ok(Some(charset))
}
}

View File

@ -0,0 +1,58 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::one::property::PropertyType;
use crate::parser::onestore::object::Object;
/// A RGBA color value.
///
/// See [\[MS-ONE\] 2.2.7]
///
/// [\[MS-ONE\] 2.2.7]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/6e4a87f9-18f0-4ad6-bc7d-0f326d61e136
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Color {
alpha: u8,
r: u8,
g: u8,
b: u8,
}
impl Color {
/// The color's transparency value.
pub fn alpha(&self) -> u8 {
self.alpha
}
/// The color's red value.
pub fn r(&self) -> u8 {
self.r
}
/// The color's green value.
pub fn g(&self) -> u8 {
self.g
}
/// The color's blue value.
pub fn b(&self) -> u8 {
self.b
}
}
impl Color {
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<Color>> {
let value = match object.props().get(prop_type) {
Some(value) => value
.to_u32()
.ok_or_else(|| ErrorKind::MalformedOneNoteFileData("color is not a u32".into()))?,
None => return Ok(None),
};
let bytes = value.to_le_bytes();
Ok(Some(Color {
alpha: 255 - bytes[3],
r: bytes[0],
g: bytes[1],
b: bytes[2],
}))
}
}

View File

@ -0,0 +1,54 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::one::property::PropertyType;
use crate::parser::onestore::object::Object;
/// An RGB color value.
///
/// See [\[MS-ONE\] 2.2.8]
///
/// [\[MS-ONE\] 2.2.8]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/3796cb27-7ec3-4dc9-b43e-7c31cc5b765d
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum ColorRef {
/// Determined by the application.
Auto,
/// A manually specified color
Manual {
/// The color's red value.
r: u8,
/// The color's green value.
g: u8,
/// The color's blue value
b: u8,
},
}
impl ColorRef {
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<ColorRef>> {
let value = match object.props().get(prop_type) {
Some(value) => value.to_u32().ok_or_else(|| {
ErrorKind::MalformedOneNoteFileData("color ref is not a u32".into())
})?,
None => return Ok(None),
};
let bytes = value.to_le_bytes();
let color = match bytes[3] {
0xFF => ColorRef::Auto,
0x00 => ColorRef::Manual {
r: bytes[0],
g: bytes[1],
b: bytes[2],
},
_ => {
return Err(ErrorKind::MalformedOneNoteFileData(
format!("invalid color ref: 0x{:08X}", value).into(),
)
.into())
}
};
Ok(Some(color))
}
}

View File

@ -0,0 +1,44 @@
use crate::parser::errors::{ErrorKind, Result};
use crate::parser::one::property::PropertyType;
use crate::parser::onestore::object::Object;
/// An embedded file's file type.
///
/// See [\[MS-ONE\] 2.3.62].
///
/// [\[MS-ONE\] 2.3.62]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/112836a0-ed3b-4be1-bc4b-49f0f7b02295
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum FileType {
/// Unknown
Unknown,
/// An audio file.
Audio,
/// A video file.
Video,
}
impl FileType {
pub(crate) fn parse(object: &Object) -> Result<FileType> {
let value = match object.props().get(PropertyType::IRecordMedia) {
Some(value) => value.to_u32().ok_or_else(|| {
ErrorKind::MalformedOneNoteFileData("file type status is not a u32".into())
})?,
None => return Ok(FileType::Unknown),
};
let file_type = match value {
1 => FileType::Audio,
2 => FileType::Video,
_ => {
return Err(ErrorKind::MalformedOneNoteFileData(
format!("invalid file type: {}", value).into(),
)
.into())
}
};
Ok(file_type)
}
}

Some files were not shown because too many files have changed in this diff Show More