diff --git a/Cargo.lock b/Cargo.lock index 93bf9793d5..b58779948c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,7 @@ dependencies = [ "affine_nbstore", "affine_preview", "affine_sqlite_v1", + "bytes", "chrono", "hex", "infer", diff --git a/Cargo.toml b/Cargo.toml index f6876f8e87..966e4d6c14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ resolver = "3" base64 = "0.22.1" base64-simd = "0.8" block2 = "0.6" + bytes = "1" chrono = "0.4" core-foundation = "0.10" coreaudio-rs = "0.12" diff --git a/packages/frontend/native/Cargo.toml b/packages/frontend/native/Cargo.toml index dc14c911f2..12a6a68d4a 100644 --- a/packages/frontend/native/Cargo.toml +++ b/packages/frontend/native/Cargo.toml @@ -36,6 +36,9 @@ typed-path = { workspace = true } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] affine_preview = { workspace = true, features = ["mermaid", "typst"] } +[target.'cfg(windows)'.dependencies] +bytes = { workspace = true } + [target.'cfg(not(target_os = "linux"))'.dependencies] mimalloc = { workspace = true } diff --git a/packages/frontend/native/src/import/onenote/mod.rs b/packages/frontend/native/src/import/onenote/mod.rs index 53f65c69a2..4bbc262aeb 100644 --- a/packages/frontend/native/src/import/onenote/mod.rs +++ b/packages/frontend/native/src/import/onenote/mod.rs @@ -6,6 +6,8 @@ use std::{ }; mod content; +#[cfg(windows)] +mod windows_fs; use affine_importer::{ FolderHierarchyDelta, ImportBatch, ImportBatchLimits, ImportCursor, ImportError, ImportOptions, ImportProgress, @@ -19,6 +21,8 @@ use onenote_parser::{ section::{Section, SectionEntry, SectionGroup}, }; use typed_path::TypedPath; +#[cfg(windows)] +use windows_fs::WindowsFs; pub struct OneNoteImportProvider; @@ -81,7 +85,7 @@ impl OneNotePlanner { } fn read_file(&mut self, path: &Path) -> ImportResult<()> { - let parser = Parser::new(); + let parser = parser(); match extension(path).as_deref() { Some("one") => { let source_path = path.to_string_lossy(); @@ -121,7 +125,7 @@ impl OneNotePlanner { path.to_string_lossy() )) })?; - let parser = Parser::new(); + let parser = parser(); let toc_path = toc.to_string_lossy(); let notebook = parser .parse_notebook(TypedPath::derive(toc_path.as_ref())) @@ -210,6 +214,16 @@ impl OneNotePlanner { } } +#[cfg(not(windows))] +fn parser() -> Parser { + Parser::new() +} + +#[cfg(windows)] +fn parser() -> Parser { + Parser::new_with_fs(WindowsFs) +} + struct OneNoteImportCursor { docs: Vec, folders: Vec, diff --git a/packages/frontend/native/src/import/onenote/windows_fs.rs b/packages/frontend/native/src/import/onenote/windows_fs.rs new file mode 100644 index 0000000000..ac8eba0dce --- /dev/null +++ b/packages/frontend/native/src/import/onenote/windows_fs.rs @@ -0,0 +1,152 @@ +use std::{ + ffi::OsString, + fs::{self, File}, + io::{Error, Read}, + path::{Path, PathBuf}, + sync::Arc, +}; + +use bytes::Bytes; +use onenote_parser::{ + FileSystem, + fs::{FileSource, file_source::CachedFileSource}, +}; +use typed_path::{TypedPath, TypedPathBuf}; + +#[derive(Clone, Copy)] +pub(super) struct WindowsFs; + +impl FileSystem for WindowsFs { + fn is_directory(&self, path: TypedPath) -> Result { + match fs::metadata(resolve_path(path)?) { + Ok(meta) => Ok(meta.is_dir()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(error), + } + } + + fn read_dir(&self, path: TypedPath) -> Result, Error> { + fs::read_dir(resolve_path(path)?)? + .map(|entry| entry.and_then(|entry| as_typed_path(&entry.path()))) + .collect() + } + + fn read_file(&self, path: TypedPath) -> Result, Error> { + fs::read(resolve_path(path)?) + } + + fn write_file(&self, path: TypedPath, data: &[u8]) -> Result<(), Error> { + fs::write(resolve_path(path)?, data) + } + + fn stream_to_file(&self, path: TypedPath, reader: &mut dyn Read) -> Result<(), Error> { + let mut file = File::create(resolve_path(path)?)?; + std::io::copy(reader, &mut file)?; + Ok(()) + } + + fn make_dir(&self, path: TypedPath) -> Result<(), Error> { + fs::create_dir_all(resolve_path(path)?) + } + + fn canonicalize(&self, path: TypedPath) -> Result { + as_typed_path(&fs::canonicalize(resolve_path(path)?)?) + } + + fn exists(&self, path: TypedPath) -> Result { + fs::exists(resolve_path(path)?) + } + + fn open_file(&self, path: TypedPath) -> Result, Error> { + let file = File::open(resolve_path(path)?)?; + let byte_length = file.metadata()?.len(); + Ok(Arc::new(CachedFileSource::new(WindowsFileSource { file, byte_length }))) + } +} + +fn resolve_path(path: TypedPath) -> Result { + let path = if path.is_windows() { + path.to_path_buf() + } else { + path + .with_windows_encoding_checked() + .map_err(|error| Error::new(std::io::ErrorKind::InvalidInput, error))? + }; + let path = path + .to_str() + .ok_or_else(|| Error::new(std::io::ErrorKind::InvalidData, "path bytes are not valid UTF-8"))?; + to_verbatim(PathBuf::from(path)) +} + +fn as_typed_path(path: &Path) -> Result { + let path = path + .to_str() + .ok_or_else(|| Error::new(std::io::ErrorKind::InvalidData, "path is not valid UTF-8"))?; + Ok(TypedPath::windows(path.as_bytes()).to_path_buf()) +} + +fn to_verbatim(path: PathBuf) -> Result { + let path = std::path::absolute(path)?.to_string_lossy().replace('/', "\\"); + if path.starts_with(r"\\?\") { + return Ok(PathBuf::from(path)); + } + let mut verbatim = OsString::new(); + if let Some(path) = path.strip_prefix(r"\\") { + verbatim.push(r"\\?\UNC\"); + verbatim.push(path); + } else { + verbatim.push(r"\\?\"); + verbatim.push(path); + } + Ok(PathBuf::from(verbatim)) +} + +struct WindowsFileSource { + file: File, + byte_length: u64, +} + +impl FileSource for WindowsFileSource { + fn byte_length(&self) -> u64 { + self.byte_length + } + + fn read_at(&self, offset: u64, len: usize) -> Result { + use std::os::windows::fs::FileExt; + + let mut bytes = vec![0; len]; + let mut read = 0; + while read < len { + let count = self.file.seek_read(&mut bytes[read..], offset + read as u64)?; + if count == 0 { + return Err(Error::new( + std::io::ErrorKind::UnexpectedEof, + "unexpected end of OneNote file", + )); + } + read += count; + } + Ok(Bytes::from(bytes)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preserves_windows_absolute_path_prefixes() { + assert_eq!( + resolve_path(TypedPath::derive(r"C:\Users\Alice\Notebook.one")).unwrap(), + PathBuf::from(r"\\?\C:\Users\Alice\Notebook.one") + ); + assert_eq!( + resolve_path(TypedPath::derive(r"\\server\share\Notebook.one")).unwrap(), + PathBuf::from(r"\\?\UNC\server\share\Notebook.one") + ); + assert_eq!( + resolve_path(TypedPath::derive(r"\\?\D:\Notebook.one")).unwrap(), + PathBuf::from(r"\\?\D:\Notebook.one") + ); + } +}