use crate::security_warnings::{security_warning_link, W_SEC_019};
use crate::RuntimeManifest;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownManifestKey {
pub key: String,
pub block_path: String,
pub nearest_known: Option<String>,
}
impl UnknownManifestKey {
fn location(&self) -> String {
if self.block_path.is_empty() {
"at the top level".to_string()
} else {
format!("in {}", self.block_path)
}
}
}
fn edit_distance(left: &str, right: &str) -> usize {
let left: Vec<char> = left.chars().collect();
let right: Vec<char> = right.chars().collect();
if left.is_empty() {
return right.len();
}
if right.is_empty() {
return left.len();
}
let mut grid = vec![vec![0usize; right.len() + 1]; left.len() + 1];
for (i, row) in grid.iter_mut().enumerate() {
row[0] = i;
}
for (j, cell) in grid[0].iter_mut().enumerate() {
*cell = j;
}
for i in 1..=left.len() {
for j in 1..=right.len() {
let substitution = grid[i - 1][j - 1] + usize::from(left[i - 1] != right[j - 1]);
let mut best = substitution.min(grid[i - 1][j] + 1).min(grid[i][j - 1] + 1);
if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1] {
best = best.min(grid[i - 2][j - 2] + 1);
}
grid[i][j] = best;
}
}
grid[left.len()][right.len()]
}
#[must_use]
pub fn suggestion_threshold(declared: &str, candidate: &str) -> usize {
(declared.chars().count().max(candidate.chars().count()) / 3).max(1)
}
#[must_use]
pub fn nearest_known_key(declared: &str, known: &[&str]) -> Option<String> {
known
.iter()
.map(|candidate| (edit_distance(declared, candidate), *candidate))
.filter(|(distance, candidate)| *distance <= suggestion_threshold(declared, candidate))
.min_by_key(|(distance, _)| *distance)
.map(|(_, candidate)| candidate.to_string())
}
#[must_use]
pub fn unknown_manifest_key_warnings(keys: &[UnknownManifestKey]) -> Vec<String> {
let link = security_warning_link(W_SEC_019);
keys.iter()
.map(|unknown| {
let key = &unknown.key;
let location = unknown.location();
match &unknown.nearest_known {
Some(nearest) => format!(
"[murmur-artifact] warning[{W_SEC_019}]: unrecognized key '{key}' {location} \
— did you mean '{nearest}'? The manifest has a spelling problem here: the \
key was parsed and ignored, so whatever it declared is not in effect. \
Correct it in murmur.yaml ({link})"
),
None => format!(
"[murmur-artifact] warning[{W_SEC_019}]: unrecognized key '{key}' {location} \
— this build of mur does not recognize it and no key it does recognize there \
is close to it, so the key may come from a newer mur. It was parsed and \
ignored; nothing here says the manifest is misspelled ({link})"
),
}
})
.collect()
}
pub fn warn_on_unknown_manifest_keys(manifest: &RuntimeManifest) {
for line in unknown_manifest_key_warnings(&manifest.unknown_keys) {
crate::warn_line(format_args!("{line}"));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unknown(key: &str, path: &str, nearest: Option<&str>) -> UnknownManifestKey {
UnknownManifestKey {
key: key.to_string(),
block_path: path.to_string(),
nearest_known: nearest.map(str::to_string),
}
}
#[test]
fn edit_distance_counts_the_usual_single_edits() {
assert_eq!(edit_distance("read_only", "read_only"), 0);
assert_eq!(edit_distance("read-only", "read_only"), 1);
assert_eq!(edit_distance("", "abc"), 3);
assert_eq!(edit_distance("abc", ""), 3);
assert_eq!(edit_distance("allwo", "allow"), 1);
assert_eq!(edit_distance("abc", "cba"), 2);
}
#[test]
fn a_one_character_typo_suggests_the_real_key() {
assert_eq!(
nearest_known_key("read-only", &["scope", "workdir_exec", "read_only"]),
Some("read_only".to_string())
);
}
#[test]
fn an_unrelated_key_suggests_nothing() {
assert_eq!(
nearest_known_key(
"quantum_teleport",
&["name", "version", "artifacts", "capabilities", "context"]
),
None
);
}
#[test]
fn a_near_match_is_worded_as_a_spelling_problem() {
let lines = unknown_manifest_key_warnings(&[unknown(
"read-only",
"capabilities.filesystem",
Some("read_only"),
)]);
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("unrecognized key 'read-only' in capabilities.filesystem"));
assert!(lines[0].contains("did you mean 'read_only'?"));
assert!(lines[0].contains("spelling problem"));
assert!(lines[0].contains("#w-sec-019"));
}
#[test]
fn no_near_match_is_worded_as_a_possibly_newer_key() {
let lines = unknown_manifest_key_warnings(&[unknown("quantum_teleport", "", None)]);
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("unrecognized key 'quantum_teleport' at the top level"));
assert!(lines[0].contains("may come from a newer mur"));
assert!(!lines[0].contains("did you mean"));
assert!(!lines[0].contains("spelling"));
}
#[test]
fn a_manifest_with_no_unrecognized_key_emits_nothing_at_all() {
assert!(unknown_manifest_key_warnings(&[]).is_empty());
}
}