mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
SVG import: hrefs that need no folder stay as written, and images in a non-absolute folder are read
SVG import checks each linked image's path with craft-docpath, and two of its results were wrong, most visibly on Windows. A missing image's link kept the path that craft-docpath returned, with its separators rewritten: an href of /abs/photo.png became \abs\photo.png on Windows, and SVG export with linked images writes that back as the href. A folder that is not absolute, such as /tmp on Windows or a relative folder on any system, made each relative href, and on Windows each href rooted without a drive, resolve to a path that is not absolute, so those images were not read. Two vectorcraft-svg tests failed on Windows because of this. A link now keeps the href as written (decoded) when the href resolves to the same path without the SVG's folder, and keeps the path found when it needs the folder: a relative path, or on Windows a path rooted without a drive. A file: URL keeps the path it names. The folder is made absolute against the working directory when it is not, as the engine's reader does with the document's path. Hrefs that were refused are still refused: paths on other computers, devices and URLs are not read.
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
Reference in New Issue
Block a user