crates/gunnar-ui-web/src/name.rs
//! **Gunnar is a name** (owner, 2026-10-06): every word the console puts in
//! front of a person — a title, a heading, a sentence on a page, a refusal the
//! server hands the page, the invite mail — says `Gunnar`, never `gunnar`.
//!
//! What stays lower case is what is not the name but a thing typed or
//! resolved: a domain or an address (`gunnar.rs`, `git.gunnar.rs`,
//! `gunnar@gunnar.rs`), a path or a file (`/gunnar_light.webp`), an identifier
//! (`gunnar_crypt`, `gunnar-ui`, a product id), and a command (`gunnar crypt unlock`,
//! `` `gunnar control repo reclaim` ``).
//!
//! The test below reads the console's own sources — this crate's Rust and its
//! `index.html`, and the server crate's Rust, pages and scripts — and refuses
//! the bare word wherever a person would read it. It is a source test rather
//! than a browser one because it sees every sentence, including the ones only
//! an unusual state draws.
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
/// `gunnar <verb>` is the command, and the command is spelled as typed.
const COMMANDS: &[&str] = &[
"unlock", "token", "forge", "control", "crypt", "clone", "cli", "enrol", "enroll", "serve", "attest", "init",
"login", "ssh", "key", "keys", "push", "pull", "fetch", "repo", "admin", "user",
];
/// Server files that are not the console's face: the operator's env file,
/// the binary's `--help` and start-up errors, the systemd description, and
/// the event-stream log lines the kit's log gate reads by their words.
const NOT_SHOWN: &[&str] = &["envfile.rs", "main.rs", "program.rs", "lifecycle.rs", "signer.rs", "sse.rs"];
/// A log line is not a page: tracing's macros, a print, a panic or an assert.
const LOG_CALLS: &[&str] = &[
"info!", "warn!", "error!", "debug!", "trace!", "event!", "eprintln!", "println!", "eprint!", "print!", "panic!",
"unreachable!", "todo!", "unimplemented!", "assert!", "assert_eq!", "assert_ne!", "debug_assert!", "expect",
];
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
/// Every `gunnar` in `text` a person reads as the name, as byte offsets.
fn bare_names(text: &str) -> Vec<usize> {
let mut out = Vec::new();
let mut from = 0;
while let Some(i) = text[from..].find("gunnar").map(|i| i + from) {
from = i + 6;
let before = text[..i].chars().next_back();
let after = &text[i + 6..];
let next = after.chars().next();
// Part of a longer word, a domain, a path, an address, an identifier.
if before.is_some_and(|c| is_word(c) || "./@:-%".contains(c)) {
continue;
}
if next.is_some_and(|c| is_word(c) || "-@/".contains(c)) {
continue;
}
if after.starts_with("::") || (after.starts_with('.') && after[1..].chars().next().is_some_and(is_word)) {
continue;
}
// In backticks: code.
if text[..i].matches('`').count() % 2 == 1 {
continue;
}
// The command.
let rest = after.trim_start_matches([' ', '\u{a0}']);
if rest.len() < after.len() {
let verb: String = rest.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
if COMMANDS.contains(&verb.as_str()) {
continue;
}
}
out.push(i);
}
out
}
/// The identifier of each call enclosing `pos`, innermost first.
fn enclosing(src: &str, pos: usize) -> Vec<String> {
let b = src.as_bytes();
let mut out = Vec::new();
let mut level = 0i32;
let mut i = pos;
while i > 0 && out.len() < 4 {
i -= 1;
match b[i] {
b')' | b']' | b'}' => level += 1,
b'(' | b'[' | b'{' if level > 0 => level -= 1,
b'(' | b'[' | b'{' => {
let mut j = i;
while j > 0 && (b[j - 1].is_ascii_alphanumeric() || b"_!:.".contains(&b[j - 1])) {
j -= 1;
}
out.push(src[j..i].to_string());
}
b';' if level == 0 => break,
_ => {}
}
}
out
}
/// `(line, literal)` for every Rust string literal outside comments and
/// before the file's first `#[cfg(test)]`, with its enclosing calls.
fn literals(src: &str) -> Vec<(usize, String, Vec<String>)> {
let cut = src.find("#[cfg(test)]").unwrap_or(src.len());
let b = src.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < cut {
if src[i..].starts_with("//") {
i = src[i..].find('\n').map_or(src.len(), |j| i + j);
continue;
}
if src[i..].starts_with("/*") {
i = src[i + 2..].find("*/").map_or(src.len(), |j| i + 2 + j + 2);
continue;
}
let c = b[i];
if c == b'r' && (i == 0 || !(b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_')) {
let hashes = src[i + 1..].bytes().take_while(|&x| x == b'#').count();
if b.get(i + 1 + hashes) == Some(&b'"') {
let start = i + 2 + hashes;
let close = format!("\"{}", "#".repeat(hashes));
let end = src[start..].find(&close).map_or(src.len(), |j| start + j);
out.push((src[..start].matches('\n').count() + 1, src[start..end].to_string(), enclosing(src, i)));
i = end + close.len();
continue;
}
}
if c == b'\'' {
// A char literal ('"' included) or a lifetime.
let rest = &src[i + 1..];
match rest.chars().next() {
// '\n', '\'', '\u{2014}': past the escape, to the closing quote.
Some('\\') => i += 1 + 2 + rest[2..].find('\'').map_or(0, |j| j + 1),
Some(ch) if rest[ch.len_utf8()..].starts_with('\'') => i += 1 + ch.len_utf8() + 1,
_ => i += 1,
}
continue;
}
if c == b'"' {
let mut j = i + 1;
while j < b.len() && b[j] != b'"' {
j += if b[j] == b'\\' { 2 } else { 1 };
}
out.push((src[..i].matches('\n').count() + 1, src[i + 1..j.min(src.len())].to_string(), enclosing(src, i)));
i = j + 1;
continue;
}
i += 1;
}
out
}
/// The words of a page or a script: its comments taken out, line numbers kept.
fn page_text(src: &str) -> String {
let mut s = src.to_string();
for (open, close) in [("<!--", "-->"), ("/*", "*/")] {
let mut out = String::with_capacity(s.len());
let mut rest = s.as_str();
while let Some(i) = rest.find(open) {
out.push_str(&rest[..i]);
let end = rest[i..].find(close).map_or(rest.len(), |j| i + j + close.len());
out.push_str(&"\n".repeat(rest[i..end].matches('\n').count()));
rest = &rest[end..];
}
out.push_str(rest);
s = out;
}
// `// …` to the end of the line, where it starts a comment (not `https://`).
s.lines()
.map(|l| match l.find("//") {
Some(i) if i == 0 || l[..i].ends_with(char::is_whitespace) => &l[..i],
_ => l,
})
.collect::<Vec<_>>()
.join("\n")
}
fn files(dir: &Path, ext: &[&str], out: &mut Vec<PathBuf>) {
for e in std::fs::read_dir(dir).unwrap().flatten() {
let p = e.path();
if p.is_dir() {
files(&p, ext, out);
} else if p.extension().and_then(|x| x.to_str()).is_some_and(|x| ext.contains(&x)) {
out.push(p);
}
}
}
/// `#[cfg(test)]`, or an `all(..)` with `test` among its terms (`#[cfg(all(test, feature =
/// "metered"))]`, the metered sweeps): the module under it is compiled for tests alone.
fn test_cfg(line: &str) -> bool {
let l = line.trim();
if l == "#[cfg(test)]" {
return true;
}
l.strip_prefix("#[cfg(all(")
.and_then(|x| x.strip_suffix("))]"))
.is_some_and(|terms| terms.split(',').any(|t| t.trim() == "test"))
}
#[test]
fn a_module_under_a_test_cfg_is_a_tests_and_one_under_a_feature_alone_is_not() {
for test in ["#[cfg(test)]", " #[cfg(all(test, feature = \"metered\"))]", "#[cfg(all(feature = \"metered\", test))]"] {
assert!(test_cfg(test), "{test}");
}
for shown in ["#[cfg(feature = \"metered\")]", "#[cfg(not(test))]", "#[cfg(any(test, feature = \"metered\"))]", "#[cfg(all(not(test), unix))]", "#[cfg(all(testing))]"] {
assert!(!test_cfg(shown), "{shown}");
}
}
/// The files of every module declared `#[cfg(test)] mod x;` — a test's
/// fixtures and its stand-in servers, never a page. `x.rs` and `x/` beside
/// a `lib.rs` / `mod.rs`, under `foo/` beside a `foo.rs`.
fn test_only(rust: &[PathBuf]) -> Vec<PathBuf> {
let mut out = Vec::new();
for f in rust {
let src = std::fs::read_to_string(f).unwrap();
let lines: Vec<&str> = src.lines().collect();
for (n, l) in lines.iter().enumerate() {
if !test_cfg(l) {
continue;
}
let Some(next) = lines.get(n + 1).map(|x| x.trim()) else { continue };
let Some(m) = next.strip_prefix("pub mod ").or_else(|| next.strip_prefix("mod ")).and_then(|x| x.strip_suffix(';')) else {
continue;
};
let stem = f.file_stem().unwrap().to_string_lossy();
let dir = if stem == "lib" || stem == "mod" { f.parent().unwrap().to_path_buf() } else { f.with_extension("") };
out.push(dir.join(format!("{m}.rs")));
out.push(dir.join(m));
}
}
out
}
/// What a person would read, file by file: `file:line: the sentence`.
fn offences(rust: &[PathBuf], pages: &[PathBuf]) -> Vec<String> {
let mut bad = Vec::new();
let tests = test_only(rust);
for f in rust {
if tests.iter().any(|t| f.starts_with(t)) {
continue;
}
let name = f.file_name().unwrap().to_string_lossy().to_string();
if NOT_SHOWN.contains(&name.as_str()) && f.to_string_lossy().contains("gunnar-ui-server") {
continue;
}
// This module names the word in order to look for it.
if name == "name.rs" {
continue;
}
let src = std::fs::read_to_string(f).unwrap();
for (line, lit, calls) in literals(&src) {
if calls.iter().any(|c| LOG_CALLS.iter().any(|m| c.ends_with(m))) {
continue;
}
// A bare `"gunnar"` is the product's id where it is named one
// (`const PRODUCT`: what monetize sells, never shown).
if lit == "gunnar" && src.lines().nth(line - 1).unwrap_or("").contains("const PRODUCT") {
continue;
}
if !bare_names(&lit).is_empty() {
bad.push(format!("{}:{line}: {:?}", f.display(), lit.chars().take(120).collect::<String>()));
}
}
}
for f in pages {
let text = page_text(&std::fs::read_to_string(f).unwrap());
for (n, l) in text.lines().enumerate() {
if !bare_names(l).is_empty() {
bad.push(format!("{}:{}: {}", f.display(), n + 1, l.trim()));
}
}
}
bad
}
#[test]
fn the_word_is_told_apart_from_a_domain_a_path_and_a_command() {
for named in [
"gunnar did not answer",
"This gunnar does not report the machine",
"HTTP 502 · gunnar: unavailable",
"control gunnar.",
"gunnar's reason",
"Buy a private gunnar",
] {
assert_eq!(bare_names(named).len(), 1, "{named}");
}
for not_named in [
"git.gunnar.rs",
"gunnar.rs",
"mail gunnar@gunnar.rs",
"/gunnar_light.webp",
"gunnar-ui-server",
"gunnar_crypt::keyring",
"Run `gunnar crypt unlock` in a clone",
"use `gunnar token add|list|revoke` on the server",
"gunnar control repo reclaim",
"subject=Gunnar%20signup",
"Gunnar is not responding",
"gunnarsson",
] {
assert!(bare_names(not_named).is_empty(), "{not_named}");
}
}
#[test]
fn a_literal_is_read_outside_comments_and_logs() {
let src = "// gunnar in a comment\nfn f() { let a = \"gunnar says\"; tracing::warn!(\"gunnar went away\"); \
let c = '\"'; let b = r#\"raw gunnar\"#; }\n#[cfg(test)]\nmod t { const X: &str = \"gunnar\"; }";
let lits: Vec<(String, bool)> = literals(src)
.into_iter()
.map(|(_, l, calls)| (l, calls.iter().any(|c| LOG_CALLS.iter().any(|m| c.ends_with(m)))))
.collect();
assert_eq!(
lits,
vec![("gunnar says".into(), false), ("gunnar went away".into(), true), ("raw gunnar".into(), false)]
);
}
/// ★ **The rule itself**: nothing the console shows a person calls the
/// product `gunnar`.
#[test]
fn gunnar_is_a_name_wherever_the_console_shows_it() {
let web = Path::new(env!("CARGO_MANIFEST_DIR"));
let server = web.join("../gunnar-ui-server");
let (mut rust, mut pages) = (Vec::new(), vec![web.join("index.html")]);
files(&web.join("src"), &["rs"], &mut rust);
files(&server.join("src"), &["rs"], &mut rust);
files(&server.join("src"), &["html", "js"], &mut pages);
assert!(rust.len() > 100 && pages.len() > 4, "the sources were not found: {} rust, {} pages", rust.len(), pages.len());
let bad = offences(&rust, &pages);
assert!(bad.is_empty(), "`gunnar` where a person reads the name — write `Gunnar`:\n{}", bad.join("\n"));
}
}