use crate::ast::{self, Arg, Decl, Expr, Field, FieldValue, Program, Result, Span, Stmt, VclError};
use crate::vmod::{Sig, VmodSpec};
use std::collections::{BTreeMap, HashMap, HashSet};
#[derive(Default)]
struct SymTab {
backends: HashMap<String, Span>,
probes: HashMap<String, Span>,
acls: HashMap<String, Span>,
subs: HashSet<String>,
imports: HashMap<String, Span>,
objects: HashMap<String, (String, String)>,
}
pub fn validate(p: &Program, specs: &BTreeMap<String, VmodSpec>) -> Result<()> {
let mut sym = SymTab::default();
for d in &p.decls {
match d {
Decl::Import { name, span, .. } => {
sym.imports.insert(name.clone(), *span);
}
Decl::Backend { name, span, .. } => {
if sym.backends.contains_key(name) {
return Err(VclError::new(*span, format!("duplicate backend '{name}'")));
}
sym.backends.insert(name.clone(), *span);
}
Decl::Probe { name, span, .. } => {
if sym.probes.contains_key(name) {
return Err(VclError::new(*span, format!("duplicate probe '{name}'")));
}
sym.probes.insert(name.clone(), *span);
}
Decl::Acl { name, span, .. } => {
if sym.acls.contains_key(name) {
return Err(VclError::new(*span, format!("duplicate acl '{name}'")));
}
sym.acls.insert(name.clone(), *span);
}
Decl::Sub { name, span, .. } => {
if name.starts_with("vcl_")
&& !ast::BUILTIN_SUB_ORDER.contains(&name.as_str())
&& !sym.subs.contains(name)
{
eprintln!(
"warning: '{name}' is not a recognized builtin vcl_* subroutine name -- possible typo, or a vmod-specific hook this tool doesn't know about (treated as a hook: kept as-is, may be declared more than once)"
);
}
if !ast::is_builtin_sub(name) && sym.subs.contains(name) {
return Err(VclError::new(*span, format!("duplicate sub '{name}'")));
}
sym.subs.insert(name.clone());
}
}
}
let reachable = reachable_from_vcl_init(&p.decls);
for d in &p.decls {
if let Decl::Sub { name, body, .. } = d {
if reachable.contains(name.as_str()) {
collect_new_in_vcl_init(body, &mut sym, specs)?;
} else {
forbid_new(body)?;
}
}
}
for d in &p.decls {
match d {
Decl::Backend {
body: Some(fields), ..
} => {
for f in fields {
resolve_field(f, &sym, specs)?;
}
}
Decl::Probe { body, .. } => {
for f in body {
resolve_field(f, &sym, specs)?;
}
}
Decl::Sub { body, .. } => {
for s in body {
resolve_stmt(s, &sym, specs)?;
}
}
_ => {}
}
}
Ok(())
}
fn reachable_from_vcl_init(decls: &[Decl]) -> HashSet<String> {
let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
for d in decls {
if let Decl::Sub { name, body, .. } = d {
let mut callees = Vec::new();
collect_calls(body, &mut callees);
graph.entry(name.as_str()).or_default().extend(callees);
}
}
let mut reachable: HashSet<String> = HashSet::new();
let mut stack = vec!["vcl_init"];
while let Some(n) = stack.pop() {
if reachable.insert(n.to_string()) {
if let Some(callees) = graph.get(n) {
for &c in callees {
if !reachable.contains(c) {
stack.push(c);
}
}
}
}
}
reachable
}
fn collect_calls<'a>(body: &'a [Stmt], out: &mut Vec<&'a str>) {
for s in body {
match s {
Stmt::Call { sub, .. } => out.push(sub.as_str()),
Stmt::If {
arms, else_body, ..
} => {
for (_, b) in arms {
collect_calls(b, out);
}
if let Some(b) = else_body {
collect_calls(b, out);
}
}
_ => {}
}
}
}
fn collect_new_in_vcl_init(
body: &[Stmt],
sym: &mut SymTab,
specs: &BTreeMap<String, VmodSpec>,
) -> Result<()> {
for s in body {
match s {
Stmt::New {
name,
vmod,
ctor,
args,
span,
} => {
if !sym.imports.contains_key(vmod) {
return Err(VclError::new(
*span,
format!("new: vmod '{vmod}' is not imported"),
));
}
if sym.objects.contains_key(name) {
return Err(VclError::new(
*span,
format!("duplicate vmod object name '{name}'"),
));
}
match specs.get(vmod).and_then(|spec| spec.objects.get(ctor)) {
Some(objspec) => check_args(args, &objspec.init, *span, sym, specs)?,
None if specs.get(vmod).is_some() => {
return Err(VclError::new(
*span,
format!("unknown constructor '{vmod}.{ctor}'"),
));
}
None => {
for a in args {
resolve_expr(&a.value, *span, sym, specs)?;
}
}
}
sym.objects
.insert(name.clone(), (vmod.clone(), ctor.clone()));
}
Stmt::If {
arms, else_body, ..
} => {
for (_, b) in arms {
collect_new_in_vcl_init(b, sym, specs)?;
}
if let Some(b) = else_body {
collect_new_in_vcl_init(b, sym, specs)?;
}
}
_ => {}
}
}
Ok(())
}
fn forbid_new(body: &[Stmt]) -> Result<()> {
for s in body {
match s {
Stmt::New { span, .. } => {
return Err(VclError::new(
*span,
"'new' is only allowed inside sub vcl_init",
));
}
Stmt::If {
arms, else_body, ..
} => {
for (_, b) in arms {
forbid_new(b)?;
}
if let Some(b) = else_body {
forbid_new(b)?;
}
}
_ => {}
}
}
Ok(())
}
fn resolve_field(f: &Field, sym: &SymTab, specs: &BTreeMap<String, VmodSpec>) -> Result<()> {
match &f.value {
FieldValue::Expr(e) => resolve_expr(e, f.span, sym, specs),
FieldValue::Probe(fields) => {
for inner in fields {
resolve_field(inner, sym, specs)?;
}
Ok(())
}
FieldValue::ProbeRef(name) => {
if sym.probes.contains_key(name) {
Ok(())
} else {
Err(VclError::new(f.span, format!("undefined probe '{name}'")))
}
}
FieldValue::StringList(_) => Ok(()),
}
}
fn resolve_stmt(s: &Stmt, sym: &SymTab, specs: &BTreeMap<String, VmodSpec>) -> Result<()> {
let ctx = s.span();
match s {
Stmt::Set { lhs, rhs, .. }
if matches!(
lhs.parts.last().map(String::as_str),
Some("backend_hint" | "backend")
) =>
{
if let Expr::Var(parts) = rhs {
if parts.len() == 1 && !sym.backends.contains_key(&parts[0]) {
return Err(VclError::new(
ctx,
format!("undefined reference '{}'", parts[0]),
));
}
}
resolve_expr(rhs, ctx, sym, specs)
}
Stmt::Set { rhs, .. } => resolve_expr(rhs, ctx, sym, specs),
Stmt::Unset { .. } => Ok(()),
Stmt::Call { sub, span } => {
if ast::is_builtin_sub(sub) {
return Err(VclError::new(
*span,
format!("cannot 'call' builtin sub '{sub}'"),
));
}
if !sym.subs.contains(sub) {
return Err(VclError::new(*span, format!("undefined sub '{sub}'")));
}
Ok(())
}
Stmt::Return { action, .. } => {
if let Some(a) = action {
for e in &a.args {
resolve_expr(e, ctx, sym, specs)?;
}
}
Ok(())
}
Stmt::Synthetic { value, .. } => resolve_expr(value, ctx, sym, specs),
Stmt::If {
arms, else_body, ..
} => {
for (cond, body) in arms {
resolve_expr(cond, ctx, sym, specs)?;
for st in body {
resolve_stmt(st, sym, specs)?;
}
}
if let Some(b) = else_body {
for st in b {
resolve_stmt(st, sym, specs)?;
}
}
Ok(())
}
Stmt::New { .. } => Ok(()),
Stmt::Expr { expr, .. } => resolve_expr(expr, ctx, sym, specs),
}
}
fn resolve_expr(
e: &Expr,
ctx: Span,
sym: &SymTab,
specs: &BTreeMap<String, VmodSpec>,
) -> Result<()> {
match e {
Expr::Var(parts) if parts.len() == 1 => {
let name = &parts[0];
if !(sym.backends.contains_key(name)
|| sym.probes.contains_key(name)
|| sym.acls.contains_key(name))
{
eprintln!(
"warning: '{name}' does not match any declared backend/probe/acl -- possible typo, or a vmod-defined enum constant this tool doesn't know about"
);
}
Ok(())
}
Expr::Var(_) => Ok(()),
Expr::Call { target, args } => {
let mut args_resolved = false;
if target.len() >= 2 {
let ns = &target[0];
let member = &target[target.len() - 1];
if let Some((vmod_name, ctor)) = sym.objects.get(ns) {
if let Some(spec) = specs.get(vmod_name) {
if let Some(objspec) = spec.objects.get(ctor) {
match objspec.methods.get(member) {
Some(sig) => {
check_args(args, sig, ctx, sym, specs)?;
args_resolved = true;
}
None => {
return Err(VclError::new(
ctx,
format!("unknown method '{member}' on object '{ns}'"),
))
}
}
}
}
} else if sym.imports.contains_key(ns) {
if let Some(spec) = specs.get(ns) {
match spec.funcs.get(member) {
Some(sig) => {
check_args(args, sig, ctx, sym, specs)?;
args_resolved = true;
}
None => {
return Err(VclError::new(
ctx,
format!("unknown function '{member}' in vmod '{ns}'"),
))
}
}
}
} else {
return Err(VclError::new(
ctx,
format!("call to unknown object or vmod '{ns}'"),
));
}
}
if !args_resolved {
for a in args {
resolve_expr(&a.value, ctx, sym, specs)?;
}
}
Ok(())
}
Expr::Unary { expr, .. } => resolve_expr(expr, ctx, sym, specs),
Expr::Binary { lhs, rhs, .. } => {
resolve_expr(lhs, ctx, sym, specs)?;
resolve_expr(rhs, ctx, sym, specs)
}
Expr::Str(_)
| Expr::Num(_)
| Expr::Duration(_)
| Expr::Bytes(_)
| Expr::Bool(_)
| Expr::Omitted
| Expr::CSource(_) => Ok(()),
}
}
fn check_args(
args: &[Arg],
sig: &Sig,
ctx: Span,
sym: &SymTab,
specs: &BTreeMap<String, VmodSpec>,
) -> Result<()> {
for a in args {
if let Some(n) = &a.name {
if !sig
.args
.iter()
.any(|s| s.name.as_deref() == Some(n.as_str()))
{
return Err(VclError::new(ctx, format!("unknown named argument '{n}'")));
}
}
}
let positional = args.iter().filter(|a| a.name.is_none()).count();
if positional > sig.args.len() {
return Err(VclError::new(ctx, "too many positional arguments"));
}
let mut next_pos = 0usize;
for a in args {
let slot = match &a.name {
None => {
let i = next_pos;
next_pos += 1;
sig.args.get(i)
}
Some(n) => sig
.args
.iter()
.find(|s| s.name.as_deref() == Some(n.as_str())),
};
match (slot.and_then(|s| s.enum_values.as_ref()), &a.value) {
(Some(values), Expr::Var(parts)) if parts.len() == 1 => {
if !values.contains(&parts[0]) {
return Err(VclError::new(
ctx,
format!(
"'{}' is not a valid value for this enum argument (expected one of: {})",
parts[0],
values.join(", ")
),
));
}
}
_ => resolve_expr(&a.value, ctx, sym, specs)?,
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_str;
use crate::vmod::ArgSpec;
use std::collections::BTreeMap;
fn no_specs() -> BTreeMap<String, VmodSpec> {
BTreeMap::new()
}
fn ok(src: &str) {
let p = parse_str(src).expect("parse failed");
validate(&p, &no_specs()).expect("expected validate() to succeed");
}
fn err(src: &str) -> String {
let p = parse_str(src).expect("parse failed");
let e = validate(&p, &no_specs()).expect_err("expected validate() to fail");
e.msg
}
#[test]
fn s1_duplicate_names_error_same_name_subs_ok() {
let dup_import = r#"
vcl 4.1;
import std;
import std;
sub vcl_recv { return (hash); }
"#;
ok(dup_import);
let dup_backend = r#"
vcl 4.1;
backend b { .host = "1.2.3.4"; .port = "80"; }
backend b { .host = "1.2.3.5"; .port = "80"; }
sub vcl_recv { return (hash); }
"#;
assert!(err(dup_backend).contains("duplicate backend"));
let dup_probe = r#"
vcl 4.1;
probe p { .url = "/"; }
probe p { .url = "/x"; }
sub vcl_recv { return (hash); }
"#;
assert!(err(dup_probe).contains("duplicate probe"));
let dup_acl = r#"
vcl 4.1;
acl a { "127.0.0.1"; }
acl a { "127.0.0.2"; }
sub vcl_recv { return (hash); }
"#;
assert!(err(dup_acl).contains("duplicate acl"));
let same_sub = r#"
vcl 4.1;
sub vcl_recv { set req.http.X-A = "1"; }
sub vcl_recv { set req.http.X-B = "2"; }
"#;
ok(same_sub);
let dup_custom_sub = r#"
vcl 4.1;
sub helper { set req.http.a = "1"; }
sub helper { set req.http.b = "2"; }
sub vcl_recv { call helper; }
"#;
assert!(err(dup_custom_sub).contains("duplicate sub"));
}
#[test]
fn s1b_unrecognized_vcl_prefixed_sub_name_is_not_an_error() {
let typo_builtin = r#"
vcl 4.1;
backend default none;
sub vcl_recv { set req.http.x = "1"; }
sub vcl_devliver { set resp.http.y = "1"; }
"#;
ok(typo_builtin);
let real_builtin = r#"
vcl 4.1;
sub vcl_deliver { set resp.http.y = "1"; }
"#;
ok(real_builtin);
let vcl_connect = r#"
vcl 4.1;
sub vcl_connect { return (accept); }
"#;
ok(vcl_connect);
}
#[test]
fn s2_undefined_references_error() {
let bad_call = r#"
vcl 4.1;
sub vcl_recv { call missing; }
"#;
assert!(err(bad_call).contains("undefined sub"));
let bad_probe_ref = r#"
vcl 4.1;
backend b {
.host = "1.2.3.4";
.port = "80";
.probe = missing;
}
sub vcl_recv { return (hash); }
"#;
assert!(err(bad_probe_ref).contains("undefined probe"));
let bad_bare_var = r#"
vcl 4.1;
backend b { .host = "1.2.3.4"; .port = "80"; }
sub vcl_recv {
set req.backend_hint = missing;
return (hash);
}
"#;
assert!(err(bad_bare_var).contains("undefined reference"));
}
#[test]
fn s3_new_placement_and_object_resolution() {
let new_outside_init = r#"
vcl 4.1;
import std;
sub vcl_recv {
new obj = std.foo();
return (hash);
}
"#;
assert!(err(new_outside_init).contains("vcl_init"));
let new_inside_init_and_used = r#"
vcl 4.1;
import directors;
backend web01 { .host = "1.2.3.4"; .port = "80"; }
sub vcl_init {
new vdir = directors.round_robin();
vdir.add_backend(web01);
}
sub vcl_recv {
set req.backend_hint = vdir.backend();
return (hash);
}
"#;
ok(new_inside_init_and_used);
let unknown_object_method = r#"
vcl 4.1;
sub vcl_recv {
set req.http.X = foo.bar();
return (hash);
}
"#;
assert!(err(unknown_object_method).contains("unknown object or vmod"));
}
#[test]
fn s3b_new_legal_transitively_reachable_from_vcl_init() {
let transitively_reachable = r#"
vcl 4.1;
import directors;
backend web01 { .host = "1.2.3.4"; .port = "80"; }
sub vcl_init { call setup; }
sub setup { call helper; }
sub helper {
new vdir = directors.round_robin();
vdir.add_backend(web01);
}
sub vcl_recv {
set req.backend_hint = vdir.backend();
return (hash);
}
"#;
ok(transitively_reachable);
let reachable_only_from_recv = r#"
vcl 4.1;
import directors;
sub not_reachable_from_init { new vdir = directors.round_robin(); }
sub vcl_recv { call not_reachable_from_init; return (hash); }
"#;
assert!(err(reachable_only_from_recv).contains("vcl_init"));
}
#[test]
fn s4_call_builtin_is_error() {
let src = r#"
vcl 4.1;
sub vcl_recv {
call vcl_recv;
return (hash);
}
"#;
assert!(err(src).contains("builtin"));
}
#[test]
fn s5_vmod_signature_checks() {
let mut specs = BTreeMap::new();
specs.insert(
"std".to_string(),
VmodSpec {
funcs: {
let mut m = BTreeMap::new();
m.insert(
"tolower".to_string(),
Sig {
args: vec![ArgSpec {
name: Some("s".to_string()),
default: None,
optional: false,
enum_values: None,
}],
},
);
m
},
objects: BTreeMap::new(),
},
);
let parse_and_validate = |src: &str| -> Result<()> {
let p = parse_str(src).unwrap();
validate(&p, &specs)
};
let unknown_func = r#"
vcl 4.1;
import std;
sub vcl_recv {
set req.http.X = std.nonexistent(req.url);
return (hash);
}
"#;
let e = parse_and_validate(unknown_func).expect_err("expected error");
assert!(e.msg.contains("unknown function"), "{}", e.msg);
let bad_arity = r#"
vcl 4.1;
import std;
sub vcl_recv {
set req.http.X = std.tolower(req.url, "a", "b");
return (hash);
}
"#;
let e = parse_and_validate(bad_arity).expect_err("expected error");
assert!(e.msg.contains("too many"), "{}", e.msg);
let unknown_named = r#"
vcl 4.1;
import std;
sub vcl_recv {
set req.http.X = std.tolower(bogus = req.url);
return (hash);
}
"#;
let e = parse_and_validate(unknown_named).expect_err("expected error");
assert!(e.msg.contains("unknown named argument"), "{}", e.msg);
let ok_call = r#"
vcl 4.1;
import std;
sub vcl_recv {
set req.http.X = std.tolower(req.url);
return (hash);
}
"#;
parse_and_validate(ok_call).expect("expected success");
}
#[test]
fn s5b_enum_argument_values_checked_against_spec() {
let mut specs = BTreeMap::new();
specs.insert(
"blob".to_string(),
VmodSpec {
funcs: {
let mut m = BTreeMap::new();
m.insert(
"encode".to_string(),
Sig {
args: vec![
ArgSpec {
name: Some("encoding".to_string()),
default: Some("IDENTITY".to_string()),
optional: true,
enum_values: Some(
["IDENTITY", "BASE64", "BASE64URL", "HEX", "URL"]
.iter()
.map(|s| s.to_string())
.collect(),
),
},
ArgSpec {
name: Some("blob".to_string()),
default: None,
optional: false,
enum_values: None,
},
],
},
);
m
},
objects: BTreeMap::new(),
},
);
let parse_and_validate = |src: &str| -> Result<()> {
let p = parse_str(src).unwrap();
validate(&p, &specs)
};
let valid_enum = r#"
vcl 4.1;
import blob;
sub vcl_recv {
set req.http.X = blob.encode(encoding = HEX, blob = req.hash);
return (hash);
}
"#;
parse_and_validate(valid_enum).expect("HEX is a legal encoding value");
let invalid_enum = r#"
vcl 4.1;
import blob;
sub vcl_recv {
set req.http.X = blob.encode(encoding = BOGUS, blob = req.hash);
return (hash);
}
"#;
let e = parse_and_validate(invalid_enum).expect_err("BOGUS is not a legal encoding value");
assert!(e.msg.contains("BOGUS"), "{}", e.msg);
assert!(e.msg.contains("not a valid value"), "{}", e.msg);
}
}