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
一些检查失败了
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
这个提交包含在:
@@ -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);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户