use chrono::{DateTime, Utc};
pub struct Rule {
pub ecosystem: String,
pub package: String,
pub version_range: String,
pub justification: String,
pub proposed_by: Option<String>,
pub proposed_at: DateTime<Utc>,
}
pub fn emit_yaml(rule: &Rule) -> String {
let mut out = String::new();
out.push_str("risk_accepted:\n");
out.push_str(&format!(" - ecosystem: {}\n", yaml_inline(&rule.ecosystem)));
out.push_str(&format!(" package: {}\n", yaml_inline(&rule.package)));
out.push_str(&format!(" version_range: \"{}\"\n", yaml_escape(&rule.version_range)));
out.push_str(&format!(
" justification: \"{}\"\n",
yaml_escape(&rule.justification)
));
if let Some(by) = &rule.proposed_by {
out.push_str(&format!(" proposed_by: \"{}\"\n", yaml_escape(by)));
}
out.push_str(&format!(
" proposed_at: \"{}\"\n",
rule.proposed_at.to_rfc3339()
));
out
}
fn yaml_inline(s: &str) -> String {
let plain_safe = !s.is_empty()
&& s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | '@'))
&& !looks_like_non_string_scalar(s);
if plain_safe {
s.to_string()
} else {
format!("\"{}\"", yaml_escape(s))
}
}
fn looks_like_non_string_scalar(s: &str) -> bool {
if s.is_empty() {
return true;
}
if matches!(
s.to_ascii_lowercase().as_str(),
"true" | "false" | "yes" | "no" | "on" | "off" | "null" | "~" | "y" | "n"
) {
return true;
}
s.parse::<i64>().is_ok() || s.parse::<f64>().is_ok()
}
fn yaml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn fixed_ts() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 5, 21, 10, 0, 0).unwrap()
}
#[test]
fn emit_minimal_rule_no_proposed_by() {
let rule = Rule {
ecosystem: "maven".to_string(),
package: "log4j-core".to_string(),
version_range: "<2.15".to_string(),
justification: "Legacy system; vendor patch pending Q3 2026".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
let expected = "risk_accepted:\n - ecosystem: maven\n package: log4j-core\n version_range: \"<2.15\"\n justification: \"Legacy system; vendor patch pending Q3 2026\"\n proposed_at: \"2026-05-21T10:00:00+00:00\"\n";
assert_eq!(out, expected);
}
#[test]
fn emit_full_rule_with_proposed_by() {
let rule = Rule {
ecosystem: "maven".to_string(),
package: "log4j-core".to_string(),
version_range: "<2.15".to_string(),
justification: "Legacy".to_string(),
proposed_by: Some("admin@company.com".to_string()),
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains("proposed_by: \"admin@company.com\""));
}
#[test]
fn justification_with_quotes_escaped() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "left-pad".to_string(),
version_range: "0.0.3".to_string(),
justification: r#"customer said "we need this""#.to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains(r#"justification: "customer said \"we need this\"""#));
}
#[test]
fn justification_with_backslash_escaped() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "lodash".to_string(),
version_range: "1.0".to_string(),
justification: r"path\to\thing".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains(r#"justification: "path\\to\\thing""#));
}
#[test]
fn justification_with_newline_escaped() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "x".to_string(),
version_range: "*".to_string(),
justification: "line1\nline2".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains(r#"justification: "line1\nline2""#));
}
#[test]
fn package_with_npm_scope_quoted() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "@my-org/foo".to_string(),
version_range: "^1.0".to_string(),
justification: "x".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains("package: @my-org/foo\n"));
}
#[test]
fn package_with_space_gets_quoted() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "weird package".to_string(),
version_range: "1".to_string(),
justification: "x".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(out.contains("package: \"weird package\"\n"));
}
#[test]
fn ecosystem_field_is_emitted_first() {
let rule = Rule {
ecosystem: "pypi".to_string(),
package: "requests".to_string(),
version_range: "2.31.0".to_string(),
justification: "vendor pin".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
let eco_pos = out.find("ecosystem: pypi").expect("ecosystem field present");
let pkg_pos = out.find("package: requests").expect("package field present");
assert!(eco_pos < pkg_pos, "ecosystem must precede package in YAML");
}
#[test]
fn cleanlib_784_numeric_package_is_quoted() {
let rule = Rule {
ecosystem: "npm".to_string(),
package: "15764".to_string(),
version_range: "2.8.6".to_string(),
justification: "approve".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(
out.contains("package: \"15764\"\n"),
"numeric package must stay a quoted string, got: {out}"
);
}
#[test]
fn cleanlib_784_numeric_ecosystem_is_quoted() {
let rule = Rule {
ecosystem: "15764".to_string(),
package: "cors".to_string(),
version_range: "2.8.6".to_string(),
justification: "approve".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(
out.contains("ecosystem: \"15764\"\n"),
"numeric ecosystem must stay a quoted string, got: {out}"
);
}
#[test]
fn cleanlib_784_float_and_scientific_notation_are_quoted() {
for value in ["0.1", "1e5"] {
let rule = Rule {
ecosystem: "npm".to_string(),
package: value.to_string(),
version_range: "1.0".to_string(),
justification: "x".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(
out.contains(&format!("package: \"{value}\"\n")),
"float-shaped value {value} must stay quoted, got: {out}"
);
}
}
#[test]
fn cleanlib_784_yaml11_reserved_words_are_quoted() {
for value in ["true", "false", "null", "~", "no", "on", "off", "yes", "TRUE", "Off"] {
let rule = Rule {
ecosystem: "npm".to_string(),
package: value.to_string(),
version_range: "1.0".to_string(),
justification: "x".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(
out.contains(&format!("package: \"{value}\"\n")),
"YAML 1.1 reserved word {value} must stay quoted, got: {out}"
);
}
}
#[test]
fn cleanlib_784_ordinary_identifiers_stay_unquoted() {
for value in ["log4j-core", "left-pad", "npm", "pypi", "my.package", "@scope/pkg"] {
let rule = Rule {
ecosystem: "npm".to_string(),
package: value.to_string(),
version_range: "1.0".to_string(),
justification: "x".to_string(),
proposed_by: None,
proposed_at: fixed_ts(),
};
let out = emit_yaml(&rule);
assert!(
out.contains(&format!("package: {value}\n")),
"ordinary identifier {value} must stay unquoted (no regression), got: {out}"
);
}
}
}