1#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::sync::Mutex;
22
23use chrono::DateTime;
24use itertools::Itertools as _;
25use rand::prelude::*;
26use rand_chacha::ChaCha20Rng;
27use serde::Deserialize;
28
29use crate::backend::ChangeId;
30use crate::backend::Commit;
31use crate::backend::Signature;
32use crate::backend::Timestamp;
33use crate::config::ConfigGetError;
34use crate::config::ConfigGetResultExt as _;
35use crate::config::ConfigTable;
36use crate::config::ConfigValue;
37use crate::config::StackedConfig;
38use crate::config::ToConfigNamePath;
39use crate::file_util::expand_home_path;
40use crate::fmt_util::binary_prefix;
41use crate::ref_name::RemoteNameBuf;
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 home_dir: Option<PathBuf>,
61}
62
63pub type RemoteSettingsMap = HashMap<RemoteNameBuf, RemoteSettings>;
64
65#[derive(Debug, Clone, serde::Deserialize)]
66#[serde(rename_all = "kebab-case")]
67pub struct RemoteSettings {
68 #[serde(default)]
70 pub auto_track_bookmarks: Option<String>,
71 #[serde(default)]
74 pub auto_track_created_bookmarks: Option<String>,
75 #[serde(default)]
77 pub fetch_bookmarks: Option<String>,
78 #[serde(default)]
80 pub fetch_tags: Option<String>,
81}
82
83impl RemoteSettings {
84 pub fn table_from_settings(
85 settings: &UserSettings,
86 ) -> Result<RemoteSettingsMap, ConfigGetError> {
87 settings
88 .table_keys("remotes")
89 .map(|name| Ok((name.into(), settings.get(["remotes", name])?)))
90 .try_collect()
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
96#[serde(rename_all = "kebab-case")]
97pub enum SignBehavior {
98 Drop,
102 Keep,
106 Own,
109 Force,
112}
113
114#[derive(Debug, Clone)]
116pub struct SignSettings {
117 pub behavior: SignBehavior,
119 pub user_email: String,
122}
123
124impl SignSettings {
125 pub fn should_sign(&self, commit: &Commit) -> bool {
128 match self.behavior {
129 SignBehavior::Drop => false,
130 SignBehavior::Keep => {
131 commit.secure_sig.is_some() && commit.author.email == self.user_email
132 }
133 SignBehavior::Own => commit.author.email == self.user_email,
134 SignBehavior::Force => true,
135 }
136 }
137}
138
139fn to_timestamp(value: ConfigValue) -> Result<Timestamp, Box<dyn std::error::Error + Send + Sync>> {
140 if let Some(s) = value.as_str() {
143 Ok(Timestamp::from_datetime(DateTime::parse_from_rfc3339(s)?))
144 } else if let Some(d) = value.as_datetime() {
145 let s = d.to_string();
147 Ok(Timestamp::from_datetime(DateTime::parse_from_rfc3339(&s)?))
148 } else {
149 let ty = value.type_name();
150 Err(format!("invalid type: {ty}, expected a date-time").into())
151 }
152}
153
154impl UserSettings {
155 pub fn from_config_and_home_dir(
157 config: StackedConfig,
158 home_dir: Option<PathBuf>,
159 ) -> Result<Self, ConfigGetError> {
160 let rng_seed = config.get::<u64>("debug.randomness-seed").optional()?;
161 Self::from_inner(config, home_dir, Arc::new(JJRng::new(rng_seed)))
162 }
163
164 fn from_inner(
165 config: StackedConfig,
166 home_dir: Option<PathBuf>,
167 rng: Arc<JJRng>,
168 ) -> Result<Self, ConfigGetError> {
169 let user_name = config.get("user.name")?;
170 let user_email = config.get("user.email")?;
171 let commit_timestamp = config
172 .get_value_with("debug.commit-timestamp", to_timestamp)
173 .optional()?;
174 let operation_timestamp = config
175 .get_value_with("debug.operation-timestamp", to_timestamp)
176 .optional()?;
177 let operation_hostname = config.get("operation.hostname")?;
178 let operation_username = config.get("operation.username")?;
179 let signing_behavior = config.get("signing.behavior")?;
180 let data = UserSettingsData {
181 user_name,
182 user_email,
183 commit_timestamp,
184 operation_timestamp,
185 operation_hostname,
186 operation_username,
187 signing_behavior,
188 home_dir,
189 };
190 Ok(Self {
191 config: Arc::new(config),
192 data: Arc::new(data),
193 rng,
194 })
195 }
196
197 pub fn with_new_config(&self, config: StackedConfig) -> Result<Self, ConfigGetError> {
202 Self::from_inner(config, self.data.home_dir.clone(), self.rng.clone())
203 }
204
205 pub fn get_rng(&self) -> Arc<JJRng> {
206 self.rng.clone()
207 }
208
209 pub fn user_name(&self) -> &str {
210 &self.data.user_name
211 }
212
213 pub fn user_email(&self) -> &str {
214 &self.data.user_email
215 }
216
217 pub fn commit_timestamp(&self) -> Option<Timestamp> {
218 self.data.commit_timestamp
219 }
220
221 pub fn operation_timestamp(&self) -> Option<Timestamp> {
222 self.data.operation_timestamp
223 }
224
225 pub fn operation_hostname(&self) -> &str {
226 &self.data.operation_hostname
227 }
228
229 pub fn operation_username(&self) -> &str {
230 &self.data.operation_username
231 }
232
233 pub fn signature(&self) -> Signature {
234 let timestamp = self.data.commit_timestamp.unwrap_or_else(Timestamp::now);
235 Signature {
236 name: self.user_name().to_owned(),
237 email: self.user_email().to_owned(),
238 timestamp,
239 }
240 }
241
242 pub fn config(&self) -> &StackedConfig {
246 &self.config
247 }
248
249 pub fn remote_settings(&self) -> Result<RemoteSettingsMap, ConfigGetError> {
250 RemoteSettings::table_from_settings(self)
251 }
252
253 pub fn signing_backend(&self) -> Result<Option<String>, ConfigGetError> {
256 let backend = self.get_string("signing.backend")?;
257 Ok((backend != "none").then_some(backend))
258 }
259
260 pub fn sign_settings(&self) -> SignSettings {
261 SignSettings {
262 behavior: self.data.signing_behavior,
263 user_email: self.data.user_email.clone(),
264 }
265 }
266}
267
268impl UserSettings {
270 pub fn get<'de, T: Deserialize<'de>>(
272 &self,
273 name: impl ToConfigNamePath,
274 ) -> Result<T, ConfigGetError> {
275 self.config.get(name)
276 }
277
278 pub fn get_string(&self, name: impl ToConfigNamePath) -> Result<String, ConfigGetError> {
280 self.get(name)
281 }
282
283 pub fn get_int(&self, name: impl ToConfigNamePath) -> Result<i64, ConfigGetError> {
285 self.get(name)
286 }
287
288 pub fn get_bool(&self, name: impl ToConfigNamePath) -> Result<bool, ConfigGetError> {
290 self.get(name)
291 }
292
293 pub fn get_path(&self, name: impl ToConfigNamePath) -> Result<PathBuf, ConfigGetError> {
295 let value = self.get_string(name)?;
296 Ok(expand_home_path(&value, self.data.home_dir.as_deref()))
297 }
298
299 pub fn get_value(&self, name: impl ToConfigNamePath) -> Result<ConfigValue, ConfigGetError> {
301 self.config.get_value(name)
302 }
303
304 pub fn get_value_with<T, E: Into<Box<dyn std::error::Error + Send + Sync>>>(
306 &self,
307 name: impl ToConfigNamePath,
308 convert: impl FnOnce(ConfigValue) -> Result<T, E>,
309 ) -> Result<T, ConfigGetError> {
310 self.config.get_value_with(name, convert)
311 }
312
313 pub fn get_table(&self, name: impl ToConfigNamePath) -> Result<ConfigTable, ConfigGetError> {
318 self.config.get_table(name)
319 }
320
321 pub fn table_keys(&self, name: impl ToConfigNamePath) -> impl Iterator<Item = &str> {
323 self.config.table_keys(name)
324 }
325}
326
327#[derive(Debug)]
331pub struct JJRng(Mutex<ChaCha20Rng>);
332impl JJRng {
333 pub fn new_change_id(&self, length: usize) -> ChangeId {
334 let mut rng = self.0.lock().unwrap();
335 let random_bytes = (0..length).map(|_| rng.random::<u8>()).collect();
336 ChangeId::new(random_bytes)
337 }
338
339 fn new(seed: Option<u64>) -> Self {
342 Self(Mutex::new(Self::internal_rng_from_seed(seed)))
343 }
344
345 fn internal_rng_from_seed(seed: Option<u64>) -> ChaCha20Rng {
346 match seed {
347 Some(seed) => ChaCha20Rng::seed_from_u64(seed),
348 None => rand::make_rng(),
349 }
350 }
351}
352
353#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
355pub struct HumanByteSize(pub u64);
356
357impl std::fmt::Display for HumanByteSize {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 let (value, prefix) = binary_prefix(self.0 as f32);
360 write!(f, "{value:.1}{prefix}B")
361 }
362}
363
364impl FromStr for HumanByteSize {
365 type Err = &'static str;
366
367 fn from_str(s: &str) -> Result<Self, Self::Err> {
368 match s.parse() {
369 Ok(bytes) => Ok(Self(bytes)),
370 Err(_) => {
371 let bytes = parse_human_byte_size(s)?;
372 Ok(Self(bytes))
373 }
374 }
375 }
376}
377
378impl TryFrom<ConfigValue> for HumanByteSize {
379 type Error = &'static str;
380
381 fn try_from(value: ConfigValue) -> Result<Self, Self::Error> {
382 if let Some(n) = value.as_integer() {
383 let n = u64::try_from(n).map_err(|_| "Integer out of range")?;
384 Ok(Self(n))
385 } else if let Some(s) = value.as_str() {
386 s.parse()
387 } else {
388 Err("Expected a positive integer or a string in '<number><unit>' form")
389 }
390 }
391}
392
393fn parse_human_byte_size(v: &str) -> Result<u64, &'static str> {
394 let digit_end = v.find(|c: char| !c.is_ascii_digit()).unwrap_or(v.len());
395 if digit_end == 0 {
396 return Err("must start with a number");
397 }
398 let (digits, trailing) = v.split_at(digit_end);
399 let exponent = match trailing.trim_start() {
400 "" | "B" => 0,
401 unit => {
402 const PREFIXES: [char; 8] = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
403 let Some(prefix) = PREFIXES.iter().position(|&x| unit.starts_with(x)) else {
404 return Err("unrecognized unit prefix");
405 };
406 let ("" | "B" | "i" | "iB") = &unit[1..] else {
407 return Err("unrecognized unit");
408 };
409 prefix as u32 + 1
410 }
411 };
412 let factor = digits.parse::<u64>().unwrap_or(u64::MAX);
415 Ok(factor.saturating_mul(1024u64.saturating_pow(exponent)))
416}
417
418#[cfg(test)]
419mod tests {
420 use std::assert_matches;
421
422 use super::*;
423
424 #[test]
425 fn byte_size_parse() {
426 assert_eq!(parse_human_byte_size("0"), Ok(0));
427 assert_eq!(parse_human_byte_size("42"), Ok(42));
428 assert_eq!(parse_human_byte_size("42B"), Ok(42));
429 assert_eq!(parse_human_byte_size("42 B"), Ok(42));
430 assert_eq!(parse_human_byte_size("42K"), Ok(42 * 1024));
431 assert_eq!(parse_human_byte_size("42 K"), Ok(42 * 1024));
432 assert_eq!(parse_human_byte_size("42 KB"), Ok(42 * 1024));
433 assert_eq!(parse_human_byte_size("42 KiB"), Ok(42 * 1024));
434 assert_eq!(
435 parse_human_byte_size("42 LiB"),
436 Err("unrecognized unit prefix")
437 );
438 assert_eq!(parse_human_byte_size("42 KiC"), Err("unrecognized unit"));
439 assert_eq!(parse_human_byte_size("42 KC"), Err("unrecognized unit"));
440 assert_eq!(
441 parse_human_byte_size("KiB"),
442 Err("must start with a number")
443 );
444 assert_eq!(parse_human_byte_size(""), Err("must start with a number"));
445 }
446
447 #[test]
448 fn byte_size_from_config_value() {
449 assert_eq!(
450 HumanByteSize::try_from(ConfigValue::from(42)).unwrap(),
451 HumanByteSize(42)
452 );
453 assert_eq!(
454 HumanByteSize::try_from(ConfigValue::from("42K")).unwrap(),
455 HumanByteSize(42 * 1024)
456 );
457 assert_matches!(
458 HumanByteSize::try_from(ConfigValue::from(-1)),
459 Err("Integer out of range")
460 );
461 }
462}