use crate::errors::ProxyError;
const MAX_YAML_BYTES: usize = 4_194_304;
const MAX_EXPANDED_BYTES: usize = 16_777_216;
pub(crate) fn check_yaml_safety(raw: &str) -> Result<(), ProxyError> {
check_yaml_size(raw)?;
check_yaml_expansion(raw, MAX_EXPANDED_BYTES)
}
fn check_yaml_size(raw: &str) -> Result<(), ProxyError> {
if raw.len() > MAX_YAML_BYTES {
return Err(ProxyError::Config(format!(
"YAML input too large ({} bytes, max {MAX_YAML_BYTES})",
raw.len()
)));
}
Ok(())
}
fn check_yaml_expansion(raw: &str, threshold: usize) -> Result<(), ProxyError> {
let Ok(value) = serde_yaml::from_str::<serde_yaml::Value>(raw) else {
return Ok(());
};
let size = estimate_value_size(&value);
if size > threshold {
return Err(ProxyError::Config(format!(
"YAML alias expansion too large ({size} bytes estimated from {} bytes raw, \
max {threshold})",
raw.len()
)));
}
Ok(())
}
fn estimate_value_size(value: &serde_yaml::Value) -> usize {
match value {
serde_yaml::Value::Null => 4,
serde_yaml::Value::Bool(_) => 5,
serde_yaml::Value::Number(n) => {
if let Some(u) = n.as_u64() {
count_digits(u)
} else if let Some(i) = n.as_i64() {
count_digits(i.unsigned_abs()) + usize::from(i.is_negative())
} else {
16
}
},
serde_yaml::Value::String(s) => s.len() + 2,
serde_yaml::Value::Sequence(seq) => 2 + seq.iter().map(estimate_value_size).sum::<usize>(),
serde_yaml::Value::Mapping(map) => {
2 + map
.iter()
.map(|(k, v)| estimate_value_size(k) + estimate_value_size(v) + 2)
.sum::<usize>()
},
serde_yaml::Value::Tagged(t) => t.tag.to_string().len() + estimate_value_size(&t.value),
}
}
fn count_digits(n: u64) -> usize {
if n == 0 {
return 1;
}
(n.ilog10() as usize) + 1
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::needless_raw_strings,
clippy::needless_raw_string_hashes,
reason = "tests use unwrap/expect/indexing/raw strings for brevity"
)]
mod tests {
use super::*;
#[test]
fn reject_oversized_yaml() {
let huge = "x".repeat(5 * 1024 * 1024);
let err = check_yaml_size(&huge).unwrap_err();
assert!(err.to_string().contains("too large"), "should reject oversized YAML");
}
#[test]
fn accept_small_yaml() {
check_yaml_size("a: 1\n").expect("small YAML should pass size check");
}
#[test]
fn reject_yaml_alias_bomb() {
let err = check_yaml_expansion("a: &a x\nb: &b [*a,*a,*a]\nlisteners: []\n", 5);
assert!(err.is_err(), "should reject expansion exceeding threshold");
assert!(
err.unwrap_err().to_string().contains("alias expansion too large"),
"error message should mention alias expansion"
);
}
#[test]
fn accept_yaml_within_expansion_threshold() {
check_yaml_expansion("a: &a x\nb: *a\nlisteners: []\n", 1_000_000)
.expect("small expansion within threshold should pass");
}
#[test]
fn safety_check_rejects_oversized() {
let huge = "x".repeat(5 * 1024 * 1024);
let err = check_yaml_safety(&huge).unwrap_err();
assert!(err.to_string().contains("too large"), "should reject oversized YAML");
}
#[test]
fn accept_yaml_at_exact_max_size() {
let exact = "x".repeat(MAX_YAML_BYTES);
check_yaml_size(&exact).expect("YAML at exactly MAX_YAML_BYTES should pass");
}
#[test]
fn reject_yaml_one_byte_over_max() {
let over = "x".repeat(MAX_YAML_BYTES + 1);
let err = check_yaml_size(&over).unwrap_err();
assert!(err.to_string().contains("too large"), "got: {err}");
}
#[test]
fn safety_check_passes_valid_yaml() {
check_yaml_safety("a: 1\n").expect("valid small YAML should pass all safety checks");
}
#[test]
fn expansion_check_unparseable_yaml_passes() {
let result = check_yaml_expansion("{{{{invalid yaml", MAX_EXPANDED_BYTES);
assert!(
result.is_ok(),
"unparseable YAML should pass expansion check (deferred to real parse)"
);
}
#[test]
fn count_digits_zero_returns_one() {
assert_eq!(count_digits(0), 1, "0 has one digit");
}
#[test]
fn count_digits_single() {
assert_eq!(count_digits(9), 1, "9 has one digit");
}
#[test]
fn count_digits_multi() {
assert_eq!(count_digits(100), 3, "100 has three digits");
assert_eq!(count_digits(999), 3, "999 has three digits");
assert_eq!(count_digits(1000), 4, "1000 has four digits");
}
#[test]
fn estimate_null_size() {
assert_eq!(estimate_value_size(&serde_yaml::Value::Null), 4, "null is 4 bytes");
}
#[test]
fn estimate_bool_size() {
assert_eq!(
estimate_value_size(&serde_yaml::Value::Bool(true)),
5,
"bool is 5 bytes"
);
}
#[test]
fn estimate_string_includes_quotes() {
let v = serde_yaml::Value::String("abc".to_owned());
assert_eq!(estimate_value_size(&v), 5, "3-char string + 2 quote bytes = 5");
}
}