mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
MCP: replies hide each sent path the same way whatever the outcome
Each hidden id or path is replaced once, chosen from the reply as given,
so a hidden path inside another library's shown id is not replaced
again, and a readable id written with %XX escapes keeps the hidden path
it begins with. A path quoted with {:?} is matched with its quotes and
control characters escaped. A path outside the read roots that the
request sent is shown by its file name in a reply that succeeded as in
one that failed, and a path param counts with a relative path too. A
relative path is not replaced where it ends a longer path. Without a
read root, a file: URL that names another host stays in ui.inspect's
recorded steps.
This commit is contained in:
+117
-99
@@ -401,13 +401,19 @@ impl Hiding {
|
||||
}
|
||||
|
||||
/// The paths outside the read roots that `v`'s strings and keys name at any depth, written as paths
|
||||
/// ([`unreadable_path_like`]), each as a hidden library id (`loaded/<path>`) with `loaded/<file name>`
|
||||
/// shown. The error of a failed request hides them as it hides a hidden library's path, with that file
|
||||
/// name also for a hidden library shown as `loaded/<file name> (2)`, so that it reads the same whether
|
||||
/// or not a library was loaded from one of them.
|
||||
/// ([`unreadable_path_like`]) or held by a path param
|
||||
/// ([`vectorcraft_engine::file_access::is_path_key`]), a relative one too, each as a hidden library id
|
||||
/// (`loaded/<path>`) with `loaded/<file name>` shown. A reply hides them as it hides a hidden library's
|
||||
/// path, with that file name also for a hidden library shown as `loaded/<file name> (2)`, so that it
|
||||
/// reads the same whether or not a library was loaded from one of them.
|
||||
fn sent_paths(v: &Value, out: &mut Vec<(String, String)>) {
|
||||
fn add(s: &str, out: &mut Vec<(String, String)>) {
|
||||
if !unreadable_path_like(s) {
|
||||
collect_sent_paths(v, false, out);
|
||||
}
|
||||
|
||||
/// [`sent_paths`] on `v`, which a path param holds when `path_param` is set.
|
||||
fn collect_sent_paths(v: &Value, path_param: bool, out: &mut Vec<(String, String)>) {
|
||||
fn add(s: &str, path_param: bool, out: &mut Vec<(String, String)>) {
|
||||
if !unreadable_path_like(s) && !(path_param && !readable_value(s)) {
|
||||
return;
|
||||
}
|
||||
// A URL that names another host as the path after its scheme (`//host/share/…`).
|
||||
@@ -423,12 +429,12 @@ fn sent_paths(v: &Value, out: &mut Vec<(String, String)>) {
|
||||
}
|
||||
}
|
||||
match v {
|
||||
Value::String(s) => add(s, out),
|
||||
Value::Array(items) => items.iter().for_each(|item| sent_paths(item, out)),
|
||||
Value::String(s) => add(s, path_param, out),
|
||||
Value::Array(items) => items.iter().for_each(|item| collect_sent_paths(item, path_param, out)),
|
||||
Value::Object(fields) => {
|
||||
for (key, item) in fields {
|
||||
add(key, out);
|
||||
sent_paths(item, out);
|
||||
add(key, false, out);
|
||||
collect_sent_paths(item, vectorcraft_engine::file_access::is_path_key(key), out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -470,7 +476,7 @@ fn collect_library_panel_ids(v: &Value, ids: &mut std::collections::BTreeSet<Str
|
||||
/// ([`begins_known_id`]): a readable library's id that begins with a hidden one keeps its path. On
|
||||
/// 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.hidden);
|
||||
let pairs = hidden_text(hiding);
|
||||
if pairs.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -478,16 +484,22 @@ fn hide_library_ids(v: &mut Value, hiding: &Hiding) {
|
||||
}
|
||||
|
||||
/// 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 with each `\`
|
||||
/// doubled (a path quoted with `{:?}`).
|
||||
fn hidden_text(ids: &[(String, String)]) -> Vec<(String, String)> {
|
||||
/// 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)> {
|
||||
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 ids {
|
||||
for (id, shown) in &hiding.hidden {
|
||||
let path = &id["loaded/".len()..];
|
||||
let name = shown.strip_prefix("loaded/").unwrap_or(shown);
|
||||
let back = path.replace('/', "\\");
|
||||
for path in [path.to_string(), path.replace('\\', "/"), back.replace('\\', "\\\\"), back] {
|
||||
for (from, to) in [(format!("loaded/{path}"), shown.clone()), (path, name.to_string())] {
|
||||
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));
|
||||
}
|
||||
@@ -556,46 +568,70 @@ 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. `None` when nothing changed.
|
||||
/// 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.
|
||||
fn replace_text(s: &str, r: &Rewrite) -> Option<String> {
|
||||
let replaced = replace_plain_text(s, r);
|
||||
let text = replaced.as_deref().unwrap_or(s);
|
||||
if !text.contains('%') {
|
||||
return replaced;
|
||||
}
|
||||
let Some((decoded, spans)) = decoded_with_spans(text) else { return replaced };
|
||||
if decoded == text {
|
||||
return replaced;
|
||||
}
|
||||
// The spans of `text` to replace, found in the decoded text, longest `from` first.
|
||||
let mut cuts: Vec<(usize, usize, &str)> = Vec::new();
|
||||
let mut taken: Vec<(usize, usize)> = Vec::new();
|
||||
for (from, to) in &r.pairs {
|
||||
for at in occurrences(&decoded, from, r.fold) {
|
||||
let end = at + from.len();
|
||||
let free = !taken.iter().any(|&(a, b)| at < b && a < end);
|
||||
if free && !begins_known_id(&decoded, at, end, r) {
|
||||
taken.push((at, end));
|
||||
cuts.push((spans[at].0, spans[end - 1].1, to.as_str()));
|
||||
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);
|
||||
}
|
||||
found.claim(s, None, from, to, fold, r);
|
||||
}
|
||||
}
|
||||
if cuts.is_empty() {
|
||||
return replaced;
|
||||
if found.cuts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut cuts = found.cuts;
|
||||
cuts.sort_unstable_by_key(|&(start, _, _)| start);
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut copied = 0;
|
||||
for (start, stop, to) in cuts {
|
||||
out.push_str(&text[copied..start]);
|
||||
out.push_str(&s[copied..start]);
|
||||
out.push_str(to);
|
||||
copied = stop;
|
||||
}
|
||||
out.push_str(&text[copied..]);
|
||||
out.push_str(&s[copied..]);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The spans of a text that [`replace_text`] replaces, each with its replacement, and every span it has
|
||||
/// taken: the ones it replaces and the loaded library ids it keeps.
|
||||
#[derive(Default)]
|
||||
struct Found<'a> {
|
||||
cuts: Vec<(usize, usize, &'a str)>,
|
||||
taken: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl<'a> Found<'a> {
|
||||
/// Takes each occurrence of `from` in `text` that overlaps no span taken: as a span to write as `to`,
|
||||
/// or, where it begins a loaded library's id ([`begins_known_id`]), that id's span, kept. `spans`
|
||||
/// gives the range of the original text each byte of a decoded `text` was read from.
|
||||
fn claim(&mut self, text: &str, spans: Option<&[(usize, usize)]>, from: &str, to: &'a str, fold: bool, r: &Rewrite) {
|
||||
let span = |at: usize, end: usize| spans.map_or((at, end), |s| (s[at].0, s[end - 1].1));
|
||||
for at in occurrences(text, from, fold) {
|
||||
let end = at + from.len();
|
||||
let (start, stop) = span(at, end);
|
||||
if self.taken.iter().any(|&(a, b)| start < b && a < stop) || ends_a_longer_path(text, at, from) {
|
||||
continue;
|
||||
}
|
||||
match begins_known_id(text, at, end, r) {
|
||||
Some(len) => self.taken.push(span(at, at + len)),
|
||||
None => {
|
||||
self.taken.push((start, stop));
|
||||
self.cuts.push((start, stop, to));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` with its `%XX` escapes decoded, and for each byte of the decoded text the range of `text`
|
||||
/// it was read from. A decoded byte that is not part of UTF-8 text is read as U+FFFD.
|
||||
fn decoded_with_spans(text: &str) -> Option<(String, Vec<(usize, usize)>)> {
|
||||
@@ -639,44 +675,18 @@ fn decoded_with_spans(text: &str) -> Option<(String, Vec<(usize, usize)>)> {
|
||||
Some((decoded, decoded_spans))
|
||||
}
|
||||
|
||||
/// `s` with the `from` texts of the rewrite's pairs written as their `to` texts, as [`replace_text`]
|
||||
/// writes them in text without `%XX` escapes. With the rewrite's `fold`, the ids and paths as written
|
||||
/// are replaced 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_plain_text(s: &str, r: &Rewrite) -> Option<String> {
|
||||
let exact = replace_matches(s, r, false);
|
||||
if !r.fold {
|
||||
return exact;
|
||||
}
|
||||
replace_matches(exact.as_deref().unwrap_or(s), r, true).or(exact)
|
||||
}
|
||||
|
||||
/// `s` with the `from` texts of the rewrite's pairs written as their `to` texts, matched in any case
|
||||
/// of ASCII letters with `fold`. `None` when nothing changed.
|
||||
fn replace_matches(s: &str, r: &Rewrite, fold: bool) -> Option<String> {
|
||||
let mut out: Option<String> = None;
|
||||
for (from, to) in &r.pairs {
|
||||
let text = out.as_deref().unwrap_or(s);
|
||||
let found = occurrences(text, from, fold);
|
||||
if found.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut next = String::with_capacity(text.len());
|
||||
let mut copied = 0;
|
||||
for at in found {
|
||||
let end = at + from.len();
|
||||
if !begins_known_id(text, at, end, r) {
|
||||
next.push_str(&text[copied..at]);
|
||||
next.push_str(to);
|
||||
copied = end;
|
||||
}
|
||||
}
|
||||
if copied > 0 {
|
||||
next.push_str(&text[copied..]);
|
||||
out = Some(next);
|
||||
}
|
||||
}
|
||||
out
|
||||
/// 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.
|
||||
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, '_' | '-' | '.' | '~' | '/' | '\\'))
|
||||
}
|
||||
|
||||
/// Does text that follows a path continue it: a letter, a digit, `_` or a separator, after any `.`,
|
||||
@@ -687,17 +697,18 @@ fn continues_path(rest: &str) -> bool {
|
||||
|
||||
/// Does the occurrence `text[at..end]` begin a longer loaded library id ([`Hiding::readable`]), written
|
||||
/// with or without its `loaded/` and with no `.` or `..` part, that ends where the text stops
|
||||
/// continuing a path ([`continues_path`])?
|
||||
/// continuing a path ([`continues_path`])? The length of that id as written, when it does.
|
||||
/// A readable library's id that begins with a hidden one stays, also with a space after the hidden
|
||||
/// part; a hidden path followed by other path text (`<path>.bak`, `<path>/../x`) is still hidden.
|
||||
fn begins_known_id(text: &str, at: usize, end: usize, r: &Rewrite) -> bool {
|
||||
fn begins_known_id(text: &str, at: usize, end: usize, r: &Rewrite) -> Option<usize> {
|
||||
let rest = &text[at..];
|
||||
let same = |a: &str, b: &str| if r.fold { a.eq_ignore_ascii_case(b) } else { a == b };
|
||||
let dotted = |id: &str| id.split(['/', '\\']).any(|part| part == "." || part == "..");
|
||||
let prefixed = rest.get(.."loaded/".len()).is_some_and(|head| same(head, "loaded/"));
|
||||
r.hiding.readable.iter().filter(|known| !dotted(known)).any(|known| {
|
||||
r.hiding.readable.iter().filter(|known| !dotted(known)).find_map(|known| {
|
||||
let id = if prefixed { known.as_str() } else { known.strip_prefix("loaded/").unwrap_or(known) };
|
||||
id.len() > end - at && rest.get(..id.len()).is_some_and(|head| same(head, id)) && !continues_path(&rest[id.len()..])
|
||||
let begins = id.len() > end - at && rest.get(..id.len()).is_some_and(|head| same(head, id)) && !continues_path(&rest[id.len()..]);
|
||||
begins.then_some(id.len())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -786,11 +797,11 @@ fn readable(path: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Is `s` a path the request may not read, written as an absolute path (either separator, with or
|
||||
/// without a drive) or a `file:` URL ([`file_url_path`])? A `file:` URL that names another host is
|
||||
/// never readable.
|
||||
/// without a drive) or a `file:` URL ([`file_url_path`])? Under a read root, a `file:` URL that names
|
||||
/// another host is never readable.
|
||||
fn unreadable_absolute(s: &str) -> bool {
|
||||
match file_url_path(s) {
|
||||
Some(path) => !path.is_some_and(|p| readable(&p)),
|
||||
Some(path) => !readable_url_path(path),
|
||||
None => {
|
||||
let absolute = s.starts_with(['/', '\\']) || matches!(s.as_bytes(), [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic());
|
||||
absolute && !readable(s)
|
||||
@@ -814,11 +825,20 @@ fn unreadable_path_like(s: &str) -> bool {
|
||||
/// May the request read the path `s` names, a path or a `file:` URL ([`file_url_path`])?
|
||||
fn readable_value(s: &str) -> bool {
|
||||
match file_url_path(s) {
|
||||
Some(path) => path.is_some_and(|p| readable(&p)),
|
||||
Some(path) => readable_url_path(path),
|
||||
None => readable(s),
|
||||
}
|
||||
}
|
||||
|
||||
/// May the request read the local path a `file:` URL names ([`file_url_path`])? A URL that names
|
||||
/// another host may be read only without a read root in force.
|
||||
fn readable_url_path(path: Option<String>) -> bool {
|
||||
match path {
|
||||
Some(path) => readable(&path),
|
||||
None => vectorcraft_engine::file_access::current().is_none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The local path that `s` names when it is a `file:` URL, with the scheme in any case and its `%XX`
|
||||
/// escapes decoded: `Some(None)` for a URL that names another host (`file://host/share`).
|
||||
/// `file:///C:/x` names `C:/x`.
|
||||
@@ -1001,8 +1021,8 @@ const DOCKING_ACTION: &str = "action";
|
||||
/// in a [`LIBRARY_PARAMS`] param, and the reply shows that id. Under a read root, the reply shows each
|
||||
/// such library's id and path as `ui.inspect` does ([`hide_library_ids`]) in every string and key,
|
||||
/// errors included, an error from a panic too: the libraries hidden before it ran and after it. The
|
||||
/// error of a failed request also hides the paths outside the read root that the request sent
|
||||
/// ([`sent_paths`]). A reply with no `/`, `\` or `%` in it is not searched, since every hidden path
|
||||
/// reply also shows each path outside the read root that the request sent by its file name, whether it
|
||||
/// succeeded or failed ([`sent_paths`]). A reply with no `/`, `\` or `%` in it is not searched, since every hidden path
|
||||
/// holds a separator, written or escaped. A screenshot's reply is hidden when it is sent
|
||||
/// ([`hide_in_deferred_reply`]); a `ui.pointer` reply sent after the tool's edit holds no library id
|
||||
/// or path.
|
||||
@@ -1037,19 +1057,17 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
Outcome::Done(reply)
|
||||
}
|
||||
|
||||
/// Hides in `reply` the libraries hidden now and in `before`, and for a failed request the paths
|
||||
/// outside the read root that `sent`, the request's params, holds ([`handle`]).
|
||||
/// Hides in `reply` the libraries hidden now and in `before`, and the paths outside the read root that
|
||||
/// `sent`, the request's params, holds ([`handle`]).
|
||||
fn hide_in_reply(app: &VectorcraftApp, reply: &mut Value, sent: &Value, before: Hiding) {
|
||||
if !holds_a_separator(reply) {
|
||||
return;
|
||||
}
|
||||
let mut hiding = unreadable_library_ids(app);
|
||||
hiding.merge(before);
|
||||
if reply.get("ok").and_then(Value::as_bool) == Some(false) {
|
||||
let mut paths = Vec::new();
|
||||
sent_paths(sent, &mut paths);
|
||||
hiding.set_hidden(paths, cfg!(windows));
|
||||
}
|
||||
let mut paths = Vec::new();
|
||||
sent_paths(sent, &mut paths);
|
||||
hiding.set_hidden(paths, cfg!(windows));
|
||||
hide_library_ids(reply, &hiding);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ fn hide_under_a_root(tag: &str, reply: &mut Value, hidden: &super::Hiding) {
|
||||
#[test]
|
||||
fn a_reply_keeps_encoded_data_that_holds_a_hidden_path() {
|
||||
let hidden = hiding(&[("loaded/assets/icon", "loaded/icon")], &[]);
|
||||
let data = "QUJDassets/iconREVGxQQ==";
|
||||
let data = "QUJ+assets/iconREVGxQQ==";
|
||||
let mut reply = json!({"ok": true, "result": {
|
||||
"files": [{"name": "a.png", "dataBase64": data, "pngBase64": "assets/icon.png", "thumbnailBase64": data}],
|
||||
"status": "from assets/icon",
|
||||
@@ -61,7 +61,7 @@ fn a_reply_keeps_encoded_data_that_holds_a_hidden_path() {
|
||||
assert_eq!(reply["result"]["status"], "from icon", "{reply}");
|
||||
assert_eq!(reply["result"]["dataBase64"]["note"], "from icon", "{reply}");
|
||||
assert_eq!(reply["result"]["pngBase64"][0], "from icon", "{reply}");
|
||||
assert_eq!(reply["result"]["otherBase64"], "QUJDiconREVGxQQ==", "{reply}");
|
||||
assert_eq!(reply["result"]["otherBase64"], "QUJ+iconREVGxQQ==", "{reply}");
|
||||
}
|
||||
|
||||
/// A hidden path followed by more text stays only when the longer text is another loaded library's
|
||||
@@ -96,19 +96,19 @@ fn a_reply_keeps_a_hidden_path_only_inside_a_known_library_id() {
|
||||
fn a_reply_hides_a_path_in_any_case_and_escaped() {
|
||||
let hidden = hiding(&[("loaded/C:/Libs/My Swatches/b.svg", "loaded/b.svg")], &[]);
|
||||
let mut reply = json!({"error": "from c:/LIBS/my swatches/B.SVG and file:///C:/Libs/My%20Swatches/b.svg"});
|
||||
let pairs = super::hidden_text(&hidden.hidden);
|
||||
let pairs = super::hidden_text(&hidden);
|
||||
let rewrite = super::Rewrite { pairs, hiding: &hidden, fold: true };
|
||||
super::replace_in_value(&mut reply, &rewrite);
|
||||
assert_eq!(reply["error"], "from b.svg and file:///b.svg", "{reply}");
|
||||
// Two libraries whose paths differ only in letter case keep their own ids.
|
||||
let two = hiding(&[("loaded//libs/b.svg", "loaded/b.svg"), ("loaded//libs/B.svg", "loaded/B.svg")], &[]);
|
||||
let mut reply = json!({"error": "x /libs/B.svg y /libs/b.svg z /LIBS/b.svg"});
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&two.hidden), hiding: &two, fold: true });
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&two), hiding: &two, fold: true });
|
||||
assert_eq!(reply["error"], "x B.svg y b.svg z b.svg", "{reply}");
|
||||
// A readable id that begins with a hidden one stays whole in any case, `loaded/` included.
|
||||
let nested = hiding(&[("loaded//libs/in", "loaded/in")], &["loaded//libs/in/a.svg"]);
|
||||
let mut reply = json!({"error": "LOADED//LIBS/IN/A.SVG"});
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&nested.hidden), hiding: &nested, fold: true });
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&nested), hiding: &nested, fold: true });
|
||||
assert_eq!(reply["error"], "LOADED//LIBS/IN/A.SVG", "{reply}");
|
||||
let mut reply = json!({"error": "file:///C:/Libs/My%20Swatches/b.svg"});
|
||||
hide_under_a_root("escaped", &mut reply, &hidden);
|
||||
@@ -132,8 +132,8 @@ fn a_sent_path_replaces_a_hidden_id_for_the_same_path() {
|
||||
}
|
||||
|
||||
/// The paths outside the read root that a request sends, as a path, a `file:` URL (also one that names
|
||||
/// another host), under `~` or starting with `../`, are hidden by their file names; plain text and a
|
||||
/// path inside the root are not.
|
||||
/// another host), under `~` or starting with `../`, or as a relative path in a path param, are hidden
|
||||
/// by their file names; plain text and a path inside the root are not.
|
||||
#[test]
|
||||
fn a_request_sends_paths_that_an_error_hides() {
|
||||
let base = std::env::temp_dir().join(format!("vc-sent-paths-{}", std::process::id()));
|
||||
@@ -141,13 +141,14 @@ fn a_request_sends_paths_that_an_error_hides() {
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let inside = root.join("a.svg").to_string_lossy().into_owned();
|
||||
let keep = json!(["a/b", inside, "file://host/g.svg"]);
|
||||
let params = json!({"path": "/x/my lib/b.svg", "list": ["file:///y/c%20d.svg", "~/e.svg", "../f.svg"], "other": keep});
|
||||
let params = json!({"path": "/x/my lib/b.svg", "file": "outside/h.svg", "list": ["file:///y/c%20d.svg", "~/e.svg", "../f.svg"], "other": keep});
|
||||
let mut sent = Vec::new();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap();
|
||||
vectorcraft_engine::file_access::confine(roots.as_ref(), || super::sent_paths(¶ms, &mut sent));
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
let pair = |path: &str, name: &str| (format!("loaded/{path}"), format!("loaded/{name}"));
|
||||
let want = [
|
||||
pair("outside/h.svg", "h.svg"),
|
||||
pair("/y/c d.svg", "c d.svg"),
|
||||
pair("~/e.svg", "e.svg"),
|
||||
pair("../f.svg", "f.svg"),
|
||||
@@ -183,6 +184,72 @@ fn an_absolute_path_outside_the_read_root_is_unreadable() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// Without a read root, a `file:` URL that names another host may be read, and a recorded step keeps
|
||||
/// it.
|
||||
#[test]
|
||||
fn a_url_naming_another_host_stays_without_a_read_root() {
|
||||
assert!(!super::unreadable_absolute("file://host/share/a.svg"));
|
||||
assert!(super::readable_value("file://host/share/a.svg"));
|
||||
let mut params = json!({"path": "file://host/share/a.svg", "list": ["file://host/share/b.svg"]});
|
||||
let fields = params.as_object_mut().unwrap();
|
||||
super::leave_out_unreadable_params(fields);
|
||||
assert_eq!(params, json!({"path": "file://host/share/a.svg", "list": ["file://host/share/b.svg"]}));
|
||||
}
|
||||
|
||||
/// A hidden path quoted with `{:?}` is hidden with its quotes and control characters escaped.
|
||||
#[test]
|
||||
fn a_reply_hides_a_quoted_path_with_escaped_quotes() {
|
||||
let hidden = hiding(&[("loaded//outside/a\"b\t.svg", "loaded/a\"b\t.svg")], &[]);
|
||||
let mut reply = json!({"error": format!("can't read {:?}", "/outside/a\"b\t.svg")});
|
||||
hide_under_a_root("quoted", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "can't read \"a\\\"b\\t.svg\"", "{reply}");
|
||||
}
|
||||
|
||||
/// Each replacement is chosen from the text as given: a hidden path inside the id shown for another
|
||||
/// hidden library is not replaced again, and a readable id written with `%XX` escapes keeps the hidden
|
||||
/// path it begins with.
|
||||
#[test]
|
||||
fn a_reply_replaces_each_hidden_text_once() {
|
||||
let hidden = hiding(&[("loaded//b.svg", "loaded/b.svg")], &[]);
|
||||
let mut reply = json!({"error": "no loaded//b.svg"});
|
||||
hide_under_a_root("root-file", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "no loaded/b.svg", "{reply}");
|
||||
let hidden = hiding(&[("loaded//art/in", "loaded/in")], &["loaded//art/in work/a.svg"]);
|
||||
let mut reply = json!({"error": "file:///art/in%20work/a.svg"});
|
||||
hide_under_a_root("escaped-readable", &mut reply, &hidden);
|
||||
assert_eq!(reply["error"], "file:///art/in%20work/a.svg", "{reply}");
|
||||
}
|
||||
|
||||
/// A hidden relative path is hidden where it stands alone or after `./`, and stays where it ends a
|
||||
/// longer path.
|
||||
#[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}");
|
||||
}
|
||||
|
||||
/// A path outside the read root that a request sent is shown by its file name in a reply that succeeded
|
||||
/// as in one that failed.
|
||||
#[test]
|
||||
fn a_sent_path_reads_the_same_whatever_the_outcome() {
|
||||
let app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
let root = std::env::temp_dir().join(format!("vc-sent-outcome-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap();
|
||||
let sent = json!({"path": "/outside/b.svg"});
|
||||
let mut done = json!({"ok": true, "result": {"path": "/outside/b.svg"}});
|
||||
let mut failed = json!({"ok": false, "error": "can't read /outside/b.svg"});
|
||||
vectorcraft_engine::file_access::confine(roots.as_ref(), || {
|
||||
super::hide_in_reply(&app, &mut done, &sent, super::Hiding::default());
|
||||
super::hide_in_reply(&app, &mut failed, &sent, super::Hiding::default());
|
||||
});
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
assert_eq!(done["result"]["path"], "b.svg", "{done}");
|
||||
assert_eq!(failed["error"], "can't read b.svg", "{failed}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_pointer_reply_uses_the_gesture_document_after_file_new() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
|
||||
@@ -729,7 +729,8 @@ fn a_panic_reply_hides_a_library_hidden_before_the_request() {
|
||||
|
||||
/// An error shows a path outside the read root that the request sent by its file name, whether or not
|
||||
/// a hidden library was loaded from that path, so that it reads the same either way. A path the
|
||||
/// request sends does not show the app's own text that holds it, in an error or a result.
|
||||
/// request sends does not show the app's own text that holds it, in an error or a result, and a result
|
||||
/// shows that path by its file name as an error does.
|
||||
#[test]
|
||||
fn an_error_hides_a_sent_path_whether_or_not_a_library_was_loaded_from_it() {
|
||||
let (mut app, tx, base) = confined("client-echo");
|
||||
@@ -746,7 +747,7 @@ fn an_error_hides_a_sent_path_whether_or_not_a_library_was_loaded_from_it() {
|
||||
assert!(r["error"].as_str().is_some_and(|e| e.ends_with(&format!(": {name}"))), "{r}");
|
||||
}
|
||||
let r = request(&mut app, &tx, "test.status", json!({"guess": path}));
|
||||
assert_eq!(r["result"], "Loaded b.svg (2)", "{r}");
|
||||
assert_eq!(r["result"], "Loaded b.svg", "{r}");
|
||||
let r = request(&mut app, &tx, "test.panic", json!({"guess": path}));
|
||||
assert_eq!(r["error"], "internal error: failed on Loaded b.svg (please report this bug)", "{r}");
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
|
||||
+5
-3
@@ -169,9 +169,11 @@ the suite) keep the agent to the folders you give it:
|
||||
(`ui.palette_query`), a script's report (`scriptReport`), a screenshot's reply and the error from an internal
|
||||
fault included, for the libraries hidden before the request ran and after it. The path is also matched with `%XX` escapes as a `file:`
|
||||
URL writes it, and on Windows in any case of its letters. A hidden id or path stays only where it begins
|
||||
another loaded library's whole id with no `.` or `..` part, so a readable library's id that begins with a hidden one stays. The error of a failed request 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, so that the
|
||||
error reads the same either way. Encoded file data, a base64 string under `dataBase64`,
|
||||
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`,
|
||||
`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