ucifer 0.3.0

OpenWrt UCI Parser and Exporter
Documentation
/* Copyright © 2025 CZ.NIC z.s.p.o. (http://www.nic.cz/)
 *
 * This file is part of the ucifer library
 *
 * Ucifer is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Ucifer is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * For more information, see /LICENSE.txt
 */

//! This module contains parser structure, which is made a type for convenience
//! only.

use {
	super::lexer::Error as LexError,
	crate::syntax::{
		Directive,
		lexer::{Lexer, Token},
	},
	alloc::{
		borrow::{Cow, ToOwned},
		string::String,
	},
	core::error::Error as ErrorTrait,
	derive_more::{Display, Error},
	descape::InvalidEscape,
	logos::Span,
};

/// Directive Parser
pub struct Parser<'lexer, 'src> {
	/// Inner lexer
	pub lexer: &'lexer mut Lexer<'src>,
}

impl<'lexer, 'src> Parser<'lexer, 'src>
where
	'src: 'lexer,
{
	/// Attempt to parse one directive
	pub fn run(mut self) -> Result<Option<Directive<'src>>, Error> {
		// Get next non-newline unquoted token
		let keyword = loop {
			match self.lexer.next() {
				Some(Ok(Token::LineBreak)) => (),
				Some(Ok(Token::UnquotedString(string))) => break string,
				Some(Ok(Token::QuotedString(_))) => {
					return Err(self.span_error(ErrorKind::QuotedKeyword));
				}
				Some(Err(e)) => return Err(self.span_error(ErrorKind::from_lex(e, self.lexer))),
				None => return Ok(None),
			}
		};

		match keyword {
			"package" => self.package(),
			"config" => self.config(),
			"option" => self.option(),
			"list" => self.list(),
			_ => Err(self.span_error(ErrorKind::UnexpectedToken {
				expected: "package, config, option, list",
			})),
		}
		.map(Some)
	}

	/// Parse package directive
	///
	/// ```sh
	/// package <name>
	/// ```
	fn package(&mut self) -> Result<Directive<'src>, Error> {
		let name = self.next()?.into_cow_str();
		Ok(Directive::Package(name))
	}

	/// Parse configuration section directive
	///
	/// ```sh
	/// config <type> [name]
	/// ```
	fn config(&mut self) -> Result<Directive<'src>, Error> {
		let type_ = self.next()?.into_cow_str();
		let name = self.maybe_next_not_nl_string()?;
		Ok(Directive::Section { type_, name })
	}

	/// Parse option directive
	///
	/// ```sh
	/// option <key> [value]
	/// ```
	fn option(&mut self) -> Result<Directive<'src>, Error> {
		let key = self.next()?.into_cow_str();
		let value = self.maybe_next_not_nl_string()?;
		Ok(Directive::Option { key, value })
	}

	/// Parse list directive
	///
	/// ```sh
	/// list <key> <value>
	/// ```
	fn list(&mut self) -> Result<Directive<'src>, Error> {
		let key = self.next()?.into_cow_str();
		let value = self.next()?.into_cow_str();
		Ok(Directive::List { key, value })
	}

	/// Attempt to lex next token
	fn next(&mut self) -> Result<Token<'src>, Error> {
		match self.lexer.next() {
			Some(Ok(token)) => Ok(token),
			Some(Err(e)) => Err(ErrorKind::from_lex(e, self.lexer)),
			None => Err(ErrorKind::UnexpectedEnd),
		}
		.map_err(|kind| self.span_error(kind))
	}

	/// Attempt to lex next token to string
	fn maybe_next_not_nl_string(&mut self) -> Result<Option<Cow<'src, str>>, Error> {
		match self.lexer.next() {
			Some(Ok(Token::LineBreak)) | None => Ok(None),
			Some(Ok(token)) => Ok(Some(token.into_cow_str())),
			Some(Err(e)) => Err(self.span_error(ErrorKind::from_lex(e, self.lexer))),
		}
	}

	/// Construct error at current token span
	fn span_error(&self, kind: ErrorKind) -> Error {
		Error {
			kind,
			at: self.lexer.span(),
		}
	}
}

/// Error variant without a span. To be wrapped in [`struct@Error`]
#[derive(Clone, Debug, Display, PartialEq, Eq, Error)]
pub enum ErrorKind {
	/// Error produced by string unescaping
	#[display("encountered invalid string escape: {_0}")]
	Unescape(InvalidEscape),
	/// Found codepoints not making any known token
	#[display("invalid token: \"{_0}\"")]
	InvalidToken(#[error(not(source))] String),
	/// Input ended unexpectedly (like unclosed string literal)
	#[display("unexpected end of input")]
	UnexpectedEnd,
	/// Expected different token
	#[display("unexpected token, expected: {expected}")]
	UnexpectedToken {
		/// Hint on which token(s) were expected
		expected: &'static str,
	},
	/// Found quoted string in keyword position
	#[display("keywords can't be quoted")]
	QuotedKeyword,
}

impl ErrorKind {
	/// Convert [`LexError`] to [`ErrorKind`]
	///
	/// This also takes `lexer` as a parameter as [`ErrorKind::InvalidToken`]
	/// stored the bogus »token« string
	fn from_lex(error: LexError, lexer: &Lexer) -> Self {
		match error {
			LexError::UnexpectedEndOfPattern => Self::UnexpectedEnd,
			LexError::InvalidToken => Self::InvalidToken(lexer.slice().to_owned()),
			LexError::Escape(error) => Self::Unescape(error),
		}
	}
}

/// Implements error, adds span info
#[derive(Clone, Debug, Display, PartialEq, Eq)]
#[display("parse error: {kind} at {at:?}")]
pub struct Error {
	/// Error type
	pub kind: ErrorKind,
	/// Error location in bytes
	pub at: Span,
}

impl ErrorTrait for Error {
	fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
		ErrorTrait::source(&self.kind)
	}
}