use anyhow::{Result, bail};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Residual {
pub snippet: String,
}
const MAX_SNIPPET: usize = 120;
fn find_unrendered_raw(text: &str) -> Option<String> {
let open = text.find("{{")?;
let rest = &text[open..];
let mut end = match rest.find("}}") {
Some(close) => (close + 2).min(MAX_SNIPPET),
None => rest.len().min(MAX_SNIPPET),
};
while end > 0 && !rest.is_char_boundary(end) {
end -= 1;
}
Some(rest[..end].to_string())
}
pub fn assert_no_unrendered(
text: &str,
label: &str,
is_strict: bool,
redact: impl Fn(&str) -> String,
) -> Result<Option<Residual>> {
match find_unrendered_raw(text) {
None => Ok(None),
Some(raw) => {
let snippet = redact(&raw);
if is_strict {
bail!("{}", unrendered_message(label, &snippet));
}
Ok(Some(Residual { snippet }))
}
}
}
pub fn assert_no_unrendered_logged(
text: &str,
label: &str,
is_strict: bool,
redact: impl Fn(&str) -> String,
warn: impl FnOnce(&str),
) -> Result<()> {
if let Some(residual) = assert_no_unrendered(text, label, is_strict, redact)? {
warn(&unrendered_message(label, &residual.snippet));
}
Ok(())
}
fn unrendered_message(label: &str, snippet: &str) -> String {
format!(
"{label}: unrendered template delimiter in generated manifest: {snippet:?} \
(a user-supplied config field was emitted without template rendering)"
)
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(s: &str) -> String {
s.to_string()
}
#[test]
fn clean_manifest_passes_strict_and_lenient() {
let clean = "url = \"https://example.com/v1.2.3/tool.tar.gz\"\nsha256 = \"abc\"";
assert_eq!(
assert_no_unrendered(clean, "test", true, identity).unwrap(),
None
);
assert_eq!(
assert_no_unrendered(clean, "test", false, identity).unwrap(),
None
);
}
#[test]
fn ruby_and_nix_interpolation_do_not_false_positive() {
let ruby = "depends_on macos: :catalina\ncaveats { \"installed #{version}\" }";
let nix = "src = fetchurl { url = \"${baseUrl}/tool\"; };";
assert_eq!(
assert_no_unrendered(ruby, "homebrew", true, identity).unwrap(),
None
);
assert_eq!(
assert_no_unrendered(nix, "nix", true, identity).unwrap(),
None
);
}
#[test]
fn leaked_tag_fails_in_strict_mode() {
let leaked = "docs_url = \"https://x/y/blob/{{ .Tag }}/docs.md\"";
let err = assert_no_unrendered(leaked, "chocolatey nuspec", true, identity).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("chocolatey nuspec"), "label missing: {msg}");
assert!(msg.contains("{{ .Tag }}"), "snippet missing: {msg}");
}
#[test]
fn leaked_tag_warns_in_lenient_mode() {
let leaked = "url=\"https://x/{{ .Version }}/t\"";
let residual = assert_no_unrendered(leaked, "aur PKGBUILD", false, identity)
.unwrap()
.expect("expected a residual");
assert!(residual.snippet.contains("{{ .Version }}"));
}
#[test]
fn snippet_is_redacted_before_surfacing() {
let leaked = "token = {{ ghp_supersecrettoken123 }}";
let redactor = |s: &str| s.replace("ghp_supersecrettoken123", "$REDACTED");
let err = assert_no_unrendered(leaked, "x", true, redactor).unwrap_err();
assert!(!err.to_string().contains("ghp_supersecrettoken123"));
assert!(err.to_string().contains("$REDACTED"));
let residual = assert_no_unrendered(leaked, "x", false, redactor)
.unwrap()
.unwrap();
assert!(!residual.snippet.contains("ghp_supersecrettoken123"));
assert!(residual.snippet.contains("$REDACTED"));
}
#[test]
fn unbalanced_open_delimiter_is_bounded() {
let runaway = format!("{{{{ {}", "A".repeat(500));
let residual = assert_no_unrendered(&runaway, "x", false, identity)
.unwrap()
.unwrap();
assert!(residual.snippet.len() <= MAX_SNIPPET, "snippet not bounded");
}
#[test]
fn unbalanced_open_delimiter_with_multibyte_does_not_panic() {
let runaway = format!("{{{{ {}", "日本語".repeat(40));
let residual = assert_no_unrendered(&runaway, "x", false, identity)
.unwrap()
.unwrap();
assert!(residual.snippet.len() <= MAX_SNIPPET, "snippet not bounded");
assert!(std::str::from_utf8(residual.snippet.as_bytes()).is_ok());
let emoji = format!("{{{{ {}", "🦀".repeat(40));
let r2 = assert_no_unrendered(&emoji, "x", false, identity)
.unwrap()
.unwrap();
assert!(r2.snippet.len() <= MAX_SNIPPET);
assert!(std::str::from_utf8(r2.snippet.as_bytes()).is_ok());
}
#[test]
fn logged_helper_warns_once_in_lenient_and_bails_in_strict() {
let leaked = "url=\"https://x/{{ .Version }}/t\"";
let mut warns: Vec<String> = Vec::new();
assert_no_unrendered_logged(leaked, "scoop manifest", false, identity, |m| {
warns.push(m.to_string())
})
.unwrap();
assert_eq!(warns.len(), 1, "warn must fire exactly once");
assert!(warns[0].contains("scoop manifest"));
assert!(warns[0].contains("{{ .Version }}"));
assert!(warns[0].contains("unrendered template delimiter"));
let mut strict_warns = 0usize;
let err = assert_no_unrendered_logged(leaked, "scoop manifest", true, identity, |_| {
strict_warns += 1
})
.unwrap_err();
assert_eq!(strict_warns, 0, "strict path must not warn");
assert!(err.to_string().contains("unrendered template delimiter"));
assert!(err.to_string().contains("scoop manifest"));
let mut clean_warns = 0usize;
assert_no_unrendered_logged("clean", "x", false, identity, |_| clean_warns += 1).unwrap();
assert_eq!(clean_warns, 0);
}
}