From 3cf55504985ab252266de264b0c0b896476c1f89 Mon Sep 17 00:00:00 2001 From: Ron Waldon-Howe Date: Sat, 9 Nov 2024 11:18:52 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Parse=20into=20intermed?= =?UTF-8?q?iate=20list=20of=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list of elements idea was inspired by work performed by @elmarco over in #23 , which brings a lot of benefits that I was trying to provide using explicit low-level readers and writers of XML events: namely that we have access to element sequence (important for "last `` wins" and the correct interpolation of ``). serde offers a [try_from](https://serde.rs/container-attrs.html#try_from) attribute that allows us to abstract this away from the consumer. --- src/config.rs | 94 +++++++++++++++++++------ src/config/transform.rs | 150 ---------------------------------------- 2 files changed, 72 insertions(+), 172 deletions(-) delete mode 100644 src/config/transform.rs diff --git a/src/config.rs b/src/config.rs index 39b284b..80386bc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,8 +4,6 @@ use anyhow::{Error, Result}; use quick_xml::{events::Event, Reader}; use serde::Deserialize; -mod transform; - const EXPECTED_DOCTYPE_PARTS: &[&str] = &[ "busconfig", "PUBLIC", @@ -19,11 +17,11 @@ const EXPECTED_DOCTYPE_PARTS: &[&str] = &[ /// implements [`dbus-daemon`'s Configuration File](https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file) #[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(try_from = "Document")] pub struct BusConfig { /// If `Some`, connections that authenticated using the ANONYMOUS mechanism will be authorized to connect. /// This option has no practical effect unless the ANONYMOUS mechanism has also been enabled using the `auth` option. - // TODO: consider finding a way to make this `bool` instead of `Option<()>` - pub allow_anonymous: Option<()>, + pub allow_anonymous: bool, /// Lists permitted authorization mechanisms. /// If this element doesn't exist, then all known mechanisms are allowed. @@ -33,18 +31,16 @@ pub struct BusConfig { pub auth: HashSet, /// If `Some`, the bus daemon becomes a real daemon (forks into the background, etc.). - // TODO: consider finding a way to make this `bool` instead of `Option<()>` - pub fork: Option<()>, + pub fork: bool, /// If `Some`, the bus daemon keeps its original umask when forking. /// This may be useful to avoid affecting the behavior of child processes. - // TODO: consider finding a way to make this `bool` instead of `Option<()>` - pub keep_umask: Option<()>, + pub keep_umask: bool, /// Address(es) that the bus should listen on. /// The address is in the standard D-Bus format that contains a transport name plus possible parameters/options. #[serde(default)] - pub listen: Vec, + pub listen: HashSet, /// The bus daemon will write its pid to the specified file. pub pidfile: Option, @@ -55,8 +51,7 @@ pub struct BusConfig { pub servicedir: Vec, /// If `Some`, the bus daemon will log to syslog. - // TODO: consider finding a way to make this `bool` instead of `Option<()>` - pub syslog: Option<()>, + pub syslog: bool, /// This element only controls which message bus specific environment variables are set in activated clients. pub r#type: Option, @@ -67,6 +62,37 @@ pub struct BusConfig { pub user: Option, } +impl TryFrom for BusConfig { + type Error = Error; + + fn try_from(value: Document) -> Result { + let mut bc = BusConfig::default(); + + for element in value.busconfig { + match element { + Element::AllowAnonymous => bc.allow_anonymous = true, + Element::Auth(s) => { + bc.auth.insert(s); + } + Element::Fork => bc.fork = true, + Element::KeepUmask => bc.keep_umask = true, + Element::Listen(s) => { + bc.listen.insert(s); + } + Element::Pidfile(p) => bc.pidfile = Some(p), + Element::Servicedir(p) => { + bc.servicedir.push(p); + } + Element::Syslog => bc.syslog = true, + Element::Type(TypeElement { r#type: value }) => bc.r#type = Some(value), + Element::User(s) => bc.user = Some(s), + } + } + + Ok(bc) + } +} + impl BusConfig { pub fn parse(s: &str) -> Result { let mut reader = Reader::from_reader(s.as_bytes()); @@ -104,10 +130,7 @@ impl BusConfig { } } - let reader = transform::resolve_includes(s.as_bytes())?; - let reader = transform::last_occurrence_wins(reader.into_inner().as_slice())?; - - quick_xml::de::from_reader(reader.into_inner().as_slice()).map_err(Error::from) + quick_xml::de::from_str(s).map_err(Error::from) } } @@ -118,6 +141,33 @@ pub enum Type { System, } +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +struct Document { + #[serde(rename = "$value", default)] + busconfig: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +enum Element { + AllowAnonymous, + Auth(String), + Fork, + KeepUmask, + Listen(String), + Pidfile(PathBuf), + Servicedir(PathBuf), + Syslog, + Type(TypeElement), + User(String), +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +struct TypeElement { + #[serde(rename = "$text")] + r#type: Type, +} + #[cfg(test)] mod tests { use super::*; @@ -188,10 +238,10 @@ mod tests { assert_eq!( busconfig, BusConfig { - allow_anonymous: Some(()), - fork: Some(()), - keep_umask: Some(()), - syslog: Some(()), + allow_anonymous: true, + fork: true, + keep_umask: true, + syslog: true, ..Default::default() } ); @@ -230,11 +280,11 @@ mod tests { assert_eq!( busconfig, BusConfig { - listen: vec![ + listen: HashSet::from_iter(vec![ String::from("unix:path=/tmp/foo"), String::from("tcp:host=localhost,port=1234"), String::from("tcp:host=localhost,port=0,family=ipv4"), - ], + ]), ..Default::default() } ); @@ -275,7 +325,7 @@ mod tests { assert_eq!( busconfig, BusConfig { - servicedir: vec![PathBuf::from("/example"), PathBuf::from("/anotherexample"),], + servicedir: vec![PathBuf::from("/example"), PathBuf::from("/anotherexample")], ..Default::default() } ); diff --git a/src/config/transform.rs b/src/config/transform.rs deleted file mode 100644 index 56fb1ea..0000000 --- a/src/config/transform.rs +++ /dev/null @@ -1,150 +0,0 @@ -#![allow(dead_code)] -use std::{collections::HashMap, io::Cursor}; - -use anyhow::Result; -use quick_xml::{ - events::{BytesText, Event}, - Reader, Writer, -}; - -// https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file -// > The last element "wins" (previous values are ignored). -// > The last entry in the file "wins", the others are ignored. -const LAST_OCCURRENCE_WINS_TAGS: &[&[u8]] = &[b"type", b"user"]; - -pub fn last_occurrence_wins(input: &[u8]) -> Result>> { - let mut reader = Reader::from_reader(input); - let mut writer = Writer::new(Cursor::new(Vec::new())); - let mut last_occurrences: HashMap = HashMap::new(); - loop { - match reader.read_event()? { - Event::Eof => break, - Event::End(bytes_end) => { - if bytes_end.name().as_ref() == b"busconfig" { - // write out the last occurrence of our "last element wins" tags, - // before we end our XML document - let mut tags: Vec<_> = last_occurrences.keys().collect(); - tags.sort(); - for tag in tags { - let value = last_occurrences - .get(tag) - .expect("should find value associated with known-present key"); - writer.write_serializable(tag, value)?; - } - writer.write_event(Event::End(bytes_end))?; - } else { - writer.write_event(Event::End(bytes_end))?; - } - } - Event::Start(bytes_start) => { - if LAST_OCCURRENCE_WINS_TAGS.contains(&bytes_start.name().as_ref()) { - let text = reader.read_text(bytes_start.name())?.into_owned(); - last_occurrences.insert( - String::from_utf8_lossy(bytes_start.name().as_ref()).into_owned(), - text, - ); - writer.write_event(Event::Comment(BytesText::new("")))?; - } else { - writer.write_event(Event::Start(bytes_start))?; - } - } - event => writer.write_event(event)?, - } - } - Ok(Reader::from_reader(writer.into_inner().into_inner())) -} - -pub fn resolve_includes(input: &[u8]) -> Result>> { - let mut reader = Reader::from_reader(input); - let mut writer = Writer::new(Cursor::new(Vec::new())); - loop { - match reader.read_event()? { - Event::Eof => break, - Event::Start(bytes_start) => { - let name = bytes_start.name(); - if name.as_ref() == b"include" || name.as_ref() == b"includedir" { - let _file_path = reader.read_text(bytes_start.name())?; - // TODO: read the addressed file and write out its contents - // TODO: handle absolute paths - // TODO: handle paths relative to the current file - // TODO: handle ignore_missing="yes" - writer.write_event(Event::Comment(BytesText::new("TODO")))?; - } else { - writer.write_event(Event::Start(bytes_start))?; - } - } - event => writer.write_event(event)?, - } - } - Ok(Reader::from_reader(writer.into_inner().into_inner())) -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn last_occurrence_wins_ok() -> Result<()> { - let input = r#" - - 1000 - system - session - 2000 - - "#; - - let reader = last_occurrence_wins(input.as_bytes())?; - let bytes = reader.into_inner(); - let output = String::from_utf8_lossy(bytes.as_slice()); - - assert_eq!( - output, - r#" - - - - - - session2000 - "# - ); - - Ok(()) - } - - #[test] - fn resolve_includes_replaces_content_ok() -> Result<()> { - let input = r#" - - system - hello - goodbye - session - - "#; - - let reader = resolve_includes(input.as_bytes())?; - let bytes = reader.into_inner(); - let output = String::from_utf8_lossy(bytes.as_slice()); - - assert_eq!( - output, - r#" - - system - - - session - - "# - ); - - Ok(()) - } -}