1use crate::environment::Environment;
4use crate::error::{BadNumericSource, ConfigError, Error, Result};
5
6use super::{
7 canonical_key, parse_bool, parse_git_config_int_strict, parse_path, ConfigSet,
8 GitConfigIntStrictError,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ConfigValueType {
14 Bool,
16 Int,
18 Path,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum TypedConfigValue {
25 Bool(bool),
27 Int(i64),
29 Path(String),
31}
32
33impl TypedConfigValue {
34 #[must_use]
36 pub fn to_canonical_string(&self) -> String {
37 match self {
38 Self::Bool(b) => if *b { "true" } else { "false" }.to_owned(),
39 Self::Int(n) => n.to_string(),
40 Self::Path(p) => p.clone(),
41 }
42 }
43}
44
45pub fn canonicalize_config_write_value(
51 key: &str,
52 ty: ConfigValueType,
53 value: &str,
54 env: &Environment,
55) -> Result<String> {
56 let typed = parse_typed_value(key, ty, value, env)?;
57 Ok(typed.to_canonical_string())
58}
59
60pub fn parse_typed_value(
66 key: &str,
67 ty: ConfigValueType,
68 value: &str,
69 env: &Environment,
70) -> Result<TypedConfigValue> {
71 match ty {
72 ConfigValueType::Bool => parse_bool(value).map(TypedConfigValue::Bool).map_err(|_| {
73 Error::Config(ConfigError::Other(format!(
74 "bad boolean config value '{value}' for '{key}'"
75 )))
76 }),
77 ConfigValueType::Int => parse_git_config_int_strict(value)
78 .map(TypedConfigValue::Int)
79 .map_err(|e| int_parse_error(key, value, e)),
80 ConfigValueType::Path => Ok(TypedConfigValue::Path(parse_path(env, value))),
81 }
82}
83
84fn int_parse_error(key: &str, value: &str, err: GitConfigIntStrictError) -> Error {
85 let reason = match err {
86 GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
87 GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
88 };
89 Error::Config(ConfigError::BadNumericValue {
90 key: key.to_owned(),
91 value: value.to_owned(),
92 reason,
93 })
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97enum ListPrefixMatch {
98 KeyPrefix(String),
100 ExactKey(String),
102}
103
104impl ConfigSet {
105 pub fn get_typed(
113 &self,
114 key: &str,
115 ty: ConfigValueType,
116 env: &Environment,
117 ) -> Result<Option<TypedConfigValue>> {
118 let _canon = canonical_key(key)?;
119 let Some(entry) = self.get_last_entry(key) else {
120 return Ok(None);
121 };
122 let raw = entry.value.as_deref().unwrap_or("true");
123 parse_typed_value(key, ty, raw, env).map(Some)
124 }
125
126 pub(crate) fn get_i64_strict(&self, key: &str) -> Result<Option<i64>> {
135 let Some(raw) = self.get(key) else {
136 return Ok(None);
137 };
138 parse_git_config_int_strict(&raw)
139 .map(Some)
140 .map_err(|e| int_parse_error(key, &raw, e))
141 }
142
143 pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
151 let matcher = list_prefix_match(prefix)?;
152 Ok(self
153 .entries()
154 .iter()
155 .filter(|e| match &matcher {
156 ListPrefixMatch::ExactKey(key) => &e.key == key,
157 ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
158 })
159 .cloned()
160 .collect())
161 }
162}
163
164fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
165 let prefix = prefix.trim();
166 if prefix.is_empty() {
167 return Err(Error::Config(ConfigError::Other(
168 "list prefix must not be empty".into(),
169 )));
170 }
171 if let Ok(canon) = canonical_key(prefix) {
172 return Ok(ListPrefixMatch::ExactKey(canon));
173 }
174 let section_part = prefix.trim_end_matches('.');
175 if section_part.is_empty() {
176 return Err(Error::Config(ConfigError::Other(
177 "list prefix must not be empty".into(),
178 )));
179 }
180 let (sec, sub) = match section_part.find('.') {
181 Some(i) => (§ion_part[..i], Some(§ion_part[i + 1..])),
182 None => (section_part, None),
183 };
184 if sec.is_empty()
185 || !sec
186 .chars()
187 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
188 {
189 return Err(Error::Config(ConfigError::Other(format!(
190 "invalid list prefix '{prefix}'"
191 ))));
192 }
193 let sec_lower = sec.to_ascii_lowercase();
194 let needle = match sub {
195 None => format!("{sec_lower}."),
196 Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
197 Some(_) => {
198 return Err(Error::Config(ConfigError::Other(format!(
199 "invalid list prefix '{prefix}'"
200 ))));
201 }
202 };
203 Ok(ListPrefixMatch::KeyPrefix(needle))
204}