Merge pull request #2837 from dotCooCoo/fix/svg-import-paths-on-windows

SVG import: hrefs that need no folder stay as written, and images in a non-absolute folder are read
This commit is contained in:
BFlat
2026-10-18 13:16:15 +07:00
committed by GitHub
2 changed files with 65 additions and 20 deletions
+63 -19
View File
@@ -79,25 +79,34 @@ fn percent_decoded(s: &str) -> String {
String::from_utf8_lossy(&out).into_owned()
}
/// Where href `h` points (a path, or a URL) and whether it is a file that may be read: a local
/// file, or one on the network share the SVG is on. A relative path is taken from `folder`, the
/// SVG's own; without it, only absolute paths are read (see `craft_docpath`).
fn target(h: &str, folder: Option<&str>) -> (String, bool) {
/// Where href `h` points: the path its link keeps, and the file to read when it may be read (a
/// local file, or one on the network share the SVG is on). A relative path is taken from
/// `folder`, the SVG's own, made absolute against the working directory when it isn't; without
/// a folder, only absolute paths are read (see `craft_docpath`). The link keeps the path as
/// written (decoded) when the folder does not change the path it resolves to, and the path found
/// when it does (a relative path, or on Windows a path rooted without a drive); the link of a
/// `file:` URL keeps the path it names.
fn target(h: &str, folder: Option<&str>) -> (String, Option<String>) {
let h = h.trim();
// `craft_docpath` decodes a `file:` URL itself; any other href is decoded here, once.
let file_url = h.get(..5).is_some_and(|s| s.eq_ignore_ascii_case("file:"));
let stored = if file_url { h.to_string() } else { percent_decoded(h) };
if !file_url && stored.contains("://") {
return (stored, false);
return (stored, None);
}
// Resolved as a document inside `folder`: only the folder is used.
let document = folder.map(|f| Path::new(f).join("-"));
let document = folder.map(|f| {
let f = Path::new(f);
if f.is_absolute() { f.to_path_buf() } else { std::path::absolute(f).unwrap_or_else(|_| f.to_path_buf()) }.join("-")
});
match craft_docpath::resolve(&stored, document.as_deref()) {
Ok(path) => {
let local = path.is_absolute();
(path.to_string_lossy().into_owned(), local)
let file = path.is_absolute().then(|| path.to_string_lossy().into_owned());
let as_written = !file_url && craft_docpath::resolve(&stored, None).as_ref() == Ok(&path);
let link = if as_written { stored } else { path.to_string_lossy().into_owned() };
(link, file)
}
Err(_) => (stored, false),
Err(_) => (stored, None),
}
}
@@ -116,8 +125,8 @@ impl Files {
for n in xml.descendants().filter(|n| n.is_element() && n.tag_name().name() == "image") {
let Some(h) = href(n).filter(|h| !h.trim_start().starts_with("data:")) else { continue };
if !files.sources.contains_key(h) {
let (path, local) = target(h, opts.folder);
let read = if local { opts.read.and_then(|read| read(&path)) } else { None };
let (path, file) = target(h, opts.folder);
let read = file.and_then(|file| opts.read.and_then(|read| read(&file)));
let source = match read {
Some((bytes, link)) => files.source(bytes, link).ok_or("is not an image VectorCraft reads"),
None => Err("not found"),
@@ -229,17 +238,52 @@ mod tests {
let sep = std::path::MAIN_SEPARATOR;
let art = std::env::temp_dir().join("art");
let art = art.to_str().unwrap();
assert_eq!(target("photo%20one.png", Some(art)), (format!("{art}{sep}photo one.png"), true));
assert!(!target("photo.png", None).1);
assert_eq!(target("file:///C:/art/a.png", None), (r"C:\art\a.png".into(), cfg!(windows)));
assert_eq!(target("https://example.com/a.png", Some(art)), ("https://example.com/a.png".into(), false));
let found = format!("{art}{sep}photo one.png");
assert_eq!(target("photo%20one.png", Some(art)), (found.clone(), Some(found)));
assert!(target("photo.png", None).1.is_none());
assert_eq!(target("file:///C:/art/a.png", None), (r"C:\art\a.png".into(), cfg!(windows).then(|| r"C:\art\a.png".into())));
assert_eq!(target("https://example.com/a.png", Some(art)), ("https://example.com/a.png".into(), None));
#[cfg(unix)]
{
assert_eq!(target("/abs/photo.png", Some("/art")), ("/abs/photo.png".into(), true));
assert_eq!(target("file:///home/a.png", None), ("/home/a.png".into(), true));
assert_eq!(target("/abs/photo.png", Some("/art")), ("/abs/photo.png".into(), Some("/abs/photo.png".into())));
assert_eq!(target("file:///home/a.png", None), ("/home/a.png".into(), Some("/home/a.png".into())));
}
}
/// Without a folder, a link keeps its path as the href spells it, on every system; with one, a
/// drive path keeps it too. The file to read is the absolute path that `craft_docpath`
/// resolves on the running system.
#[test]
fn links_keep_hrefs_as_written() {
let art = std::env::temp_dir().join("art");
let art = art.to_str();
for h in ["/abs/photo.png", r"\abs\photo.png", "C:/art/a.png", r"C:\art\a.png", "img/a.png"] {
assert_eq!(target(h, None).0, h);
}
for h in ["C:/art/a.png", r"C:\art\a.png"] {
assert_eq!(target(h, art).0, h);
}
assert_eq!(target("/abs/photo%20one.png", None).0, "/abs/photo one.png", "decoded");
// The SVG's folder gives a rooted path its drive on Windows, and the link keeps that.
let (link, file) = target("/abs/photo.png", art);
let file = file.unwrap();
assert!(Path::new(&file).is_absolute() && Path::new(&file).ends_with("abs/photo.png"), "{file}");
assert_eq!(link, if cfg!(windows) { file } else { "/abs/photo.png".into() });
}
/// A folder that isn't absolute (`/tmp` on Windows, `art`) is taken from the working
/// directory, so the files its SVG links to are read.
#[test]
fn a_folder_that_is_not_absolute_is_made_absolute() {
let here = std::env::current_dir().unwrap();
let (link, file) = target("photo.png", Some("art"));
assert_eq!(file.as_deref().map(Path::new), Some(here.join("art").join("photo.png").as_path()));
assert_eq!(link, file.unwrap());
let (_, file) = target("photo.png", Some("/tmp"));
let file = file.unwrap();
assert!(Path::new(&file).is_absolute() && file.ends_with("photo.png"), "{file}");
}
#[test]
fn hrefs_to_other_computers_are_not_read() {
let art = std::env::temp_dir().join("art");
@@ -250,8 +294,8 @@ mod tests {
"file://192.0.2.1/share/x.png",
"%5C%5C192.0.2.1%5Cshare%5Cx.png",
] {
assert!(!target(h, art.to_str()).1, "{h}");
assert!(!target(h, None).1, "{h}");
assert!(target(h, art.to_str()).1.is_none(), "{h}");
assert!(target(h, None).1.is_none(), "{h}");
}
}
+2 -1
View File
@@ -524,7 +524,8 @@ pub type ReadFile<'a> = &'a (dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::L
/// warning), so it can be relinked.
#[derive(Clone, Copy, Default)]
pub struct ImportOptions<'a> {
/// The folder relative links are found in: the SVG file's own. Without it they can't be read.
/// The folder relative links are found in: the SVG file's own (taken from the working
/// directory when it isn't absolute). Without it they can't be read.
pub folder: Option<&'a str>,
/// Reads a linked file. Without it no file is read.
pub read: Option<ReadFile<'a>>,