caddyfile 0.1.1

A library for working with Caddy's Caddyfile format
Documentation
// Copyright 2015 Matthew Holt and The Caddy Authors
// Copyright 2023 Aaron Dewes and The Nirvati Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::Read;

#[derive(Default, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Token {
    pub file: Option<String>,
    pub line: u64,
    pub text: String,
    pub was_quoted: Option<char>,
    pub in_snippet: bool,
    pub snippet_name: String,
}

pub struct Tokenizer<'a> {
    reader: &'a mut dyn std::io::Read,
    pub file: Option<String>,
    token: Token,
    line: u64,
    skipped_lines: u64,
}

impl Iterator for Tokenizer<'_> {
    type Item = Token;

    fn next(&mut self) -> Option<Self::Item> {
        let mut val: String = String::new();
        let mut comment: bool = false;
        let mut quoted: bool = false;
        let mut bt_quoted: bool = false;
        let mut escaped: bool = false;

        macro_rules! return_token {
            () => {
                self.token.text = val.clone();
                self.token.was_quoted = None;
                self.token.file = self.file.clone();
                return Some(self.token.clone());
            };
            ( $ch:expr ) => {
                self.token.text = val.clone();
                self.token.was_quoted = Some($ch);
                self.token.file = self.file.clone();
                return Some(self.token.clone());
            };
        }
        while let Some(Ok(ch)) = self.reader.bytes().next() {
            // Check if it is a boom
            if self.line == 1 {
                if ch == 0xEF {
                    let mut buf = [0; 2];
                    if let Some(Ok(ch)) = self.reader.bytes().next() {
                        buf[0] = ch;
                    }
                    if let Some(Ok(ch)) = self.reader.bytes().next() {
                        buf[1] = ch;
                    }
                    if buf == [0xBB, 0xBF] {
                        continue;
                    }
                }
            }
            let ch = ch as char;
            if self.line == 1 && val.is_empty() && ch == '\u{FEFF}' {
                continue;
            }
            if !escaped && !bt_quoted && ch == '\\' {
                escaped = true;
                continue;
            }

            if quoted || bt_quoted {
                if quoted && escaped {
                    // all is literal in quoted area,
                    // so only escape quotes
                    if ch != '"' {
                        val.push('\\');
                    }
                    escaped = false;
                } else {
                    if quoted && ch == '"' {
                        return_token!('"');
                    }
                    if bt_quoted && ch == '`' {
                        return_token!('`');
                    }
                }
                if ch == '\n' {
                    self.line += 1 + self.skipped_lines;
                    self.skipped_lines = 0;
                }
                val.push(ch);
                continue;
            }

            if ch.is_whitespace() {
                if ch == '\r' {
                    continue;
                }
                if ch == '\n' {
                    if escaped {
                        self.skipped_lines += 1;
                        escaped = false;
                    } else {
                        self.line += 1 + self.skipped_lines;
                        self.skipped_lines = 0;
                    }
                    comment = false;
                }
                if !val.is_empty() {
                    return_token!();
                }
                continue;
            }

            if ch == '#' && val.is_empty() {
                comment = true;
            }
            if comment {
                continue;
            }

            if val.is_empty() {
                self.token = Token {
                    line: self.line,
                    ..Default::default()
                };
                if ch == '"' {
                    quoted = true;
                    continue;
                }
                if ch == '`' {
                    bt_quoted = true;
                    continue;
                }
            }

            if escaped {
                val.push('\\');
                escaped = false
            }

            val.push(ch);
        }

        if !val.is_empty() {
            return_token!();
        } else {
            None
        }
    }
}

impl<'a> Tokenizer<'a> {
    pub fn new(reader: &'a mut dyn std::io::Read, file: Option<String>) -> Self {
        Self {
            reader,
            file,
            token: Token {
                line: 1,
                ..Default::default()
            },
            line: 1,
            skipped_lines: 0,
        }
    }
}