review: a key split by an invisible character is still a secret

has_secret masked each character a character rule matches to a space, so one
zero-width space inside a key cut it into two runs too short to match. The
secret rules now also run on the text with those characters deleted, and the
text is a secret if either matches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:12:12 -07:00
co-authored by Claude Opus 5.5
parent 3d266df9de
commit bd4cf9fb7f
2 changed files with 60 additions and 13 deletions
+16 -13
View File
@@ -78,21 +78,24 @@ pub fn peer_name(speaker: &str) -> String {
/// Whether `text` holds a known-prefix secret, by `refuse`'s secret rules alone. `refuse` gives
/// only the first rule a text breaks, and its character rules come first, so each character they
/// match is first replaced by a space: it is not a key character and not a letter, so a secret's
/// prefix and run are found as before, and a carriage return cannot hide a key.
/// match is taken out of the way, in two ways, and the text is a secret if either is: replaced by
/// a space, which is not a key character and not a letter, so a secret's prefix and run are found
/// as before and a carriage return cannot hide a key; and deleted, so that one inside a key cannot
/// split it into two runs too short to match.
pub fn has_secret(text: &str) -> bool {
let mut buf = [0u8; 4];
let masked: String = text
.chars()
.map(|c| {
if refuse(c.encode_utf8(&mut buf)).is_some() {
' '
} else {
c
}
})
.collect();
refuse(&masked).is_some_and(|r| r.rule.starts_with("secret-"))
let mut masked = String::with_capacity(text.len());
let mut deleted = String::with_capacity(text.len());
for c in text.chars() {
if refuse(c.encode_utf8(&mut buf)).is_some() {
masked.push(' ');
} else {
masked.push(c);
deleted.push(c);
}
}
let secret = |t: &str| refuse(t).is_some_and(|r| r.rule.starts_with("secret-"));
secret(&masked) || secret(&deleted)
}
/// The turn's user text and its speaker, from its first record, the `User` one.