nginx-config 0.13.2

A parser, AST and formatter for nginx configuration files.
Documentation
use std::path::PathBuf;

use combine::{many, many1, Parser};
use combine::{choice, optional};
use combine::error::StreamError;
use combine::easy::Error;

use ast::{self, Item};
use grammar::{value, bool, block, Code};
use helpers::{semi, ident, string, prefix};
use tokenizer::{TokenStream, Token};
use value::Value;


fn error_page<'a>()
    -> impl Parser<Output=Item, Input=TokenStream<'a>>
{
    use ast::ErrorPageResponse;
    use value::Item::*;

    fn lit<'a, 'x>(val: &'a Value) -> Result<&'a str, Error<Token<'x>, Token<'x>>> {
        if val.data.is_empty() {
            return Err(Error::unexpected_message(
                "empty error codes are not supported"));
        }
        if val.data.len() > 1 {
            return Err(Error::unexpected_message(
                "only last argument of error_codes \
                can contain variables"));
        }
        match val.data[0] {
            Literal(ref x) => return Ok(x),
            _ => return Err(Error::unexpected_message(
                "only last argument of error_codes \
                can contain variables")),
        }
    }

    let is_eq = |val: &Value| -> Result<bool, Error<_, _>> {
        Ok(lit(val)?.starts_with('='))
    };

    ident("error_page")
    .with(many(value()))
    .and_then(move |mut v: Vec<_>| {
        if v.is_empty() {
            return Err(Error::unexpected_message(
                "error_page directive must not be empty"));
        }
        let uri = v.pop().unwrap();

        let response_code = if v.last().is_some() && is_eq(v.last().unwrap())? {
            let dest = v.pop().unwrap();
            let dest = lit(&dest)?;
            if dest == "=" {
                ErrorPageResponse::Keep
            } else {
                match Code::parse(&dest[1..])? {
                    Code::Redirect(code) => ErrorPageResponse::Redirect(code),
                    Code::Normal(code) => ErrorPageResponse::Replace(code),
                }
            }
        } else {
            ErrorPageResponse::Target
        };
        let mut codes = Vec::new();
        for code in v {
            codes.push(Code::parse(lit(&code)?)?.as_code());
        }

        Ok(Item::ErrorPage(::ast::ErrorPage {
            codes,
            response_code,
            uri,
        }))
    })
    .skip(semi())
}

enum ListenParts {
    DefaultServer,
    Ssl,
    Ext(ast::HttpExt),
    ProxyProtocol,
    SetFib(i32),
    FastOpen(u32),
    Backlog(i32),
    RcvBuf(u64),
    SndBuf(u64),
    Deferred,
    Bind,
    Ipv6Only(bool),
    ReusePort,
}

fn listen<'a>()
    -> impl Parser<Output=Item, Input=TokenStream<'a>>
{
    use ast::{Address, Listen, HttpExt};
    use self::ListenParts::*;

    ident("listen")
    .with(string().and_then(|s| -> Result<_, Error<_, _>> {
        let v = if s.value.starts_with("unix:") {
            Address::Unix(PathBuf::from(&s.value[6..]))
        } else if s.value.starts_with("*:") {
            Address::StarPort(s.value[2..].parse()?)
        } else {
            s.value.parse().map(Address::Port)
            .or_else(|_| s.value.parse().map(Address::Ip))?
        };
        Ok(v)
    }))
    .and(many::<Vec<_>, _>(choice((
        ident("default_server").map(|_| DefaultServer),
        ident("ssl").map(|_| Ssl),
        ident("http2").map(|_| Ext(HttpExt::Http2)),
        ident("spdy").map(|_| Ext(HttpExt::Spdy)),
        ident("proxy_protocol").map(|_| ProxyProtocol),
        prefix("setfib=").and_then(|val| val.parse().map(SetFib)),
        prefix("fastopen=").and_then(|val| val.parse().map(FastOpen)),
        prefix("backlog=").and_then(|val| val.parse().map(Backlog)),
        prefix("rcvbuf=").and_then(|val| val.parse().map(RcvBuf)),
        prefix("sndbuf=").and_then(|val| val.parse().map(SndBuf)),
        ident("deferred").map(|_| Deferred),
        ident("bind").map(|_| Bind),
        prefix("ipv6only=").and_then(|val| Ok(Ipv6Only(match val {
            "on" => true,
            "off" => false,
            _ => return Err(Error::unexpected_message("only on/off supported")),
        }))),
        ident("reuseport").map(|_| ReusePort),
    ))))
    .map(|(addr, items)| {
        let mut lst = Listen::new(addr);
        for item in items {
            match item {
                DefaultServer => lst.default_server = true,
                Ssl => lst.ssl = true,
                Ext(ext) => lst.ext = Some(ext),
                ProxyProtocol => lst.proxy_protocol = true,
                SetFib(v) => lst.setfib = Some(v),
                FastOpen(v) => lst.fastopen = Some(v),
                Backlog(v) => lst.backlog = Some(v),
                RcvBuf(v) => lst.rcvbuf = Some(v),
                SndBuf(v) => lst.sndbuf = Some(v),
                Deferred => lst.deferred = true,
                Bind => lst.bind = true,
                Ipv6Only(v) => lst.ipv6only = Some(v),
                ReusePort => lst.reuseport = true,
            }
        }
        return lst;
    })
    .skip(semi())
    .map(Item::Listen)
}

fn limit_except<'a>()
    -> impl Parser<Output=Item, Input=TokenStream<'a>>
{
    ident("limit_except")
    .with(many1(string().map(|x| x.value.to_string())))
    .and(block())
    .map(|(methods, (position, directives))| {
        Item::LimitExcept(ast::LimitExcept { methods, position, directives })
    })
}

pub fn directives<'a>()
    -> impl Parser<Output=Item, Input=TokenStream<'a>>
{
    choice((
        error_page(),
        listen(),
        limit_except(),
        ident("root").with(value()).skip(semi()).map(Item::Root),
        ident("alias").with(value()).skip(semi()).map(Item::Alias),
        ident("default_type").with(value()).skip(semi())
            .map(Item::DefaultType),
        ident("internal").skip(semi()).map(|_| Item::Internal),
        ident("etag").with(bool()).skip(semi()).map(Item::Etag),
        ident("server_tokens").with(value()).skip(semi())
            .map(Item::ServerTokens),
        ident("recursive_error_pages").with(bool()).skip(semi())
            .map(Item::RecursiveErrorPages),
        ident("chunked_transfer_encoding").with(bool()).skip(semi())
            .map(Item::ChunkedTransferEncoding),
        ident("keepalive_timeout")
            .with(value())
            .and(optional(value()))
            .map(|(timeo, htimeo)| Item::KeepaliveTimeout(timeo, htimeo))
            .skip(semi()),
        ident("error_log").with(value())
            .and(optional(string().and_then(|t| {
                use ast::ErrorLevel::*;
                match t.value {
                    "debug" => Ok(Debug),
                    "info" => Ok(Info),
                    "notice" => Ok(Notice),
                    "warn" => Ok(Warn),
                    "error" => Ok(Error),
                    "crit" => Ok(Crit),
                    "alert" => Ok(Alert),
                    "emerg" => Ok(Emerg),
                    _ => Err(::combine::easy::Error::unexpected_message(
                            "invalid log level")),
                }
            })))
            .skip(semi())
            .map(|(file, level)| Item::ErrorLog { file, level })
    ))
}