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(crate) fn get_i64_strict(&self, key: &str) -> Result<Option<i64>> {
114 let Some(raw) = self.get(key) else {
115 return Ok(None);
116 };
117 parse_git_config_int_strict(&raw)
118 .map(Some)
119 .map_err(|e| int_parse_error(key, &raw, e))
120 }
121
122 pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
130 let matcher = list_prefix_match(prefix)?;
131 Ok(self
132 .entries()
133 .iter()
134 .filter(|e| match &matcher {
135 ListPrefixMatch::ExactKey(key) => &e.key == key,
136 ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
137 })
138 .cloned()
139 .collect())
140 }
141}
142
143fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
144 let prefix = prefix.trim();
145 if prefix.is_empty() {
146 return Err(Error::Config(ConfigError::Other(
147 "list prefix must not be empty".into(),
148 )));
149 }
150 if let Ok(canon) = canonical_key(prefix) {
151 return Ok(ListPrefixMatch::ExactKey(canon));
152 }
153 let section_part = prefix.trim_end_matches('.');
154 if section_part.is_empty() {
155 return Err(Error::Config(ConfigError::Other(
156 "list prefix must not be empty".into(),
157 )));
158 }
159 let (sec, sub) = match section_part.find('.') {
160 Some(i) => (§ion_part[..i], Some(§ion_part[i + 1..])),
161 None => (section_part, None),
162 };
163 if sec.is_empty()
164 || !sec
165 .chars()
166 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
167 {
168 return Err(Error::Config(ConfigError::Other(format!(
169 "invalid list prefix '{prefix}'"
170 ))));
171 }
172 let sec_lower = sec.to_ascii_lowercase();
173 let needle = match sub {
174 None => format!("{sec_lower}."),
175 Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
176 Some(_) => {
177 return Err(Error::Config(ConfigError::Other(format!(
178 "invalid list prefix '{prefix}'"
179 ))));
180 }
181 };
182 Ok(ListPrefixMatch::KeyPrefix(needle))
183}