M5b plan: task 11, bxctl sanitize check

The task file, the skeleton crates/bxctl/src/sanitize.rs (five todo!()
bodies: parse_sanitize, code_point, token, prefix, tag_count) and two
given tests: sanitize_check.rs (8, runs the binary) and
sanitize_report.rs (10, the library).

Decisions: the one argument after `check` is the text, whatever it
looks like; none means standard input. Output: refuse line (first rule
only; a character as U+XXXX, a tag as its token id, a secret as its
prefix and an ellipsis), flag line (rule only), neutralise line (every
occurrence of every tag). Exit 0 nothing fires, 1 something fires, 2
usage error or unreadable text. Nothing of the text is printed.

Checks: every given test fails before the task as the task says (the
report file does not compile: one E0432, two E0599; the binary tests
exit 2); the skeleton state gives 10 and 8 not-yet-implemented
failures; the reference passes make gate with 1099 tests (1081 + 18);
the task's edits applied to a fresh ref-10 copy with its own build
directory give a tree byte-identical to the reference; 21 mutations,
each caught; 40 runs under load, no failure; /private/tmp unchanged
around a gate run; rustfmt and the line limit on the given files; no
tag or key-shaped string in the plan or given files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 10:18:55 -07:00
co-authored by Claude Opus 5.5
parent 7eb65a276b
commit ef2add03f0
4 changed files with 996 additions and 0 deletions
@@ -0,0 +1,115 @@
//! `bxctl sanitize check` (M5b spec, section 6.6): which of the sanitizer's rules a text breaks,
//! and what each would do. It calls `bxcore::sanitize` directly, as `memoryd` and `loopd` do, so
//! it needs no daemon, answers as they would, and writes nothing.
//!
//! Nothing of the checked text is printed: a refusal shows a secret's prefix from `PREFIXES`, a
//! character as `U+XXXX` or a tag as its token id, and a flag names its rule only. Every byte
//! printed is fixed text or a number, so a key, a tag or a control character never reaches the
//! terminal, and no escaping is needed.
use std::io::Write;
use bxcore::sanitize::{Refusal, TAGS, flag, refuse};
use crate::cli::{Command, UsageError};
/// The prefixes of `bxcore`'s secret rules (`refuse.rs`), and the start of a private-key line:
/// what a refusal shows of a secret, followed by `…`. No prefix is the start of another.
const PREFIXES: &[&str] = &[
"ghp_",
"gho_",
"ghu_",
"ghs_",
"ghr_",
"github_pat_",
"glpat-",
"sk-",
"xoxb-",
"xoxa-",
"xoxp-",
"xoxr-",
"xoxs-",
"AKIA",
"ASIA",
"-----BEGIN",
];
/// `bxctl sanitize check [TEXT]`: `args` are the words after `sanitize`. `["check"]` gives
/// `Command::SanitizeCheck { text: None }` (the text is read from standard input);
/// `["check", text]` gives `Some(text.clone())`, whatever the text looks like, `-` and `--x`
/// included. Anything else is `Err(UsageError)`. One `match` on `args`, with slice patterns and
/// guards: `[check] if check == "check" => …`.
pub fn parse_sanitize(args: &[String]) -> Result<Command, UsageError> {
todo!()
}
/// The lines `bxctl sanitize check` prints for `text`, in this order, each only if it fires: the
/// first refuse rule (only the first: memory answers with that one), the first flag rule, and the
/// number of tags `neutralise` would rewrite. No line: nothing fires.
pub fn report(text: &str) -> Vec<String> {
let mut lines = Vec::new();
if let Some(refusal) = refuse(text) {
lines.push(format!(
"refuse {:<18} {} at {}",
refusal.rule,
what(text, &refusal),
refusal.at
));
}
if let Some(flagged) = flag(text) {
lines.push(format!("flag {}", flagged.rule));
}
let tags = tag_count(text);
if tags > 0 {
let noun = if tags == 1 { "tag" } else { "tags" };
lines.push(format!("neutralise {tags} {noun}"));
}
lines
}
/// Writes each line of `report(text)` to `out`, with a line feed after each. True if any rule
/// fired, that is, if there was a line.
pub fn run(text: &str, out: &mut dyn Write) -> std::io::Result<bool> {
let lines = report(text);
for line in &lines {
writeln!(out, "{line}")?;
}
Ok(!lines.is_empty())
}
/// What a refusal shows of the text at `refusal.at`: a character rule its code point, the tag
/// rule the tag's token id, any other rule (the secrets) the prefix and `…`.
fn what(text: &str, refusal: &Refusal) -> String {
match refusal.rule {
"control" | "invisible" | "private-use" | "noncharacter" => code_point(text, refusal.at),
"tag" => token(text, refusal.at),
_ => format!("{}…", prefix(text, refusal.at)),
}
}
/// The character that starts at byte `at` of `text`, as `U+` and at least four upper-case hex
/// digits (`format!("U+{:04X}", u32::from(c))`: `U+0007`, `U+202E`, `U+10FFFF`). `"…"` if no
/// character starts there.
fn code_point(text: &str, at: usize) -> String {
todo!()
}
/// The listed tag that starts at byte `at` of `text`, as `token ` and its id (`token 248068`):
/// the first tag in `TAGS` whose `text` the rest of `text` (`text.get(at..)`, `""` for `None`)
/// starts with. `"…"` if none does.
fn token(text: &str, at: usize) -> String {
todo!()
}
/// The first entry of `PREFIXES` that the rest of `text` from byte `at` (`text.get(at..)`, `""`
/// for `None`) starts with, or `""` if none does.
fn prefix(text: &str, at: usize) -> &'static str {
todo!()
}
/// How many tags `neutralise` would rewrite: for each tag in `TAGS`, the number of times its
/// `text` occurs in `text` (`str::matches` and `count`), summed. Tags never overlap, so this is
/// every occurrence, each counted once.
fn tag_count(text: &str) -> usize {
todo!()
}