fix(core): onenote import (#15332)

#### PR Dependency Tree


* **PR #15332** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added improved Windows support for importing OneNote files, including
`.one`, `.onetoc2`, and `.onepkg` formats.
* Enabled reliable handling of Windows paths, including UNC and verbatim
paths.
* Added filesystem operations for reading, writing, discovering, and
opening OneNote content on Windows.

* **Bug Fixes**
* Improved file access and path resolution during OneNote imports on
Windows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-23 18:45:28 +08:00
committed by GitHub
parent 174ad9bc55
commit 8001451fd5
5 changed files with 173 additions and 2 deletions
Generated
+1
View File
@@ -165,6 +165,7 @@ dependencies = [
"affine_nbstore",
"affine_preview",
"affine_sqlite_v1",
"bytes",
"chrono",
"hex",
"infer",
+1
View File
@@ -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"
+3
View File
@@ -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 }
@@ -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<WindowsFs> {
Parser::new_with_fs(WindowsFs)
}
struct OneNoteImportCursor {
docs: Vec<OneNoteDoc>,
folders: Vec<FolderHierarchyDelta>,
@@ -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<bool, Error> {
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<Vec<TypedPathBuf>, 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<Vec<u8>, 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<TypedPathBuf, Error> {
as_typed_path(&fs::canonicalize(resolve_path(path)?)?)
}
fn exists(&self, path: TypedPath) -> Result<bool, Error> {
fs::exists(resolve_path(path)?)
}
fn open_file(&self, path: TypedPath) -> Result<Arc<dyn FileSource>, 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<PathBuf, Error> {
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<TypedPathBuf, Error> {
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<PathBuf, Error> {
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<Bytes, Error> {
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")
);
}
}