use noyalib::compat::serde_yaml as serde_yml;
use super::CompileError;
use super::manifest::is_uri_scheme;
use super::trailer::TrailerKind;
fn forbidden_field(key: &str, tls_context: bool) -> Option<&'static str> {
match key {
"routeFiles" | "routeFilesFromRoot" | "route_files" | "route_files_from_root" => {
Some("route source")
}
"profiles" => Some("profile"),
"includes" => Some("include"),
"cert" | "certPath" | "cert_path" if tls_context => Some("certificate"),
"key" | "keyPath" | "key_path" if tls_context => Some("private key"),
"client_ca" | "clientCaPath" | "client_ca_path" if tls_context => Some("client CA"),
"wasm" => Some("wasm module"),
"plugin" => Some("plugin file"),
"xslt" => Some("xslt stylesheet"),
"xsd" => Some("xsd schema"),
"sql" => Some("sql file"),
"static_dir" | "staticDir" => Some("static directory"),
_ => None,
}
}
fn is_asset_context(key: &str) -> bool {
matches!(
key,
"tls" | "ssl" | "sslContext" | "ssl_context" | "rest" | "mcp"
)
}
fn uri_strings<'a>(key: &str, val: &'a serde_yml::Value) -> Vec<&'a str> {
let enrich = matches!(key, "enrich" | "poll_enrich" | "pollEnrich");
if (key == "from"
|| key == "to"
|| key == "wire_tap"
|| key == "wireTap"
|| key == "dead_letter_channel"
|| key == "deadLetterChannel"
|| enrich)
&& let Some(uri) = val.as_str()
{
return vec![uri];
}
if enrich && let Some(uri) = val.get("uri").and_then(|uri| uri.as_str()) {
return vec![uri];
}
if (key == "scatter_gather" || key == "scatterGather")
&& let Some(endpoints) = val.get("endpoints").and_then(|e| e.as_sequence())
{
return endpoints.iter().filter_map(|e| e.as_str()).collect();
}
Vec::new()
}
fn forbidden_scheme(scheme: &str) -> Option<&'static str> {
Some(match scheme {
"wasm" => "wasm module",
"xslt" => "xslt stylesheet",
"validator" => "xsd schema",
_ => return None,
})
}
fn label(kind: TrailerKind, class: &str) -> String {
match (kind, class) {
(TrailerKind::Job, "route source" | "profile" | "include") => {
format!("job dependency: {class}")
}
_ => class.to_string(),
}
}
pub fn reject_unsupported_assets(
document_text: &str,
kind: TrailerKind,
) -> Result<(), CompileError> {
let root: serde_yml::Value = serde_yml::from_str(document_text)
.map_err(|e| CompileError::InvalidDocument(format!("not a YAML/JSON document: {e}")))?;
let mut violations: Vec<String> = Vec::new();
walk(&root, kind, &mut violations, false);
if violations.is_empty() {
Ok(())
} else {
Err(CompileError::UnsupportedAsset(violations.join("; ")))
}
}
fn walk(
value: &serde_yml::Value,
kind: TrailerKind,
violations: &mut Vec<String>,
tls_context: bool,
) {
match value {
serde_yml::Value::Mapping(map) => {
for (key, val) in map {
let key = key.as_str();
if let Some(class) = forbidden_field(key, tls_context) {
violations.push(format!(
"field '{key}' ({}, {})",
label(kind, class),
value_shape(val)
));
} else if let Some(file) = secret_file(key, val) {
let class = label(kind, "secret file");
violations.push(format!(
"field '{key}' ({class} '{file}', {})",
value_shape(val)
));
} else {
for uri in uri_strings(key, val) {
if let Some((scheme, _)) = uri.split_once(':')
&& is_uri_scheme(scheme)
&& let Some(class) = forbidden_scheme(scheme)
{
violations.push(format!("endpoint '{uri}' ({class})"));
}
}
}
walk(val, kind, violations, tls_context || is_asset_context(key));
}
}
serde_yml::Value::Sequence(seq) => {
for item in seq {
walk(item, kind, violations, tls_context);
}
}
_ => {}
}
}
fn value_shape(value: &serde_yml::Value) -> &'static str {
if any_string(value, |s| s.contains("${env:")) {
"dynamic ${env:} placeholder"
} else if any_string(value, |s| {
s.contains('*') || s.contains('?') || (s.contains('[') && s.contains(']'))
}) {
"glob pattern"
} else {
"file reference"
}
}
fn secret_file(key: &str, value: &serde_yml::Value) -> Option<String> {
if key != "secrets" && key != "secret" {
return None;
}
let mut stack = vec![value];
while let Some(value) = stack.pop() {
match value {
serde_yml::Value::Mapping(map) => {
for (k, v) in map {
if k.as_str() == "file"
&& let Some(file) = v.as_str()
{
return Some(file.to_string());
}
stack.push(v);
}
}
serde_yml::Value::Sequence(seq) => stack.extend(seq.iter()),
_ => {}
}
}
None
}
fn any_string(value: &serde_yml::Value, pred: fn(&str) -> bool) -> bool {
match value {
serde_yml::Value::String(s) => pred(s),
serde_yml::Value::Mapping(map) => map.values().any(|v| any_string(v, pred)),
serde_yml::Value::Sequence(seq) => seq.iter().any(|v| any_string(v, pred)),
_ => false,
}
}