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)
73 .map(TypedConfigValue::Bool)
74 .map_err(|e| e.for_key(key).into()),
75 ConfigValueType::Int => parse_git_config_int_strict(value)
76 .map(TypedConfigValue::Int)
77 .map_err(|e| int_parse_error(key, value, e)),
78 ConfigValueType::Path => Ok(TypedConfigValue::Path(parse_path(env, value))),
79 }
80}
81
82fn int_parse_error(key: &str, value: &str, err: GitConfigIntStrictError) -> Error {
83 let reason = match err {
84 GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
85 GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
86 };
87 Error::Config(ConfigError::BadNumericValue {
88 key: key.to_owned(),
89 value: value.to_owned(),
90 reason,
91 })
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95enum ListPrefixMatch {
96 KeyPrefix(String),
98 ExactKey(String),
100}
101
102impl ConfigSet {
103 pub(crate) fn get_i64_strict(&self, key: &str) -> Result<Option<i64>> {
112 let Some(raw) = self.get(key) else {
113 return Ok(None);
114 };
115 parse_git_config_int_strict(&raw)
116 .map(Some)
117 .map_err(|e| int_parse_error(key, &raw, e))
118 }
119
120 pub fn entries_with_prefix(&self, prefix: &str) -> Result<Vec<super::ConfigEntry>> {
128 let matcher = list_prefix_match(prefix)?;
129 Ok(self
130 .entries()
131 .iter()
132 .filter(|e| match &matcher {
133 ListPrefixMatch::ExactKey(key) => &e.key == key,
134 ListPrefixMatch::KeyPrefix(needle) => e.key.starts_with(needle.as_str()),
135 })
136 .cloned()
137 .collect())
138 }
139}
140
141fn list_prefix_match(prefix: &str) -> Result<ListPrefixMatch> {
142 let prefix = prefix.trim();
143 if prefix.is_empty() {
144 return Err(Error::Config(ConfigError::Other(
145 "list prefix must not be empty".into(),
146 )));
147 }
148 if let Ok(canon) = canonical_key(prefix) {
149 return Ok(ListPrefixMatch::ExactKey(canon));
150 }
151 let section_part = prefix.trim_end_matches('.');
152 if section_part.is_empty() {
153 return Err(Error::Config(ConfigError::Other(
154 "list prefix must not be empty".into(),
155 )));
156 }
157 let (sec, sub) = match section_part.find('.') {
158 Some(i) => (§ion_part[..i], Some(§ion_part[i + 1..])),
159 None => (section_part, None),
160 };
161 if sec.is_empty()
162 || !sec
163 .chars()
164 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
165 {
166 return Err(Error::Config(ConfigError::Other(format!(
167 "invalid list prefix '{prefix}'"
168 ))));
169 }
170 let sec_lower = sec.to_ascii_lowercase();
171 let needle = match sub {
172 None => format!("{sec_lower}."),
173 Some(sub) if !sub.is_empty() => format!("{sec_lower}.{sub}."),
174 Some(_) => {
175 return Err(Error::Config(ConfigError::Other(format!(
176 "invalid list prefix '{prefix}'"
177 ))));
178 }
179 };
180 Ok(ListPrefixMatch::KeyPrefix(needle))
181}