From 027035668b78174384bfeda7689516dcf2b86673 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:07:30 -0800 Subject: [PATCH] MCP: a readable library id keeps its text when a space follows the hidden part A hidden id or path stays only where it begins another loaded library's whole id with no `.` or `..` part, also when that id has a space after the hidden part. --- crates/ui-egui/src/control.rs | 41 ++++++++++++++--------------- crates/ui-egui/src/control/tests.rs | 6 +++++ docs/mcp.md | 5 ++-- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 64d9e7b061..29c9fbbb5c 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -406,7 +406,7 @@ struct Hiding { /// shown instead ([`unreadable_library_ids`]). hidden: Vec<(String, String)>, /// The other loaded library ids the UI state and the session hold: an occurrence of a hidden id or - /// path that continues into one of them stays ([`known_continuation`]). + /// path that begins one of them stays ([`begins_known_id`]). readable: std::collections::BTreeSet, } @@ -506,9 +506,8 @@ fn collect_library_panel_ids(v: &Value, ids: &mut std::collections::BTreeSet`), status text, a dialog, the command palette query, a script's -/// report and a recorded step. An occurrence followed by a character that continues a path -/// ([`continues_path`]) stays only when the longer text is another loaded library's id -/// ([`known_continuation`]): a readable library's id that begins with a hidden one keeps its path. On +/// report and a recorded step. An occurrence that begins another loaded library's id stays +/// ([`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); @@ -596,8 +595,8 @@ fn occurrences(text: &str, needle: &str, fold: bool) -> Vec { } /// `s` with the `from` texts of the rewrite's pairs written as their `to` texts, at each occurrence -/// not followed by a character that continues a path ([`continues_path`]) into another loaded library's id -/// ([`known_continuation`]). A hidden id or path written with `%XX` escapes (in a `file:` URL) is +/// 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. fn replace_text(s: &str, r: &Rewrite) -> Option { let replaced = replace_plain_text(s, r); @@ -616,7 +615,7 @@ fn replace_text(s: &str, r: &Rewrite) -> Option { 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 && (!continues_path(&decoded[end..]) || !known_continuation(&decoded, at, end, r)) { + 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())); } @@ -706,7 +705,7 @@ fn replace_matches(s: &str, r: &Rewrite, fold: bool) -> Option { let mut copied = 0; for at in found { let end = at + from.len(); - if !continues_path(&text[end..]) || !known_continuation(text, at, end, r) { + if !begins_known_id(text, at, end, r) { next.push_str(&text[copied..at]); next.push_str(to); copied = end; @@ -726,19 +725,19 @@ fn continues_path(rest: &str) -> bool { rest.trim_start_matches(['.', '-', '~']).starts_with(|c: char| c.is_alphanumeric() || matches!(c, '_' | '/' | '\\')) } -/// Is the longer text that the occurrence `text[at..end]` starts, up to the first space, quote, -/// bracket, `,`, `;` or `|` after `end`, another loaded library's id ([`Hiding::readable`]), written -/// with or without its `loaded/`, with no `.` or `..` part? A readable library's id that begins with a -/// hidden one stays; a hidden path followed by more path text (`.bak`) is still hidden. -fn known_continuation(text: &str, at: usize, end: usize, r: &Rewrite) -> bool { - let stop = text[end..].find(|c: char| c.is_whitespace() || "\"'`()[]{}<>,;|".contains(c)).map_or(text.len(), |n| end + n); - let longer = &text[at..stop]; - if longer.split(['/', '\\']).any(|part| part == "." || part == "..") { - return false; - } - let same = |id: &str, want: &str| if r.fold { id.eq_ignore_ascii_case(want) } else { id == want }; - let id = if longer.starts_with("loaded/") { longer.to_string() } else { format!("loaded/{longer}") }; - r.hiding.readable.iter().any(|known| same(known, &id)) +/// 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`])? +/// 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 (`.bak`, `/../x`) is still hidden. +fn begins_known_id(text: &str, at: usize, end: usize, r: &Rewrite) -> bool { + 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 == ".."); + r.hiding.readable.iter().filter(|known| !dotted(known)).any(|known| { + let id = if rest.starts_with("loaded/") { 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()..]) + }) } /// Text that a person or a script wrote, by its key: a command palette query, a script's report and a diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index badf602cc9..800a57c795 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -82,6 +82,12 @@ fn a_reply_keeps_a_hidden_path_only_inside_a_known_library_id() { vectorcraft_engine::file_access::confine(roots.as_ref(), || super::hide_library_ids(&mut reply, &hidden)); let _ = std::fs::remove_dir_all(base); assert_eq!(reply["error"], json!(format!("kept b.svg.bak, b.svg (2){up} and {known}")), "{reply}"); + // A readable library's id with a space after the hidden part stays whole. + // A readable id with a `..` part does not keep the hidden part. + let hidden = hiding(&[("loaded//art/in", "loaded/in")], &["loaded//art/in work/a.svg", "loaded//art/in/../ok/c.svg"]); + let mut reply = json!({"error": "x loaded//art/in work/a.svg, /art/in work/a.svg and /art/in work; /art/in/../ok/c.svg"}); + hide_under_a_root("space-continuation", &mut reply, &hidden); + assert_eq!(reply["error"], "x loaded//art/in work/a.svg, /art/in work/a.svg and in work; in/../ok/c.svg", "{reply}"); } /// With matching in any case (as on Windows), a hidden path written in other letter cases is hidden diff --git a/docs/mcp.md b/docs/mcp.md index 05e2cab208..938833d02d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -156,9 +156,8 @@ the suite) keep the agent to the folders you give it: path as the file name in every string and key, the status text (`ui.status`), dialogs, the command palette query (`ui.palette_query`), a script's report (`scriptReport`) 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 longer text that begins with a hidden id or path - keeps it only when the longer text is another loaded library's 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 + 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`, `pngBase64` or `thumbnailBase64`, is sent as it is.