mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
MCP: hidden library names follow the platform's path rules
- Outside Windows a `\` stays part of a hidden library's or sent path's file name; another platform's absolute path is still split on `\`. - A hidden relative path is matched after leading `./` and `../` parts only when they start the path, so `/work/./data/icon.svg` keeps its text. - `ui.inspect` and `ui.docking` replies are hidden once, so a library at the top of the file system shows as `loaded/<file name>` with its separator.
This commit is contained in:
@@ -471,6 +471,15 @@ fn file_name(path: &str) -> String {
|
||||
std::path::Path::new(path).file_name().map_or_else(|| path.to_string(), |n| n.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// The file name a concealed library or path is shown by: the last part of `path` as this platform
|
||||
/// splits a path, so that outside Windows a `\` stays part of the name. Another platform's absolute
|
||||
/// path ([`crate::file_access::written_elsewhere`]), and a path this platform gives no file name (one
|
||||
/// that ends in `..`), are split after the last `/` or `\` instead. `path` itself when it has no such part.
|
||||
pub fn concealed_file_name(path: &str) -> String {
|
||||
let native = (!crate::file_access::written_elsewhere(path)).then(|| std::path::Path::new(path).file_name()).flatten();
|
||||
native.map_or_else(|| path.rsplit(['/', '\\']).find(|part| !part.is_empty()).unwrap_or(path).to_string(), |n| n.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// May the request being served read `path` ([`crate::file_access::check_read`])? Always, without
|
||||
/// roots in force. Another platform's absolute path ([`crate::file_access::written_elsewhere`]) is
|
||||
/// never readable under a read root.
|
||||
@@ -480,8 +489,8 @@ fn readable(path: &str) -> bool {
|
||||
|
||||
/// The loaded library ids (`loaded/<path>`) whose file the request being served may not read, each
|
||||
/// with the id a reply shows instead: `loaded/<file name>`, as a library loaded without a path is
|
||||
/// named. The file name is the last part of the path after either separator, and a path saved as
|
||||
/// another platform's absolute path counts as one the request may not read. The ids are those of the
|
||||
/// named. The file name is [`concealed_file_name`], and a path saved as another platform's absolute
|
||||
/// path counts as one the request may not read. The ids are those of the
|
||||
/// session's swatch, brush, symbol and graphic style libraries, then `others` (ids the UI state refers
|
||||
/// to). When another id already is or shows that id, the next free
|
||||
/// one of `loaded/<file name> (2)`, `(3)` and so on is shown, so that two libraries stay two. A library
|
||||
@@ -501,11 +510,7 @@ pub fn concealed_library_ids(s: &Session, others: impl IntoIterator<Item = Strin
|
||||
let mut taken = BTreeSet::new();
|
||||
for ids in [session, others] {
|
||||
for id in ids {
|
||||
let name = id.strip_prefix("loaded/").filter(|path| !path.is_empty()).map(|path| {
|
||||
// Either separator: a path saved on another platform counts as a path here too.
|
||||
let name = path.rsplit(['/', '\\']).find(|part| !part.is_empty()).unwrap_or(path).to_string();
|
||||
(name, path)
|
||||
});
|
||||
let name = id.strip_prefix("loaded/").filter(|path| !path.is_empty()).map(|path| (concealed_file_name(path), path));
|
||||
match name {
|
||||
Some((name, path)) if name != path && !readable(path) => hidden.push((id, name)),
|
||||
_ => {
|
||||
|
||||
@@ -238,6 +238,25 @@ fn swatch_library_replies_leave_out_paths_outside_the_read_root() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// Outside Windows a `\` is part of a file name: a concealed library saved at `<folder>/weird\name.gpl`
|
||||
/// is shown under its whole file name. Another platform's absolute path is still named by the part
|
||||
/// after its last `\`.
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn a_concealed_library_keeps_a_backslash_in_its_file_name() {
|
||||
let base = folder("backslash-name");
|
||||
let roots = AutomationRoots::new(&[&base.join("in")], &[]).unwrap();
|
||||
let native = format!("loaded/{}/weird\\name.gpl", text(&base.join("a")));
|
||||
let foreign = r"loaded/C:\Users\someone\other.gpl".to_string();
|
||||
let s = Session::new();
|
||||
let mut hidden = confine(roots.as_ref(), || cmd::swatchlib::concealed_library_ids(&s, [native.clone(), foreign.clone()]));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
hidden.sort();
|
||||
let mut want = vec![(foreign, "loaded/other.gpl".to_string()), (native, "loaded/weird\\name.gpl".to_string())];
|
||||
want.sort();
|
||||
assert_eq!(hidden, want);
|
||||
}
|
||||
|
||||
/// A concealed library keeps the id a client was shown when another library with the same file name,
|
||||
/// whose full id sorts before it, is loaded later: the id still finds the first library.
|
||||
#[test]
|
||||
|
||||
@@ -251,7 +251,7 @@ pub fn inspect(app: &VectorcraftApp, ctx: &egui::Context) -> Value {
|
||||
fields.remove("floating_panels");
|
||||
leave_out_unreadable_paths(fields);
|
||||
}
|
||||
let mut inspected = json!({
|
||||
json!({
|
||||
"scriptRunning": app.session.script_running(),
|
||||
"scriptReport": app.session.shown_script_report(),
|
||||
"tool": app.session.tool_id(),
|
||||
@@ -307,9 +307,7 @@ pub fn inspect(app: &VectorcraftApp, ctx: &egui::Context) -> Value {
|
||||
// A native file dialog open off the UI thread (`open`, `place`, `save`, `folder`), waiting
|
||||
// for the person at the computer: what asked for it runs again when it answers.
|
||||
"fileDialog": app.picks.waiting_kind(),
|
||||
});
|
||||
hide_library_ids(&mut inspected, &unreadable_library_ids(app));
|
||||
inspected
|
||||
})
|
||||
}
|
||||
|
||||
/// The fields of the UI state, by their keys in `ui.inspect`'s `ui`, that hold panel ids: the panel
|
||||
@@ -435,7 +433,10 @@ fn collect_sent_paths(v: &Value, path_param: bool, out: &mut Vec<(String, String
|
||||
Some(None) => percent_decoded(&s[5..]),
|
||||
None => s.to_string(),
|
||||
};
|
||||
let Some(name) = path.rsplit(['/', '\\']).find(|part| !part.is_empty()).filter(|name| *name != path) else { return };
|
||||
let name = vectorcraft_engine::cmd::swatchlib::concealed_file_name(&path);
|
||||
if name == path {
|
||||
return;
|
||||
}
|
||||
let pair = (format!("loaded/{path}"), format!("loaded/{name}"));
|
||||
if !out.contains(&pair) {
|
||||
out.push(pair);
|
||||
@@ -709,17 +710,22 @@ fn decoded_with_spans(text: &str) -> Option<(String, Vec<(usize, usize)>)> {
|
||||
}
|
||||
|
||||
/// Is `from`, a relative path (one that does not start with a separator, a drive or `loaded/`), at
|
||||
/// `text[at..]` the end of a longer path: is it preceded by a letter, a digit, `_`, `-`, `.`, `~` or a
|
||||
/// separator other than one after `.` or `..`? `/work/data/icon.svg` does not hold the path
|
||||
/// `data/icon.svg`; `./data/icon.svg` does.
|
||||
/// `text[at..]` the end of a longer path: after any `.` and `..` parts just before it, is it preceded by
|
||||
/// a letter, a digit, `_`, `-`, `.`, `~` or a separator? `/work/data/icon.svg` and
|
||||
/// `/work/./data/icon.svg` do not hold the path `data/icon.svg`; `./data/icon.svg` and
|
||||
/// `../data/icon.svg` do.
|
||||
fn ends_a_longer_path(text: &str, at: usize, from: &str) -> bool {
|
||||
let absolute =
|
||||
from.starts_with(['/', '\\']) || from.starts_with("loaded/") || matches!(from.as_bytes(), [drive, b':', ..] if drive.is_ascii_alphabetic());
|
||||
let before = &text[..at];
|
||||
!absolute
|
||||
&& !before.ends_with("./")
|
||||
&& !before.ends_with(".\\")
|
||||
&& before.ends_with(|c: char| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | '~' | '/' | '\\'))
|
||||
let mut before = &text[..at];
|
||||
while let Some(head) = before.strip_suffix(['/', '\\']) {
|
||||
let Some(rest) = head.strip_suffix("..").or_else(|| head.strip_suffix('.')) else { break };
|
||||
before = rest;
|
||||
if !before.ends_with(['/', '\\']) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
!absolute && before.ends_with(|c: char| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | '~' | '/' | '\\'))
|
||||
}
|
||||
|
||||
/// Does text that follows a path continue it: a letter, a digit, `_` or a separator, after any `.`,
|
||||
@@ -1191,12 +1197,7 @@ fn dispatch(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest)
|
||||
"engine.commands" => ok(all_commands(app)),
|
||||
"document.inspect" => wrap(app.run("document.inspect", p.clone())),
|
||||
"ui.inspect" => ok(inspect(app, ctx)),
|
||||
"ui.docking" => {
|
||||
let mut docking = crate::panel_docking::inspect(app, ctx);
|
||||
let ids = unreadable_library_ids(app);
|
||||
hide_library_ids(&mut docking, &ids);
|
||||
ok(docking)
|
||||
}
|
||||
"ui.docking" => ok(crate::panel_docking::inspect(app, ctx)),
|
||||
// A request that stops with a panic quoting the status text, after clearing the Color Guide's
|
||||
// limit with `forget`.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -278,14 +278,54 @@ fn another_platforms_absolute_path_is_unreadable() {
|
||||
assert_eq!(sent, [(format!("loaded/{path}"), "loaded/private.svg".to_string())]);
|
||||
}
|
||||
|
||||
/// A hidden relative path is hidden where it stands alone or after `./`, and stays where it ends a
|
||||
/// longer path.
|
||||
/// A hidden relative path is hidden where it stands alone or after leading `./` and `../` parts, and
|
||||
/// stays where it ends a longer path, also one with a `.` or `..` part before it.
|
||||
#[test]
|
||||
fn a_reply_keeps_a_longer_path_that_ends_with_a_hidden_relative_path() {
|
||||
let hidden = hiding(&[("loaded/data/icon.svg", "loaded/icon.svg")], &[]);
|
||||
let mut reply = json!({"result": "kept /work/data/icon.svg and xdata/icon.svg, hid ./data/icon.svg, \"data/icon.svg\" and loaded/data/icon.svg"});
|
||||
hide_under_a_root("relative", &mut reply, &hidden);
|
||||
assert_eq!(reply["result"], "kept /work/data/icon.svg and xdata/icon.svg, hid ./icon.svg, \"icon.svg\" and loaded/icon.svg", "{reply}");
|
||||
let mut reply = json!({"result": "kept /allowed/./data/icon.svg, /allowed/../data/icon.svg and a./data/icon.svg, hid ../data/icon.svg and './../data/icon.svg'"});
|
||||
hide_under_a_root("relative-dots", &mut reply, &hidden);
|
||||
assert_eq!(
|
||||
reply["result"], "kept /allowed/./data/icon.svg, /allowed/../data/icon.svg and a./data/icon.svg, hid ../icon.svg and './../icon.svg'",
|
||||
"{reply}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Outside Windows a `\` is part of a file name: a sent path outside the read root is shown by its
|
||||
/// whole file name.
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn a_sent_path_keeps_a_backslash_in_its_file_name() {
|
||||
let root = std::env::temp_dir().join(format!("vc-sent-backslash-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap();
|
||||
let mut sent = Vec::new();
|
||||
vectorcraft_engine::file_access::confine(roots.as_ref(), || super::sent_paths(&json!({"path": "/out/weird\\name.svg"}), &mut sent));
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
assert_eq!(sent, [("loaded//out/weird\\name.svg".to_string(), "loaded/weird\\name.svg".to_string())]);
|
||||
}
|
||||
|
||||
/// `ui.inspect` and `ui.docking` show a hidden library whose file is at the top of the file system
|
||||
/// (`/b.svg`) as `loaded/b.svg`, with its separator.
|
||||
#[test]
|
||||
fn a_hidden_library_at_the_top_of_the_file_system_keeps_its_shown_id() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
let panel = "library:swatches:loaded//b.svg".to_string();
|
||||
app.ui.color_guide_limit = "loaded//b.svg".to_string();
|
||||
app.ui.open_panel = Some(panel);
|
||||
let root = std::env::temp_dir().join(format!("vc-top-level-library-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap();
|
||||
let (inspected, docking) = vectorcraft_engine::file_access::confine(roots.as_ref(), || {
|
||||
(input_request(&mut app, "ui.inspect", json!({})), input_request(&mut app, "ui.docking", json!({})))
|
||||
});
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
assert_eq!(inspected["result"]["ui"]["color_guide_limit"], "loaded/b.svg", "{inspected}");
|
||||
assert_eq!(inspected["result"]["ui"]["open_panel"], "library:swatches:loaded/b.svg", "{inspected}");
|
||||
assert_eq!(docking["result"]["flyout"], "library:swatches:loaded/b.svg", "{docking}");
|
||||
}
|
||||
|
||||
/// A path outside the read root that a request sent is shown by its file name in a reply that succeeded
|
||||
|
||||
+2
-1
@@ -156,7 +156,8 @@ the suite) keep the agent to the folders you give it:
|
||||
Color Guide's `ui.color_guide_limit`, a closed panel's key in `ui.docking_hidden` and the `library` and `panel`
|
||||
params of a recorded action's steps in `ui.action_sets`, also when its panel is closed. A path written with
|
||||
either separator counts as a path, also one saved on another platform, and outside Windows a path that starts
|
||||
with a drive (`C:`) or a `\` counts as outside the roots. When another hidden library has the same
|
||||
with a drive (`C:`) or a `\` counts as outside the roots. Outside Windows, any other path keeps a `\` in its
|
||||
file name (`/libs/a\b.ase` shows as `loaded/a\b.ase`). When another hidden library has the same
|
||||
file name, the libraries take `loaded/<file name>`, `loaded/<file name> (2)`, `(3)` and so on in the order they
|
||||
are first shown. Libraries first shown in the same reply are numbered in the order of their full ids, the
|
||||
libraries loaded in this session first, then those only the UI state names. Each one keeps its id for the rest
|
||||
|
||||
Reference in New Issue
Block a user