mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Links: a link on a drive mapped to another share is not followed
craft-docpath follows a stored path only on this computer or on the share the document is on, but its check is lexical and cannot see mapped drives. fileio::document_path therefore followed a link stored as Y:\img\logo.png, where Y: is mapped to \\server\other, from any document: opening the document, links.check, Link Info and the linked-file watch read or stamped the file on that server. On Windows, document_path now reads the final path of the root folder of a link's drive (std::fs::canonicalize). Windows opens that root folder on the drive's server to answer, so the check still contacts the server once per lookup; no file on it is read, stamped or watched. When the final path names a share, the link is checked against the document's path on its share, and a link on another share is refused as on another computer, as a link stored with that share's own path is. Links on this computer's drives and on the document's share are followed as before, and a link on the document's own drive is not looked up. A document opened as \\?\Z:\... is looked up on its share as Z:\... is. Other systems are unchanged. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -1153,9 +1153,11 @@ pub(crate) fn absolute_path(path: &str) -> String {
|
||||
/// path that is still relative (no document) is refused, as the working directory isn't the
|
||||
/// 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.
|
||||
/// document's path on that share. A path on a drive mapped to a network share other than the
|
||||
/// document's ([`drive_share_path`]) is refused as on another computer. 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_on(stored, document, share_path, drive_share_path)
|
||||
}
|
||||
|
||||
/// [`document_path`], with `share` giving a document's path on the network share behind its drive.
|
||||
@@ -1180,6 +1182,68 @@ pub(crate) fn document_path_with(
|
||||
path.into_os_string().into_string().map_err(|_| Refused::Invalid)
|
||||
}
|
||||
|
||||
/// [`document_path_with`], also refusing a path on a drive that is mapped to a network share other
|
||||
/// than the document's (or, for a document on this computer or without a path, to any share) as
|
||||
/// [`craft_docpath::Refused::Network`]. `drive_share` gives a path's path on the share behind its
|
||||
/// drive (`Y:\art\x.png` on `\\server\other\art\x.png`), or `None` for a path on this computer. A
|
||||
/// path on the document's own drive is not looked up. A document written `\\?\Z:\…` is looked up
|
||||
/// on its share as `Z:\…` is.
|
||||
pub(crate) fn document_path_on(
|
||||
stored: &str,
|
||||
document: Option<&str>,
|
||||
share: impl Fn(&str) -> Option<String>,
|
||||
drive_share: impl FnOnce(&str) -> Option<String>,
|
||||
) -> std::result::Result<String, craft_docpath::Refused> {
|
||||
let share = |d: &str| share(d.strip_prefix(r"\\?\").filter(|d| drive_letter(d).is_some()).unwrap_or(d));
|
||||
let path = document_path_with(stored, document, share)?;
|
||||
let document = document.map(absolute_path);
|
||||
let Some(drive) = drive_letter(&path) else { return Ok(path) };
|
||||
if document.as_deref().and_then(drive_letter).is_some_and(|d| d.eq_ignore_ascii_case(&drive)) {
|
||||
return Ok(path);
|
||||
}
|
||||
let Some(on_share) = drive_share(&path) else { return Ok(path) };
|
||||
let home = document.map(|d| share(&d).unwrap_or(d));
|
||||
craft_docpath::resolve(&on_share, home.as_deref().map(std::path::Path::new))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// The drive letter of an absolute path on a drive (`C:\…`, `C:/…` or `\\?\C:\…`).
|
||||
fn drive_letter(path: &str) -> Option<char> {
|
||||
let mut chars = path.strip_prefix(r"\\?\").unwrap_or(path).chars();
|
||||
match (chars.next(), chars.next(), chars.next()) {
|
||||
(Some(l), Some(':'), Some('\\' | '/')) if l.is_ascii_alphabetic() => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The path of `path` (on a drive, `Y:\art\x.png`) on the network share the drive is mapped to
|
||||
/// (`\\server\other\art\x.png`), when the drive's root folder is on one and automation may read
|
||||
/// `path`. The root folder's final path ([`std::fs::canonicalize`]) names the share.
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn drive_share_path(path: &str) -> Option<String> {
|
||||
may_read(path).ok()?;
|
||||
drive_share_of(path, |root| std::fs::canonicalize(root).ok())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub(crate) fn drive_share_path(_: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The path of `path` on the network share its drive is mapped to, given `final_path`, which
|
||||
/// returns the final path of the drive's root folder (`Y:\`): as [`share_of`] gives it for a
|
||||
/// document, also for a path written `\\?\Y:\…`.
|
||||
#[cfg(any(windows, test))]
|
||||
pub(crate) fn drive_share_of(path: &str, final_path: impl FnOnce(&str) -> Option<std::path::PathBuf>) -> Option<String> {
|
||||
let path = path.strip_prefix(r"\\?\").unwrap_or(path);
|
||||
share_of(path, |folder| {
|
||||
let (root, rest) = (folder.get(..3)?, folder.get(3..)?);
|
||||
let root = final_path(root)?;
|
||||
let root = root.to_str()?.trim_end_matches('\\');
|
||||
Some(if rest.is_empty() { root.into() } else { format!(r"{root}\{rest}").into() })
|
||||
})
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -136,6 +136,46 @@ fn a_document_on_a_mapped_drive_is_on_the_share_behind_it() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The final path Windows gives a folder on a drive: `Z:` and `V:` are mapped to `\\server\share`,
|
||||
/// `Y:` is mapped to `\\server\other` and `W:` is mapped to `\\far\jobs`; `C:` and `D:` are volumes
|
||||
/// of this computer.
|
||||
fn mapped(folder: &str) -> Option<std::path::PathBuf> {
|
||||
let root = match folder.get(..1)?.to_ascii_uppercase().as_str() {
|
||||
"Z" | "V" => r"\\?\UNC\server\share",
|
||||
"Y" => r"\\?\UNC\server\other",
|
||||
"W" => r"\\?\UNC\far\jobs",
|
||||
"C" => r"\\?\C:",
|
||||
"D" => r"\\?\D:",
|
||||
_ => return None,
|
||||
};
|
||||
let rest = folder.get(2..)?.trim_start_matches(['\\', '/']).replace('/', "\\");
|
||||
Some(if rest.is_empty() { root.into() } else { format!(r"{root}\{rest}").into() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_on_a_mapped_drive_is_on_the_share_behind_it() {
|
||||
use crate::cmd::fileio::drive_share_of;
|
||||
// Only the drive's root folder is looked up, so a path in a folder that doesn't exist still gets
|
||||
// its path on the share.
|
||||
let mut asked = vec![];
|
||||
let mut ask = |root: &str| {
|
||||
asked.push(root.to_string());
|
||||
mapped(root)
|
||||
};
|
||||
assert_eq!(drive_share_of(r"Y:\art\gone\x.png", &mut ask).as_deref(), Some(r"\\server\other\art\gone\x.png"));
|
||||
assert_eq!(drive_share_of(r"Y:\x.png", &mut ask).as_deref(), Some(r"\\server\other\x.png"));
|
||||
assert_eq!(drive_share_of(r"\\?\Y:\x.png", &mut ask).as_deref(), Some(r"\\server\other\x.png"));
|
||||
assert_eq!(drive_share_of("y:/art/x.png", &mut ask).as_deref(), Some(r"\\server\other\art\x.png"));
|
||||
assert_eq!(asked, [r"Y:\", r"Y:\", r"Y:\", "y:/"]);
|
||||
// A volume of this computer, and a drive that can't be opened, give no path on a share.
|
||||
assert_eq!(drive_share_of(r"D:\art\x.png", mapped), None);
|
||||
assert_eq!(drive_share_of(r"Q:\art\x.png", mapped), None);
|
||||
// UNC, root-relative, relative and drive-relative paths, and a drive's root, are not looked up.
|
||||
for path in [r"\\server\other\x.png", r"\\?\UNC\server\other\x.png", "/Users/a/x.png", "x.png", "Y:x.png", r"Y:\"] {
|
||||
assert_eq!(drive_share_of(path, |r: &str| panic!("{path}: looked up {r}")), None, "{path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn links_on_a_mapped_drives_share_are_followed() {
|
||||
@@ -155,16 +195,69 @@ fn links_on_a_mapped_drives_share_are_followed() {
|
||||
assert_eq!(document_path_with(r"D:\art\logo.png", doc, unasked).as_deref(), Ok(r"D:\art\logo.png"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn links_on_a_drive_mapped_to_another_share_are_not_followed() {
|
||||
use crate::cmd::fileio::{document_path_on, drive_share_of, share_of};
|
||||
let share = |d: &str| share_of(d, mapped);
|
||||
let drive = |p: &str| drive_share_of(p, mapped);
|
||||
let refused = Err(craft_docpath::Refused::Network);
|
||||
// From a document on this computer, a drive mapped to any share is another computer.
|
||||
let local = Some(r"C:\art\doc.vectorcraft");
|
||||
for link in [r"Y:\img\logo.png", r"Z:\logo.png", r"\\?\Y:\logo.png", "file:///Y:/logo.png", r"Y:\gone\logo.png"] {
|
||||
assert_eq!(document_path_on(link, local, share, drive), refused, "{link}");
|
||||
}
|
||||
// Links on a mapped drive are refused from a document on a third share, given by its drive or
|
||||
// by its share, and from a document without a path.
|
||||
for doc in [Some(r"W:\jobs\doc.vectorcraft"), Some(r"\\far\jobs\doc.vectorcraft"), None] {
|
||||
assert_eq!(document_path_on(r"Y:\img\logo.png", doc, share, drive), refused, "{doc:?}");
|
||||
assert_eq!(document_path_on(r"Z:\img\logo.png", doc, share, drive), refused, "{doc:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn links_on_the_documents_share_or_this_computer_are_followed() {
|
||||
use crate::cmd::fileio::{document_path_on, drive_share_of, share_of};
|
||||
let share = |d: &str| share_of(d, mapped);
|
||||
let drive = |p: &str| drive_share_of(p, mapped);
|
||||
// Links on the document's share are followed: through another drive mapped to it, by the share's
|
||||
// own path, and from the document given by its share.
|
||||
let doc = Some(r"Z:\jobs\doc.vectorcraft");
|
||||
assert_eq!(document_path_on(r"V:\img\logo.png", doc, share, drive).as_deref(), Ok(r"V:\img\logo.png"));
|
||||
let by_share = Some(r"\\server\share\jobs\doc.vectorcraft");
|
||||
assert_eq!(document_path_on(r"Z:\img\logo.png", by_share, share, drive).as_deref(), Ok(r"Z:\img\logo.png"));
|
||||
assert_eq!(document_path_on(r"\\server\share\logo.png", doc, share, drive).as_deref(), Ok(r"\\server\share\logo.png"));
|
||||
// A document written `\\?\Z:\…` is on the same share.
|
||||
let verbatim = Some(r"\\?\Z:\jobs\doc.vectorcraft");
|
||||
assert_eq!(document_path_on(r"V:\img\logo.png", verbatim, share, drive).as_deref(), Ok(r"V:\img\logo.png"));
|
||||
assert_eq!(document_path_on(r"\\server\share\logo.png", verbatim, share, drive).as_deref(), Ok(r"\\server\share\logo.png"));
|
||||
assert_eq!(document_path_on(r"Y:\img\logo.png", verbatim, share, drive), Err(craft_docpath::Refused::Network));
|
||||
// Links on this computer and on a drive that can't be opened are followed, from a document
|
||||
// anywhere.
|
||||
for doc in [Some(r"C:\art\doc.vectorcraft"), doc, Some(r"\\far\jobs\doc.vectorcraft"), None] {
|
||||
assert_eq!(document_path_on(r"D:\art\logo.png", doc, share, drive).as_deref(), Ok(r"D:\art\logo.png"), "{doc:?}");
|
||||
assert_eq!(document_path_on(r"Q:\art\logo.png", doc, share, drive).as_deref(), Ok(r"Q:\art\logo.png"), "{doc:?}");
|
||||
}
|
||||
// A path on the document's own drive is not looked up.
|
||||
let unasked = |p: &str| -> Option<String> { panic!("looked up {p}") };
|
||||
assert_eq!(document_path_on(r"img\logo.png", doc, share, unasked).as_deref(), Ok(r"Z:\jobs\img\logo.png"));
|
||||
assert_eq!(document_path_on(r"z:\logo.png", doc, share, unasked).as_deref(), Ok(r"z:\logo.png"));
|
||||
assert_eq!(document_path_on(r"C:\x\logo.png", Some(r"C:\art\doc.vectorcraft"), share, unasked).as_deref(), Ok(r"C:\x\logo.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_off_a_mapped_drive_keeps_its_path() {
|
||||
use crate::cmd::fileio::share_path;
|
||||
use crate::cmd::fileio::{drive_share_path, 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.
|
||||
assert_eq!(drive_share_path(&dir.file("gone/x.png")), None);
|
||||
// Elsewhere than Windows, no document or link is looked up.
|
||||
if cfg!(not(windows)) {
|
||||
assert_eq!(share_path(r"Z:\jobs\doc.vectorcraft"), None);
|
||||
assert_eq!(drive_share_path(r"Y:\img\logo.png"), None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user