mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
MCP: a readable library's id keeps a hidden path that it holds after its start
A reply hid a hidden library's absolute path wherever it appeared, also inside another loaded library's id when the match began after the id's start. With `/libs/Kit.gpl` hidden, the readable id `loaded//project/libs/Kit.gpl` came back as `loaded//projectKit.gpl`, and a request that used it failed. Each whole readable id in a reply (with or without `loaded/`, or in a `file:` URL with no host or `localhost`) is now found first, and a hidden id or path that lies inside it stays. The same path after a drive or in a URL with another host is still hidden.
This commit is contained in:
@@ -650,7 +650,10 @@ fn occurrences(text: &str, needle: &str, fold: bool) -> Vec<usize> {
|
||||
/// changed.
|
||||
fn replace_text(s: &str, r: &Rewrite) -> Option<String> {
|
||||
let decoded = if has_percent_escape(s) { decoded_with_spans(s).filter(|(decoded, _)| decoded != s) } else { None };
|
||||
let mut found = Found::default();
|
||||
let mut found = Found { kept: readable_ids_in(s, None, r), ..Found::default() };
|
||||
if let Some((text, spans)) = &decoded {
|
||||
found.kept.extend(readable_ids_in(text, Some(spans.as_slice()), 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 {
|
||||
@@ -677,24 +680,28 @@ fn replace_text(s: &str, r: &Rewrite) -> Option<String> {
|
||||
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.
|
||||
/// The spans of a text that [`replace_text`] replaces, each with its replacement, every span it has
|
||||
/// taken (the ones it replaces and the loaded library ids it keeps), and the whole readable library ids
|
||||
/// the text holds ([`readable_ids_in`]).
|
||||
#[derive(Default)]
|
||||
struct Found<'a> {
|
||||
cuts: Vec<(usize, usize, &'a str)>,
|
||||
taken: Vec<(usize, usize)>,
|
||||
kept: 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.
|
||||
/// Takes each occurrence of `from` in `text` that overlaps no span taken and lies inside no whole
|
||||
/// readable library id: 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) {
|
||||
let inside_readable = self.kept.iter().any(|&(a, b)| a <= start && stop <= b);
|
||||
if inside_readable || 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) {
|
||||
@@ -823,6 +830,40 @@ fn begins_known_id(text: &str, at: usize, end: usize, r: &Rewrite) -> Option<usi
|
||||
})
|
||||
}
|
||||
|
||||
/// The spans of `text` that hold a whole loaded library id ([`Hiding::readable`]), written with or
|
||||
/// without its `loaded/` and with no `.` or `..` part: text that does not continue a path follows it
|
||||
/// ([`continues_path`]), and no path text comes before it unless a `file:` URL holds it ([`url_start`]).
|
||||
/// The URL must name no host or `localhost` ([`in_local_file_url`]).
|
||||
/// A hidden id or path found inside such a span stays, wherever it begins. `spans` gives the range of
|
||||
/// the original text each byte of a decoded `text` was read from.
|
||||
fn readable_ids_in(text: &str, spans: Option<&[(usize, usize)]>, r: &Rewrite) -> Vec<(usize, usize)> {
|
||||
let span = |at: usize, end: usize| spans.map_or((at, end), |s| (s[at].0, s[end - 1].1));
|
||||
let dotted = |id: &str| id.split(['/', '\\']).any(|part| part == "." || part == "..");
|
||||
let mut kept = Vec::new();
|
||||
for known in r.hiding.readable.iter().filter(|known| !dotted(known)) {
|
||||
let bare = known.strip_prefix("loaded/").filter(|bare| !bare.is_empty());
|
||||
for id in std::iter::once(known.as_str()).chain(bare) {
|
||||
for at in occurrences(text, id, r.fold) {
|
||||
let end = at + id.len();
|
||||
let after_path = text[..at].ends_with(|c: char| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | '~' | ':' | '/' | '\\'));
|
||||
if !continues_path(&text[end..]) && (!after_path || in_local_file_url(text, at, id)) {
|
||||
kept.push(span(at, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
kept
|
||||
}
|
||||
|
||||
/// Is `path`, an absolute path at `text[at..]`, held by a `file:` URL ([`url_start`]) that names no host
|
||||
/// or `localhost`, in any case of its letters?
|
||||
fn in_local_file_url(text: &str, at: usize, path: &str) -> bool {
|
||||
let url = url_start(text, at, path);
|
||||
let Some(between) = text.get(url + "file:".len()..at).filter(|_| url < at) else { return false };
|
||||
let host = between.strip_prefix("//").unwrap_or(between).trim_end_matches('/');
|
||||
host.is_empty() || host.eq_ignore_ascii_case("localhost")
|
||||
}
|
||||
|
||||
/// Text that a person or a script wrote, by its key: a command palette query, a script's report and a
|
||||
/// workspace's name. [`swap_library_ids`] leaves it as written.
|
||||
const TEXT_FIELDS: [&str; 3] = ["palette_query", "scriptReport", "name"];
|
||||
|
||||
@@ -94,6 +94,30 @@ fn a_reply_keeps_a_hidden_path_only_inside_a_known_library_id() {
|
||||
assert_eq!(reply["error"], "x loaded//art/in work/a.svg, /art/in work/a.svg and in work; in/../ok/c.svg", "{reply}");
|
||||
}
|
||||
|
||||
/// A readable library's id that holds a hidden absolute path after its start stays whole: with and
|
||||
/// without its `loaded/`, in a `file:` URL and with `%XX` escapes, in any case with matching in any
|
||||
/// case. The hidden path on its own, and followed by other path text, is still hidden.
|
||||
#[test]
|
||||
fn a_reply_keeps_a_readable_id_that_holds_a_hidden_path_inside() {
|
||||
let hidden = hiding(&[("loaded//libs/Kit.gpl", "loaded/Kit.gpl")], &["loaded//project/libs/Kit.gpl"]);
|
||||
let text = "loaded//project/libs/Kit.gpl, /project/libs/Kit.gpl, file:///project/libs/Kit.gpl and file:///project%2Flibs/Kit.gpl; /libs/Kit.gpl and /libs/Kit.gpl.bak";
|
||||
for fold in [false, true] {
|
||||
let mut reply = json!({"error": text});
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&hidden), hiding: &hidden, fold });
|
||||
let kept = "loaded//project/libs/Kit.gpl, /project/libs/Kit.gpl, file:///project/libs/Kit.gpl and file:///project%2Flibs/Kit.gpl";
|
||||
assert_eq!(reply["error"], json!(format!("{kept}; Kit.gpl and Kit.gpl.bak")), "fold {fold}: {reply}");
|
||||
}
|
||||
let mut reply = json!({"error": "LOADED//PROJECT/LIBS/KIT.GPL and file://LocalHost/project/libs/Kit.gpl"});
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&hidden), hiding: &hidden, fold: true });
|
||||
assert_eq!(reply["error"], "LOADED//PROJECT/LIBS/KIT.GPL and file://LocalHost/project/libs/Kit.gpl", "{reply}");
|
||||
// The readable id's path after a drive or in a URL that names another host is another path.
|
||||
for text in ["C:/project/libs/Kit.gpl", "file://remote/project/libs/Kit.gpl"] {
|
||||
let mut reply = json!({"error": text});
|
||||
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&hidden), hiding: &hidden, fold: false });
|
||||
assert!(!reply["error"].as_str().unwrap_or_default().contains("/libs/Kit.gpl"), "{text}: {reply}");
|
||||
}
|
||||
}
|
||||
|
||||
/// With matching in any case (as on Windows), a hidden path written in other letter cases is hidden
|
||||
/// too; a path in a `file:` URL with `%XX` escapes is hidden on every platform, the whole URL shown as
|
||||
/// the file name.
|
||||
|
||||
+2
-2
@@ -178,8 +178,8 @@ 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 `file:` URL of an absolute path is shown as the file
|
||||
name alone, without `file:` or its host. 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. A reply also shows a path outside the
|
||||
name alone, without `file:` or its host. A hidden id or path stays only where it lies inside
|
||||
another loaded library's whole id with no `.` or `..` part, so a readable library's id that begins with or holds 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. The one exception is the `path`
|
||||
and `files` entries of a reply's result, a `command.batch` step's result included, that name the file the
|
||||
|
||||
Reference in New Issue
Block a user