docpath: hold a non-Unicode document's root to the namespace rules

A relative link next to a document whose path is not Unicode was joined
to the document's folder natively, skipping the parsing that refuses
device and object-manager namespaces. On Windows a document path with an
unpaired surrogate under \\.\, \\?\GLOBALROOT or \??\ therefore let
the link resolve inside that namespace. The native join now refuses those
roots as resolve does for a Unicode path; drives and shares are kept.
This commit is contained in:
Dylan M. Taylor committed 2026-11-02 21:10:35 -05:00
1 parent 47c9f62530
commit dccbdc949d
2 files changed
+53 -2

No files matched your search

+22 -2
View File
@@ -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<PathBuf, Refused> {
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<PathBuf, Refused> {
}
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(()),
}
}
+31
View File
@@ -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<u16> = 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}");
}
}