implement case insensitivity for names

This commit is contained in:
Edward Shen 2021-03-06 01:23:23 -05:00
parent 6167914277
commit 9715790574
Signed by: edward
GPG key ID: 19182661E818369F
8 changed files with 240 additions and 82 deletions

1
LICENSE-APACHE Symbolic link
View file

@ -0,0 +1 @@
../LICENSE-APACHE

1
LICENSE-MIT Symbolic link
View file

@ -0,0 +1 @@
../LICENSE-MIT

18
README.md Normal file
View file

@ -0,0 +1,18 @@
# git-config
**git-config is a library for interacting with `git-config` files.**
#### License
<sup>
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
</sup>
<br>
<sub>
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in git-config by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
</sub>

View file

@ -1,23 +1,25 @@
//! This module provides a high level wrapper around a single `git-config` file. //! This module provides a high level wrapper around a single `git-config` file.
use crate::parser::{parse_from_bytes, Error, Event, ParsedSectionHeader, Parser}; use crate::parser::{
use crate::values::{normalize_bytes, normalize_cow, normalize_vec}; parse_from_bytes, Error, Event, Key, ParsedSectionHeader, Parser, SectionHeaderName,
use std::{borrow::Borrow, convert::TryFrom, ops::Deref};
use std::{borrow::Cow, fmt::Display};
use std::{
collections::{HashMap, VecDeque},
ops::DerefMut,
}; };
use crate::values::{normalize_bytes, normalize_cow, normalize_vec};
use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::convert::TryFrom;
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
/// All possible error types that may occur from interacting with [`GitConfig`]. /// All possible error types that may occur from interacting with [`GitConfig`].
#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)] #[derive(PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Debug)]
pub enum GitConfigError<'a> { pub enum GitConfigError<'a> {
/// The requested section does not exist. /// The requested section does not exist.
SectionDoesNotExist(&'a str), SectionDoesNotExist(SectionHeaderName<'a>),
/// The requested subsection does not exist. /// The requested subsection does not exist.
SubSectionDoesNotExist(Option<&'a str>), SubSectionDoesNotExist(Option<&'a str>),
/// The key does not exist in the requested section. /// The key does not exist in the requested section.
KeyDoesNotExist(&'a str), KeyDoesNotExist(Key<'a>),
/// The conversion into the provided type for methods such as /// The conversion into the provided type for methods such as
/// [`GitConfig::get_value`] failed. /// [`GitConfig::get_value`] failed.
FailedConversion, FailedConversion,
@ -68,7 +70,7 @@ enum LookupTreeNode<'a> {
/// time. /// time.
pub struct MutableValue<'borrow, 'lookup, 'event> { pub struct MutableValue<'borrow, 'lookup, 'event> {
section: &'borrow mut Vec<Event<'event>>, section: &'borrow mut Vec<Event<'event>>,
key: &'lookup str, key: Key<'lookup>,
index: usize, index: usize,
size: usize, size: usize,
} }
@ -109,7 +111,7 @@ impl MutableValue<'_, '_, '_> {
latest_value latest_value
.map(normalize_cow) .map(normalize_cow)
.or_else(|| partial_value.map(normalize_vec)) .or_else(|| partial_value.map(normalize_vec))
.ok_or(GitConfigError::KeyDoesNotExist(self.key)) .ok_or(GitConfigError::KeyDoesNotExist(self.key.to_owned()))
} }
/// Update the value to the provided one. This modifies the value such that /// Update the value to the provided one. This modifies the value such that
@ -131,8 +133,10 @@ impl MutableValue<'_, '_, '_> {
self.section self.section
.insert(self.index, Event::Value(Cow::Owned(input))); .insert(self.index, Event::Value(Cow::Owned(input)));
self.section.insert(self.index, Event::KeyValueSeparator); self.section.insert(self.index, Event::KeyValueSeparator);
self.section self.section.insert(
.insert(self.index, Event::Key(Cow::Owned(self.key.into()))); self.index,
Event::Key(Key(Cow::Owned(self.key.0.to_string()))),
);
} }
/// Removes the value. Does nothing when called multiple times in /// Removes the value. Does nothing when called multiple times in
@ -193,7 +197,7 @@ impl DerefMut for Offset {
#[derive(Eq, PartialEq, Debug)] #[derive(Eq, PartialEq, Debug)]
pub struct MutableMultiValue<'borrow, 'lookup, 'event> { pub struct MutableMultiValue<'borrow, 'lookup, 'event> {
section: &'borrow mut HashMap<SectionId, Vec<Event<'event>>>, section: &'borrow mut HashMap<SectionId, Vec<Event<'event>>>,
key: &'lookup str, key: Key<'lookup>,
/// Each entry data struct provides sufficient information to index into /// Each entry data struct provides sufficient information to index into
/// [`Self::offsets`]. This layer of indirection is used for users to index /// [`Self::offsets`]. This layer of indirection is used for users to index
/// into the offsets rather than leaking the internal data structures. /// into the offsets rather than leaking the internal data structures.
@ -244,7 +248,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
} }
if values.is_empty() { if values.is_empty() {
return Err(GitConfigError::KeyDoesNotExist(self.key)); return Err(GitConfigError::KeyDoesNotExist(self.key.to_owned()));
} }
Ok(values) Ok(values)
@ -296,7 +300,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
offset_index, offset_index,
} = self.indices_and_sizes[index]; } = self.indices_and_sizes[index];
MutableMultiValue::set_value_inner( MutableMultiValue::set_value_inner(
self.key, self.key.to_owned(),
&mut self.offsets, &mut self.offsets,
self.section.get_mut(&section_id).unwrap(), self.section.get_mut(&section_id).unwrap(),
section_id, section_id,
@ -323,7 +327,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
) in self.indices_and_sizes.iter().zip(input) ) in self.indices_and_sizes.iter().zip(input)
{ {
Self::set_value_inner( Self::set_value_inner(
self.key, self.key.to_owned(),
&mut self.offsets, &mut self.offsets,
self.section.get_mut(section_id).unwrap(), self.section.get_mut(section_id).unwrap(),
*section_id, *section_id,
@ -350,7 +354,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
} in &self.indices_and_sizes } in &self.indices_and_sizes
{ {
Self::set_value_inner( Self::set_value_inner(
self.key, self.key.to_owned(),
&mut self.offsets, &mut self.offsets,
self.section.get_mut(section_id).unwrap(), self.section.get_mut(section_id).unwrap(),
*section_id, *section_id,
@ -373,7 +377,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
} in &self.indices_and_sizes } in &self.indices_and_sizes
{ {
Self::set_value_inner( Self::set_value_inner(
self.key, self.key.to_owned(),
&mut self.offsets, &mut self.offsets,
self.section.get_mut(section_id).unwrap(), self.section.get_mut(section_id).unwrap(),
*section_id, *section_id,
@ -384,7 +388,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
} }
fn set_value_inner<'a: 'event>( fn set_value_inner<'a: 'event>(
key: &'lookup str, key: Key<'lookup>,
offsets: &mut HashMap<SectionId, Vec<Offset>>, offsets: &mut HashMap<SectionId, Vec<Offset>>,
section: &mut Vec<Event<'event>>, section: &mut Vec<Event<'event>>,
section_id: SectionId, section_id: SectionId,
@ -398,7 +402,7 @@ impl<'lookup, 'event> MutableMultiValue<'_, 'lookup, 'event> {
MutableMultiValue::set_offset(offsets, section_id, offset_index, 3); MutableMultiValue::set_offset(offsets, section_id, offset_index, 3);
section.insert(offset, Event::Value(input)); section.insert(offset, Event::Value(input));
section.insert(offset, Event::KeyValueSeparator); section.insert(offset, Event::KeyValueSeparator);
section.insert(offset, Event::Key(Cow::Owned(key.into()))); section.insert(offset, Event::Key(Key(Cow::Owned(key.0.to_string()))));
} }
/// Removes the value at the given index. Does nothing when called multiple /// Removes the value at the given index. Does nothing when called multiple
@ -530,7 +534,7 @@ pub struct GitConfig<'a> {
/// `git-config` file prohibits global values, this vec is limited to only /// `git-config` file prohibits global values, this vec is limited to only
/// comment, newline, and whitespace events. /// comment, newline, and whitespace events.
frontmatter_events: Vec<Event<'a>>, frontmatter_events: Vec<Event<'a>>,
section_lookup_tree: HashMap<Cow<'a, str>, Vec<LookupTreeNode<'a>>>, section_lookup_tree: HashMap<SectionHeaderName<'a>, Vec<LookupTreeNode<'a>>>,
/// SectionId to section mapping. The value of this HashMap contains actual /// SectionId to section mapping. The value of this HashMap contains actual
/// events. /// events.
/// ///
@ -675,6 +679,7 @@ impl<'event> GitConfig<'event> {
// the "last one wins" resolution strategy by `git-config`). // the "last one wins" resolution strategy by `git-config`).
let section_ids = let section_ids =
self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
let key = Key(key.into());
for section_id in section_ids.iter().rev() { for section_id in section_ids.iter().rev() {
let mut found_key = false; let mut found_key = false;
@ -728,6 +733,7 @@ impl<'event> GitConfig<'event> {
) -> Result<MutableValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> { ) -> Result<MutableValue<'_, 'lookup, 'event>, GitConfigError<'lookup>> {
let section_ids = let section_ids =
self.get_section_ids_by_name_and_subname(section_name, subsection_name)?; self.get_section_ids_by_name_and_subname(section_name, subsection_name)?;
let key = Key(key.into());
for section_id in section_ids.iter().rev() { for section_id in section_ids.iter().rev() {
let mut size = 0; let mut size = 0;
@ -814,7 +820,7 @@ impl<'event> GitConfig<'event> {
subsection_name: Option<&'lookup str>, subsection_name: Option<&'lookup str>,
key: &'lookup str, key: &'lookup str,
) -> Result<Vec<Cow<'_, [u8]>>, GitConfigError<'lookup>> { ) -> Result<Vec<Cow<'_, [u8]>>, GitConfigError<'lookup>> {
let key = key; let key = Key(key.into());
let mut values = vec![]; let mut values = vec![];
for section_id in self.get_section_ids_by_name_and_subname(section_name, subsection_name)? { for section_id in self.get_section_ids_by_name_and_subname(section_name, subsection_name)? {
let mut found_key = false; let mut found_key = false;
@ -912,6 +918,7 @@ impl<'event> GitConfig<'event> {
let section_ids = self let section_ids = self
.get_section_ids_by_name_and_subname(section_name, subsection_name)? .get_section_ids_by_name_and_subname(section_name, subsection_name)?
.to_vec(); .to_vec();
let key = Key(key.into());
let mut offsets = HashMap::new(); let mut offsets = HashMap::new();
let mut entries = vec![]; let mut entries = vec![];
@ -1103,26 +1110,23 @@ impl<'event> GitConfig<'event> {
subsection_name: impl Into<Option<Cow<'event, str>>>, subsection_name: impl Into<Option<Cow<'event, str>>>,
) { ) {
self.push_section( self.push_section(
Some(section_name.into()), Some(SectionHeaderName(section_name.into())),
subsection_name.into(), subsection_name.into(),
&mut Some(vec![]), &mut Some(vec![]),
) )
} }
/// Removes the section, returning the events it had, if any. /// Removes the section, returning the events it had, if any.
pub fn remove_section( pub fn remove_section<'lookup>(
&mut self, &mut self,
section_name: impl Into<Cow<'event, str>>, section_name: &'lookup str,
subsection_name: impl Into<Option<Cow<'event, str>>>, subsection_name: impl Into<Option<&'lookup str>>,
) -> Option<Vec<Event>> { ) -> Option<Vec<Event>> {
let mut section_ids = self let section_ids =
.get_section_ids_by_name_and_subname( self.get_section_ids_by_name_and_subname(section_name, subsection_name.into());
&section_name.into(), let section_ids = section_ids.ok()?.pop()?;
subsection_name.into().as_deref(),
)
.ok()?;
self.sections.remove(&section_ids.pop()?) self.sections.remove(&section_ids)
} }
} }
@ -1131,7 +1135,7 @@ impl<'event> GitConfig<'event> {
/// Used during initialization. /// Used during initialization.
fn push_section( fn push_section(
&mut self, &mut self,
current_section_name: Option<Cow<'event, str>>, current_section_name: Option<SectionHeaderName<'event>>,
current_subsection_name: Option<Cow<'event, str>>, current_subsection_name: Option<Cow<'event, str>>,
maybe_section: &mut Option<Vec<Event<'event>>>, maybe_section: &mut Option<Vec<Event<'event>>>,
) { ) {
@ -1184,21 +1188,22 @@ impl<'event> GitConfig<'event> {
/// Returns the mapping between section and subsection name to section ids. /// Returns the mapping between section and subsection name to section ids.
fn get_section_ids_by_name_and_subname<'lookup>( fn get_section_ids_by_name_and_subname<'lookup>(
&self, &self,
section_name: &'lookup str, section_name: impl Into<SectionHeaderName<'lookup>>,
subsection_name: Option<&'lookup str>, subsection_name: Option<&'lookup str>,
) -> Result<Vec<SectionId>, GitConfigError<'lookup>> { ) -> Result<Vec<SectionId>, GitConfigError<'lookup>> {
let section_name = section_name.into();
let section_ids = self let section_ids = self
.section_lookup_tree .section_lookup_tree
.get(section_name) .get(&section_name)
.ok_or(GitConfigError::SectionDoesNotExist(section_name))?; .ok_or(GitConfigError::SectionDoesNotExist(section_name))?;
let mut maybe_ids = None; let mut maybe_ids = None;
// Don't simplify if and matches here -- the for loop currently needs // Don't simplify if and matches here -- the for loop currently needs
// `n + 1` checks, while the if and matches will result in the for loop // `n + 1` checks, while the if and matches will result in the for loop
// needing `2n` checks. // needing `2n` checks.
if let Some(subsect_name) = subsection_name { if let Some(subsection_name) = subsection_name {
for node in section_ids { for node in section_ids {
if let LookupTreeNode::NonTerminal(subsection_lookup) = node { if let LookupTreeNode::NonTerminal(subsection_lookup) = node {
maybe_ids = subsection_lookup.get(subsect_name); maybe_ids = subsection_lookup.get(subsection_name);
break; break;
} }
} }
@ -1252,7 +1257,7 @@ impl<'a> From<Parser<'a>> for GitConfig<'a> {
}; };
// Current section that we're building // Current section that we're building
let mut current_section_name: Option<Cow<'a, str>> = None; let mut current_section_name: Option<SectionHeaderName<'_>> = None;
let mut current_subsection_name: Option<Cow<'a, str>> = None; let mut current_subsection_name: Option<Cow<'a, str>> = None;
let mut maybe_section: Option<Vec<Event<'a>>> = None; let mut maybe_section: Option<Vec<Event<'a>>> = None;
@ -1613,16 +1618,48 @@ mod mutable_multi_value {
.get_raw_multi_value_mut("core", None, "a") .get_raw_multi_value_mut("core", None, "a")
.unwrap(); .unwrap();
values.delete_all(); values.delete_all();
assert!(values.get().is_err());
assert_eq!( assert_eq!(
git_config.to_string(), git_config.to_string(),
"[core]\n \n [core]\n \n ", "[core]\n \n [core]\n \n ",
); );
} }
#[test]
fn partial_values_are_supported() {
let mut git_config = GitConfig::try_from(
r#"[core]
a=b\
"100"
[core]
a=d\
b
a=f\
a"#,
)
.unwrap();
let mut values = git_config
.get_raw_multi_value_mut("core", None, "a")
.unwrap();
assert_eq!(
&*values.get().unwrap(),
vec![
Cow::<[u8]>::Owned(b"b100".to_vec()),
Cow::<[u8]>::Borrowed(b"db"),
Cow::<[u8]>::Borrowed(b"fa"),
]
);
values.delete_all();
assert!(values.get().is_err());
}
} }
#[cfg(test)] #[cfg(test)]
mod from_parser { mod from_parser {
use super::{Cow, Event, GitConfig, HashMap, LookupTreeNode, SectionId, TryFrom}; use super::{Cow, Event, GitConfig, HashMap, LookupTreeNode, SectionId, TryFrom};
use crate::parser::SectionHeaderName;
use crate::test_util::{name_event, newline_event, section_header, value_event}; use crate::test_util::{name_event, newline_event, section_header, value_event};
#[test] #[test]
@ -1648,7 +1685,7 @@ mod from_parser {
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); let mut tree = HashMap::new();
tree.insert( tree.insert(
Cow::Borrowed("core"), SectionHeaderName(Cow::Borrowed("core")),
vec![LookupTreeNode::Terminal(vec![SectionId(0)])], vec![LookupTreeNode::Terminal(vec![SectionId(0)])],
); );
tree tree
@ -1677,10 +1714,10 @@ mod from_parser {
#[test] #[test]
fn parse_single_subsection() { fn parse_single_subsection() {
let mut config = GitConfig::try_from("[core.subsec]\na=b\nc=d").unwrap(); let mut config = GitConfig::try_from("[core.sub]\na=b\nc=d").unwrap();
let expected_separators = { let expected_separators = {
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(SectionId(0), section_header("core", (".", "subsec"))); map.insert(SectionId(0), section_header("core", (".", "sub")));
map map
}; };
assert_eq!(config.section_headers, expected_separators); assert_eq!(config.section_headers, expected_separators);
@ -1688,9 +1725,9 @@ mod from_parser {
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); let mut tree = HashMap::new();
let mut inner_tree = HashMap::new(); let mut inner_tree = HashMap::new();
inner_tree.insert(Cow::Borrowed("subsec"), vec![SectionId(0)]); inner_tree.insert(Cow::Borrowed("sub"), vec![SectionId(0)]);
tree.insert( tree.insert(
Cow::Borrowed("core"), SectionHeaderName(Cow::Borrowed("core")),
vec![LookupTreeNode::NonTerminal(inner_tree)], vec![LookupTreeNode::NonTerminal(inner_tree)],
); );
tree tree
@ -1731,11 +1768,11 @@ mod from_parser {
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); let mut tree = HashMap::new();
tree.insert( tree.insert(
Cow::Borrowed("core"), SectionHeaderName(Cow::Borrowed("core")),
vec![LookupTreeNode::Terminal(vec![SectionId(0)])], vec![LookupTreeNode::Terminal(vec![SectionId(0)])],
); );
tree.insert( tree.insert(
Cow::Borrowed("other"), SectionHeaderName(Cow::Borrowed("other")),
vec![LookupTreeNode::Terminal(vec![SectionId(1)])], vec![LookupTreeNode::Terminal(vec![SectionId(1)])],
); );
tree tree
@ -1784,7 +1821,7 @@ mod from_parser {
let expected_lookup_tree = { let expected_lookup_tree = {
let mut tree = HashMap::new(); let mut tree = HashMap::new();
tree.insert( tree.insert(
Cow::Borrowed("core"), SectionHeaderName(Cow::Borrowed("core")),
vec![LookupTreeNode::Terminal(vec![SectionId(0), SectionId(1)])], vec![LookupTreeNode::Terminal(vec![SectionId(0), SectionId(1)])],
); );
tree tree
@ -1823,6 +1860,7 @@ mod from_parser {
#[cfg(test)] #[cfg(test)]
mod get_raw_value { mod get_raw_value {
use super::{Cow, GitConfig, GitConfigError, TryFrom}; use super::{Cow, GitConfig, GitConfigError, TryFrom};
use crate::parser::{Key, SectionHeaderName};
#[test] #[test]
fn single_section() { fn single_section() {
@ -1860,7 +1898,9 @@ mod get_raw_value {
let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
assert_eq!( assert_eq!(
config.get_raw_value("foo", None, "a"), config.get_raw_value("foo", None, "a"),
Err(GitConfigError::SectionDoesNotExist("foo")) Err(GitConfigError::SectionDoesNotExist(SectionHeaderName(
"foo".into()
)))
); );
} }
@ -1878,7 +1918,7 @@ mod get_raw_value {
let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
assert_eq!( assert_eq!(
config.get_raw_value("core", None, "aaaaaa"), config.get_raw_value("core", None, "aaaaaa"),
Err(GitConfigError::KeyDoesNotExist("aaaaaa")) Err(GitConfigError::KeyDoesNotExist(Key("aaaaaa".into())))
); );
} }
@ -1918,6 +1958,7 @@ mod get_value {
#[cfg(test)] #[cfg(test)]
mod get_raw_multi_value { mod get_raw_multi_value {
use super::{Cow, GitConfig, GitConfigError, TryFrom}; use super::{Cow, GitConfig, GitConfigError, TryFrom};
use crate::parser::{Key, SectionHeaderName};
#[test] #[test]
fn single_value_is_identical_to_single_value_query() { fn single_value_is_identical_to_single_value_query() {
@ -1955,7 +1996,9 @@ mod get_raw_multi_value {
let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
assert_eq!( assert_eq!(
config.get_raw_multi_value("foo", None, "a"), config.get_raw_multi_value("foo", None, "a"),
Err(GitConfigError::SectionDoesNotExist("foo")) Err(GitConfigError::SectionDoesNotExist(SectionHeaderName(
"foo".into()
)))
); );
} }
@ -1973,7 +2016,7 @@ mod get_raw_multi_value {
let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap(); let config = GitConfig::try_from("[core]\na=b\nc=d").unwrap();
assert_eq!( assert_eq!(
config.get_raw_multi_value("core", None, "aaaaaa"), config.get_raw_multi_value("core", None, "aaaaaa"),
Err(GitConfigError::KeyDoesNotExist("aaaaaa")) Err(GitConfigError::KeyDoesNotExist(Key("aaaaaa".into())))
); );
} }

View file

@ -18,9 +18,10 @@ use nom::error::{Error as NomError, ErrorKind};
use nom::multi::{many0, many1}; use nom::multi::{many0, many1};
use nom::sequence::delimited; use nom::sequence::delimited;
use nom::IResult; use nom::IResult;
use std::borrow::Cow; use std::convert::TryFrom;
use std::fmt::Display;
use std::iter::FusedIterator; use std::iter::FusedIterator;
use std::{convert::TryFrom, fmt::Display}; use std::{borrow::Cow, hash::Hash};
/// Syntactic events that occurs in the config. Despite all these variants /// Syntactic events that occurs in the config. Despite all these variants
/// holding a [`Cow`] instead over a simple reference, the parser will only emit /// holding a [`Cow`] instead over a simple reference, the parser will only emit
@ -42,7 +43,7 @@ pub enum Event<'a> {
/// exists. /// exists.
SectionHeader(ParsedSectionHeader<'a>), SectionHeader(ParsedSectionHeader<'a>),
/// A name to a value in a section. /// A name to a value in a section.
Key(Cow<'a, str>), Key(Key<'a>),
/// A completed value. This may be any string, including the empty string, /// A completed value. This may be any string, including the empty string,
/// if an implicit boolean value is used. Note that these values may contain /// if an implicit boolean value is used. Note that these values may contain
/// spaces and any special character. This value is also unprocessed, so it /// spaces and any special character. This value is also unprocessed, so it
@ -92,7 +93,8 @@ impl Display for Event<'_> {
} }
Self::Comment(e) => e.fmt(f), Self::Comment(e) => e.fmt(f),
Self::SectionHeader(e) => e.fmt(f), Self::SectionHeader(e) => e.fmt(f),
Self::Key(e) | Self::Newline(e) | Self::Whitespace(e) => e.fmt(f), Self::Key(e) => e.fmt(f),
Self::Newline(e) | Self::Whitespace(e) => e.fmt(f),
Self::KeyValueSeparator => write!(f, "="), Self::KeyValueSeparator => write!(f, "="),
} }
} }
@ -110,7 +112,8 @@ impl Into<Vec<u8>> for &Event<'_> {
Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(), Event::Value(e) | Event::ValueNotDone(e) | Event::ValueDone(e) => e.to_vec(),
Event::Comment(e) => e.into(), Event::Comment(e) => e.into(),
Event::SectionHeader(e) => e.into(), Event::SectionHeader(e) => e.into(),
Event::Key(e) | Event::Newline(e) | Event::Whitespace(e) => e.as_bytes().to_vec(), Event::Key(e) => e.0.as_bytes().to_vec(),
Event::Newline(e) | Event::Whitespace(e) => e.as_bytes().to_vec(),
Event::KeyValueSeparator => vec![b'='], Event::KeyValueSeparator => vec![b'='],
} }
} }
@ -143,7 +146,7 @@ impl Display for ParsedSection<'_> {
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ParsedSectionHeader<'a> { pub struct ParsedSectionHeader<'a> {
/// The name of the header. /// The name of the header.
pub name: Cow<'a, str>, pub name: SectionHeaderName<'a>,
/// The separator used to determine if the section contains a subsection. /// The separator used to determine if the section contains a subsection.
/// This is either a period `.` or a string of whitespace. Note that /// This is either a period `.` or a string of whitespace. Note that
/// reconstruction of subsection format is dependent on this value. If this /// reconstruction of subsection format is dependent on this value. If this
@ -154,6 +157,69 @@ pub struct ParsedSectionHeader<'a> {
pub subsection_name: Option<Cow<'a, str>>, pub subsection_name: Option<Cow<'a, str>>,
} }
macro_rules! generate_case_insensitive {
($name:ident, $inner_type:ty, $comment:literal) => {
/// Wrapper struct for $comment, since $comment are case-insensitive.
#[derive(Clone, Eq, Ord, Debug, Default)]
pub struct $name<'a>(pub $inner_type);
impl PartialEq for $name<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Display for $name<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialOrd for $name<'_> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0
.to_ascii_lowercase()
.partial_cmp(&other.0.to_ascii_lowercase())
}
}
impl std::hash::Hash for $name<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_ascii_lowercase().hash(state)
}
}
impl<'a> From<&'a str> for $name<'a> {
fn from(s: &'a str) -> Self {
Self(Cow::Borrowed(s))
}
}
impl<'a> From<Cow<'a, str>> for $name<'a> {
fn from(s: Cow<'a, str>) -> Self {
Self(s)
}
}
impl<'a> std::ops::Deref for $name<'a> {
type Target = $inner_type;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> std::ops::DerefMut for $name<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
};
}
generate_case_insensitive!(SectionHeaderName, Cow<'a, str>, "section names");
generate_case_insensitive!(Key, Cow<'a, str>, "keys");
impl ParsedSectionHeader<'_> { impl ParsedSectionHeader<'_> {
/// Generates a byte representation of the value. This should be used when /// Generates a byte representation of the value. This should be used when
/// non-UTF-8 sequences are present or a UTF-8 representation can't be /// non-UTF-8 sequences are present or a UTF-8 representation can't be
@ -386,10 +452,10 @@ impl Display for ParserNode {
/// non-significant events that occur in addition to the ones you may expect: /// non-significant events that occur in addition to the ones you may expect:
/// ///
/// ``` /// ```
/// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str}; /// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str, SectionHeaderName, Key};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # let section_header = ParsedSectionHeader { /// # let section_header = ParsedSectionHeader {
/// # name: Cow::Borrowed("core"), /// # name: SectionHeaderName(Cow::Borrowed("core")),
/// # separator: None, /// # separator: None,
/// # subsection_name: None, /// # subsection_name: None,
/// # }; /// # };
@ -398,7 +464,7 @@ impl Display for ParserNode {
/// Event::SectionHeader(section_header), /// Event::SectionHeader(section_header),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
/// Event::Whitespace(Cow::Borrowed(" ")), /// Event::Whitespace(Cow::Borrowed(" ")),
/// Event::Key(Cow::Borrowed("autocrlf")), /// Event::Key(Key(Cow::Borrowed("autocrlf"))),
/// Event::Whitespace(Cow::Borrowed(" ")), /// Event::Whitespace(Cow::Borrowed(" ")),
/// Event::KeyValueSeparator, /// Event::KeyValueSeparator,
/// Event::Whitespace(Cow::Borrowed(" ")), /// Event::Whitespace(Cow::Borrowed(" ")),
@ -425,10 +491,10 @@ impl Display for ParserNode {
/// which means that the corresponding event won't appear either: /// which means that the corresponding event won't appear either:
/// ///
/// ``` /// ```
/// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str}; /// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str, SectionHeaderName, Key};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # let section_header = ParsedSectionHeader { /// # let section_header = ParsedSectionHeader {
/// # name: Cow::Borrowed("core"), /// # name: SectionHeaderName(Cow::Borrowed("core")),
/// # separator: None, /// # separator: None,
/// # subsection_name: None, /// # subsection_name: None,
/// # }; /// # };
@ -437,7 +503,7 @@ impl Display for ParserNode {
/// Event::SectionHeader(section_header), /// Event::SectionHeader(section_header),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
/// Event::Whitespace(Cow::Borrowed(" ")), /// Event::Whitespace(Cow::Borrowed(" ")),
/// Event::Key(Cow::Borrowed("autocrlf")), /// Event::Key(Key(Cow::Borrowed("autocrlf"))),
/// Event::Value(Cow::Borrowed(b"")), /// Event::Value(Cow::Borrowed(b"")),
/// # ]); /// # ]);
/// ``` /// ```
@ -459,10 +525,10 @@ impl Display for ParserNode {
/// relevant event stream emitted is thus emitted as: /// relevant event stream emitted is thus emitted as:
/// ///
/// ``` /// ```
/// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str}; /// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str, SectionHeaderName, Key};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # let section_header = ParsedSectionHeader { /// # let section_header = ParsedSectionHeader {
/// # name: Cow::Borrowed("core"), /// # name: SectionHeaderName(Cow::Borrowed("core")),
/// # separator: None, /// # separator: None,
/// # subsection_name: None, /// # subsection_name: None,
/// # }; /// # };
@ -470,11 +536,11 @@ impl Display for ParserNode {
/// # assert_eq!(parse_from_str(section_data).unwrap().into_vec(), vec![ /// # assert_eq!(parse_from_str(section_data).unwrap().into_vec(), vec![
/// Event::SectionHeader(section_header), /// Event::SectionHeader(section_header),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
/// Event::Key(Cow::Borrowed("autocrlf")), /// Event::Key(Key(Cow::Borrowed("autocrlf"))),
/// Event::KeyValueSeparator, /// Event::KeyValueSeparator,
/// Event::Value(Cow::Borrowed(br#"true"""#)), /// Event::Value(Cow::Borrowed(br#"true"""#)),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
/// Event::Key(Cow::Borrowed("filemode")), /// Event::Key(Key(Cow::Borrowed("filemode"))),
/// Event::KeyValueSeparator, /// Event::KeyValueSeparator,
/// Event::Value(Cow::Borrowed(br#"fa"lse""#)), /// Event::Value(Cow::Borrowed(br#"fa"lse""#)),
/// # ]); /// # ]);
@ -496,10 +562,10 @@ impl Display for ParserNode {
/// split value accordingly: /// split value accordingly:
/// ///
/// ``` /// ```
/// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str}; /// # use git_config::parser::{Event, ParsedSectionHeader, parse_from_str, SectionHeaderName, Key};
/// # use std::borrow::Cow; /// # use std::borrow::Cow;
/// # let section_header = ParsedSectionHeader { /// # let section_header = ParsedSectionHeader {
/// # name: Cow::Borrowed("some-section"), /// # name: SectionHeaderName(Cow::Borrowed("some-section")),
/// # separator: None, /// # separator: None,
/// # subsection_name: None, /// # subsection_name: None,
/// # }; /// # };
@ -507,7 +573,7 @@ impl Display for ParserNode {
/// # assert_eq!(parse_from_str(section_data).unwrap().into_vec(), vec![ /// # assert_eq!(parse_from_str(section_data).unwrap().into_vec(), vec![
/// Event::SectionHeader(section_header), /// Event::SectionHeader(section_header),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
/// Event::Key(Cow::Borrowed("file")), /// Event::Key(Key(Cow::Borrowed("file"))),
/// Event::KeyValueSeparator, /// Event::KeyValueSeparator,
/// Event::ValueNotDone(Cow::Borrowed(b"a")), /// Event::ValueNotDone(Cow::Borrowed(b"a")),
/// Event::Newline(Cow::Borrowed("\n")), /// Event::Newline(Cow::Borrowed("\n")),
@ -772,12 +838,12 @@ fn section_header(i: &[u8]) -> IResult<&[u8], ParsedSectionHeader> {
// subsection syntax at this point. // subsection syntax at this point.
let header = match memchr::memrchr(b'.', name.as_bytes()) { let header = match memchr::memrchr(b'.', name.as_bytes()) {
Some(index) => ParsedSectionHeader { Some(index) => ParsedSectionHeader {
name: Cow::Borrowed(&name[..index]), name: SectionHeaderName(Cow::Borrowed(&name[..index])),
separator: name.get(index..=index).map(|slice| Cow::Borrowed(slice)), separator: name.get(index..=index).map(|slice| Cow::Borrowed(slice)),
subsection_name: name.get(index + 1..).map(|slice| Cow::Borrowed(slice)), subsection_name: name.get(index + 1..).map(|slice| Cow::Borrowed(slice)),
}, },
None => ParsedSectionHeader { None => ParsedSectionHeader {
name: Cow::Borrowed(name), name: SectionHeaderName(Cow::Borrowed(name)),
separator: None, separator: None,
subsection_name: None, subsection_name: None,
}, },
@ -808,7 +874,7 @@ fn section_header(i: &[u8]) -> IResult<&[u8], ParsedSectionHeader> {
Ok(( Ok((
i, i,
ParsedSectionHeader { ParsedSectionHeader {
name: Cow::Borrowed(name), name: SectionHeaderName(Cow::Borrowed(name)),
separator: Some(Cow::Borrowed(whitespace)), separator: Some(Cow::Borrowed(whitespace)),
// We know that there's some section name here, so if we get an // 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. // empty vec here then we actually parsed an empty section name.
@ -826,7 +892,7 @@ fn section_body<'a, 'b, 'c>(
*node = ParserNode::ConfigName; *node = ParserNode::ConfigName;
let (i, name) = config_name(i)?; let (i, name) = config_name(i)?;
items.push(Event::Key(Cow::Borrowed(name))); items.push(Event::Key(Key(Cow::Borrowed(name))));
let (i, whitespace) = opt(take_spaces)(i)?; let (i, whitespace) = opt(take_spaces)(i)?;

View file

@ -1,7 +1,7 @@
//! This module is only included for tests, and contains common unit test helper //! This module is only included for tests, and contains common unit test helper
//! functions. //! functions.
use crate::parser::{Event, ParsedComment, ParsedSectionHeader}; use crate::parser::{Event, Key, ParsedComment, ParsedSectionHeader};
use std::borrow::Cow; use std::borrow::Cow;
pub fn section_header_event( pub fn section_header_event(
@ -32,7 +32,7 @@ pub fn section_header(
} }
pub(crate) fn name_event(name: &'static str) -> Event<'static> { pub(crate) fn name_event(name: &'static str) -> Event<'static> {
Event::Key(Cow::Borrowed(name)) Event::Key(Key(Cow::Borrowed(name)))
} }
pub(crate) fn value_event(value: &'static str) -> Event<'static> { pub(crate) fn value_event(value: &'static str) -> Event<'static> {

View file

@ -97,3 +97,33 @@ fn get_value_looks_up_all_sections_before_failing() -> Result<(), Box<dyn std::e
Ok(()) Ok(())
} }
#[test]
fn section_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
let config = "[core] bool-implicit";
let file = GitConfig::try_from(config)?;
assert!(file
.get_value::<Boolean>("core", None, "bool-implicit")
.is_ok());
assert_eq!(
file.get_value::<Boolean>("core", None, "bool-implicit"),
file.get_value::<Boolean>("CORE", None, "bool-implicit")
);
Ok(())
}
#[test]
fn value_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
let config = "[core]
a = true
A = false";
let file = GitConfig::try_from(config)?;
assert_eq!(file.get_multi_value::<Boolean>("core", None, "a")?.len(), 2);
assert_eq!(
file.get_value::<Boolean>("core", None, "a"),
file.get_value::<Boolean>("core", None, "A")
);
Ok(())
}

View file

@ -1,7 +1,6 @@
use git_config::parser::{parse_from_str, Event, Key, ParsedSectionHeader, SectionHeaderName};
use std::borrow::Cow; use std::borrow::Cow;
use git_config::parser::{parse_from_str, Event, ParsedSectionHeader};
pub fn section_header_event( pub fn section_header_event(
name: &str, name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>, subsection: impl Into<Option<(&'static str, &'static str)>>,
@ -13,7 +12,7 @@ pub fn section_header(
name: &str, name: &str,
subsection: impl Into<Option<(&'static str, &'static str)>>, subsection: impl Into<Option<(&'static str, &'static str)>>,
) -> ParsedSectionHeader<'_> { ) -> ParsedSectionHeader<'_> {
let name = Cow::Borrowed(name); let name = SectionHeaderName(Cow::Borrowed(name));
if let Some((separator, subsection_name)) = subsection.into() { if let Some((separator, subsection_name)) = subsection.into() {
ParsedSectionHeader { ParsedSectionHeader {
name, name,
@ -30,7 +29,7 @@ pub fn section_header(
} }
fn name(name: &'static str) -> Event<'static> { fn name(name: &'static str) -> Event<'static> {
Event::Key(Cow::Borrowed(name)) Event::Key(Key(Cow::Borrowed(name)))
} }
fn value(value: &'static str) -> Event<'static> { fn value(value: &'static str) -> Event<'static> {