feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,21 @@
[package]
name = "parser-utils"
version = "0.1.0"
edition = "2024"
[dependencies]
paste = "1.0"
bytes = "1.2.0"
thiserror = "1.0"
widestring = "1.0.2"
uuid = "1.1.2"
lazy_static = "1.4"
wasm-bindgen = "0.2"
sanitize-filename = "0.3.0"
parser-macros = { path = "../parser-macros" }
[dependencies.web-sys]
version = "0.3"
features = [
"console"
]

查看文件

@@ -0,0 +1,77 @@
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 normalizeAndWriteFile(filePath, data) {
filePath = path.normalize(filePath);
fs.writeFileSync(filePath, data);
}
function normalizeAndAppendFile(filePath, data) {
filePath = path.normalize(filePath);
fs.appendFileSync(filePath, data);
}
function fileReader(path) {
const fd = fs.openSync(path);
// TODO: When Node v20 is EOL, replace this with the { bigint: true }
// parameter variant.
const size = BigInt(fs.fstatSync(fd).size);
return {
read: (bigPosition, bigLength) => {
// Rust uses u64 for position/length which is transferred to JS as a BigInt.
// Convert:
const length = Number(bigLength);
const position = Number(bigPosition);
const data = Buffer.alloc(length);
const sizeRead = fs.readSync(fd, data, { length, position });
// Make data.size match the number of bytes read:
return data.subarray(0, sizeRead);
},
size: () => {
return size;
},
close: () => {
fs.closeSync(fd);
},
};
}
function isWindows() {
return process.platform === 'win32';
}
module.exports = {
mkdirSyncRecursive,
isDirectory,
readDir,
removePrefix,
normalizeAndWriteFile,
normalizeAndAppendFile,
fileReader,
isWindows,
};

查看文件

@@ -0,0 +1,15 @@
/// A struct that has a specific `fmt::Debug` serialization.
/// Useful when customizing a `struct`'s debug output.
pub struct DebugOutput<'a>(&'a str);
impl<'a> From<&'a str> for DebugOutput<'a> {
fn from(value: &'a str) -> Self {
Self(value)
}
}
impl<'a> std::fmt::Debug for DebugOutput<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}

查看文件

@@ -0,0 +1,167 @@
//! OneNote parsing error handling.
use std::borrow::Cow;
use std::num::TryFromIntError;
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 {
pub 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 {
if err.kind() == std::io::ErrorKind::UnexpectedEof {
ErrorKind::UnexpectedEof(err.to_string().into()).into()
} else {
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<widestring::error::Utf16Error> for Error {
fn from(err: widestring::error::Utf16Error) -> Self {
ErrorKind::from(err).into()
}
}
impl From<TryFromIntError> for Error {
fn from(err: TryFromIntError) -> 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: {0}")]
UnexpectedEof(Cow<'static, str>),
/// 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>),
/// The file was expected to be FSSHTTPB data, but was not.
#[error("Not FSSHTTPB data: {0}")]
NotFssHttpBData(Cow<'static, str>),
/// The file was expected to be FSSHTTPB data, but was not.
#[error("Not local OneStore data: {0}")]
NotLocalOneStore(Cow<'static, str>),
#[error("Post-parse validation failed: {0}")]
ParseValidationFailed(Cow<'static, str>),
#[error("Failed to resolve: {0}")]
ResolutionFailed(Cow<'static, str>),
#[error("Type conversion failed: {err}")]
TypeConversionFailed {
#[from]
err: TryFromIntError,
},
/// 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 different type of malformed UTF-16 string was encountered during parsing.
#[error("Malformed UTF-16 string: {err}")]
Utf16LibError {
#[from]
err: widestring::error::Utf16Error,
},
/// 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,
},
}

查看文件

@@ -0,0 +1,74 @@
use sanitize_filename::{Options as SanitizeOptions, sanitize_with_options};
use std::io::{Read, Seek};
pub type ApiResult<T> = std::result::Result<T, std::io::Error>;
pub trait FileHandle: Read + Seek {
fn byte_length(&self) -> u64;
}
pub trait FileApiDriver: Send + Sync {
fn is_windows(&self) -> bool;
fn is_directory(&self, path: &str) -> ApiResult<bool>;
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>>;
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>>;
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()>;
fn make_dir(&self, path: &str) -> ApiResult<()>;
fn exists(&self, path: &str) -> ApiResult<bool>;
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>>;
/// Writes data from `stream` to the file at `path`.
/// Note: If `stream.read` fails, the file may be left in a partially-written state.
fn stream_to_file(&self, path: &str, stream: &mut dyn Read) -> ApiResult<()>;
// These functions correspond to the similarly-named
// NodeJS path functions and should behave like the NodeJS
// functions (rather than the corresponding Rust functions).
fn get_file_name(&self, path: &str) -> Option<String>;
fn get_file_extension(&self, path: &str) -> String;
fn get_dir_name(&self, path: &str) -> String;
/// This function should behave like NodeJS's `path.join` function.
/// As a result, unlike Rust's `Path::join`, if `path_2` starts with "/",
/// `path_2` is still appended to `path_1`.
fn join(&self, path_1: &str, path_2: &str) -> String;
fn sanitize_file_name(&self, file_name: &str) -> String {
sanitize_with_options(
file_name.trim(),
SanitizeOptions {
// Override "windows". By default, sanitize_filename can
// incorrectly detect the host OS when compiled to WASM.
windows: self.is_windows(),
// Otherwise, match the default sanitize_filename options:
truncate: true,
replacement: "",
},
)
}
/// Splits filename into (base, extension).
fn split_file_name(&self, filename: &str) -> (String, String) {
let ext = self.get_file_extension(filename);
let base = filename.strip_suffix(&ext).unwrap_or(filename);
(base.into(), ext)
}
fn remove_prefix<'a>(&self, full_path: &'a str, prefix: &str) -> &'a str {
if let Some(without_prefix) = full_path.strip_prefix(prefix) {
without_prefix
} else {
full_path
}
}
fn get_output_path(&self, input_dir: &str, output_dir: &str, file_path: &str) -> String {
let base_path = self.remove_prefix(file_path, input_dir);
let rebased_output = self.join(output_dir, base_path);
self.get_dir_name(&rebased_output)
}
fn get_parent_dir(&self, path: &str) -> Option<String> {
let dir_name = self.get_dir_name(path);
let result = self.get_file_name(&dir_name);
result.filter(|value| !value.is_empty())
}
}

查看文件

@@ -0,0 +1,37 @@
pub mod api;
pub use api::ApiResult;
pub use api::FileApiDriver;
pub use api::FileHandle;
use lazy_static::lazy_static;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
mod wasm_driver;
#[cfg(target_arch = "wasm32")]
use wasm_driver::FileApiDriverImpl;
#[cfg(not(target_arch = "wasm32"))]
mod native_driver;
#[cfg(not(target_arch = "wasm32"))]
use native_driver::FileApiDriverImpl;
lazy_static! {
static ref FS_DRIVER: Arc<dyn FileApiDriver> = Arc::new(FileApiDriverImpl {});
}
pub fn fs_driver() -> Arc<dyn FileApiDriver> {
FS_DRIVER.clone()
}
#[cfg(test)]
mod test {
use super::fs_driver;
#[test]
fn sanitize_simple() {
assert_eq!(fs_driver().sanitize_file_name("a.txt"), "a.txt");
assert_eq!(fs_driver().sanitize_file_name("a/b.txt"), "ab.txt");
assert_eq!(fs_driver().sanitize_file_name("a\0a/b.txt"), "aab.txt");
assert_eq!(fs_driver().sanitize_file_name("a\\b\\.txt "), "ab.txt");
}
}

查看文件

@@ -0,0 +1,127 @@
use super::ApiResult;
use super::FileApiDriver;
use super::FileHandle;
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::path;
use std::path::Path;
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
#[cfg(target_os = "windows")]
fn is_windows(&self) -> bool {
true
}
#[cfg(not(target_os = "windows"))]
fn is_windows(&self) -> bool {
false
}
fn is_directory(&self, path: &str) -> ApiResult<bool> {
let metadata = fs::metadata(path)?;
let file_type = metadata.file_type();
Ok(file_type.is_dir())
}
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>> {
let mut result: Vec<String> = Vec::new();
for item in fs::read_dir(path)? {
let item = item?.path();
result.push(item.to_string_lossy().into())
}
Ok(result)
}
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>> {
fs::read(path)
}
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
Ok(Box::new(BufReader::new(fs::File::open(path)?)))
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
fs::write(path, data)
}
fn stream_to_file(&self, path: &str, data: &mut dyn std::io::Read) -> ApiResult<()> {
let mut f = File::create(path)?;
std::io::copy(data, &mut f)?;
Ok(())
}
fn exists(&self, path: &str) -> ApiResult<bool> {
fs::exists(path)
}
fn make_dir(&self, path: &str) -> ApiResult<()> {
let result = fs::create_dir(path);
// Don't fail if it already existed
if self.exists(path)? { Ok(()) } else { result }
}
fn get_file_name(&self, path: &str) -> Option<String> {
Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into())
}
fn get_file_extension(&self, path: &str) -> String {
match Path::new(path).extension() {
Some(ext) => {
let extension = String::from(ext.to_string_lossy());
String::from(".") + &extension
}
None => String::from(""),
}
}
fn get_dir_name(&self, path: &str) -> String {
match Path::new(path).parent() {
Some(parent) => parent.to_string_lossy().into(),
None => String::from(""),
}
}
fn join(&self, path_1: &str, path_2: &str) -> String {
// Remove the / prefix prior to joining: Match the behavior of the
// WASM/NodeJS file API.
let path_2 = if path_2.starts_with(path::MAIN_SEPARATOR) {
path_2.strip_prefix(path::MAIN_SEPARATOR).unwrap()
} else {
path_2
};
Path::new(path_1).join(path_2).to_string_lossy().into()
}
}
impl FileHandle for BufReader<fs::File> {
fn byte_length(&self) -> u64 {
self.get_ref().metadata().map(|m| m.len()).unwrap_or(0)
}
}
#[cfg(test)]
mod test {
use crate::file_api::FileApiDriver;
use super::FileApiDriverImpl;
#[test]
fn should_split_file_name() {
let fs_driver = FileApiDriverImpl {};
assert_eq!(
fs_driver.split_file_name("a.txt"),
(String::from("a"), String::from(".txt"))
);
assert_eq!(
fs_driver.split_file_name("a"),
(String::from("a"), String::from(""))
);
assert_eq!(
fs_driver.split_file_name("a test.a.b"),
(String::from("a test.a"), String::from(".b"))
);
}
}

查看文件

@@ -0,0 +1,308 @@
use super::ApiResult;
use super::FileApiDriver;
use super::FileHandle;
use std::io::{BufReader, Read, Seek, SeekFrom};
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use web_sys::js_sys;
use web_sys::js_sys::Uint8Array;
#[wasm_bindgen(module = "/node_functions.js")]
extern "C" {
#[wasm_bindgen(js_name = mkdirSyncRecursive, catch)]
fn make_dir(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = pathSep, catch)]
fn path_sep() -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = removePrefix, catch)]
fn remove_prefix(base_path: String, prefix: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = getOutputPath, catch)]
fn get_output_path(
input_dir: &str,
output_dir: &str,
file_path: &str,
) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = normalizeAndWriteFile, catch)]
fn write_file(path: &str, data: &[u8]) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = normalizeAndAppendFile, catch)]
fn append_file(path: &str, data: &[u8]) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = isDirectory, catch)]
fn is_directory(path: &str) -> std::result::Result<bool, JsValue>;
#[wasm_bindgen(js_name = readDir, catch)]
fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = fileReader, catch)]
fn open_file_handle(path: &str) -> std::result::Result<JsFileHandle, JsValue>;
#[wasm_bindgen(js_name = isWindows)]
fn is_windows() -> bool;
}
#[wasm_bindgen]
extern "C" {
type JsFileHandle;
#[wasm_bindgen(structural, method, catch)]
fn read(
this: &JsFileHandle,
offset: u64,
size: u64,
) -> std::result::Result<Uint8Array, JsValue>;
#[wasm_bindgen(structural, method)]
fn size(this: &JsFileHandle) -> u64;
#[wasm_bindgen(structural, method, catch)]
fn close(this: &JsFileHandle) -> std::result::Result<(), JsValue>;
}
#[wasm_bindgen(module = "fs")]
extern "C" {
#[wasm_bindgen(js_name = readFileSync, catch)]
fn read_file(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = existsSync, catch)]
fn exists(path: &str) -> std::result::Result<bool, JsValue>;
}
#[wasm_bindgen(module = "path")]
extern "C" {
#[wasm_bindgen(js_name = basename, catch)]
fn get_file_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = extname, catch)]
fn get_file_extension(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = dirname, catch)]
fn get_dir_name(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = join, catch)]
fn join_path(path_1: &str, path_2: &str) -> std::result::Result<JsValue, JsValue>;
}
fn handle_error(error: JsValue, source: &str) -> std::io::Error {
use std::io::Error;
use std::io::ErrorKind;
let error = js_sys::Error::from(error);
match error.name().to_string() {
_ => Error::new(
ErrorKind::Other,
String::from(format!("Err({}): {:?}", source, error)),
),
}
}
pub struct FileApiDriverImpl {}
impl FileApiDriver for FileApiDriverImpl {
fn is_windows(&self) -> bool {
is_windows()
}
fn is_directory(&self, path: &str) -> ApiResult<bool> {
match is_directory(path) {
Ok(is_dir) => Ok(is_dir),
Err(e) => Err(handle_error(e, "checking is_directory")),
}
}
fn read_dir(&self, path: &str) -> ApiResult<Vec<String>> {
let result_ptr = read_dir_js(path).unwrap();
let result_str: String = match result_ptr.as_string() {
Some(x) => x,
_ => String::new(),
};
Ok(result_str.split('\n').map(|s| s.to_string()).collect())
}
fn read_file(&self, path: &str) -> ApiResult<Vec<u8>> {
match read_file(path) {
Ok(file) => Ok(Uint8Array::new(&file).to_vec()),
Err(e) => Err(handle_error(e, &format!("reading file {}", path))),
}
}
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
match open_file_handle(path) {
Ok(handle) => {
let file = BufReader::new(SeekableFileHandle { handle, offset: 0 });
Ok(Box::new(file))
}
Err(e) => Err(handle_error(e, &format!("opening file {}", path))),
}
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
if let Err(error) = write_file(path, data) {
Err(handle_error(error, &format!("writing file {}", path)))
} else {
Ok(())
}
}
fn stream_to_file(&self, path: &str, data: &mut dyn std::io::Read) -> ApiResult<()> {
// Create and clear the file. This is important for zero-size files
self.write_file(path, &[])?;
let mut chunk_size = 1024 * 1024; // 1 MB
let max_chunk_size = 50 * 1024 * 1024;
let mut buffer = vec![0; chunk_size];
loop {
let size = match data.read(&mut buffer) {
Ok(size) => size,
// Interrupted errors can be retried
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
continue;
}
Err(err) => {
return Err(err);
}
};
if size == 0 {
break;
}
if let Err(error) = append_file(path, &buffer[0..size]) {
return Err(handle_error(error, &format!("writing file {}", path)));
}
// For performance, try to increase the chunk size
if size == chunk_size && chunk_size < max_chunk_size {
chunk_size = (chunk_size * 2).min(max_chunk_size);
buffer.resize(chunk_size, 0);
}
}
Ok(())
}
fn exists(&self, path: &str) -> ApiResult<bool> {
match exists(path) {
Ok(exists) => Ok(exists),
Err(e) => Err(handle_error(e, &format!("checking exists {}", path))),
}
}
fn make_dir(&self, path: &str) -> ApiResult<()> {
if let Err(error) = make_dir(path) {
Err(handle_error(error, &format!("mkdir {}", path)))
} else {
Ok(())
}
}
fn get_file_name(&self, path: &str) -> Option<String> {
let file_name = get_file_name(path).unwrap().as_string().unwrap();
if file_name == "" {
None
} else {
Some(file_name)
}
}
fn get_file_extension(&self, path: &str) -> String {
get_file_extension(path).unwrap().as_string().unwrap()
}
fn get_dir_name(&self, path: &str) -> String {
get_dir_name(path).unwrap().as_string().unwrap()
}
fn join(&self, path_1: &str, path_2: &str) -> String {
join_path(path_1, path_2).unwrap().as_string().unwrap()
}
}
struct SeekableFileHandle {
handle: JsFileHandle,
offset: u64,
}
impl Read for SeekableFileHandle {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
let file_size = self.handle.size();
let bytes_remaining = if self.offset < file_size {
file_size - self.offset
} else {
0
};
let maximum_read_size = bytes_remaining.min(out.len() as u64);
match self.handle.read(self.offset, maximum_read_size) {
Ok(data) => {
let data = data.to_vec();
let size = data.len();
self.offset += size as u64;
// Verify that handle.read respected the maximum length:
if size > out.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Invariant violation: Size read must be less than or equal to the maximum_read_size.",
));
}
let (target_mem, padding) = out.split_at_mut(size);
target_mem.copy_from_slice(&data);
padding.fill(0);
Ok(size)
}
Err(error) => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Read failed: {:?}.", error),
));
}
}
}
}
impl Seek for SeekableFileHandle {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match pos {
SeekFrom::Start(pos) => {
self.offset = pos;
}
SeekFrom::Current(offset) => {
// Disallow seeking to a negative position
if offset < 0 && offset.unsigned_abs() > self.offset {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Attempted to seek before the beginning of the file.",
));
}
self.offset = (self.offset as i64 + offset) as u64;
}
SeekFrom::End(offset) => {
self.offset = self.handle.size();
self.seek(SeekFrom::Current(offset))?;
}
}
Ok(self.offset)
}
}
impl Drop for SeekableFileHandle {
fn drop(&mut self) {
if let Err(error) = self.handle.close() {
// Use web_sys directly -- log_warn! can't be used from within the parser-utils package:
let message: JsValue =
format!("OneNote converter: Failed to close file: Error: {error:?}").into();
web_sys::console::warn_1(&message);
}
}
}
impl FileHandle for BufReader<SeekableFileHandle> {
fn byte_length(&self) -> u64 {
self.get_ref().handle.size()
}
}

查看文件

@@ -0,0 +1,32 @@
//! Utilities shared between the parser and renderer.
use widestring::U16CString;
pub mod debug;
pub mod errors;
mod file_api;
pub mod log;
pub mod parse;
pub mod reader;
pub use errors::Result;
pub use file_api::FileHandle;
pub use file_api::fs_driver;
pub type Reader<'a, 'b> = &'b mut crate::reader::Reader<'a>;
pub trait Utf16ToString {
fn utf16_to_string(&self) -> Result<String>;
}
impl Utf16ToString for &[u8] {
fn utf16_to_string(&self) -> Result<String> {
let data: Vec<_> = self
.chunks_exact(2)
.map(|v| u16::from_le_bytes([v[0], v[1]]))
.collect();
let value = U16CString::from_vec_truncate(data);
value.to_string().map_err(|err| err.into())
}
}

查看文件

@@ -0,0 +1,56 @@
use std::sync::Mutex;
use lazy_static::lazy_static;
#[macro_export]
#[cfg(target_arch = "wasm32")]
macro_rules! log {
( $( $t:tt )* ) => {
#[cfg(debug_assertions)]
web_sys::console::log_2(&format!("OneNoteConverter: ").into(), &format!( $( $t )* ).into());
}
}
#[macro_export]
#[cfg(target_arch = "wasm32")]
macro_rules! log_warn {
( $( $t:tt )* ) => {
use parser_utils::log::get_current_page;
web_sys::console::warn_1(&format!("OneNoteConverter: Warning around the following page: {}", get_current_page()).into());
web_sys::console::warn_2(&format!("OneNoteConverter: ").into(), &format!( $( $t )* ).into());
}
}
#[macro_export]
#[cfg(not(target_arch = "wasm32"))]
macro_rules! log {
( $( $t:tt )* ) => {
#[cfg(debug_assertions)]
println!( $( $t )* );
}
}
#[macro_export]
#[cfg(not(target_arch = "wasm32"))]
macro_rules! log_warn {
( $( $t:tt )* ) => {
println!("Warning: {}, near {}", &format!( $( $t )* ), parser_utils::log::get_current_page());
}
}
lazy_static! {
static ref CURRENT_PAGE: Mutex<Option<String>> = Mutex::new(None);
}
pub fn set_current_page(page_name: String) {
let mut current_page = CURRENT_PAGE.lock().unwrap();
*current_page = Some(page_name.to_string());
}
pub fn get_current_page() -> String {
let current_page = CURRENT_PAGE.lock().unwrap();
current_page
.clone()
.unwrap_or_else(|| String::from("[None]"))
}

查看文件

@@ -0,0 +1,91 @@
use super::Reader;
use super::errors::Result;
pub use parser_macros::Parse;
/// Applies to `struct`s that support parsing from a binary stream.
///
/// In many cases, it is possible to auto-derive implementations for this trait. For example,
/// ```
/// use parser_utils::parse::Parse;
///
/// #[derive(Parse)]
/// #[validate(a > 3)]
/// struct Example {
/// a: u32,
/// b: u8,
/// }
/// ```
pub trait Parse
where
Self: Sized,
{
fn parse(reader: Reader) -> Result<Self>;
}
/// An alternate `Parse` implementation specific to the FSSHTTPB OneNote packaging.
/// Use this if a `struct` needs to be parsed in a different way in FSSHTTPB documents.
pub trait ParseHttpb
where
Self: Sized,
{
fn parse(reader: Reader) -> Result<Self>;
}
pub trait ParseWithCount
where
Self: Sized,
{
fn parse(reader: Reader, count: usize) -> Result<Self>;
}
impl Parse for u8 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_u8()
}
}
impl Parse for u16 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_u16()
}
}
impl Parse for u32 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_u32()
}
}
impl Parse for u64 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_u64()
}
}
impl Parse for u128 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_u128()
}
}
impl Parse for f32 {
fn parse(reader: Reader) -> Result<Self> {
reader.get_f32()
}
}
impl Parse for () {
fn parse(_reader: Reader) -> Result<Self> {
Ok(())
}
}
impl<T: Parse> ParseWithCount for Vec<T> {
fn parse(reader: Reader, size: usize) -> Result<Self> {
let mut result = Vec::new();
for _i in 0..size {
result.push(T::parse(reader)?);
}
Ok(result)
}
}

查看文件

@@ -0,0 +1,356 @@
use crate::{
FileHandle,
errors::{ErrorKind, Result},
};
use bytes::Buf;
use paste::paste;
use std::{
cell::RefCell,
io::{Cursor, Read, Seek, SeekFrom},
mem,
rc::Rc,
};
macro_rules! try_get {
($this:ident, $typ:tt) => {{
if $this.remaining() < mem::size_of::<$typ>() as u64 {
Err(ErrorKind::UnexpectedEof(format!("Getting {:}", stringify!($typ)).into()).into())
} else {
let mut buff = [0; mem::size_of::<$typ>()];
$this.read_exact(&mut buff)?;
let mut buff_ref: &[u8] = &mut buff;
Ok(paste! {buff_ref. [< get_ $typ >]()})
}
}};
($this:ident, $typ:tt::$endian:tt) => {{
if $this.remaining() < mem::size_of::<$typ>() as u64 {
Err(ErrorKind::UnexpectedEof(
format!("Getting {:} ({:})", stringify!($typ), stringify!($endian)).into(),
)
.into())
} else {
let mut buff = [0; mem::size_of::<$typ>()];
$this.read_exact(&mut buff)?;
let mut buff_ref: &[u8] = &mut buff;
Ok(paste! {buff_ref. [< get_ $typ _ $endian >]()})
}
}};
}
type ReaderFileHandle = Rc<RefCell<Box<dyn FileHandle>>>;
enum ReaderData<'a> {
/// Wraps a buffer owned by calling logic. This is more efficient for
/// small amounts of data.
BufferRef { buffer: &'a [u8] },
/// Wraps a handle to a file. This handles large amounts of data
/// that won't necessarily fit into memory.
/// Invariant: The internal file handle's offset should match the
/// `data_offset` of the main reader.
File(ReaderFileHandle),
}
pub struct Reader<'a> {
data: ReaderData<'a>,
data_len: u64,
data_offset: u64,
}
pub struct ReaderOffset(u64);
impl<'a> Seek for Reader<'a> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_offset = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::Current(n) => self.data_offset as i64 + n,
SeekFrom::End(n) => (self.data_len as i64) + n,
};
if new_offset < 0 || new_offset as u64 > self.data_len {
Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"New offset {} is out-of-bounds (data length: {}).",
new_offset, self.data_len
),
))
} else {
self.data_offset = new_offset as u64;
// Sync the internal file with the new offset. This is done rather than seek the file
// directly to avoid inconsistency if e.g. the file resizes and we're seeking from the end.
if let ReaderData::File(f) = &mut self.data {
f.borrow_mut().seek(SeekFrom::Start(self.data_offset))?;
}
Ok(self.data_offset)
}
}
}
impl<'a> Reader<'a> {
pub fn new(buffer: &'a [u8]) -> Self {
Reader {
data_len: buffer.len() as u64,
data_offset: 0,
data: ReaderData::BufferRef { buffer },
}
}
pub fn read(&mut self, count: usize) -> Result<Vec<u8>> {
let mut buff = vec![0; count];
self.read_exact(&mut buff)?;
Ok(buff)
}
fn read_exact(&mut self, output: &mut [u8]) -> Result<()> {
let count = output.len();
if self.remaining() < count as u64 {
return Err(
ErrorKind::UnexpectedEof("Unexpected EOF (Reader.read_exact)".into()).into(),
);
}
match &mut self.data {
ReaderData::BufferRef { buffer } => {
let start = self.data_offset as usize;
(&buffer[start..start + count]).copy_to_slice(output);
}
ReaderData::File(file) => {
file.borrow_mut().read_exact(output)?;
}
};
self.data_offset += count as u64;
Ok(())
}
pub fn peek_u8(&mut self) -> Result<Option<u8>> {
match &mut self.data {
ReaderData::BufferRef { buffer, .. } => {
Ok(buffer.get(self.data_offset as usize).copied())
}
ReaderData::File(file) => {
let mut file = file.borrow_mut();
let mut buf = [0u8];
let read_result = file.read(&mut buf);
// Reset the original position
file.seek(SeekFrom::Start(self.data_offset))?;
match read_result {
Ok(size) => Ok(if size < 1 { None } else { Some(buf[0]) }),
Err(error) => Err(error)?,
}
}
}
}
pub fn as_data_ref(&mut self, size: usize) -> Result<ReaderDataRef> {
if self.remaining() < size as u64 {
return Err(
ErrorKind::UnexpectedEof("Unexpected EOF (Reader.as_data_ref)".into()).into(),
);
}
match &mut self.data {
ReaderData::BufferRef { buffer } => {
let start = self.data_offset as usize;
// Cloning needs to be done early with BufferRef, since we don't own the original
// data. Large data should generally use `ReaderData::File`.
Ok(ReaderDataRef::Vec(buffer[start..start + size].to_vec()))
}
ReaderData::File(file) => Ok(ReaderDataRef::FilePointer(ReaderFilePointer {
file: file.clone(),
start_offset: self.data_offset,
end_offset: self.data_offset + (size as u64),
})),
}
}
pub fn remaining(&self) -> u64 {
assert!(self.data_len >= self.data_offset);
self.data_len - self.data_offset
}
pub fn advance(&mut self, count: u64) -> Result<()> {
if self.remaining() < count {
return Err(ErrorKind::UnexpectedEof(
format!(
"Reader.advance was unable to advance {} bytes. Only {} bytes are available",
count,
self.remaining(),
)
.into(),
)
.into());
}
assert!(count < i64::MAX as u64);
self.seek(SeekFrom::Current(count as i64))?;
Ok(())
}
pub fn save_position(&self) -> ReaderOffset {
ReaderOffset(self.data_offset)
}
pub fn restore_position(&mut self, offset: ReaderOffset) -> Result<()> {
self.seek(SeekFrom::Start(offset.0))?;
Ok(())
}
pub fn offset(&self) -> u64 {
self.data_offset
}
pub fn get_u8(&mut self) -> Result<u8> {
try_get!(self, u8)
}
pub fn get_u16(&mut self) -> Result<u16> {
try_get!(self, u16::le)
}
pub fn get_u32(&mut self) -> Result<u32> {
try_get!(self, u32::le)
}
pub fn get_u64(&mut self) -> Result<u64> {
try_get!(self, u64::le)
}
pub fn get_u128(&mut self) -> Result<u128> {
try_get!(self, u128::le)
}
pub fn get_f32(&mut self) -> Result<f32> {
try_get!(self, f32::le)
}
}
impl<'a> TryFrom<Box<dyn FileHandle>> for Reader<'a> {
type Error = crate::errors::Error;
fn try_from(mut handle: Box<dyn FileHandle>) -> Result<Self> {
let initial_offset = handle.seek(SeekFrom::Current(0))?;
Ok(Self {
data_len: handle.byte_length(),
data_offset: initial_offset,
data: ReaderData::File(Rc::new(RefCell::new(handle))),
})
}
}
impl<'a> From<&'a [u8]> for Reader<'a> {
fn from(value: &'a [u8]) -> Self {
Self {
data_len: value.len() as u64,
data_offset: 0,
data: ReaderData::BufferRef { buffer: value },
}
}
}
pub enum ReaderDataRef {
Vec(Vec<u8>),
FilePointer(ReaderFilePointer),
}
impl ReaderDataRef {
pub fn read(&self) -> Box<dyn Read> {
match self {
ReaderDataRef::Vec(slice) => Box::new(Cursor::new(slice.to_vec())),
ReaderDataRef::FilePointer(ptr) => Box::new(ptr.clone()),
}
}
}
#[derive(Clone)]
pub struct ReaderFilePointer {
file: ReaderFileHandle,
start_offset: u64,
end_offset: u64,
}
impl Read for ReaderFilePointer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let offset = self.start_offset;
if offset >= self.end_offset {
return Ok(0);
}
// Don't read past the end of the region:
let remaining = self.end_offset - offset;
let maximum_buf_len = buf.len().min(remaining as usize);
let buf = &mut buf[0..maximum_buf_len];
let mut file = self.file.borrow_mut();
let original_offset = file.seek(SeekFrom::Current(0))?;
let read_result = (|| {
file.seek(SeekFrom::Start(offset))?;
file.read(buf)
})();
file.seek(SeekFrom::Start(original_offset))?;
match read_result {
Ok(size) => {
self.start_offset += size as u64;
Ok(size)
}
Err(err) => Err(err),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn with_start_index_should_seek() {
let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let mut reader = Reader::from(&data as &[u8]);
assert_eq!(reader.get_u8().unwrap(), 1);
assert_eq!(reader.get_u8().unwrap(), 2);
assert_eq!(reader.get_u8().unwrap(), 3);
reader.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(reader.get_u8().unwrap(), 1);
assert_eq!(reader.get_u8().unwrap(), 2);
assert_eq!(reader.get_u8().unwrap(), 3);
assert_eq!(reader.get_u8().unwrap(), 4);
reader.seek_relative(-3).unwrap();
assert_eq!(reader.get_u8().unwrap(), 2);
assert_eq!(reader.get_u8().unwrap(), 3);
reader.seek_relative(-2).unwrap();
assert_eq!(reader.get_u8().unwrap(), 2);
assert_eq!(reader.get_u8().unwrap(), 3);
assert_eq!(reader.get_u8().unwrap(), 4);
}
#[test]
fn should_read_from_offset() {
let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let mut reader = Reader::from(&data as &[u8]);
reader.advance(1).unwrap();
let mut data_ref = reader.as_data_ref(4).unwrap().read();
reader.advance(2).unwrap();
// The data_ref should read from the offset from where it was created
let mut output = vec![];
data_ref.read_to_end(&mut output).unwrap();
assert_eq!(output, [2, 3, 4, 5]);
// Reading the data_ref should not affect the original reader
assert_eq!(reader.get_u8().unwrap(), 4);
}
}

查看文件

@@ -0,0 +1,18 @@
use parser_utils::{parse::Parse, reader::Reader};
#[derive(Parse)]
struct Test {
#[pad_to_alignment(4)]
a: u8,
b: u8,
}
#[test]
fn test_parse() {
let data: [u8; 5] = [1, 2, 3, 4, 5];
let mut reader = Reader::new(&data);
let parsed = Test::parse(&mut reader).unwrap();
assert_eq!(parsed.a, 1);
assert_eq!(parsed.b, 5);
assert_eq!(reader.remaining(), 0);
}