mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Links: a document opened from a mapped drive finds links on its share
craft-docpath follows a network path only on the share the document is on, and its caller contract asks for a document opened from a mapped drive to be passed by its UNC path. fileio::document_path passed the drive path as given, so a document opened as Z:\jobs\doc.vectorcraft treated a link stored as \\server\share\img\logo.png, on the share behind Z:, as on another computer: the image was missing, and document.open, links.check and links.info gave refused: "the path is on another computer". On Windows, when a stored path is refused as on another computer and the document is on a drive, document_path now reads the final path of the document's folder (std::fs::canonicalize). When that names a share (\\?\UNC\server\share\...), the path is checked again against the document's path on that share. Links on other computers stay refused, and paths that were followed resolve as before. Opening a document, Link Info, links.check and the linked-file watch all use it. Other systems are unchanged. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -1103,14 +1103,74 @@ pub(crate) fn absolute_path(path: &str) -> String {
|
||||
/// stored in, when it has one: a relative path is taken from its folder. Only files on this
|
||||
/// computer, or on the network share the document is on, are followed (see `craft_docpath`); a
|
||||
/// path that is still relative (no document) is refused, as the working directory isn't the
|
||||
/// document's choice. Every stored path reaches the file system through this.
|
||||
/// document's choice. On Windows, a document on a mapped drive is on the share behind the drive
|
||||
/// ([`share_path`]): a path refused as on another computer is checked again against the
|
||||
/// document's path on that share. Every stored path reaches the file system through this.
|
||||
pub(crate) fn document_path(stored: &str, document: Option<&str>) -> std::result::Result<String, craft_docpath::Refused> {
|
||||
document_path_with(stored, document, share_path)
|
||||
}
|
||||
|
||||
/// [`document_path`], with `share` giving a document's path on the network share behind its drive.
|
||||
pub(crate) fn document_path_with(
|
||||
stored: &str,
|
||||
document: Option<&str>,
|
||||
share: impl FnOnce(&str) -> Option<String>,
|
||||
) -> std::result::Result<String, craft_docpath::Refused> {
|
||||
use craft_docpath::Refused;
|
||||
let document = document.map(absolute_path);
|
||||
let path = craft_docpath::resolve(stored, document.as_deref().map(std::path::Path::new))?;
|
||||
let resolve = |document: Option<&str>| craft_docpath::resolve(stored, document.map(std::path::Path::new));
|
||||
let path = match resolve(document.as_deref()) {
|
||||
Err(Refused::Network) => {
|
||||
let share = document.as_deref().and_then(share).ok_or(Refused::Network)?;
|
||||
resolve(Some(share.as_str()))?
|
||||
}
|
||||
path => path?,
|
||||
};
|
||||
if !path.is_absolute() {
|
||||
return Err(craft_docpath::Refused::Invalid);
|
||||
return Err(Refused::Invalid);
|
||||
}
|
||||
path.into_os_string().into_string().map_err(|_| craft_docpath::Refused::Invalid)
|
||||
path.into_os_string().into_string().map_err(|_| Refused::Invalid)
|
||||
}
|
||||
|
||||
/// The path of `document` (on a drive, `Z:\jobs\doc.vectorcraft`) on the network share the drive is
|
||||
/// mapped to (`\\server\share\jobs\doc.vectorcraft`), when its folder is on one and automation may
|
||||
/// read the folder. The folder's final path ([`std::fs::canonicalize`]) names the share as
|
||||
/// `\\?\UNC\server\share\…`.
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn share_path(document: &str) -> Option<String> {
|
||||
share_of(document, |folder| {
|
||||
may_read(folder).ok()?;
|
||||
std::fs::canonicalize(folder).ok()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub(crate) fn share_path(_: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The path of `document` on the network share its drive is mapped to, given `final_path`, which
|
||||
/// returns the final path of a folder: `None` when `document` is not on a drive (`C:\` or `C:/`)
|
||||
/// or `final_path` gives no share (`\\?\UNC\server\share\…`), such as a folder on a local volume.
|
||||
#[cfg(any(windows, test))]
|
||||
pub(crate) fn share_of(document: &str, final_path: impl FnOnce(&str) -> Option<std::path::PathBuf>) -> Option<String> {
|
||||
let mut chars = document.chars();
|
||||
if !matches!((chars.next(), chars.next(), chars.next()), (Some(l), Some(':'), Some('\\' | '/')) if l.is_ascii_alphabetic()) {
|
||||
return None;
|
||||
}
|
||||
// The drive's root folder keeps its separator (`Z:\`).
|
||||
let split = document.rfind(['\\', '/'])?;
|
||||
let (folder, name) = (document.get(..split.max(3))?, document.get(split + 1..)?);
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let target = final_path(folder)?;
|
||||
let target = target.to_str()?;
|
||||
let unc = target.get(..8).filter(|p| p.eq_ignore_ascii_case(r"\\?\UNC\")).and(target.get(8..))?.trim_end_matches('\\');
|
||||
if unc.split('\\').filter(|c| !c.is_empty()).count() < 2 {
|
||||
return None;
|
||||
}
|
||||
Some(format!(r"\\{unc}\{name}"))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -88,6 +88,86 @@ fn linked_files_on_other_computers_are_never_read() {
|
||||
assert!(s.link_stamps.keys().all(|(_, p)| !far.contains(p)), "{:?}", s.link_stamps.keys());
|
||||
}
|
||||
|
||||
/// The final path Windows gives a folder of the drive `Z:` mapped to `\\server\share`. The
|
||||
/// share's root folder has no trailing separator (`\\?\UNC\server\share`).
|
||||
fn on_share(folder: &str) -> Option<std::path::PathBuf> {
|
||||
let rest = folder.get(2..)?.trim_start_matches(['\\', '/']).replace('/', "\\");
|
||||
let root = r"\\?\UNC\server\share";
|
||||
Some(if rest.is_empty() { root.into() } else { format!(r"{root}\{rest}").into() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_on_a_mapped_drive_is_on_the_share_behind_it() {
|
||||
use crate::cmd::fileio::share_of;
|
||||
// craft-docpath refuses a link on the share when the document is given by its drive letter.
|
||||
let link = r"\\server\share\img\logo.png";
|
||||
assert_eq!(craft_docpath::resolve(link, Some(std::path::Path::new(r"Z:\jobs\doc.vectorcraft"))), Err(craft_docpath::Refused::Network));
|
||||
assert!(craft_docpath::resolve(link, Some(std::path::Path::new(r"\\server\share\jobs\doc.vectorcraft"))).is_ok());
|
||||
|
||||
// The document's path on the share, from its folder's final path.
|
||||
let mut asked = vec![];
|
||||
let mut ask = |folder: &str| {
|
||||
asked.push(folder.to_string());
|
||||
on_share(folder)
|
||||
};
|
||||
assert_eq!(share_of(r"Z:\jobs\doc.vectorcraft", &mut ask).as_deref(), Some(r"\\server\share\jobs\doc.vectorcraft"));
|
||||
assert_eq!(share_of(r"Z:\doc.vectorcraft", &mut ask).as_deref(), Some(r"\\server\share\doc.vectorcraft"));
|
||||
assert_eq!(share_of("Z:/jobs/doc.vectorcraft", &mut ask).as_deref(), Some(r"\\server\share\jobs\doc.vectorcraft"));
|
||||
assert_eq!(asked, [r"Z:\jobs", r"Z:\", "Z:/jobs"]);
|
||||
let unc = |p: &'static str| move |_: &str| Some(std::path::PathBuf::from(p));
|
||||
assert_eq!(share_of(r"Z:\doc.vectorcraft", unc(r"\\?\unc\server\share\")).as_deref(), Some(r"\\server\share\doc.vectorcraft"));
|
||||
|
||||
// A folder on a local volume (also behind a `subst` drive), a folder that can't be opened,
|
||||
// and a final path without a share name give no path on a share.
|
||||
assert_eq!(share_of(r"C:\art\doc.vectorcraft", unc(r"\\?\C:\art")), None);
|
||||
assert_eq!(share_of(r"S:\doc.vectorcraft", unc(r"\\?\D:\real")), None);
|
||||
assert_eq!(share_of(r"Z:\gone\doc.vectorcraft", |_| None), None);
|
||||
assert_eq!(share_of(r"Z:\doc.vectorcraft", unc(r"\\?\UNC\server")), None);
|
||||
// A document given by its share, or not by a drive, is not looked up.
|
||||
for doc in [
|
||||
r"\\server\share\doc.vectorcraft",
|
||||
r"\\?\Z:\doc.vectorcraft",
|
||||
"/Users/a/doc.vectorcraft",
|
||||
"doc.vectorcraft",
|
||||
"Z:doc.vectorcraft",
|
||||
r"Z:\jobs\",
|
||||
] {
|
||||
assert_eq!(share_of(doc, |f: &str| panic!("{doc}: looked up {f}")), None, "{doc}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn links_on_a_mapped_drives_share_are_followed() {
|
||||
use crate::cmd::fileio::{document_path_with, share_of};
|
||||
let doc = Some(r"Z:\jobs\doc.vectorcraft");
|
||||
let share = |d: &str| share_of(d, on_share);
|
||||
// A link on the share behind the drive is followed; one on another share still isn't.
|
||||
assert_eq!(document_path_with(r"\\server\share\img\logo.png", doc, share).as_deref(), Ok(r"\\server\share\img\logo.png"));
|
||||
assert_eq!(document_path_with(r"\\SERVER\Share\logo.png", doc, share).as_deref(), Ok(r"\\SERVER\Share\logo.png"));
|
||||
assert_eq!(document_path_with(r"\\other\share\logo.png", doc, share), Err(craft_docpath::Refused::Network));
|
||||
assert_eq!(document_path_with("file://server/share/logo.png", doc, share), Err(craft_docpath::Refused::Network));
|
||||
// On a local volume, the link stays refused.
|
||||
assert_eq!(document_path_with(r"\\server\share\logo.png", doc, |_| None), Err(craft_docpath::Refused::Network));
|
||||
// Links that were followed resolve as before, without looking the share up.
|
||||
let unasked = |d: &str| -> Option<String> { panic!("looked up {d}") };
|
||||
assert_eq!(document_path_with(r"img\logo.png", doc, unasked).as_deref(), Ok(r"Z:\jobs\img\logo.png"));
|
||||
assert_eq!(document_path_with(r"D:\art\logo.png", doc, unasked).as_deref(), Ok(r"D:\art\logo.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_off_a_mapped_drive_keeps_its_path() {
|
||||
use crate::cmd::fileio::share_path;
|
||||
let dir = Folder::new("docpath-share");
|
||||
// A folder on this computer's own disk, and one that doesn't exist.
|
||||
assert_eq!(share_path(&dir.file("doc.vectorcraft")), None);
|
||||
assert_eq!(share_path(&dir.file("gone/doc.vectorcraft")), None);
|
||||
// Elsewhere than Windows, no document is looked up.
|
||||
if cfg!(not(windows)) {
|
||||
assert_eq!(share_path(r"Z:\jobs\doc.vectorcraft"), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_images_on_other_computers_are_never_read() {
|
||||
let dir = Folder::new("docpath-svg");
|
||||
|
||||
+3
-1
@@ -2707,7 +2707,9 @@ modification time and hash, and keeps a low-resolution preview (at most 256 px a
|
||||
the preview instead of the pixels, plus the path relative to the saved file. `document.open` reads
|
||||
the linked files again, looking for each at its path, then at its relative path and by name in the document's folder
|
||||
(so a folder moved with its links still opens). Only files on this computer, or on the network share the document is
|
||||
on, are looked at: a path to another computer, a device or a URL counts as missing. The result lists `missingLinks`
|
||||
on, are looked at: a path to another computer, a device or a URL counts as missing. On Windows, a document opened from
|
||||
a mapped drive (`Z:\…`) is on the share the drive is mapped to, and links on that share (`\\server\share\…`) are
|
||||
looked at. The result lists `missingLinks`
|
||||
(their images show the preview), `modifiedLinks` (left as they were; read again only with the preference
|
||||
`updateLinks: "automatically"`, then they are in `updatedLinks`), each as `{name, path, ids}`, with `refused` (why the
|
||||
path isn't followed) when it isn't. The desktop app also checks the active document's links when its window
|
||||
|
||||
@@ -735,6 +735,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- File › Place reads Word `.docx` documents as area type with the same formatting, taken from the document defaults, the paragraph and character styles (their toggle properties reversing each other) and direct formatting, the theme's fonts included. Table rows are paragraphs with their cells separated by tabs; hidden and deleted text, field instructions, pictures, headers, footers and notes are left out (`file.place`, `crates/engine/src/tests_textimport.rs`).
|
||||
- On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
|
||||
- Missing linked files can be looked for in a folder and its subfolders: Find in Folder… in the Linked File Not Found dialog and in the Links panel menu lists the files with each one's name, those with the size and contents saved with the link first, and relinks the files chosen, copying an image's file into the document's folder when asked, without replacing a file there (`links.findFiles`, `links.relink {copy}`). In the Links panel it looks for the selected linked files when any are selected.
|
||||
- On Windows, a document opened from a mapped network drive finds its links stored with the share's own path (`\\server\share\…`), as the same document opened from the share does; links on other computers stay missing.
|
||||
- In the Links panel, Shift-click selects a range of links and Ctrl/Cmd-click adds or removes one.
|
||||
- Linked files another app changes while their document is open follow Preferences › File Handling › Update Links, as they do when it opens: Automatically reads them again (an Update Links undo step), Ask When Modified offers to, Manually leaves them modified in the Links panel. The desktop app compares the active document’s linked files by size and modification time every two seconds and when its window comes back to the front, on a worker thread; a file still being written is tried again (`links.updateChanged`).
|
||||
- Import and paste, 2026-10-13: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499).
|
||||
|
||||
Reference in New Issue
Block a user