Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

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"));
    }
}