1#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::str::FromStr;
19use std::sync::Arc;
20use std::sync::Mutex;
21
22use chrono::DateTime;
23use itertools::Itertools as _;
24use rand::prelude::*;
25use rand_chacha::ChaCha20Rng;
26use serde::Deserialize;
27
28use crate::backend::ChangeId;
29use crate::backend::Commit;
30use crate::backend::Signature;
31use crate::backend::Timestamp;
32use crate::config::ConfigGetError;
33use crate::config::ConfigGetResultExt as _;
34use crate::config::ConfigTable;
35use crate::config::ConfigValue;
36use crate::config::StackedConfig;
37use crate::config::ToConfigNamePath;
38use crate::fmt_util::binary_prefix;
39use crate::ref_name::RemoteNameBuf;
40use crate::signing::SignBehavior;
41use crate::str_util::StringPattern;
42
43#[derive(Debug, Clone)]
44pub struct UserSettings {
45 config: Arc<StackedConfig>,
46 data: Arc<UserSettingsData>,
47 rng: Arc<JJRng>,
48}
49
50#[derive(Debug)]
51struct UserSettingsData {
52 user_name: String,
53 user_email: String,
54 commit_timestamp: Option<Timestamp>,
55 operation_timestamp: Option<Timestamp>,
56 operation_hostname: String,
57 operation_username: String,
58 signing_behavior: SignBehavior,
59 signing_key: Option<String>,
60}
61
62#[derive(Debug, Clone)]
63pub struct RemoteSettings {
64 pub auto_track_bookmarks: StringPattern,
65}
66
67impl RemoteSettings {
68 pub fn table_from_settings(
69 settings: &UserSettings,
70 ) -> Result<HashMap<RemoteNameBuf, Self>, ConfigGetError> {
71 settings
72 .table_keys("remotes")
73 .map(|name| {
74 Ok((
75 name.into(),
76 Self {
77 auto_track_bookmarks: settings.get_value_with(
78 ["remotes", name, "auto-track-bookmarks"],
79 |value| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
80 Ok(StringPattern::parse(
81 value
82 .as_str()
83 .ok_or_else(|| "expected a string".to_string())?,
84 )?)
85 },
86 )?,
87 },
88 ))
89 })
90 .try_collect()
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct SignSettings {
97 pub behavior: SignBehavior,
99 pub user_email: String,
102 pub key: Option<String>,
104}
105
106impl SignSettings {
107 pub fn should_sign(&self, commit: &Commit) -> bool {
110 match self.behavior {
111 SignBehavior::Drop => false,
112 SignBehavior::Keep => {
113 commit.secure_sig.is_some() && commit.author.email == self.user_email
114 }
115 SignBehavior::Own => commit.author.email == self.user_email,
116 SignBehavior::Force => true,
117 }
118 }
119}
120
121fn to_timestamp(value: ConfigValue) -> Result<Timestamp, Box<dyn std::error::Error + Send + Sync>> {
122 if let Some(s) = value.as_str() {
125 Ok(Timestamp::from_datetime(DateTime::parse_from_rfc3339(s)?))
126 } else if let Some(d) = value.as_datetime() {
127 let s = d.to_string();
129 Ok(Timestamp::from_datetime(DateTime::parse_from_rfc3339(&s)?))
130 } else {
131 let ty = value.type_name();
132 Err(format!("invalid type: {ty}, expected a date-time").into())
133 }
134}
135
136impl UserSettings {
137 pub fn from_config(config: StackedConfig) -> Result<Self, ConfigGetError> {
138 let rng_seed = config.get::<u64>("debug.randomness-seed").optional()?;
139 Self::from_config_and_rng(config, Arc::new(JJRng::new(rng_seed)))
140 }
141
142 fn from_config_and_rng(config: StackedConfig, rng: Arc<JJRng>) -> Result<Self, ConfigGetError> {
143 let user_name = config.get("user.name")?;
144 let user_email = config.get("user.email")?;
145 let commit_timestamp = config
146 .get_value_with("debug.commit-timestamp", to_timestamp)
147 .optional()?;
148 let operation_timestamp = config
149 .get_value_with("debug.operation-timestamp", to_timestamp)
150 .optional()?;
151 let operation_hostname = config.get("operation.hostname")?;
152 let operation_username = config.get("operation.username")?;
153 let signing_behavior = config.get("signing.behavior")?;
154 let signing_key = config.get("signing.key").optional()?;
155 let data = UserSettingsData {
156 user_name,
157 user_email,
158 commit_timestamp,
159 operation_timestamp,
160 operation_hostname,
161 operation_username,
162 signing_behavior,
163 signing_key,
164 };
165 Ok(Self {
166 config: Arc::new(config),
167 data: Arc::new(data),
168 rng,
169 })
170 }
171
172 pub fn with_new_config(&self, config: StackedConfig) -> Result<Self, ConfigGetError> {
177 Self::from_config_and_rng(config, self.rng.clone())
178 }
179
180 pub fn get_rng(&self) -> Arc<JJRng> {
181 self.rng.clone()
182 }
183
184 pub fn user_name(&self) -> &str {
185 &self.data.user_name
186 }
187
188 pub const USER_NAME_PLACEHOLDER: &str = "(no name configured)";
190
191 pub fn user_email(&self) -> &str {
192 &self.data.user_email
193 }
194
195 pub const USER_EMAIL_PLACEHOLDER: &str = "(no email configured)";
198
199 pub fn commit_timestamp(&self) -> Option<Timestamp> {
200 self.data.commit_timestamp
201 }
202
203 pub fn operation_timestamp(&self) -> Option<Timestamp> {
204 self.data.operation_timestamp
205 }
206
207 pub fn operation_hostname(&self) -> &str {
208 &self.data.operation_hostname
209 }
210
211 pub fn operation_username(&self) -> &str {
212 &self.data.operation_username
213 }
214
215 pub fn signature(&self) -> Signature {
216 let timestamp = self.data.commit_timestamp.unwrap_or_else(Timestamp::now);
217 Signature {
218 name: self.user_name().to_owned(),
219 email: self.user_email().to_owned(),
220 timestamp,
221 }
222 }
223
224 pub fn config(&self) -> &StackedConfig {
228 &self.config
229 }
230
231 pub fn remote_settings(
232 &self,
233 ) -> Result<HashMap<RemoteNameBuf, RemoteSettings>, ConfigGetError> {
234 RemoteSettings::table_from_settings(self)
235 }
236
237 pub fn signing_backend(&self) -> Result<Option<String>, ConfigGetError> {
240 let backend = self.get_string("signing.backend")?;
241 Ok((backend != "none").then_some(backend))
242 }
243
244 pub fn sign_settings(&self) -> SignSettings {
245 SignSettings {
246 behavior: self.data.signing_behavior,
247 user_email: self.data.user_email.clone(),
248 key: self.data.signing_key.clone(),
249 }
250 }
251}
252
253impl UserSettings {
255 pub fn get<'de, T: Deserialize<'de>>(
257 &self,
258 name: impl ToConfigNamePath,
259 ) -> Result<T, ConfigGetError> {
260 self.config.get(name)
261 }
262
263 pub fn get_string(&self, name: impl ToConfigNamePath) -> Result<String, ConfigGetError> {
265 self.get(name)
266 }
267
268 pub fn get_int(&self, name: impl ToConfigNamePath) -> Result<i64, ConfigGetError> {
270 self.get(name)
271 }
272
273 pub fn get_bool(&self, name: impl ToConfigNamePath) -> Result<bool, ConfigGetError> {
275 self.get(name)
276 }
277
278 pub fn get_value(&self, name: impl ToConfigNamePath) -> Result<ConfigValue, ConfigGetError> {
280 self.config.get_value(name)
281 }
282
283 pub fn get_value_with<T, E: Into<Box<dyn std::error::Error + Send + Sync>>>(
285 &self,
286 name: impl ToConfigNamePath,
287 convert: impl FnOnce(ConfigValue) -> Result<T, E>,
288 ) -> Result<T, ConfigGetError> {
289 self.config.get_value_with(name, convert)
290 }
291
292 pub fn get_table(&self, name: impl ToConfigNamePath) -> Result<ConfigTable, ConfigGetError> {
297 self.config.get_table(name)
298 }
299
300 pub fn table_keys(&self, name: impl ToConfigNamePath) -> impl Iterator<Item = &str> {
302 self.config.table_keys(name)
303 }
304}
305
306#[derive(Debug)]
310pub struct JJRng(Mutex<ChaCha20Rng>);
311impl JJRng {
312 pub fn new_change_id(&self, length: usize) -> ChangeId {
313 let mut rng = self.0.lock().unwrap();
314 let random_bytes = (0..length).map(|_| rng.random::<u8>()).collect();
315 ChangeId::new(random_bytes)
316 }
317
318 fn new(seed: Option<u64>) -> Self {
321 Self(Mutex::new(Self::internal_rng_from_seed(seed)))
322 }
323
324 fn internal_rng_from_seed(seed: Option<u64>) -> ChaCha20Rng {
325 match seed {
326 Some(seed) => ChaCha20Rng::seed_from_u64(seed),
327 None => ChaCha20Rng::from_os_rng(),
328 }
329 }
330}
331
332#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
334pub struct HumanByteSize(pub u64);
335
336impl std::fmt::Display for HumanByteSize {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 let (value, prefix) = binary_prefix(self.0 as f32);
339 write!(f, "{value:.1}{prefix}B")
340 }
341}
342
343impl FromStr for HumanByteSize {
344 type Err = &'static str;
345
346 fn from_str(s: &str) -> Result<Self, Self::Err> {
347 match s.parse() {
348 Ok(bytes) => Ok(Self(bytes)),
349 Err(_) => {
350 let bytes = parse_human_byte_size(s)?;
351 Ok(Self(bytes))
352 }
353 }
354 }
355}
356
357impl TryFrom<ConfigValue> for HumanByteSize {
358 type Error = &'static str;
359
360 fn try_from(value: ConfigValue) -> Result<Self, Self::Error> {
361 if let Some(n) = value.as_integer() {
362 let n = u64::try_from(n).map_err(|_| "Integer out of range")?;
363 Ok(Self(n))
364 } else if let Some(s) = value.as_str() {
365 s.parse()
366 } else {
367 Err("Expected a positive integer or a string in '<number><unit>' form")
368 }
369 }
370}
371
372fn parse_human_byte_size(v: &str) -> Result<u64, &'static str> {
373 let digit_end = v.find(|c: char| !c.is_ascii_digit()).unwrap_or(v.len());
374 if digit_end == 0 {
375 return Err("must start with a number");
376 }
377 let (digits, trailing) = v.split_at(digit_end);
378 let exponent = match trailing.trim_start() {
379 "" | "B" => 0,
380 unit => {
381 const PREFIXES: [char; 8] = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
382 let Some(prefix) = PREFIXES.iter().position(|&x| unit.starts_with(x)) else {
383 return Err("unrecognized unit prefix");
384 };
385 let ("" | "B" | "i" | "iB") = &unit[1..] else {
386 return Err("unrecognized unit");
387 };
388 prefix as u32 + 1
389 }
390 };
391 let factor = digits.parse::<u64>().unwrap_or(u64::MAX);
394 Ok(factor.saturating_mul(1024u64.saturating_pow(exponent)))
395}
396
397#[cfg(test)]
398mod tests {
399 use assert_matches::assert_matches;
400
401 use super::*;
402
403 #[test]
404 fn byte_size_parse() {
405 assert_eq!(parse_human_byte_size("0"), Ok(0));
406 assert_eq!(parse_human_byte_size("42"), Ok(42));
407 assert_eq!(parse_human_byte_size("42B"), Ok(42));
408 assert_eq!(parse_human_byte_size("42 B"), Ok(42));
409 assert_eq!(parse_human_byte_size("42K"), Ok(42 * 1024));
410 assert_eq!(parse_human_byte_size("42 K"), Ok(42 * 1024));
411 assert_eq!(parse_human_byte_size("42 KB"), Ok(42 * 1024));
412 assert_eq!(parse_human_byte_size("42 KiB"), Ok(42 * 1024));
413 assert_eq!(
414 parse_human_byte_size("42 LiB"),
415 Err("unrecognized unit prefix")
416 );
417 assert_eq!(parse_human_byte_size("42 KiC"), Err("unrecognized unit"));
418 assert_eq!(parse_human_byte_size("42 KC"), Err("unrecognized unit"));
419 assert_eq!(
420 parse_human_byte_size("KiB"),
421 Err("must start with a number")
422 );
423 assert_eq!(parse_human_byte_size(""), Err("must start with a number"));
424 }
425
426 #[test]
427 fn byte_size_from_config_value() {
428 assert_eq!(
429 HumanByteSize::try_from(ConfigValue::from(42)).unwrap(),
430 HumanByteSize(42)
431 );
432 assert_eq!(
433 HumanByteSize::try_from(ConfigValue::from("42K")).unwrap(),
434 HumanByteSize(42 * 1024)
435 );
436 assert_matches!(
437 HumanByteSize::try_from(ConfigValue::from(-1)),
438 Err("Integer out of range")
439 );
440 }
441}