adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
use super::hcl::ast::error::Error as AstError;
pub mod ast;
mod depth;

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum Error {
    #[error(transparent)]
    Pest(#[from] Box<pest::error::Error<Rule>>),

    #[error("File is too large to parse: {len} bytes, limit is {max}")]
    #[diagnostic(code(hcl::too_large))]
    TooLarge { len: usize, max: usize },

    #[error("File nests too deeply to parse: {depth} levels, limit is {max}")]
    #[diagnostic(
        code(hcl::too_deep),
        help(
            "Brackets and chained operators each make the parser recurse, and a deep enough file overflows the stack. Split the expression."
        )
    )]
    TooDeep { depth: usize, max: usize },
}

impl From<pest::error::Error<Rule>> for Error {
    fn from(value: pest::error::Error<Rule>) -> Self {
        Self::Pest(Box::new(value))
    }
}

type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(pest_derive::Parser)]
#[grammar = "parsers/hcl/hcl.pest"]
struct HclParser;

type Pair<'i> = pest::iterators::Pair<'i, Rule>;
type Pairs<'i> = pest::iterators::Pairs<'i, Rule>;

/// Ceiling on the size of a file handed to the parser.
///
/// Unlike [`depth`], this bounds nothing about the recursion — 3 KiB of nested
/// brackets is already enough to overflow the stack. It only keeps the parser
/// from spending minutes and gigabytes on a file no Terraform project holds:
/// pest backtracks, and the measured cost is around five rule invocations per
/// byte of a well-formed input.
const MAX_INPUT_BYTES: usize = 1024 * 1024;

pub fn parse(input: &str) -> Result<(ast::Hcl<'_>, Vec<AstError>)> {
    use pest::Parser;

    if input.len() > MAX_INPUT_BYTES {
        return Err(Error::TooLarge {
            len: input.len(),
            max: MAX_INPUT_BYTES,
        });
    }

    depth::check(input)?;

    let mut pairs = HclParser::parse(Rule::Hcl, input)?;
    Ok(ast::process(pairs.next().unwrap()))
}

#[cfg(test)]
mod tests {
    use super::{Error, parse};

    /// End to end: the guard has to fire before pest does, because what it
    /// prevents is an abort. If it ever stops firing this test does not fail,
    /// it takes the whole test process down with a stack overflow.
    #[test]
    fn parse_refuses_a_file_that_would_overflow_the_stack() {
        for input in [
            format!("a = {}1{}\n", "[".repeat(2_000), "]".repeat(2_000)),
            format!("a = 1{}\n", "-1".repeat(20_000)),
            format!("{}\n{}", "b {".repeat(2_000), "}".repeat(2_000)),
        ] {
            assert!(
                matches!(parse(&input), Err(Error::TooDeep { .. })),
                "a {} byte input was handed to pest",
                input.len()
            );
        }
    }

    #[test]
    fn parse_still_reads_the_sample_the_repository_ships() {
        let input = std::fs::read_to_string("main.sample.tf").unwrap();
        let (ast, errors) = parse(&input).unwrap();

        assert!(errors.is_empty(), "{errors:?}");
        assert!(ast.get_block("terraform", None).is_some());
    }
}