diff --git a/crates/docpath/src/lib.rs b/crates/docpath/src/lib.rs index 1c5c41a7..d9abc941 100644 --- a/crates/docpath/src/lib.rs +++ b/crates/docpath/src/lib.rs @@ -75,8 +75,9 @@ //! assert_eq!(resolve_absolute("img/logo.png", None), Err(Refused::Invalid)); //! ``` +use std::ffi::OsStr; use std::fmt; -use std::path::{MAIN_SEPARATOR, Path, PathBuf}; +use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf, Prefix}; /// What a stored path refers to. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -605,8 +606,11 @@ fn render(root: &Root, comps: &[String]) -> PathBuf { PathBuf::from(prefix + &comps.join(&sep.to_string())) } -/// A relative link next to a document whose path is not UTF-8 (possible on Unix only). +/// A relative link next to a document whose path is not Unicode (bytes that are not UTF-8 on +/// Unix, an unpaired surrogate on Windows). The link inherits the document's root, so that root +/// is held to the rules [`resolve`] applies to a Unicode document path. fn join_native(doc: &Path, comps: &[String]) -> Result { + check_native_root(doc)?; let mut p = doc.parent().map(Path::to_path_buf).unwrap_or_default(); for c in comps { match c.as_str() { @@ -621,3 +625,19 @@ fn join_native(doc: &Path, comps: &[String]) -> Result { } Ok(p) } + +/// Refuses a native document path rooted in a device or object-manager namespace (`\\.\`, +/// `\\?\` other than a drive or `UNC`, `\??\`), as [`parse`] does for a Unicode one. Drives and +/// shares are kept; a share is then the link's boundary as for a Unicode path. +fn check_native_root(doc: &Path) -> Result<(), Refused> { + let mut comps = doc.components(); + match comps.next() { + Some(Component::Prefix(p)) => match p.kind() { + Prefix::Disk(_) | Prefix::VerbatimDisk(_) => Ok(()), + Prefix::UNC(host, _) | Prefix::VerbatimUNC(host, _) if host != "." && host != "?" => Ok(()), + _ => Err(Refused::Device), + }, + Some(Component::RootDir) if comps.next() == Some(Component::Normal(OsStr::new("??"))) => Err(Refused::Device), + _ => Ok(()), + } +} diff --git a/crates/docpath/tests/docpath.rs b/crates/docpath/tests/docpath.rs index 40f2098b..5876a0e2 100644 --- a/crates/docpath/tests/docpath.rs +++ b/crates/docpath/tests/docpath.rs @@ -401,3 +401,34 @@ fn refusals_have_messages() { assert!(!r.to_string().is_empty()); } } + +#[cfg(unix)] +#[test] +fn a_non_unicode_document_path_keeps_the_namespace_rules() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + let doc = Path::new(OsStr::from_bytes(b"/home/u/\xff/a.doc")); + assert_eq!(resolve("img/x.png", Some(doc)).unwrap(), Path::new(OsStr::from_bytes(b"/home/u/\xff/img/x.png"))); + let device = Path::new(OsStr::from_bytes(b"/??/\xff/a.doc")); + assert_eq!(resolve("x.png", Some(device)), Err(Refused::Device)); + assert_eq!(resolve("x.png", Some(Path::new("/??/u/a.doc"))), Err(Refused::Device)); +} + +#[cfg(windows)] +#[test] +fn a_non_unicode_document_path_keeps_the_namespace_rules() { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + let native = |s: &str| { + // An unpaired surrogate in the folder name makes the path non-Unicode. + let mut w: Vec = s.encode_utf16().collect(); + let at = w.iter().rposition(|c| *c == u16::from(b'\\')).unwrap_or(0); + w.insert(at, 0xD800); + std::path::PathBuf::from(OsString::from_wide(&w)) + }; + assert!(resolve("x.png", Some(native(r"C:\docs\a.doc").as_path())).is_ok()); + assert!(resolve("x.png", Some(native(r"\\server\share\docs\a.doc").as_path())).is_ok()); + for doc in [r"\\.\COM1\docs\a.doc", r"\\?\GLOBALROOT\Device\docs\a.doc", r"\??\C:\docs\a.doc"] { + assert_eq!(resolve("x.png", Some(native(doc).as_path())), Err(Refused::Device), "{doc}"); + } +}