mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 14:20:24 +00:00
MCP: hidden library paths are matched before the paths a request sent
A sent path whose text overlapped part of a hidden library's path could keep that path from being hidden; the hidden libraries' ids and paths are now matched first. A hidden relative path is also hidden where a reply shows the absolute path it reads as from the working folder, and a hidden library sent by that other form shows as the sent path does.
This commit is contained in:
@@ -356,7 +356,7 @@ fn unreadable_library_ids(app: &VectorcraftApp) -> Hiding {
|
||||
.collect();
|
||||
// The longest first, so that an id that begins another one does not replace part of it.
|
||||
hidden.sort_by_key(|(id, _)| std::cmp::Reverse(id.len()));
|
||||
Hiding { hidden, readable: readable_ids }
|
||||
Hiding { hidden, readable: readable_ids, sent: Vec::new() }
|
||||
}
|
||||
|
||||
/// What a reply hides ([`hide_library_ids`]).
|
||||
@@ -368,6 +368,9 @@ struct Hiding {
|
||||
/// The other loaded library ids the UI state and the session hold: an occurrence of a hidden id or
|
||||
/// path that begins one of them stays ([`begins_known_id`]).
|
||||
readable: std::collections::BTreeSet<String>,
|
||||
/// The paths outside the read roots that the request sent ([`sent_paths`]), each as a hidden
|
||||
/// library id with the id shown. They are matched after the hidden libraries.
|
||||
sent: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Hiding {
|
||||
@@ -387,16 +390,26 @@ impl Hiding {
|
||||
self.hidden.sort_by_key(|(id, _)| std::cmp::Reverse(id.len()));
|
||||
}
|
||||
|
||||
/// Adds the hidden ids of `pairs`, each with the id shown, in place of each id this one holds for
|
||||
/// the same path: written with either separator, and in any case of ASCII letters with `fold`.
|
||||
/// Adds the sent paths of `pairs`, each as a hidden id with the id shown. A hidden id this one holds
|
||||
/// for the same path (written with either separator, relative or as the absolute path it reads as
|
||||
/// ([`absolute_form`]), and in any case of ASCII letters with `fold`) is shown as the sent path is.
|
||||
fn set_hidden(&mut self, pairs: Vec<(String, String)>, fold: bool) {
|
||||
let key = |id: &str| {
|
||||
let id = id.replace('\\', "/");
|
||||
let path = id.strip_prefix("loaded/").unwrap_or(id);
|
||||
let id = absolute_form(path).unwrap_or_else(|| path.to_string()).replace('\\', "/");
|
||||
if fold { id.to_ascii_lowercase() } else { id }
|
||||
};
|
||||
let sent: Vec<String> = pairs.iter().map(|(id, _)| key(id)).collect();
|
||||
self.hidden.retain(|(id, _)| !sent.contains(&key(id)));
|
||||
self.add_hidden(pairs);
|
||||
for (id, shown) in &mut self.hidden {
|
||||
if let Some(at) = sent.iter().position(|k| *k == key(id)) {
|
||||
shown.clone_from(&pairs[at].1);
|
||||
}
|
||||
}
|
||||
for pair in pairs {
|
||||
if !self.sent.iter().any(|(id, _)| *id == pair.0) {
|
||||
self.sent.push(pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,31 +490,48 @@ fn collect_library_panel_ids(v: &Value, ids: &mut std::collections::BTreeSet<Str
|
||||
/// Windows the ids and paths are matched in any case of ASCII letters.
|
||||
fn hide_library_ids(v: &mut Value, hiding: &Hiding) {
|
||||
let pairs = hidden_text(hiding);
|
||||
if pairs.is_empty() {
|
||||
if pairs.iter().all(Vec::is_empty) {
|
||||
return;
|
||||
}
|
||||
replace_in_value(v, &Rewrite { pairs, hiding, fold: cfg!(windows) });
|
||||
}
|
||||
|
||||
/// The text [`hide_library_ids`] replaces, longest first: each hidden id with the id shown, and the
|
||||
/// path in it with the file name shown, the path written with either separator too, and as `{:?}`
|
||||
/// quotes it (each `\`, quote and control character escaped).
|
||||
fn hidden_text(hiding: &Hiding) -> Vec<(String, String)> {
|
||||
/// The text [`hide_library_ids`] replaces, in two groups: the hidden libraries' text, then the text of
|
||||
/// the paths the request sent ([`Hiding::sent`]), each longest first. For each id, that is the id with
|
||||
/// the id shown, and the path in it with the file name shown, the path written with either separator
|
||||
/// too, and as `{:?}` quotes it (each `\`, quote and control character escaped). A relative path is
|
||||
/// also replaced as the absolute path the file access check reads it as, against the working folder.
|
||||
fn hidden_text(hiding: &Hiding) -> Vec<Vec<(String, String)>> {
|
||||
vec![text_pairs(&hiding.hidden), text_pairs(&hiding.sent)]
|
||||
}
|
||||
|
||||
/// The absolute path that `path`, a relative path (one that does not start with a separator, a drive
|
||||
/// or `~`), reads as from the working folder, as the file access check reads it; `None` for any other
|
||||
/// path.
|
||||
fn absolute_form(path: &str) -> Option<String> {
|
||||
let relative = !path.starts_with(['/', '\\', '~']) && !matches!(path.as_bytes(), [drive, b':', ..] if drive.is_ascii_alphabetic());
|
||||
relative.then(|| std::path::absolute(path).ok()).flatten().map(|p| p.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// The text [`hidden_text`] replaces for the ids of `ids`, longest first.
|
||||
fn text_pairs(ids: &[(String, String)]) -> Vec<(String, String)> {
|
||||
let quoted = |s: &str| {
|
||||
let q = format!("{s:?}");
|
||||
q[1..q.len() - 1].to_string()
|
||||
};
|
||||
let mut pairs: Vec<(String, String)> = Vec::new();
|
||||
for (id, shown) in &hiding.hidden {
|
||||
for (id, shown) in ids {
|
||||
let path = &id["loaded/".len()..];
|
||||
let name = shown.strip_prefix("loaded/").unwrap_or(shown);
|
||||
let (forward, back) = (path.replace('\\', "/"), path.replace('/', "\\"));
|
||||
let debug = [quoted(path), quoted(&forward), quoted(&back)].map(|p| (p, quoted(name)));
|
||||
let plain = [path.to_string(), forward, back].map(|p| (p, name.to_string()));
|
||||
for (path, name) in plain.into_iter().chain(debug) {
|
||||
for (from, to) in [(format!("loaded/{path}"), format!("loaded/{name}")), (path, name)] {
|
||||
if !pairs.iter().any(|(f, _)| *f == from) {
|
||||
pairs.push((from, to));
|
||||
for path in std::iter::once(path.to_string()).chain(absolute_form(path)) {
|
||||
let (forward, back) = (path.replace('\\', "/"), path.replace('/', "\\"));
|
||||
let debug = [quoted(&path), quoted(&forward), quoted(&back)].map(|p| (p, quoted(name)));
|
||||
let plain = [path, forward, back].map(|p| (p, name.to_string()));
|
||||
for (path, name) in plain.into_iter().chain(debug) {
|
||||
for (from, to) in [(format!("loaded/{path}"), format!("loaded/{name}")), (path, name)] {
|
||||
if !pairs.iter().any(|(f, _)| *f == from) {
|
||||
pairs.push((from, to));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,10 +540,10 @@ fn hidden_text(hiding: &Hiding) -> Vec<(String, String)> {
|
||||
pairs
|
||||
}
|
||||
|
||||
/// What [`replace_in_value`] writes: the text to replace, what the reply hides, and whether to match
|
||||
/// in any case of ASCII letters.
|
||||
/// What [`replace_in_value`] writes: the groups of text to replace ([`hidden_text`]), what the reply
|
||||
/// hides, and whether to match in any case of ASCII letters.
|
||||
struct Rewrite<'a> {
|
||||
pairs: Vec<(String, String)>,
|
||||
pairs: Vec<Vec<(String, String)>>,
|
||||
hiding: &'a Hiding,
|
||||
fold: bool,
|
||||
}
|
||||
@@ -569,20 +599,23 @@ fn occurrences(text: &str, needle: &str, fold: bool) -> Vec<usize> {
|
||||
/// `s` with the `from` texts of the rewrite's pairs written as their `to` texts, at each occurrence
|
||||
/// that does not begin another loaded library's id ([`begins_known_id`]). A hidden id or path written
|
||||
/// with `%XX` escapes (in a `file:` URL) is replaced too; the escapes around it stay. Every occurrence
|
||||
/// is found in `s` as it was given, longest `from` first, and one that overlaps an occurrence already
|
||||
/// found, or a loaded library's id that one began, stays: a replacement's text is not searched again.
|
||||
/// With the rewrite's `fold`, the ids and paths as written are found first, so that two libraries whose
|
||||
/// paths differ only in letter case keep their own ids, and then the ones written in another case.
|
||||
/// `None` when nothing changed.
|
||||
/// is found in `s` as it was given, the hidden libraries' text before the sent paths' text, each group
|
||||
/// longest `from` first, and one that overlaps an occurrence already found, or a loaded library's id
|
||||
/// that one began, stays: a replacement's text is not searched again. With the rewrite's `fold`, the
|
||||
/// ids and paths of a group as written are found first, so that two libraries whose paths differ only
|
||||
/// in letter case keep their own ids, and then the ones written in another case. `None` when nothing
|
||||
/// changed.
|
||||
fn replace_text(s: &str, r: &Rewrite) -> Option<String> {
|
||||
let decoded = if s.contains('%') { decoded_with_spans(s).filter(|(decoded, _)| decoded != s) } else { None };
|
||||
let mut found = Found::default();
|
||||
for fold in [false, true].into_iter().take(if r.fold { 2 } else { 1 }) {
|
||||
for (from, to) in &r.pairs {
|
||||
if let Some((text, spans)) = &decoded {
|
||||
found.claim(text, Some(spans.as_slice()), from, to, fold, r);
|
||||
for group in &r.pairs {
|
||||
for fold in [false, true].into_iter().take(if r.fold { 2 } else { 1 }) {
|
||||
for (from, to) in group {
|
||||
if let Some((text, spans)) = &decoded {
|
||||
found.claim(text, Some(spans.as_slice()), from, to, fold, r);
|
||||
}
|
||||
found.claim(s, None, from, to, fold, r);
|
||||
}
|
||||
found.claim(s, None, from, to, fold, r);
|
||||
}
|
||||
}
|
||||
if found.cuts.is_empty() {
|
||||
|
||||
@@ -28,7 +28,7 @@ fn a_reply_hides_a_hidden_library_whatever_surrounds_it() {
|
||||
fn hiding(hidden: &[(&str, &str)], readable: &[&str]) -> super::Hiding {
|
||||
let mut hidden: Vec<(String, String)> = hidden.iter().map(|(id, shown)| (id.to_string(), shown.to_string())).collect();
|
||||
hidden.sort_by_key(|(id, _)| std::cmp::Reverse(id.len()));
|
||||
super::Hiding { hidden, readable: readable.iter().map(|id| id.to_string()).collect() }
|
||||
super::Hiding { hidden, readable: readable.iter().map(|id| id.to_string()).collect(), sent: Vec::new() }
|
||||
}
|
||||
|
||||
/// [`super::hide_library_ids`] on `reply`, under a read root made for `tag`.
|
||||
@@ -125,10 +125,41 @@ fn a_reply_hides_a_path_in_any_case_and_escaped() {
|
||||
fn a_sent_path_replaces_a_hidden_id_for_the_same_path() {
|
||||
let mut hidden = hiding(&[(r"loaded/C:\Libs\b.svg", "loaded/b.svg (2)"), ("loaded//other/b.svg", "loaded/b.svg")], &[]);
|
||||
hidden.set_hidden(vec![("loaded/c:/libs/B.svg".to_string(), "loaded/B.svg".to_string())], true);
|
||||
let ids: Vec<&str> = hidden.hidden.iter().map(|(id, _)| id.as_str()).collect();
|
||||
assert_eq!(ids, ["loaded/c:/libs/B.svg", "loaded//other/b.svg"]);
|
||||
hidden.set_hidden(vec![("loaded//OTHER/b.svg".to_string(), "loaded/b.svg".to_string())], false);
|
||||
assert_eq!(hidden.hidden.len(), 3, "{hidden:?}");
|
||||
let shown: Vec<&str> = hidden.hidden.iter().map(|(_, shown)| shown.as_str()).collect();
|
||||
assert_eq!(shown, ["loaded/B.svg", "loaded/b.svg"], "{hidden:?}");
|
||||
assert_eq!(hidden.sent, [("loaded/c:/libs/B.svg".to_string(), "loaded/B.svg".to_string())]);
|
||||
hidden.set_hidden(vec![("loaded//OTHER/b.svg".to_string(), "loaded/x.svg".to_string())], false);
|
||||
assert_eq!((hidden.hidden[1].1.as_str(), hidden.sent.len()), ("loaded/b.svg", 2), "{hidden:?}");
|
||||
// A library loaded by a relative path, and the absolute path it reads as, sent: the sent path's
|
||||
// file name is shown for both, not the library's numbered id.
|
||||
let mut hidden = hiding(&[("loaded/data/icon.svg", "loaded/icon.svg (2)")], &[]);
|
||||
let absolute = std::path::absolute("data/icon.svg").unwrap().to_string_lossy().into_owned();
|
||||
hidden.set_hidden(vec![(format!("loaded/{absolute}"), "loaded/icon.svg".to_string())], false);
|
||||
let mut reply = json!({"error": format!("can't read \"data/icon.svg\" or {absolute}")});
|
||||
hide_under_a_root("sent-absolute", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "can't read \"icon.svg\" or icon.svg", "{reply}");
|
||||
}
|
||||
|
||||
/// A sent path whose text overlaps part of a hidden library's path does not keep that library's path
|
||||
/// from being hidden: the hidden libraries are matched first.
|
||||
#[test]
|
||||
fn a_sent_path_does_not_stop_a_hidden_library_path_from_being_hidden() {
|
||||
let mut hidden = hiding(&[("loaded//home/secret/dir/lib", "loaded/lib")], &[]);
|
||||
hidden.set_hidden(vec![("loaded//dir/lib/sent-folder/longer".to_string(), "loaded/longer".to_string())], false);
|
||||
let mut reply = json!({"error": "can't read /home/secret/dir/lib/sent-folder/longer"});
|
||||
hide_under_a_root("sent-overlap", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "can't read lib/sent-folder/longer", "{reply}");
|
||||
}
|
||||
|
||||
/// A hidden relative path is also hidden where a reply shows it as the absolute path it reads as from
|
||||
/// the working folder.
|
||||
#[test]
|
||||
fn a_reply_hides_a_hidden_relative_path_written_as_absolute() {
|
||||
let hidden = hiding(&[("loaded/data/icon.svg", "loaded/icon.svg")], &[]);
|
||||
let absolute = std::path::absolute("data/icon.svg").unwrap().to_string_lossy().into_owned();
|
||||
let mut reply = json!({"error": format!("can't read {absolute}")});
|
||||
hide_under_a_root("relative-absolute", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "can't read icon.svg", "{reply}");
|
||||
}
|
||||
|
||||
/// The paths outside the read root that a request sends, as a path, a `file:` URL (also one that names
|
||||
|
||||
@@ -884,7 +884,7 @@ fn ui_inspect_and_ui_docking_give_a_hidden_library_the_same_id() {
|
||||
let ui = &r["result"]["ui"];
|
||||
assert_eq!(
|
||||
(ui["color_guide_limit"].as_str(), ui["open_panel"].as_str()),
|
||||
(Some("loaded/b.svg (2)"), Some("library:swatches:loaded/b.svg")),
|
||||
(Some("loaded/b.svg"), Some("library:swatches:loaded/b.svg (2)")),
|
||||
"{r}"
|
||||
);
|
||||
let d = request(&mut app, &tx, "ui.docking", json!({}));
|
||||
|
||||
+4
-2
@@ -161,8 +161,10 @@ the suite) keep the agent to the folders you give it:
|
||||
another loaded library's whole id with no `.` or `..` part, so a readable library's id that begins with a hidden one stays. A reply also shows a path outside the
|
||||
read roots that the request sent by its file name, whether or not a library was loaded from it and whether the
|
||||
request succeeded or failed, so that the reply reads the same either way. A path param (`path`, `file`,
|
||||
`destinationFolder` and the like) counts with a relative path too. A relative path is not replaced where it ends a
|
||||
longer path (`/work/data/icon.svg` keeps `data/icon.svg`). Encoded file data, a base64 string under `dataBase64`,
|
||||
`destinationFolder` and the like) counts with a relative path too. The hidden libraries' ids and paths are
|
||||
matched before the paths the request sent, so a sent path written over part of a hidden library's path does not
|
||||
keep that path in the reply. A relative path is not replaced where it ends a longer path (`/work/data/icon.svg`
|
||||
keeps `data/icon.svg`), and the absolute path it reads as from the app's working folder is replaced too. Encoded file data, a base64 string under `dataBase64`,
|
||||
`pngBase64` or `thumbnailBase64`, is sent as it is.
|
||||
- `swatch.library.list`, `brush.libraries`, `symbol.libraries` and `graphicStyle.libraries` list every library
|
||||
but leave out the paths outside the read roots: a library's `path` and the user library folder in `userFolder`
|
||||
|
||||
Reference in New Issue
Block a user