Skip to main content

gix_config/parse/section/
mod.rs

1use crate::parse::{MaybeDecoded, Span};
2
3///
4pub mod header;
5
6pub(crate) mod unvalidated;
7
8/// A parsed section header.
9#[derive(Clone, Debug)]
10pub(crate) struct HeaderData {
11    /// The name of the header.
12    pub(crate) name: Span,
13    /// The separator used to determine if the section contains a subsection.
14    /// This is either a period `.` or a string of whitespace. Note that
15    /// reconstruction of subsection format is dependent on this value. If this
16    /// is all whitespace, then the subsection name needs to be surrounded by
17    /// quotes to have perfect reconstruction.
18    pub(crate) separator: Option<Span>,
19    pub(crate) subsection_name: Option<MaybeDecoded>,
20}
21
22mod types {
23    use bstr::ByteSlice;
24
25    macro_rules! generate_case_insensitive {
26        ($name:ident, $module:ident, $err_doc:literal, $validate:ident, $cow_inner_type:ty, $comment:literal) => {
27            ///
28            pub mod $module {
29                /// The error returned when `TryFrom` is invoked to create an instance.
30                #[derive(Debug, thiserror::Error, Copy, Clone)]
31                #[error($err_doc)]
32                pub struct Error;
33            }
34
35            #[doc = $comment]
36            #[derive(Clone, Eq, Debug, Default)]
37            pub struct $name(pub(crate) bstr::BString);
38
39            impl $name {
40                pub(crate) fn from_str_unchecked(s: &str) -> Self {
41                    $name(s.into())
42                }
43                /// Clone this instance.
44                #[must_use]
45                pub fn to_owned(&self) -> $name {
46                    self.clone()
47                }
48            }
49
50            impl PartialEq for $name {
51                fn eq(&self, other: &Self) -> bool {
52                    self.0.eq_ignore_ascii_case(&other.0)
53                }
54            }
55
56            impl std::fmt::Display for $name {
57                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58                    std::fmt::Display::fmt(&self.0, f)
59                }
60            }
61
62            impl PartialOrd for $name {
63                fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
64                    Some(self.cmp(other))
65                }
66            }
67
68            impl Ord for $name {
69                fn cmp(&self, other: &Self) -> std::cmp::Ordering {
70                    let a = self.0.iter().map(|c| c.to_ascii_lowercase());
71                    let b = other.0.iter().map(|c| c.to_ascii_lowercase());
72                    a.cmp(b)
73                }
74            }
75
76            impl std::hash::Hash for $name {
77                fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
78                    for b in self.0.iter() {
79                        b.to_ascii_lowercase().hash(state);
80                    }
81                }
82            }
83
84            impl std::convert::TryFrom<&str> for $name {
85                type Error = $module::Error;
86
87                fn try_from(s: &str) -> Result<Self, Self::Error> {
88                    Self::try_from(bstr::ByteSlice::as_bstr(s.as_bytes()))
89                }
90            }
91
92            impl std::convert::TryFrom<String> for $name {
93                type Error = $module::Error;
94
95                fn try_from(s: String) -> Result<Self, Self::Error> {
96                    Self::try_from(bstr::BString::from(s))
97                }
98            }
99
100            impl std::convert::TryFrom<bstr::BString> for $name {
101                type Error = $module::Error;
102
103                fn try_from(s: bstr::BString) -> Result<Self, Self::Error> {
104                    if $validate(s.as_slice().as_bstr()) {
105                        Ok(Self(s.into()))
106                    } else {
107                        Err($module::Error)
108                    }
109                }
110            }
111
112            impl std::convert::TryFrom<&bstr::BStr> for $name {
113                type Error = $module::Error;
114
115                fn try_from(s: &bstr::BStr) -> Result<Self, Self::Error> {
116                    if $validate(s) {
117                        Ok(Self(s.into()))
118                    } else {
119                        Err($module::Error)
120                    }
121                }
122            }
123
124            impl std::ops::Deref for $name {
125                type Target = $cow_inner_type;
126
127                fn deref(&self) -> &Self::Target {
128                    self.0.as_bstr()
129                }
130            }
131
132            impl std::convert::AsRef<str> for $name {
133                fn as_ref(&self) -> &str {
134                    std::str::from_utf8(self.0.as_slice()).expect("only valid UTF8 makes it through our validation")
135                }
136            }
137        };
138    }
139
140    fn is_valid_name(n: &bstr::BStr) -> bool {
141        !n.is_empty() && n.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'-')
142    }
143    fn is_valid_value_name(n: &bstr::BStr) -> bool {
144        is_valid_name(n) && n[0].is_ascii_alphabetic()
145    }
146
147    generate_case_insensitive!(
148        Name,
149        name,
150        "Valid names consist of alphanumeric characters or dashes.",
151        is_valid_name,
152        bstr::BStr,
153        "Wrapper struct for section header names, like `remote`, since these are case-insensitive."
154    );
155
156    generate_case_insensitive!(
157        ValueName,
158        value_name,
159        "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.",
160        is_valid_value_name,
161        bstr::BStr,
162        "Wrapper struct for value names, like `path` in `include.path`, since keys are case-insensitive."
163    );
164}
165pub(crate) use types::ValueName;
166pub use types::{Name, name, value_name};
167
168#[cfg(test)]
169mod tests {
170    use std::cmp::Ordering;
171
172    use super::ValueName;
173
174    fn key(key: &str) -> ValueName {
175        ValueName::try_from(key).expect("valid test key")
176    }
177
178    #[test]
179    fn value_names_reject_invalid_formats() {
180        for invalid in ["", "1a", "a.2", "##", "\""] {
181            assert!(ValueName::try_from(invalid).is_err(), "{invalid:?} is invalid");
182        }
183    }
184
185    #[test]
186    fn value_names_are_case_insensitive() {
187        assert_eq!(key("aB-c"), key("Ab-C"));
188        assert_eq!(key("a").cmp(&key("a")), Ordering::Equal);
189        assert_eq!(key("aBc").cmp(&key("AbC")), Ordering::Equal);
190
191        fn calculate_hash<T: std::hash::Hash>(value: T) -> u64 {
192            use std::hash::Hasher;
193            let mut state = std::collections::hash_map::DefaultHasher::new();
194            value.hash(&mut state);
195            state.finish()
196        }
197        assert_eq!(calculate_hash(key("aBc")), calculate_hash(key("AbC")));
198    }
199}