MCP: a readable library's id stays only after a delimiter

A readable library's id was kept whole after any character outside a
short list, so in `/a+/project/libs/Kit.gpl` the id
`/project/libs/Kit.gpl` was kept and the hidden `/libs/Kit.gpl` inside
it showed in the reply. The same happened after `@`, `=`, `%` and `!`.

The id now stays only at the start of the text, after a space, a
quote, `,`, `;` or an opening bracket, or in a local `file:` URL.
Written with its `loaded/`, it also stays after `=`.
This commit is contained in:
dotCooCoo
2026-11-12 13:31:33 -08:00
parent cbfe5248bb
commit 2c3f61851f
2 changed files with 25 additions and 4 deletions
+11 -4
View File
@@ -832,8 +832,11 @@ 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`]).
/// ([`continues_path`]), and it begins the text or follows a space, a quote, `,`, `;`, an opening
/// bracket or, written with its `loaded/`, `=`, unless a `file:` URL holds it ([`url_start`]). Any
/// other character before it, such as `+` or `@`, can be part of a longer path's name, and so can `=`
/// before a path. 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)> {
@@ -843,10 +846,14 @@ fn readable_ids_in(text: &str, spans: Option<&[(usize, usize)]>, r: &Rewrite) ->
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) {
let prefixed = id.starts_with("loaded/");
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)) {
let after_delimiter = text[..at]
.chars()
.next_back()
.is_none_or(|c| c.is_whitespace() || matches!(c, '"' | '\'' | '`' | ',' | ';' | '(' | '[' | '{' | '<') || (prefixed && c == '='));
if !continues_path(&text[end..]) && (after_delimiter || in_local_file_url(text, at, id)) {
kept.push(span(at, end));
}
}
+14
View File
@@ -116,6 +116,20 @@ fn a_reply_keeps_a_readable_id_that_holds_a_hidden_path_inside() {
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}");
}
// The readable id's path after a character that can continue a path name ends another path.
for text in
["/a+/project/libs/Kit.gpl", "/a@/project/libs/Kit.gpl", "a=/project/libs/Kit.gpl", "/a!/project/libs/Kit.gpl", "/a%2B/project/libs/Kit.gpl"]
{
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 });
assert!(!reply["error"].as_str().unwrap_or_default().contains("/libs/Kit.gpl"), "{text}, fold {fold}: {reply}");
}
}
// Written with its `loaded/`, the readable id stays after `=` too.
let mut reply = json!({"error": "key=loaded//project/libs/Kit.gpl"});
super::replace_in_value(&mut reply, &super::Rewrite { pairs: super::hidden_text(&hidden), hiding: &hidden, fold: false });
assert_eq!(reply["error"], "key=loaded//project/libs/Kit.gpl", "{reply}");
}
/// With matching in any case (as on Windows), a hidden path written in other letter cases is hidden