more work

master
Edward Shen 2021-02-19 00:12:59 -05:00
parent 30b054df31
commit d63b1f7ab3
Signed by: edward
GPG Key ID: 19182661E818369F
6 changed files with 947 additions and 121 deletions

View File

@ -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"
[dev-dependencies]

221
src/config.rs Normal file
View File

@ -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, ()> {
Self::try_from_event_iter_with_options(parser.into_iter(), options)
}
pub fn try_from_event_iter_with_options(
iter: impl Iterator<Item = Event<'a>>,
options: ConfigOptions,
) -> Result<Self, ()> {
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<Parser<'a>> for GitConfig<'a> {
type Error = ();
fn try_from(parser: Parser<'a>) -> Result<Self, Self::Error> {
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
}
}

View File

@ -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};

View File

@ -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<Event<'a>>,
}
#[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<Comment<'a>>,
sections: Vec<Section<'a>>,
#[derive(PartialEq, Debug)]
pub enum ParserError<'a> {
InvalidInput(nom::Err<NomError<&'a str>>),
ConfigHasExtraData(&'a str),
}
pub fn parse(input: &str) -> Result<Parser<'_>, ()> {
let (i, comments) = many0(comment)(input).unwrap();
let (i, sections) = many1(section)(i).unwrap();
#[doc(hidden)]
impl<'a> From<nom::Err<NomError<&'a str>>> for ParserError<'a> {
fn from(e: nom::Err<NomError<&'a str>>) -> 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<ParsedComment<'a>>,
sections: Vec<ParsedSection<'a>>,
}
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<Self, ParserError> {
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<ParsedComment<'a>> {
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<ParsedSection<'a>> {
let mut to_return = vec![];
std::mem::swap(&mut self.sections, &mut to_return);
to_return
}
pub fn into_vec(self) -> Vec<Event<'a>> {
self.into_iter().collect()
}
pub fn into_iter(self) -> impl Iterator<Item = Event<'a>> + 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<Parser<'_>, 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<Parser<'_>, ()> {
})
}
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: ';'
})

View File

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self, Self::Err> {
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 `= <value>`.
/// For variables defined without a `= <value>`. 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<Self, Self::Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bool(true)
}
}
impl FromStr for TrueVariant {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
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<Self, Self::Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bool(false)
}
}
impl FromStr for FalseVariant {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
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<IntegerSuffix>,
}
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self, Self::Err> {
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<Self, Self::Error> {
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<ColorValue>,
background: Option<ColorValue>,
attributes: Vec<ColorAttribute>,
}
#[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::<Result<_, _>>()
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl FromStr for Color {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl FromStr for ColorValue {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self, Self::Err> {
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(()),
}
}
}

View File

@ -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: T) -> (&'static str, T) {
("", t)
}
fn section_header(name: &'static str, subname: impl Into<Option<&'static str>>) -> 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<_>>(),
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"),
]
);
}