diff --git a/Cargo.toml b/Cargo.toml index f931197..499f75c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } nom = "6" -[dev-dependencies] -serde_derive = "1.0" \ No newline at end of file +[dev-dependencies] \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a7c4507 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,221 @@ +use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser}; +use crate::values::Value; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, convert::TryFrom, io::Read}; + +type SectionConfig<'a> = HashMap<&'a str, Value<'a>>; + +/// This struct provides a high level wrapper to access `git-config` file. This +/// struct exists primarily for reading a config rather than modifying it, as +/// it discards comments and unnecessary whitespace. +#[derive(Clone, Eq, PartialEq, Debug, Default, Serialize)] +pub struct GitConfig<'a>(HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>); + +const EMPTY_MARKER: &str = "@"; // Guaranteed to not be a {sub,}section or name. + +impl<'a> GitConfig<'a> { + /// Attempts to construct a instance given a [`Parser`] instance. + /// + /// This is _not_ a zero-copy operation. Due to how partial values may be + /// provided, we necessarily need to copy and store these values until we + /// are done. + pub fn try_from_parser_with_options( + parser: Parser<'a>, + options: ConfigOptions, + ) -> Result { + Self::try_from_event_iter_with_options(parser.into_iter(), options) + } + + pub fn try_from_event_iter_with_options( + iter: impl Iterator>, + options: ConfigOptions, + ) -> Result { + let mut sections: HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>> = HashMap::new(); + let mut current_section_name = EMPTY_MARKER; + let mut current_subsection_name = EMPTY_MARKER; + let mut ignore_until_next_section = false; + let mut current_key = EMPTY_MARKER; + let mut value_scratch = String::new(); + + for event in iter { + match event { + Event::Comment(_) => (), + Event::SectionHeader(ParsedSectionHeader { + name, + subsection_name, + }) => { + current_section_name = name; + match (sections.get_mut(name), options.on_duplicate_section) { + (Some(_), OnDuplicateBehavior::Error) => todo!(), + (Some(section), OnDuplicateBehavior::Overwrite) => { + section.clear(); + } + (Some(_), OnDuplicateBehavior::KeepExisting) => { + ignore_until_next_section = true; + } + (None, _) => { + sections.insert(name, HashMap::default()); + } + } + + match subsection_name { + Some(v) => current_subsection_name = v, + None => { + current_subsection_name = EMPTY_MARKER; + continue; + } + }; + + // subsection parsing + + match ( + sections + .get_mut(current_section_name) + .unwrap() // Guaranteed to exist at this point + .get_mut(current_subsection_name), + options.on_duplicate_section, + ) { + (Some(_), OnDuplicateBehavior::Error) => todo!(), + (Some(section), OnDuplicateBehavior::Overwrite) => section.clear(), + (Some(_), OnDuplicateBehavior::KeepExisting) => { + ignore_until_next_section = true; + } + (None, _) => (), + } + } + _ if ignore_until_next_section => (), + Event::Key(key) => { + current_key = key; + } + Event::Value(v) => { + Self::insert_value( + &mut sections, + current_section_name, + current_subsection_name, + current_key, + v, + options.on_duplicate_name, + )?; + } + Event::Newline(_) => (), + Event::ValueNotDone(v) => value_scratch.push_str(v), + Event::ValueDone(v) => { + let mut completed_value = String::new(); + value_scratch.push_str(v); + std::mem::swap(&mut completed_value, &mut value_scratch); + Self::insert_value( + &mut sections, + current_section_name, + current_subsection_name, + current_key, + Value::from_string(completed_value), + options.on_duplicate_name, + )?; + } + } + } + + Ok(Self(sections)) + } + + fn insert_value( + map: &mut HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>, + section: &str, + subsection: &str, + key: &'a str, + value: Value<'a>, + on_dup: OnDuplicateBehavior, + ) -> Result<(), ()> { + let config = map.get_mut(section).unwrap().get_mut(subsection).unwrap(); + + if config.contains_key(key) { + match on_dup { + OnDuplicateBehavior::Error => return Err(()), + OnDuplicateBehavior::Overwrite => { + config.insert(key, value); + } + OnDuplicateBehavior::KeepExisting => (), + } + } else { + config.insert(key, value); + } + + Ok(()) + } + + pub fn get_section(&self, section_name: &str) -> Option<&SectionConfig<'_>> { + self.get_subsection(section_name, EMPTY_MARKER) + } + + pub fn get_section_value(&self, section_name: &str, key: &str) -> Option<&Value<'_>> { + self.get_section(section_name) + .map(|section| section.get(key)) + .flatten() + } + + pub fn get_subsection( + &self, + section_name: &str, + subsection_name: &str, + ) -> Option<&SectionConfig<'_>> { + self.0 + .get(section_name) + .map(|subsections| subsections.get(subsection_name)) + .flatten() + } + + pub fn get_subsection_value( + &self, + section_name: &str, + subsection_name: &str, + key: &str, + ) -> Option<&Value<'_>> { + self.get_subsection(section_name, subsection_name) + .map(|section| section.get(key)) + .flatten() + } +} + +impl<'a> TryFrom> for GitConfig<'a> { + type Error = (); + + fn try_from(parser: Parser<'a>) -> Result { + Self::try_from_parser_with_options(parser, ConfigOptions::default()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct ConfigOptions { + on_duplicate_section: OnDuplicateBehavior, + on_duplicate_name: OnDuplicateBehavior, +} + +impl ConfigOptions { + pub fn on_duplicate_section(&mut self, behavior: OnDuplicateBehavior) -> &mut Self { + self.on_duplicate_section = behavior; + self + } + + pub fn on_duplicate_name(&mut self, behavior: OnDuplicateBehavior) -> &mut Self { + self.on_duplicate_name = behavior; + self + } +} + +/// [`GitConfig`]'s valid possible actions when encountering a duplicate section +/// or key name within a section. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum OnDuplicateBehavior { + /// Fail the operation, returning an error instead. This is the strictest + /// behavior, and is the default. + Error, + /// Discard any data we had before on the + Overwrite, + KeepExisting, +} + +impl Default for OnDuplicateBehavior { + fn default() -> Self { + Self::Error + } +} diff --git a/src/lib.rs b/src/lib.rs index beb7fee..a967901 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ // mod de; +pub mod config; mod error; // mod ser; pub mod parser; -mod values; +pub mod values; // pub use de::{from_str, Deserializer}; pub use error::{Error, Result}; diff --git a/src/parser.rs b/src/parser.rs index 7e0124f..af5f7ee 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -8,18 +8,20 @@ use nom::multi::many1; use nom::sequence::delimited; use nom::IResult; use nom::{branch::alt, multi::many0}; +use std::iter::FusedIterator; -/// An event is any syntactic event that occurs in the config. -#[derive(PartialEq, Debug)] +/// Syntactic event that occurs in the config. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum Event<'a> { - Comment(Comment<'a>), + Comment(ParsedComment<'a>), + SectionHeader(ParsedSectionHeader<'a>), Key(&'a str), + /// + Value(Value<'a>), /// Represents any token used to signify a new line character. On Unix /// platforms, this is typically just `\n`, but can be any valid newline /// sequence. Newline(&'a str), - /// - Value(Value<'a>), /// Any value that isn't completed. This occurs when the value is continued /// onto the next line. A Newline event is guaranteed after, followed by /// either another ValueNotDone or a ValueDone. @@ -28,32 +30,188 @@ pub enum Event<'a> { ValueDone(&'a str), } -#[derive(PartialEq, Debug)] -pub struct Section<'a> { - section_header: SectionHeader<'a>, +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct ParsedSection<'a> { + section_header: ParsedSectionHeader<'a>, items: Vec>, } -#[derive(PartialEq, Debug)] -pub struct SectionHeader<'a> { - name: &'a str, - subsection_name: Option<&'a str>, +impl ParsedSection<'_> { + pub fn header(&self) -> &ParsedSectionHeader<'_> { + &self.section_header + } + + pub fn take_header(&mut self) -> ParsedSectionHeader<'_> { + self.section_header + } + + pub fn events(&self) -> &[Event<'_>] { + &self.items + } } -#[derive(PartialEq, Debug)] -pub struct Comment<'a> { +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct ParsedSectionHeader<'a> { + pub name: &'a str, + pub subsection_name: Option<&'a str>, +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct ParsedComment<'a> { comment_tag: char, comment: &'a str, } -pub struct Parser<'a> { - init_comments: Vec>, - sections: Vec>, +#[derive(PartialEq, Debug)] +pub enum ParserError<'a> { + InvalidInput(nom::Err>), + ConfigHasExtraData(&'a str), } -pub fn parse(input: &str) -> Result, ()> { - let (i, comments) = many0(comment)(input).unwrap(); - let (i, sections) = many1(section)(i).unwrap(); +#[doc(hidden)] +impl<'a> From>> for ParserError<'a> { + fn from(e: nom::Err>) -> Self { + Self::InvalidInput(e) + } +} + +/// A zero-copy `git-config` file parser. +/// +/// # Non-perfect parser +/// +/// This parser should successfully parse all sections and comments. However, +/// It will not parse whitespace. This attempts to closely follow the +/// non-normative specification found in [`git`'s documentation]. +/// +/// # Differences between a `.ini` parser +/// +/// While the `git-config` format closely resembles the [`.ini` file format], +/// there are subtle differences that make them incompatible. For one, the file +/// format is not well defined, and there exists no formal specification to +/// adhere to. Thus, attempting to use an `.ini` parser on a `git-config` file +/// may successfully parse invalid configuration files. +/// +/// For concrete examples, some notable differences are: +/// - `git-config` sections permit subsections via either a quoted string +/// (`[some-section "subsection"]`) or via the deprecated dot notation +/// (`[some-section.subsection]`). Successful parsing these section names is not +/// well defined in typical `.ini` parsers. This parser will handle these cases +/// perfectly. +/// - Comment markers are not strictly defined either. This parser will always +/// and only handle a semicolon or octothorpe (also known as a hash or number +/// sign). +/// - Global properties may be allowed in `.ini` parsers, but is strictly +/// disallowed by this parser. +/// - Only `\t`, `\n`, `\b` `\\` are valid escape characters. +/// - Quoted and semi-quoted values will be parsed (but quotes will be included +/// in event outputs). An example of a semi-quoted value is `5"hello world"`, +/// which should be interpreted as `5hello world`. +/// - Line continuations via a `\` character is supported. +/// - Whitespace handling similarly follows the `git-config` specification as +/// closely as possible, where excess whitespace after a non-quoted value are +/// trimmed, and line continuations onto a new line with excess spaces are kept. +/// - Only equal signs (optionally padded by spaces) are valid name/value +/// delimiters. +/// +/// Note that that things such as case-sensitivity or duplicate sections are +/// _not_ handled. This parser is a low level _syntactic_ interpreter (as a +/// parser should be), and higher level wrappers around this parser (which may +/// or may not be zero-copy) should handle _semantic_ values. +/// +/// # Trait Implementations +/// +/// - This struct does _not_ implement [`FromStr`] due to lifetime +/// constraints implied on the required `from_str` method, but instead provides +/// [`Parser::from_str`]. +/// +/// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file +/// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file +/// [`FromStr`]: std::str::FromStr +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct Parser<'a> { + init_comments: Vec>, + sections: Vec>, +} + +impl<'a> Parser<'a> { + /// Attempt to zero-copy parse the provided `&str`. On success, returns a + /// [`Parser`] that provides methods to accessing leading comments and sections + /// of a `git-config` file and can be converted into an iterator of [`Event`] + /// for higher level processing. + /// + /// This function is identical to [`parse`]. + /// + /// # Errors + /// + /// Returns an error if the string provided is not a valid file, or we have + /// non-section data. + pub fn from_str(s: &'a str) -> Result { + parse_from_str(s) + } + + /// Returns the leading comments (any comments before a section) from the + /// parser. Consider [`Parser::take_leading_comments`] if you need an owned + /// copy only once. + pub fn leading_comments(&self) -> &[ParsedComment<'a>] { + &self.init_comments + } + + /// Takes the leading comments (any comments before a section) from the + /// parser. Subsequent calls will return an empty vec. Consider + /// [`Parser::leading_comments`] if you only need a reference to the comments. + pub fn take_leading_comments(&mut self) -> Vec> { + let mut to_return = vec![]; + std::mem::swap(&mut self.init_comments, &mut to_return); + to_return + } + + pub fn sections(&self) -> &[ParsedSection<'a>] { + &self.sections + } + + pub fn take_sections(&mut self) -> Vec> { + let mut to_return = vec![]; + std::mem::swap(&mut self.sections, &mut to_return); + to_return + } + + pub fn into_vec(self) -> Vec> { + self.into_iter().collect() + } + + pub fn into_iter(self) -> impl Iterator> + FusedIterator { + let section_iter = self + .sections + .into_iter() + .map(|section| { + vec![Event::SectionHeader(section.section_header)] + .into_iter() + .chain(section.items) + }) + .flatten(); + self.init_comments + .into_iter() + .map(Event::Comment) + .chain(section_iter) + } +} + +/// Attempt to zero-copy parse the provided `&str`. On success, returns a +/// [`Parser`] that provides methods to accessing leading comments and sections +/// of a `git-config` file and can be converted into an iterator of [`Event`] +/// for higher level processing. +/// +/// # Errors +/// +/// Returns an error if the string provided is not a valid file, or we have +/// non-section data. +pub fn parse_from_str(input: &str) -> Result, ParserError> { + let (i, comments) = many0(comment)(input)?; + let (i, sections) = many1(section)(i)?; + + if !i.is_empty() { + return Err(ParserError::ConfigHasExtraData(i)); + } Ok(Parser { init_comments: comments, @@ -61,22 +219,22 @@ pub fn parse(input: &str) -> Result, ()> { }) } -fn comment<'a>(i: &'a str) -> IResult<&'a str, Comment<'a>> { +fn comment<'a>(i: &'a str) -> IResult<&'a str, ParsedComment<'a>> { let i = i.trim_start(); let (i, comment_tag) = one_of(";#")(i)?; let (i, comment) = take_till(is_char_newline)(i)?; Ok(( i, - Comment { + ParsedComment { comment_tag, comment, }, )) } -fn section<'a>(i: &'a str) -> IResult<&'a str, Section<'a>> { +fn section<'a>(i: &'a str) -> IResult<&'a str, ParsedSection<'a>> { + let i = i.trim_start(); let (i, section_header) = section_header(i)?; - // need alt here for eof? let (i, items) = many1(alt(( map(section_body, |(key, values)| { let mut vec = vec![Event::Key(key)]; @@ -87,14 +245,14 @@ fn section<'a>(i: &'a str) -> IResult<&'a str, Section<'a>> { )))(i)?; Ok(( i, - Section { + ParsedSection { section_header, items: items.into_iter().flatten().collect(), }, )) } -fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> { +fn section_header<'a>(i: &'a str) -> IResult<&'a str, ParsedSectionHeader<'a>> { let (i, _) = char('[')(i)?; // No spaces must be between section name and section start let (i, name) = take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '.')(i)?; @@ -103,11 +261,11 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> { // Either section does not have a subsection or using deprecated // subsection syntax at this point. let header = match name.rfind('.') { - Some(index) => SectionHeader { + Some(index) => ParsedSectionHeader { name: &name[..index], subsection_name: Some(&name[index + 1..]), }, - None => SectionHeader { + None => ParsedSectionHeader { name: name, subsection_name: None, }, @@ -128,7 +286,7 @@ fn section_header<'a>(i: &'a str) -> IResult<&'a str, SectionHeader<'a>> { Ok(( i, - SectionHeader { + ParsedSectionHeader { name: name, // We know that there's some section name here, so if we get an // empty vec here then we actually parsed an empty section name. @@ -288,7 +446,7 @@ mod parse { fn semicolon() { assert_eq!( comment("; this is a semicolon comment").unwrap(), - fully_consumed(Comment { + fully_consumed(ParsedComment { comment_tag: ';', comment: " this is a semicolon comment", }) @@ -299,7 +457,7 @@ mod parse { fn octothorpe() { assert_eq!( comment("# this is an octothorpe comment").unwrap(), - fully_consumed(Comment { + fully_consumed(ParsedComment { comment_tag: '#', comment: " this is an octothorpe comment", }) @@ -310,7 +468,7 @@ mod parse { fn multiple_markers() { assert_eq!( comment("###### this is an octothorpe comment").unwrap(), - fully_consumed(Comment { + fully_consumed(ParsedComment { comment_tag: '#', comment: "##### this is an octothorpe comment", }) @@ -326,7 +484,7 @@ mod parse { fn no_subsection() { assert_eq!( section_header("[hello]").unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: None }) @@ -337,7 +495,7 @@ mod parse { fn modern_subsection() { assert_eq!( section_header(r#"[hello "world"]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some("world") }) @@ -348,7 +506,7 @@ mod parse { fn escaped_subsection() { assert_eq!( section_header(r#"[hello "foo\\bar\""]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some(r#"foo\\bar\""#) }) @@ -359,7 +517,7 @@ mod parse { fn deprecated_subsection() { assert_eq!( section_header(r#"[hello.world]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some("world") }) @@ -370,7 +528,7 @@ mod parse { fn empty_legacy_subsection_name() { assert_eq!( section_header(r#"[hello.]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some("") }) @@ -381,7 +539,7 @@ mod parse { fn empty_modern_subsection_name() { assert_eq!( section_header(r#"[hello ""]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some("") }) @@ -402,7 +560,7 @@ mod parse { fn right_brace_in_subsection_name() { assert_eq!( section_header(r#"[hello "]"]"#).unwrap(), - fully_consumed(SectionHeader { + fully_consumed(ParsedSectionHeader { name: "hello", subsection_name: Some("]") }) @@ -439,7 +597,7 @@ mod parse { fn no_comment() { assert_eq!( value_impl("hello").unwrap(), - fully_consumed(vec![Event::Value(Value::Other("hello"))]) + fully_consumed(vec![Event::Value(Value::from_str("hello"))]) ); } @@ -447,7 +605,7 @@ mod parse { fn no_comment_newline() { assert_eq!( value_impl("hello\na").unwrap(), - ("\na", vec![Event::Value(Value::Other("hello"))]) + ("\na", vec![Event::Value(Value::from_str("hello"))]) ) } @@ -463,7 +621,7 @@ mod parse { fn semicolon_comment_not_consumed() { assert_eq!( value_impl("hello;world").unwrap(), - (";world", vec![Event::Value(Value::Other("hello")),]) + (";world", vec![Event::Value(Value::from_str("hello")),]) ); } @@ -471,7 +629,7 @@ mod parse { fn octothorpe_comment_not_consumed() { assert_eq!( value_impl("hello#world").unwrap(), - ("#world", vec![Event::Value(Value::Other("hello")),]) + ("#world", vec![Event::Value(Value::from_str("hello")),]) ); } @@ -493,7 +651,7 @@ mod parse { value_impl(r##"hello"#"world; a"##).unwrap(), ( "; a", - vec![Event::Value(Value::Other(r##"hello"#"world"##)),] + vec![Event::Value(Value::from_str(r##"hello"#"world"##)),] ) ); } @@ -502,7 +660,10 @@ mod parse { fn complex_test() { assert_eq!( value_impl(r#"value";";ahhhh"#).unwrap(), - (";ahhhh", vec![Event::Value(Value::Other(r#"value";""#)),]) + ( + ";ahhhh", + vec![Event::Value(Value::from_str(r#"value";""#)),] + ) ); } @@ -572,8 +733,8 @@ mod parse { d = "lol""#; assert_eq!( section(section_data).unwrap(), - fully_consumed(Section { - section_header: SectionHeader { + fully_consumed(ParsedSection { + section_header: ParsedSectionHeader { name: "hello", subsection_name: None, }, @@ -593,8 +754,8 @@ mod parse { fn section_single_line() { assert_eq!( section("[hello] c").unwrap(), - fully_consumed(Section { - section_header: SectionHeader { + fully_consumed(ParsedSection { + section_header: ParsedSectionHeader { name: "hello", subsection_name: None, }, @@ -615,27 +776,27 @@ mod parse { c = d"#; assert_eq!( section(section_data).unwrap(), - fully_consumed(Section { - section_header: SectionHeader { + fully_consumed(ParsedSection { + section_header: ParsedSectionHeader { name: "hello", subsection_name: None, }, items: vec![ - Event::Comment(Comment { + Event::Comment(ParsedComment { comment_tag: ';', comment: " commentA", }), Event::Key("a"), Event::Value(Value::from_str("b")), - Event::Comment(Comment { + Event::Comment(ParsedComment { comment_tag: '#', comment: " commentB", }), - Event::Comment(Comment { + Event::Comment(ParsedComment { comment_tag: ';', comment: " commentC", }), - Event::Comment(Comment { + Event::Comment(ParsedComment { comment_tag: ';', comment: " commentD", }), @@ -651,8 +812,8 @@ mod parse { // This test is absolute hell. Good luck if this fails. assert_eq!( section("[section] a = 1 \"\\\"\\\na ; e \"\\\"\\\nd # \"b\t ; c").unwrap(), - fully_consumed(Section { - section_header: SectionHeader { + fully_consumed(ParsedSection { + section_header: ParsedSectionHeader { name: "section", subsection_name: None, }, @@ -663,7 +824,7 @@ mod parse { Event::ValueNotDone(r#"a ; e "\""#), Event::Newline("\n"), Event::ValueDone("d"), - Event::Comment(Comment { + Event::Comment(ParsedComment { comment_tag: '#', comment: " \"b\t ; c" }) @@ -676,8 +837,8 @@ mod parse { fn quote_split_over_two_lines() { assert_eq!( section("[section \"a\"] b =\"\\\n;\";a").unwrap(), - fully_consumed(Section { - section_header: SectionHeader { + fully_consumed(ParsedSection { + section_header: ParsedSectionHeader { name: "section", subsection_name: Some("a") }, @@ -686,7 +847,7 @@ mod parse { Event::ValueNotDone("\""), Event::Newline("\n"), Event::ValueDone(";\""), - Event::Comment(Comment { + Event::Comment(ParsedComment { comment: "a", comment_tag: ';' }) diff --git a/src/values.rs b/src/values.rs index 0747f2d..183931e 100644 --- a/src/values.rs +++ b/src/values.rs @@ -1,50 +1,129 @@ -use std::convert::{Infallible, TryFrom}; +use std::{borrow::Cow, fmt::Display, str::FromStr}; -#[derive(PartialEq, Debug)] +use serde::{Serialize, Serializer}; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum Value<'a> { Boolean(Boolean), Integer(Integer), Color(Color), - Other(&'a str), + Other(Cow<'a, str>), } impl<'a> Value<'a> { pub fn from_str(s: &'a str) -> Self { - Self::Other(s) + // if s. + Self::Other(Cow::Borrowed(s)) + } + + pub fn from_string(s: String) -> Self { + Self::Other(Cow::Owned(s)) } } -#[derive(PartialEq, Debug)] +impl Serialize for Value<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Value::Boolean(b) => b.serialize(serializer), + Value::Integer(i) => i.serialize(serializer), + Value::Color(c) => c.serialize(serializer), + Value::Other(i) => i.serialize(serializer), + } + } +} + +// todo display for value + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum Boolean { True(TrueVariant), False(FalseVariant), } -#[derive(PartialEq, Debug)] +// todo: Display for boolean + +impl Serialize for Boolean { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Boolean::True(_) => serializer.serialize_bool(true), + Boolean::False(_) => serializer.serialize_bool(false), + } + } +} + +impl FromStr for Boolean { + type Err = (); + + fn from_str(value: &str) -> Result { + if let Ok(v) = TrueVariant::from_str(value) { + return Ok(Self::True(v)); + } + + if let Ok(v) = FalseVariant::from_str(value) { + return Ok(Self::False(v)); + } + + Err(()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum TrueVariant { Yes, On, True, One, - /// For variables defined without a `= `. + /// For variables defined without a `= `. This can never be created + /// from the FromStr trait, as an empty string is false without context. Implicit, } -impl TryFrom<&str> for TrueVariant { - type Error = (); - - fn try_from(value: &str) -> Result { - match value { - "yes" => Ok(Self::Yes), - "on" => Ok(Self::On), - "true" => Ok(Self::True), - "one" => Ok(Self::One), - _ => Err(()), +impl Display for TrueVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Yes => write!(f, "yes"), + Self::On => write!(f, "on"), + Self::True => write!(f, "true"), + Self::One => write!(f, "one"), + Self::Implicit => write!(f, "(implicit)"), } } } -#[derive(PartialEq, Debug)] +impl Serialize for TrueVariant { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bool(true) + } +} + +impl FromStr for TrueVariant { + type Err = (); + + fn from_str(value: &str) -> Result { + if value.eq_ignore_ascii_case("yes") { + Ok(Self::Yes) + } else if value.eq_ignore_ascii_case("on") { + Ok(Self::On) + } else if value.eq_ignore_ascii_case("true") { + Ok(Self::True) + } else if value.eq_ignore_ascii_case("one") { + Ok(Self::One) + } else { + Err(()) + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum FalseVariant { No, Off, @@ -53,51 +132,178 @@ pub enum FalseVariant { EmptyString, } -impl TryFrom<&str> for FalseVariant { - type Error = (); +impl Display for FalseVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::No => write!(f, "no"), + Self::Off => write!(f, "off"), + Self::False => write!(f, "false"), + Self::Zero => write!(f, "0"), + Self::EmptyString => write!(f, "\"\""), + } + } +} - fn try_from(value: &str) -> Result { - match value { - "no" => Ok(Self::No), - "off" => Ok(Self::Off), - "false" => Ok(Self::False), - "zero" => Ok(Self::Zero), - "" => Ok(Self::EmptyString), +impl Serialize for FalseVariant { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bool(false) + } +} + +impl FromStr for FalseVariant { + type Err = (); + + fn from_str(value: &str) -> Result { + if value.eq_ignore_ascii_case("no") { + Ok(Self::No) + } else if value.eq_ignore_ascii_case("off") { + Ok(Self::Off) + } else if value.eq_ignore_ascii_case("false") { + Ok(Self::False) + } else if value.eq_ignore_ascii_case("zero") { + Ok(Self::Zero) + } else if value.is_empty() { + Ok(Self::EmptyString) + } else { + Err(()) + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct Integer { + value: i64, + suffix: Option, +} + +impl Integer {} + +impl Display for Integer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value)?; + if let Some(suffix) = self.suffix { + write!(f, "{}", suffix) + } else { + Ok(()) + } + } +} + +impl Serialize for Integer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if let Some(suffix) = self.suffix { + serializer.serialize_i64(self.value << suffix.bitwise_offset()) + } else { + serializer.serialize_i64(self.value) + } + } +} + +// todo from str for integer + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +enum IntegerSuffix { + Kilo, + Mega, + Giga, +} + +impl IntegerSuffix { + fn bitwise_offset(&self) -> usize { + match self { + Self::Kilo => 10, + Self::Mega => 20, + Self::Giga => 30, + } + } +} + +impl Display for IntegerSuffix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Kilo => write!(f, "k"), + Self::Mega => write!(f, "m"), + Self::Giga => write!(f, "g"), + } + } +} + +impl Serialize for IntegerSuffix { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(match self { + Self::Kilo => "k", + Self::Mega => "m", + Self::Giga => "g", + }) + } +} + +impl FromStr for IntegerSuffix { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "k" => Ok(Self::Kilo), + "m" => Ok(Self::Mega), + "g" => Ok(Self::Giga), _ => Err(()), } } } -impl TryFrom<&str> for Boolean { - type Error = (); - - fn try_from(value: &str) -> Result { - let value = value.to_lowercase(); - let value = value.as_str(); - if let Ok(v) = TrueVariant::try_from(value) { - return Ok(Self::True(v)); - } - - if let Ok(v) = FalseVariant::try_from(value) { - return Ok(Self::False(v)); - } - - Err(()) - } -} - -// todo!() -#[derive(PartialEq, Debug)] -pub struct Integer {} - -#[derive(PartialEq, Debug)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Color { - foreground: ColorValue, + foreground: Option, background: Option, attributes: Vec, } -#[derive(PartialEq, Debug)] +impl Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(fg) = self.foreground { + fg.fmt(f)?; + } + + write!(f, " ")?; + + if let Some(bg) = self.background { + bg.fmt(f)?; + } + + self.attributes + .iter() + .map(|attr| write!(f, " ").and_then(|_| attr.fmt(f))) + .collect::>() + } +} + +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl FromStr for Color { + type Err = (); + + fn from_str(s: &str) -> Result { + todo!() + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] enum ColorValue { Normal, Black, @@ -120,8 +326,92 @@ enum ColorValue { Rgb(u8, u8, u8), } -#[derive(PartialEq, Debug)] -enum ColorAttribute { +impl Display for ColorValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Normal => write!(f, "normal"), + Self::Black => write!(f, "black"), + Self::BrightBlack => write!(f, "brightblack"), + Self::Red => write!(f, "red"), + Self::BrightRed => write!(f, "brightred"), + Self::Green => write!(f, "green"), + Self::BrightGreen => write!(f, "brightgreen"), + Self::Yellow => write!(f, "yellow"), + Self::BrightYellow => write!(f, "brightyellow"), + Self::Blue => write!(f, "blue"), + Self::BrightBlue => write!(f, "brightblue"), + Self::Magenta => write!(f, "magenta"), + Self::BrightMagenta => write!(f, "brightmagenta"), + Self::Cyan => write!(f, "cyan"), + Self::BrightCyan => write!(f, "brightcyan"), + Self::White => write!(f, "white"), + Self::BrightWhite => write!(f, "brightwhite"), + Self::Ansi(num) => num.fmt(f), + Self::Rgb(r, g, b) => write!(f, "#{:02x}{:02x}{:02x}", r, g, b), + } + } +} + +impl Serialize for ColorValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl FromStr for ColorValue { + type Err = (); + + fn from_str(s: &str) -> Result { + let bright = s.starts_with("bright"); + match s { + "normal" => return Ok(Self::Normal), + "black" if !bright => return Ok(Self::Black), + "black" if bright => return Ok(Self::BrightBlack), + "red" if !bright => return Ok(Self::Red), + "red" if bright => return Ok(Self::BrightRed), + "green" if !bright => return Ok(Self::Green), + "green" if bright => return Ok(Self::BrightGreen), + "yellow" if !bright => return Ok(Self::Yellow), + "yellow" if bright => return Ok(Self::BrightYellow), + "blue" if !bright => return Ok(Self::Blue), + "blue" if bright => return Ok(Self::BrightBlue), + "magenta" if !bright => return Ok(Self::Magenta), + "magenta" if bright => return Ok(Self::BrightMagenta), + "cyan" if !bright => return Ok(Self::Cyan), + "cyan" if bright => return Ok(Self::BrightCyan), + "white" if !bright => return Ok(Self::White), + "white" if bright => return Ok(Self::BrightWhite), + _ => (), + } + + if let Ok(v) = u8::from_str(s) { + return Ok(Self::Ansi(v)); + } + + if s.starts_with("#") { + let s = &s[1..]; + if s.len() == 6 { + let rgb = ( + u8::from_str_radix(&s[..2], 16), + u8::from_str_radix(&s[2..4], 16), + u8::from_str_radix(&s[4..], 16), + ); + match rgb { + (Ok(r), Ok(g), Ok(b)) => return Ok(Self::Rgb(r, g, b)), + _ => (), + } + } + } + + Err(()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum ColorAttribute { Bold, NoBold, Dim, @@ -138,5 +428,77 @@ enum ColorAttribute { NoStrike, } -#[derive(PartialEq, Debug)] -struct Pathname<'a>(&'a str); +impl Display for ColorAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bold => write!(f, "bold"), + Self::NoBold => write!(f, "nobold"), + Self::Dim => write!(f, "dim"), + Self::NoDim => write!(f, "nodim"), + Self::Ul => write!(f, "ul"), + Self::NoUl => write!(f, "noul"), + Self::Blink => write!(f, "blink"), + Self::NoBlink => write!(f, "noblink"), + Self::Reverse => write!(f, "reverse"), + Self::NoReverse => write!(f, "noreverse"), + Self::Italic => write!(f, "italic"), + Self::NoItalic => write!(f, "noitalic"), + Self::Strike => write!(f, "strike"), + Self::NoStrike => write!(f, "nostrike"), + } + } +} + +impl Serialize for ColorAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(match self { + Self::Bold => "bold", + Self::NoBold => "nobold", + Self::Dim => "dim", + Self::NoDim => "nodim", + Self::Ul => "ul", + Self::NoUl => "noul", + Self::Blink => "blink", + Self::NoBlink => "noblink", + Self::Reverse => "reverse", + Self::NoReverse => "noreverse", + Self::Italic => "italic", + Self::NoItalic => "noitalic", + Self::Strike => "strike", + Self::NoStrike => "nostrike", + }) + } +} + +impl FromStr for ColorAttribute { + type Err = (); + + fn from_str(s: &str) -> Result { + let inverted = s.starts_with("no"); + let mut parsed = &s[2..]; + if parsed.starts_with("-") { + parsed = &parsed[1..]; + } + + match parsed { + "bold" if !inverted => Ok(Self::Bold), + "bold" if inverted => Ok(Self::NoBold), + "dim" if !inverted => Ok(Self::Dim), + "dim" if inverted => Ok(Self::NoDim), + "ul" if !inverted => Ok(Self::Ul), + "ul" if inverted => Ok(Self::NoUl), + "blink" if !inverted => Ok(Self::Blink), + "blink" if inverted => Ok(Self::NoBlink), + "reverse" if !inverted => Ok(Self::Reverse), + "reverse" if inverted => Ok(Self::NoReverse), + "italic" if !inverted => Ok(Self::Italic), + "italic" if inverted => Ok(Self::NoItalic), + "strike" if !inverted => Ok(Self::Strike), + "strike" if inverted => Ok(Self::NoStrike), + _ => Err(()), + } + } +} diff --git a/tests/parser_integration_tests.rs b/tests/parser_integration_tests.rs new file mode 100644 index 0000000..49f1ee7 --- /dev/null +++ b/tests/parser_integration_tests.rs @@ -0,0 +1,82 @@ +use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader, Parser}; +use serde_git_config::values::Value; + +fn fully_consumed(t: T) -> (&'static str, T) { + ("", t) +} + +fn section_header(name: &'static str, subname: impl Into>) -> Event<'static> { + Event::SectionHeader(ParsedSectionHeader { + name, + subsection_name: subname.into(), + }) +} + +fn name(name: &'static str) -> Event<'static> { + Event::Key(name) +} + +fn value(value: &'static str) -> Event<'static> { + Event::Value(Value::from_str(value)) +} + +#[test] +fn personal_config() { + let config = r#"[user] + email = code@eddie.sh + name = Edward Shen +[core] + autocrlf = input +[push] + default = simple +[commit] + gpgsign = true +[gpg] + program = gpg +[url "ssh://git@github.com/"] + insteadOf = "github://" +[url "ssh://git@git.eddie.sh/edward/"] + insteadOf = "gitea://" +[pull] + ff = only +[init] + defaultBranch = master"#; + + assert_eq!( + parse_from_str(config) + .unwrap() + .into_iter() + .collect::>(), + vec![ + section_header("user", None), + name("email"), + value("code@eddie.sh"), + name("name"), + value("Edward Shen"), + section_header("core", None), + name("autocrlf"), + value("input"), + section_header("push", None), + name("default"), + value("simple"), + section_header("commit", None), + name("gpgsign"), + value("true"), + section_header("gpg", None), + name("program"), + value("gpg"), + section_header("url", "ssh://git@github.com/"), + name("insteadOf"), + value("github://"), + section_header("url", "ssh://git@git.eddie.sh/edward/"), + name("insteadOf"), + value("gitea://"), + section_header("pull", None), + name("ff"), + value("only"), + section_header("init", None), + name("defaultBranch"), + value("master"), + ] + ); +}