use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use iron_shell::env::Env;
use iron_shell::value::Value;
fn call(name: &str, args: Vec<Value>) -> anyhow::Result<Value> {
let mut env = Env::new();
iron_shell::builtins::call(name, args, &mut env)
}
fn s(v: &str) -> Value {
Value::Str(v.to_string())
}
struct Refusal {
code: String,
message: String,
builtin: String,
retryable: bool,
}
fn structured(e: &anyhow::Error) -> Refusal {
let v: serde_json::Value = serde_json::from_str(&e.to_string())
.unwrap_or_else(|_| panic!("refusal is not a structured error: {e}"));
let field = |k: &str| v["error"][k].as_str().unwrap_or_default().to_string();
Refusal {
code: field("code"),
message: field("message"),
builtin: field("builtin"),
retryable: v["error"]["retryable"].as_bool().unwrap_or(true),
}
}
fn refusal_of(name: &str, args: Vec<Value>) -> Refusal {
match call(name, args) {
Err(e) => structured(&e),
Ok(v) => panic!("{name} succeeded where a refusal was expected: {v:?}"),
}
}
fn assert_prose_only(r: &Refusal) {
assert!(
!r.message.starts_with("E_") && !r.message.contains(": E_"),
"the code is still spelled out in the message: {}",
r.message
);
}
#[test]
fn neural_parameter_budget_is_coded_budget_exceeded() {
let r = refusal_of(
"nn_create",
vec![
s("too-big"),
Value::Array(vec![Value::Int(1000), Value::Int(1000)]),
],
);
assert_eq!(r.code, "E_BUDGET_EXCEEDED");
assert!(!r.retryable, "an exhausted envelope is not retryable");
assert_eq!(r.builtin, "nn_create", "the boundary names the builtin");
assert!(r.message.contains("1000000 parameters"), "{}", r.message);
assert_prose_only(&r);
}
#[test]
fn oversized_encoded_network_is_coded_budget_exceeded() {
let mut rec = BTreeMap::new();
rec.insert(
"_data".to_string(),
Value::Str("x".repeat(64 * 1024 * 1024 + 1)),
);
let r = refusal_of("nn_info", vec![Value::Record(rec)]);
assert_eq!(r.code, "E_BUDGET_EXCEEDED");
assert!(r.message.contains("64 MiB"), "{}", r.message);
assert_prose_only(&r);
}
#[test]
fn empty_password_is_bad_arg_with_no_prose_code() {
let r = refusal_of("crypto_encrypt", vec![s("plaintext"), s("")]);
assert_eq!(r.code, "E_BAD_ARG");
assert!(
!r.message.contains("E_CRYPTO_BAD_INPUT"),
"the invented code must not leak into the message: {}",
r.message
);
assert!(r.message.contains("non-empty password"), "{}", r.message);
assert_prose_only(&r);
}
#[test]
fn fips_refusal_keeps_its_documented_code() {
std::env::set_var("IRONSHELL_FIPS", "1");
let md5 = refusal_of("md5", vec![s("abc")]);
let sha1 = refusal_of("crypto_hash", vec![s("abc"), s("sha1")]);
let ok = call("sha256", vec![s("abc")]);
std::env::remove_var("IRONSHELL_FIPS");
for r in [&md5, &sha1] {
assert_eq!(r.code, "E_FIPS_DISALLOWED", "{}", r.message);
assert!(!r.retryable);
assert!(!r.builtin.is_empty(), "the boundary must name the builtin");
assert_prose_only(r);
}
assert!(ok.is_ok(), "sha256 is FIPS-approved");
}
#[test]
fn database_open_failure_is_coded_io() {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
std::fs::create_dir_all(&dir).expect("tmpdir");
let csv = dir.join("structured_codes_import.csv");
std::fs::write(&csv, "a,b\n1,2\n").expect("write csv fixture");
let db = "structured-codes-no-such-dir/import.db";
let r = refusal_of(
"db_csv_to_sqlite",
vec![s(db), s(&csv.display().to_string())],
);
assert_eq!(r.code, "E_IO", "{}", r.message);
assert!(!r.retryable);
assert!(r.message.contains("cannot open"), "{}", r.message);
assert_prose_only(&r);
assert!(
!Path::new(db).exists(),
"a failed open must not create the database"
);
}
#[test]
fn crypto_failed_constructor_carries_its_code() {
let e = iron_shell::safety::crypto_failed("crypto_encrypt", "boom", "retry later");
let r = structured(&e);
assert_eq!(r.code, "E_CRYPTO_FAILED");
assert!(!r.retryable);
assert_eq!(r.builtin, "crypto_encrypt");
}
fn blank_comments(src: &str) -> String {
#[derive(Clone, Copy, PartialEq)]
enum St {
Code,
Str,
Raw(usize),
}
let c: Vec<char> = src.chars().collect();
let at = |i: usize| c.get(i).copied();
let mut out = String::with_capacity(src.len());
let mut st = St::Code;
let mut block = 0usize;
let mut i = 0usize;
while i < c.len() {
let ch = c[i];
if block > 0 {
if ch == '*' && at(i + 1) == Some('/') {
block -= 1;
out.push_str(" ");
i += 2;
} else if ch == '/' && at(i + 1) == Some('*') {
block += 1;
out.push_str(" ");
i += 2;
} else {
out.push(if ch == '\n' { '\n' } else { ' ' });
i += 1;
}
continue;
}
match st {
St::Str => {
out.push(ch);
if ch == '\\' && i + 1 < c.len() {
out.push(c[i + 1]);
i += 2;
continue;
}
if ch == '"' {
st = St::Code;
}
i += 1;
}
St::Raw(n) => {
if ch == '"' && (1..=n).all(|k| at(i + k) == Some('#')) {
out.push('"');
for _ in 0..n {
out.push('#');
}
st = St::Code;
i += 1 + n;
} else {
out.push(if ch == '\n' { '\n' } else { ' ' });
i += 1;
}
}
St::Code => {
if ch == '/' && at(i + 1) == Some('/') {
while i < c.len() && c[i] != '\n' {
out.push(' ');
i += 1;
}
} else if ch == '/' && at(i + 1) == Some('*') {
block = 1;
out.push_str(" ");
i += 2;
} else if ch == 'r'
&& !(i > 0 && (c[i - 1].is_alphanumeric() || c[i - 1] == '_'))
&& matches!(at(i + 1), Some('"') | Some('#'))
{
let mut j = i + 1;
while at(j) == Some('#') {
j += 1;
}
if at(j) == Some('"') {
let n = j - (i + 1);
out.push('r');
for _ in 0..n {
out.push('#');
}
out.push('"');
st = St::Raw(n);
i = j + 1;
} else {
out.push(ch);
i += 1;
}
} else if ch == '"' {
st = St::Str;
out.push(ch);
i += 1;
} else if ch == '\'' {
if at(i + 1) == Some('\\') {
let mut j = i + 2;
while j < c.len() && c[j] != '\'' && j - i < 12 {
j += 1;
}
if at(j) == Some('\'') {
out.extend(c[i..=j].iter());
i = j + 1;
continue;
}
} else if at(i + 2) == Some('\'') {
out.extend(c[i..=i + 2].iter());
i += 3;
continue;
}
out.push(ch);
i += 1;
} else {
out.push(ch);
i += 1;
}
}
}
}
out
}
fn prose_code_sites(src: &str) -> Vec<(usize, &'static str)> {
let clean = blank_comments(src);
let bytes = clean.as_bytes();
let skip_ws = |mut j: usize| {
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
j
};
let mut hits = Vec::new();
for name in ["anyhow!", "bail!", "format!", "arg_err"] {
let mut from = 0;
while let Some(pos) = clean[from..].find(name) {
let start = from + pos;
from = start + name.len();
let j = skip_ws(from);
if bytes.get(j) != Some(&b'(') {
continue;
}
let j = skip_ws(j + 1);
let is_code = bytes.get(j) == Some(&b'"')
&& bytes.get(j + 1) == Some(&b'E')
&& bytes.get(j + 2) == Some(&b'_')
&& bytes.get(j + 3).is_some_and(|b| b.is_ascii_uppercase());
if is_code {
let line = clean[..start].matches('\n').count() + 1;
hits.push((line, name));
}
}
}
hits.sort_unstable();
hits
}
fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).expect("read src directory") {
let path = entry.expect("directory entry").path();
if path.is_dir() {
rust_files(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
#[test]
fn scanner_sees_code_and_ignores_comments_and_raw_strings() {
let src = r##"
fn a() -> anyhow::Error { anyhow!("E_FOO: x") }
// anyhow!("E_COMMENTED: x")
/* bail!("E_BLOCK: x") */
fn b() { let fixture = r#"anyhow!("E_RAW: x")"#; let c = '"'; }
fn d() { arg_err(
"E_MULTI: y",
); }
fn e() -> &'static str { "E_PLAIN: no constructor here" }
"##;
let hits = prose_code_sites(src);
let names: Vec<&str> = hits.iter().map(|(_, n)| *n).collect();
assert_eq!(names, vec!["anyhow!", "arg_err"], "{hits:?}");
assert_eq!(
hits[0].0, 2,
"line numbers count from the start of the file"
);
assert_eq!(
hits[1].0, 6,
"a call split across lines is found at its macro"
);
}
#[test]
fn no_src_file_hands_a_code_name_to_an_error_as_prose() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
rust_files(&root, &mut files);
assert!(files.len() > 10, "scanned too few files: {}", files.len());
let mut offenders = Vec::new();
for file in &files {
let src = std::fs::read_to_string(file).expect("read source");
for (line, name) in prose_code_sites(&src) {
offenders.push(format!("{}:{line} {name}\"E_..\"", file.display()));
}
}
assert!(
offenders.is_empty(),
"error constructors must take a message and a structured code, not an E_ prefix:\n{}",
offenders.join("\n")
);
}