docpath: fold . and .. in a non-Unicode document's folder with the platform's path rules

A non-Unicode document path holding '..' (/a/../\xff/d.doc) was refused,
because the native join kept the '..' while the lexical result folded it.
join_native now folds the folder's components as normalize does, so such a
document resolves as a Unicode one would, and a '..' that leaves a Unix name
holding a backslash is still caught by the comparison.
This commit is contained in:
Dylan M. Taylor
2026-11-02 21:52:51 -05:00
parent 44260b9c50
commit 3c5e0201e0
2 changed files with 39 additions and 11 deletions
+26 -11
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};
/// What a stored path refers to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -619,18 +620,32 @@ fn resolve_native(raw: &str, doc: &Path, comps: &[String]) -> Result<PathBuf, Re
if parts(&native) == parts(&expected) { Ok(native) } else { Err(Refused::Invalid) }
}
/// Join a relative link's components to a non-Unicode document's folder with the platform's own
/// path rules, folding `.` and `..` as [`normalize`] does: a folder with a root (`/`, `C:\`, a
/// share) may not be climbed above, and a relative one keeps leading `..`.
fn join_native(doc: &Path, comps: &[String]) -> Result<PathBuf, Refused> {
let mut p = doc.parent().map(Path::to_path_buf).unwrap_or_default();
for c in comps {
match c.as_str() {
"." => {}
".." => {
if !p.pop() {
return Err(Refused::Invalid);
let folder = doc.parent().unwrap_or_else(|| Path::new(""));
let link = comps.iter().map(|c| match c.as_str() {
"." => Component::CurDir,
".." => Component::ParentDir,
name => Component::Normal(OsStr::new(name)),
});
let mut root = PathBuf::new();
let mut names: Vec<&OsStr> = Vec::new();
for c in folder.components().chain(link) {
match c {
Component::Prefix(_) | Component::RootDir => root.push(c),
Component::CurDir => {}
Component::ParentDir => match names.last() {
Some(last) if *last != OsStr::new("..") => {
names.pop();
}
}
_ => p.push(c),
_ if root.has_root() => return Err(Refused::Invalid),
_ => names.push(c.as_os_str()),
},
Component::Normal(name) => names.push(name),
}
}
Ok(p)
root.extend(names);
Ok(root)
}
+13
View File
@@ -425,6 +425,15 @@ fn a_non_unicode_document_path_keeps_the_namespace_rules() {
assert_eq!(resolve("x.png", Some(folder)).unwrap(), Path::new(OsStr::from_bytes(b"/home/u/a\\b/x.png")));
let name = Path::new(OsStr::from_bytes(b"/home/u/a\xff\\b.doc"));
assert_eq!(resolve("x.png", Some(name)), Err(Refused::Invalid));
// `.` and `..` in the document's folder are folded as for a Unicode document path.
let dotted = Path::new(OsStr::from_bytes(b"/home/./x/../u/\xff/a.doc"));
assert_eq!(resolve("../y/x.png", Some(dotted)).unwrap(), Path::new("/home/u/y/x.png"));
assert_eq!(resolve("../../../../x.png", Some(dotted)), Err(Refused::Invalid));
let relative = Path::new(OsStr::from_bytes(b"\xff/a.doc"));
assert_eq!(resolve("../../x.png", Some(relative)).unwrap(), Path::new("../x.png"));
// A `..` that climbs out of a name holding a `\` on Unix does not leave the share unnoticed.
let climb = Path::new(OsStr::from_bytes(b"//server/share/a\\b/../../\xff/a.doc"));
assert_eq!(resolve("x.png", Some(climb)), Err(Refused::Invalid));
}
#[cfg(windows)]
@@ -443,6 +452,10 @@ fn a_non_unicode_document_path_keeps_the_namespace_rules() {
let share = native(r"\\server\share\docs\a.doc");
assert_eq!(resolve("x.png", Some(share.as_path())).unwrap(), native(r"\\server\share\docs\x.png"));
assert_eq!(resolve(r"..\..\x.png", Some(share.as_path())), Err(Refused::Invalid));
let dotted = native(r"C:\a\..\.\docs\a.doc");
assert_eq!(resolve("x.png", Some(dotted.as_path())).unwrap(), native(r"C:\docs\x.png"));
assert_eq!(resolve(r"..\y\x.png", Some(dotted.as_path())).unwrap(), Path::new(r"C:\y\x.png"));
assert_eq!(resolve(r"..\..\x.png", Some(dotted.as_path())), Err(Refused::Invalid));
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}");
}