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
 */

//! Syntax unit [`Directive`] definition and parsing facilities for the UCI
//! configuration file format
//!
//! # UCI Format
//! UCI format consists of elements, here called [`Directive`]s. They act
//! imperatively, create or modify items and in case of `config`, switch
//! context.
//!
//! - It is indentation insensitive and if it may seem such, it is not a
//!   structured format
//! - Items are separated by any non-zero amount of non-newline whitespace
//! - Directives are separated by a newline
//! - Comments are delimited by `#` and `;` (which should not appear at the
//!   start of the line)
//!
//! <div class="warning">
//! There ain't many checks around strings used in directives and there is no
//! equivalence testing with OpenWrt's implementation.
//!
//! Display-formatting Directive, which contains some unsupported characters may
//! lead to unexpected results.
//! </div>

mod lexer;
mod parser;

pub use parser::{Error as ParseError, ErrorKind as ParseErrorKind};

use {crate::CowStr, core::fmt::Display, lexer::Token, logos::Logos};

/// A »line« of UCI config file
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Directive<'a> {
	/// Package directive should switch package context or set the name, but I
	/// haven't seen `uci` ever obeying it in any way and it seems to be a no-op
	/// (nowadays).
	///
	/// ```sh
	/// package <name>
	/// ```
	Package(CowStr<'a>),
	/// This specifies a configuration section, file can contain several ones.
	/// All options *have to* be contained in one of these.
	///
	/// Sections have a type and optional name. In case of sections without a
	/// name, they are refered to as by ordinal indices.
	///
	/// ```sh
	/// config <type> [name]
	/// ```
	Section {
		/// Type of section to look ordinal items under
		type_: CowStr<'a>,
		/// Sets name in namespace of the package for the section
		name: Option<CowStr<'a>>,
	},
	/// This specifies an option. Options do have a key and an optional value.
	/// If the value is not specified, it seems to be a no-op.
	///
	/// ```sh
	/// option <key> [value]
	/// ```
	Option {
		/// Lookup name for option
		key: CowStr<'a>,
		/// Value to set. If [`None`], it is a no-op.
		value: Option<CowStr<'a>>,
	},
	/// Appends item to a list. If list doesn't exist, it creates it. If it is
	/// already a scalar option, first boxes the option in a list.
	///
	/// ```sh
	/// list <key> <value>
	/// ```
	List {
		/// Lookup name for list
		key: CowStr<'a>,
		/// Value to append
		value: CowStr<'a>,
	},
}

impl<'a> Directive<'a> {
	/// Parse next one directive from string
	///
	/// This modifies provided string to point past end of the parse if
	/// sucessful.
	///
	/// # Errors
	/// When the syntax is not valid or complete
	pub fn next_from_str(string: &mut &'a str) -> Result<Option<Self>, ParseError> {
		let mut lexer = Token::lexer(string);
		let parse_result = parser::Parser { lexer: &mut lexer }.run();

		if parse_result.is_ok() {
			*string = lexer.remainder();
		}

		parse_result
	}
}

impl Display for Directive<'_> {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		let (directive, key, value) = match self {
			Directive::Package(s) => ("package", s, None),
			Directive::Section { type_, name } => ("config", type_, name.as_ref()),
			Directive::Option { key, value } => ("\toption", key, value.as_ref()),
			Directive::List { key, value } => ("\tlist", key, Some(value)),
		};

		write!(f, "{directive} \"{}\"", key.escape_default())?;
		if let Some(value) = value {
			write!(f, " \"{}\"", value.escape_default())?;
		}

		Ok(())
	}
}

/// Iterator of [`Directive`] until the end of input
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct DirectiveIter<'a> {
	/// Input which we will one-by-one cut
	input: &'a str,
}

impl<'a> DirectiveIter<'a> {
	/// Create a new iterator of [`Directive`] from string input
	pub const fn new(input: &'a str) -> Self {
		Self { input }
	}
}

impl<'a> Iterator for DirectiveIter<'a> {
	type Item = Result<Directive<'a>, ParseError>;

	fn next(&mut self) -> Option<Self::Item> {
		Directive::next_from_str(&mut self.input).transpose()
	}
}

impl<'a> DirectiveIter<'a> {
	/// Get rest of the input
	#[must_use]
	pub const fn remainder(&self) -> &'a str {
		self.input
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn simple_directives() {
		let mut input = r#"
			package ':3'
			config type 0name
				option 'novalue'

			list   "has" value
		"#;

		let mut next = || Directive::next_from_str(&mut input).unwrap();
		assert_eq!(next(), Some(Directive::Package(":3".into())));
		assert_eq!(
			next(),
			Some(Directive::Section {
				type_: "type".into(),
				name: Some("0name".into())
			})
		);
		assert_eq!(
			next(),
			Some(Directive::Option {
				key: "novalue".into(),
				value: None
			})
		);
		assert_eq!(
			next(),
			Some(Directive::List {
				key: "has".into(),
				value: "value".into()
			})
		);
		assert_eq!(next(), None);
	}

	#[test]
	fn display() {
		use core::fmt::Write as _;

		let directives = [
			Directive::Package(":3".into()),
			Directive::Section {
				type_: "kitty".into(),
				name: Some("emily".into()),
			},
			Directive::Option {
				key: "color".into(),
				value: Some("orange".into()),
			},
			Directive::List {
				key: "traits".into(),
				value: "Sync".into(),
			},
		];

		let mut buf = alloc::string::String::new();
		directives
			.iter()
			.try_for_each(|dir| writeln!(&mut buf, "{dir}"))
			.unwrap();

		DirectiveIter::new(&buf)
			.zip(directives)
			.for_each(|(parsed, expected)| assert_eq!(parsed, Ok(expected)));
	}
}