##// END OF EJS Templates
rust-clippy: merge "config" module definition and struct implementation...
Raphaël Gomès -
r50831:2cd8352f default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (653 lines changed) Show them Hide them
@@ -1,654 +1,1 b''
1 // config.rs
2 //
3 // Copyright 2020
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
9 1
10 use super::layer;
11 use super::values;
12 use crate::config::layer::{
13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
14 };
15 use crate::config::plain_info::PlainInfo;
16 use crate::utils::files::get_bytes_from_os_str;
17 use format_bytes::{write_bytes, DisplayBytes};
18 use std::collections::HashSet;
19 use std::env;
20 use std::fmt;
21 use std::path::{Path, PathBuf};
22 use std::str;
23
24 use crate::errors::{HgResultExt, IoResultExt};
25
26 /// Holds the config values for the current repository
27 /// TODO update this docstring once we support more sources
28 #[derive(Clone)]
29 pub struct Config {
30 layers: Vec<layer::ConfigLayer>,
31 plain: PlainInfo,
32 }
33
34 impl DisplayBytes for Config {
35 fn display_bytes(
36 &self,
37 out: &mut dyn std::io::Write,
38 ) -> std::io::Result<()> {
39 for (index, layer) in self.layers.iter().rev().enumerate() {
40 write_bytes!(
41 out,
42 b"==== Layer {} (trusted: {}) ====\n{}",
43 index,
44 if layer.trusted {
45 &b"yes"[..]
46 } else {
47 &b"no"[..]
48 },
49 layer
50 )?;
51 }
52 Ok(())
53 }
54 }
55
56 pub enum ConfigSource {
57 /// Absolute path to a config file
58 AbsPath(PathBuf),
59 /// Already parsed (from the CLI, env, Python resources, etc.)
60 Parsed(layer::ConfigLayer),
61 }
62
63 #[derive(Debug)]
64 pub struct ConfigValueParseError {
65 pub origin: ConfigOrigin,
66 pub line: Option<usize>,
67 pub section: Vec<u8>,
68 pub item: Vec<u8>,
69 pub value: Vec<u8>,
70 pub expected_type: &'static str,
71 }
72
73 impl fmt::Display for ConfigValueParseError {
74 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75 // TODO: add origin and line number information, here and in
76 // corresponding python code
77 write!(
78 f,
79 "config error: {}.{} is not a {} ('{}')",
80 String::from_utf8_lossy(&self.section),
81 String::from_utf8_lossy(&self.item),
82 self.expected_type,
83 String::from_utf8_lossy(&self.value)
84 )
85 }
86 }
87
88 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
89 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
90 // duplication with [_applyconfig] in [ui.py],
91 if !plain.is_plain() {
92 return false;
93 }
94 if section == b"alias" {
95 return plain.plainalias();
96 }
97 if section == b"revsetalias" {
98 return plain.plainrevsetalias();
99 }
100 if section == b"templatealias" {
101 return plain.plaintemplatealias();
102 }
103 if section == b"ui" {
104 let to_delete: &[&[u8]] = &[
105 b"debug",
106 b"fallbackencoding",
107 b"quiet",
108 b"slash",
109 b"logtemplate",
110 b"message-output",
111 b"statuscopies",
112 b"style",
113 b"traceback",
114 b"verbose",
115 ];
116 return to_delete.contains(&item);
117 }
118 let sections_to_delete: &[&[u8]] =
119 &[b"defaults", b"commands", b"command-templates"];
120 sections_to_delete.contains(&section)
121 }
122
123 impl Config {
124 /// The configuration to use when printing configuration-loading errors
125 pub fn empty() -> Self {
126 Self {
127 layers: Vec::new(),
128 plain: PlainInfo::empty(),
129 }
130 }
131
132 /// Load system and user configuration from various files.
133 ///
134 /// This is also affected by some environment variables.
135 pub fn load_non_repo() -> Result<Self, ConfigError> {
136 let mut config = Self::empty();
137 let opt_rc_path = env::var_os("HGRCPATH");
138 // HGRCPATH replaces system config
139 if opt_rc_path.is_none() {
140 config.add_system_config()?
141 }
142
143 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
144 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
145 config.add_for_environment_variable("PAGER", b"pager", b"pager");
146
147 // These are set by `run-tests.py --rhg` to enable fallback for the
148 // entire test suite. Alternatives would be setting configuration
149 // through `$HGRCPATH` but some tests override that, or changing the
150 // `hg` shell alias to include `--config` but that disrupts tests that
151 // print command lines and check expected output.
152 config.add_for_environment_variable(
153 "RHG_ON_UNSUPPORTED",
154 b"rhg",
155 b"on-unsupported",
156 );
157 config.add_for_environment_variable(
158 "RHG_FALLBACK_EXECUTABLE",
159 b"rhg",
160 b"fallback-executable",
161 );
162
163 // HGRCPATH replaces user config
164 if opt_rc_path.is_none() {
165 config.add_user_config()?
166 }
167 if let Some(rc_path) = &opt_rc_path {
168 for path in env::split_paths(rc_path) {
169 if !path.as_os_str().is_empty() {
170 if path.is_dir() {
171 config.add_trusted_dir(&path)?
172 } else {
173 config.add_trusted_file(&path)?
174 }
175 }
176 }
177 }
178 Ok(config)
179 }
180
181 pub fn load_cli_args(
182 &mut self,
183 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
184 color_arg: Option<Vec<u8>>,
185 ) -> Result<(), ConfigError> {
186 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
187 self.layers.push(layer)
188 }
189 if let Some(arg) = color_arg {
190 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
191 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
192 self.layers.push(layer)
193 }
194 Ok(())
195 }
196
197 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
198 if let Some(entries) = std::fs::read_dir(path)
199 .when_reading_file(path)
200 .io_not_found_as_none()?
201 {
202 let mut file_paths = entries
203 .map(|result| {
204 result.when_reading_file(path).map(|entry| entry.path())
205 })
206 .collect::<Result<Vec<_>, _>>()?;
207 file_paths.sort();
208 for file_path in &file_paths {
209 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
210 self.add_trusted_file(file_path)?
211 }
212 }
213 }
214 Ok(())
215 }
216
217 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
218 if let Some(data) = std::fs::read(path)
219 .when_reading_file(path)
220 .io_not_found_as_none()?
221 {
222 self.layers.extend(ConfigLayer::parse(path, &data)?)
223 }
224 Ok(())
225 }
226
227 fn add_for_environment_variable(
228 &mut self,
229 var: &str,
230 section: &[u8],
231 key: &[u8],
232 ) {
233 if let Some(value) = env::var_os(var) {
234 let origin = layer::ConfigOrigin::Environment(var.into());
235 let mut layer = ConfigLayer::new(origin);
236 layer.add(
237 section.to_owned(),
238 key.to_owned(),
239 get_bytes_from_os_str(value),
240 None,
241 );
242 self.layers.push(layer)
243 }
244 }
245
246 #[cfg(unix)] // TODO: other platforms
247 fn add_system_config(&mut self) -> Result<(), ConfigError> {
248 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
249 let etc = prefix.join("etc").join("mercurial");
250 self.add_trusted_file(&etc.join("hgrc"))?;
251 self.add_trusted_dir(&etc.join("hgrc.d"))
252 };
253 let root = Path::new("/");
254 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
255 // instead? TODO: can this be a relative path?
256 let hg = crate::utils::current_exe()?;
257 // TODO: this order (per-installation then per-system) matches
258 // `systemrcpath()` in `mercurial/scmposix.py`, but
259 // `mercurial/helptext/config.txt` suggests it should be reversed
260 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
261 if installation_prefix != root {
262 add_for_prefix(installation_prefix)?
263 }
264 }
265 add_for_prefix(root)?;
266 Ok(())
267 }
268
269 #[cfg(unix)] // TODO: other plateforms
270 fn add_user_config(&mut self) -> Result<(), ConfigError> {
271 let opt_home = home::home_dir();
272 if let Some(home) = &opt_home {
273 self.add_trusted_file(&home.join(".hgrc"))?
274 }
275 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
276 if !darwin {
277 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
278 .map(PathBuf::from)
279 .or_else(|| opt_home.map(|home| home.join(".config")))
280 {
281 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
282 }
283 }
284 Ok(())
285 }
286
287 /// Loads in order, which means that the precedence is the same
288 /// as the order of `sources`.
289 pub fn load_from_explicit_sources(
290 sources: Vec<ConfigSource>,
291 ) -> Result<Self, ConfigError> {
292 let mut layers = vec![];
293
294 for source in sources.into_iter() {
295 match source {
296 ConfigSource::Parsed(c) => layers.push(c),
297 ConfigSource::AbsPath(c) => {
298 // TODO check if it should be trusted
299 // mercurial/ui.py:427
300 let data = match std::fs::read(&c) {
301 Err(_) => continue, // same as the python code
302 Ok(data) => data,
303 };
304 layers.extend(ConfigLayer::parse(&c, &data)?)
305 }
306 }
307 }
308
309 Ok(Config {
310 layers,
311 plain: PlainInfo::empty(),
312 })
313 }
314
315 /// Loads the per-repository config into a new `Config` which is combined
316 /// with `self`.
317 pub(crate) fn combine_with_repo(
318 &self,
319 repo_config_files: &[PathBuf],
320 ) -> Result<Self, ConfigError> {
321 let (cli_layers, other_layers) = self
322 .layers
323 .iter()
324 .cloned()
325 .partition(ConfigLayer::is_from_command_line);
326
327 let mut repo_config = Self {
328 layers: other_layers,
329 plain: PlainInfo::empty(),
330 };
331 for path in repo_config_files {
332 // TODO: check if this file should be trusted:
333 // `mercurial/ui.py:427`
334 repo_config.add_trusted_file(path)?;
335 }
336 repo_config.layers.extend(cli_layers);
337 Ok(repo_config)
338 }
339
340 pub fn apply_plain(&mut self, plain: PlainInfo) {
341 self.plain = plain;
342 }
343
344 fn get_parse<'config, T: 'config>(
345 &'config self,
346 section: &[u8],
347 item: &[u8],
348 expected_type: &'static str,
349 parse: impl Fn(&'config [u8]) -> Option<T>,
350 ) -> Result<Option<T>, ConfigValueParseError> {
351 match self.get_inner(section, item) {
352 Some((layer, v)) => match parse(&v.bytes) {
353 Some(b) => Ok(Some(b)),
354 None => Err(ConfigValueParseError {
355 origin: layer.origin.to_owned(),
356 line: v.line,
357 value: v.bytes.to_owned(),
358 section: section.to_owned(),
359 item: item.to_owned(),
360 expected_type,
361 }),
362 },
363 None => Ok(None),
364 }
365 }
366
367 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
368 /// Otherwise, returns an `Ok(value)` if found, or `None`.
369 pub fn get_str(
370 &self,
371 section: &[u8],
372 item: &[u8],
373 ) -> Result<Option<&str>, ConfigValueParseError> {
374 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
375 str::from_utf8(value).ok()
376 })
377 }
378
379 /// Returns an `Err` if the first value found is not a valid unsigned
380 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
381 pub fn get_u32(
382 &self,
383 section: &[u8],
384 item: &[u8],
385 ) -> Result<Option<u32>, ConfigValueParseError> {
386 self.get_parse(section, item, "valid integer", |value| {
387 str::from_utf8(value).ok()?.parse().ok()
388 })
389 }
390
391 /// Returns an `Err` if the first value found is not a valid file size
392 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
393 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
394 pub fn get_byte_size(
395 &self,
396 section: &[u8],
397 item: &[u8],
398 ) -> Result<Option<u64>, ConfigValueParseError> {
399 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
400 }
401
402 /// Returns an `Err` if the first value found is not a valid boolean.
403 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
404 /// found, or `None`.
405 pub fn get_option(
406 &self,
407 section: &[u8],
408 item: &[u8],
409 ) -> Result<Option<bool>, ConfigValueParseError> {
410 self.get_parse(section, item, "boolean", values::parse_bool)
411 }
412
413 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
414 /// if the value is not found, an `Err` if it's not a valid boolean.
415 pub fn get_bool(
416 &self,
417 section: &[u8],
418 item: &[u8],
419 ) -> Result<bool, ConfigValueParseError> {
420 Ok(self.get_option(section, item)?.unwrap_or(false))
421 }
422
423 /// Returns `true` if the extension is enabled, `false` otherwise
424 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
425 let value = self.get(b"extensions", extension);
426 match value {
427 Some(c) => !c.starts_with(b"!"),
428 None => false,
429 }
430 }
431
432 /// If there is an `item` value in `section`, parse and return a list of
433 /// byte strings.
434 pub fn get_list(
435 &self,
436 section: &[u8],
437 item: &[u8],
438 ) -> Option<Vec<Vec<u8>>> {
439 self.get(section, item).map(values::parse_list)
440 }
441
442 /// Returns the raw value bytes of the first one found, or `None`.
443 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
444 self.get_inner(section, item)
445 .map(|(_, value)| value.bytes.as_ref())
446 }
447
448 /// Returns the raw value bytes of the first one found, or `None`.
449 pub fn get_with_origin(
450 &self,
451 section: &[u8],
452 item: &[u8],
453 ) -> Option<(&[u8], &ConfigOrigin)> {
454 self.get_inner(section, item)
455 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
456 }
457
458 /// Returns the layer and the value of the first one found, or `None`.
459 fn get_inner(
460 &self,
461 section: &[u8],
462 item: &[u8],
463 ) -> Option<(&ConfigLayer, &ConfigValue)> {
464 // Filter out the config items that are hidden by [PLAIN].
465 // This differs from python hg where we delete them from the config.
466 let should_ignore = should_ignore(&self.plain, section, item);
467 for layer in self.layers.iter().rev() {
468 if !layer.trusted {
469 continue;
470 }
471 //The [PLAIN] config should not affect the defaults.
472 //
473 // However, PLAIN should also affect the "tweaked" defaults (unless
474 // "tweakdefault" is part of "HGPLAINEXCEPT").
475 //
476 // In practice the tweak-default layer is only added when it is
477 // relevant, so we can safely always take it into
478 // account here.
479 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
480 {
481 continue;
482 }
483 if let Some(v) = layer.get(section, item) {
484 return Some((layer, v));
485 }
486 }
487 None
488 }
489
490 /// Return all keys defined for the given section
491 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
492 self.layers
493 .iter()
494 .flat_map(|layer| layer.iter_keys(section))
495 .collect()
496 }
497
498 /// Returns whether any key is defined in the given section
499 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
500 self.layers
501 .iter()
502 .any(|layer| layer.has_non_empty_section(section))
503 }
504
505 /// Yields (key, value) pairs for everything in the given section
506 pub fn iter_section<'a>(
507 &'a self,
508 section: &'a [u8],
509 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
510 // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
511 // available:
512 // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
513 struct Peekable<I: Iterator> {
514 iter: I,
515 /// Remember a peeked value, even if it was None.
516 peeked: Option<Option<I::Item>>,
517 }
518
519 impl<I: Iterator> Peekable<I> {
520 fn new(iter: I) -> Self {
521 Self { iter, peeked: None }
522 }
523
524 fn next(&mut self) {
525 self.peeked = None
526 }
527
528 fn peek_mut(&mut self) -> Option<&mut I::Item> {
529 let iter = &mut self.iter;
530 self.peeked.get_or_insert_with(|| iter.next()).as_mut()
531 }
532 }
533
534 // Deduplicate keys redefined in multiple layers
535 let mut keys_already_seen = HashSet::new();
536 let mut key_is_new =
537 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
538 keys_already_seen.insert(key)
539 };
540 // This is similar to `flat_map` + `filter_map`, except with a single
541 // closure that owns `key_is_new` (and therefore the
542 // `keys_already_seen` set):
543 let mut layer_iters = Peekable::new(
544 self.layers
545 .iter()
546 .rev()
547 .map(move |layer| layer.iter_section(section)),
548 );
549 std::iter::from_fn(move || loop {
550 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
551 return Some(pair);
552 } else {
553 layer_iters.next();
554 }
555 })
556 }
557
558 /// Get raw values bytes from all layers (even untrusted ones) in order
559 /// of precedence.
560 #[cfg(test)]
561 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
562 let mut res = vec![];
563 for layer in self.layers.iter().rev() {
564 if let Some(v) = layer.get(section, item) {
565 res.push(v.bytes.as_ref());
566 }
567 }
568 res
569 }
570
571 // a config layer that's introduced by ui.tweakdefaults
572 fn tweakdefaults_layer() -> ConfigLayer {
573 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
574
575 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
576 layer.add(
577 section[..].into(),
578 item[..].into(),
579 value[..].into(),
580 None,
581 );
582 };
583 // duplication of [tweakrc] from [ui.py]
584 add(b"ui", b"rollback", b"False");
585 add(b"ui", b"statuscopies", b"yes");
586 add(b"ui", b"interface", b"curses");
587 add(b"ui", b"relative-paths", b"yes");
588 add(b"commands", b"grep.all-files", b"True");
589 add(b"commands", b"update.check", b"noconflict");
590 add(b"commands", b"status.verbose", b"True");
591 add(b"commands", b"resolve.explicit-re-merge", b"True");
592 add(b"git", b"git", b"1");
593 add(b"git", b"showfunc", b"1");
594 add(b"git", b"word-diff", b"1");
595 layer
596 }
597
598 // introduce the tweaked defaults as implied by ui.tweakdefaults
599 pub fn tweakdefaults(&mut self) {
600 self.layers.insert(0, Config::tweakdefaults_layer());
601 }
602 }
603
604 #[cfg(test)]
605 mod tests {
606 use super::*;
607 use pretty_assertions::assert_eq;
608 use std::fs::File;
609 use std::io::Write;
610
611 #[test]
612 fn test_include_layer_ordering() {
613 let tmpdir = tempfile::tempdir().unwrap();
614 let tmpdir_path = tmpdir.path();
615 let mut included_file =
616 File::create(&tmpdir_path.join("included.rc")).unwrap();
617
618 included_file.write_all(b"[section]\nitem=value1").unwrap();
619 let base_config_path = tmpdir_path.join("base.rc");
620 let mut config_file = File::create(&base_config_path).unwrap();
621 let data =
622 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
623 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
624 config_file.write_all(data).unwrap();
625
626 let sources = vec![ConfigSource::AbsPath(base_config_path)];
627 let config = Config::load_from_explicit_sources(sources)
628 .expect("expected valid config");
629
630 let (_, value) = config.get_inner(b"section", b"item").unwrap();
631 assert_eq!(
632 value,
633 &ConfigValue {
634 bytes: b"value2".to_vec(),
635 line: Some(4)
636 }
637 );
638
639 let value = config.get(b"section", b"item").unwrap();
640 assert_eq!(value, b"value2",);
641 assert_eq!(
642 config.get_all(b"section", b"item"),
643 [b"value2", b"value1", b"value0"]
644 );
645
646 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
647 assert_eq!(
648 config.get_byte_size(b"section2", b"size").unwrap(),
649 Some(1024 + 512)
650 );
651 assert!(config.get_u32(b"section2", b"not-count").is_err());
652 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
653 }
654 }
This diff has been collapsed as it changes many lines, (643 lines changed) Show them Hide them
@@ -9,10 +9,649 b''
9 9
10 10 //! Mercurial config parsing and interfaces.
11 11
12 mod config;
13 12 mod layer;
14 13 mod plain_info;
15 14 mod values;
16 pub use config::{Config, ConfigSource, ConfigValueParseError};
17 15 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
18 16 pub use plain_info::PlainInfo;
17
18 use self::layer::ConfigLayer;
19 use self::layer::ConfigValue;
20 use crate::errors::{HgResultExt, IoResultExt};
21 use crate::utils::files::get_bytes_from_os_str;
22 use format_bytes::{write_bytes, DisplayBytes};
23 use std::collections::HashSet;
24 use std::env;
25 use std::fmt;
26 use std::path::{Path, PathBuf};
27 use std::str;
28
29 /// Holds the config values for the current repository
30 /// TODO update this docstring once we support more sources
31 #[derive(Clone)]
32 pub struct Config {
33 layers: Vec<layer::ConfigLayer>,
34 plain: PlainInfo,
35 }
36
37 impl DisplayBytes for Config {
38 fn display_bytes(
39 &self,
40 out: &mut dyn std::io::Write,
41 ) -> std::io::Result<()> {
42 for (index, layer) in self.layers.iter().rev().enumerate() {
43 write_bytes!(
44 out,
45 b"==== Layer {} (trusted: {}) ====\n{}",
46 index,
47 if layer.trusted {
48 &b"yes"[..]
49 } else {
50 &b"no"[..]
51 },
52 layer
53 )?;
54 }
55 Ok(())
56 }
57 }
58
59 pub enum ConfigSource {
60 /// Absolute path to a config file
61 AbsPath(PathBuf),
62 /// Already parsed (from the CLI, env, Python resources, etc.)
63 Parsed(layer::ConfigLayer),
64 }
65
66 #[derive(Debug)]
67 pub struct ConfigValueParseError {
68 pub origin: ConfigOrigin,
69 pub line: Option<usize>,
70 pub section: Vec<u8>,
71 pub item: Vec<u8>,
72 pub value: Vec<u8>,
73 pub expected_type: &'static str,
74 }
75
76 impl fmt::Display for ConfigValueParseError {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 // TODO: add origin and line number information, here and in
79 // corresponding python code
80 write!(
81 f,
82 "config error: {}.{} is not a {} ('{}')",
83 String::from_utf8_lossy(&self.section),
84 String::from_utf8_lossy(&self.item),
85 self.expected_type,
86 String::from_utf8_lossy(&self.value)
87 )
88 }
89 }
90
91 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
92 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
93 // duplication with [_applyconfig] in [ui.py],
94 if !plain.is_plain() {
95 return false;
96 }
97 if section == b"alias" {
98 return plain.plainalias();
99 }
100 if section == b"revsetalias" {
101 return plain.plainrevsetalias();
102 }
103 if section == b"templatealias" {
104 return plain.plaintemplatealias();
105 }
106 if section == b"ui" {
107 let to_delete: &[&[u8]] = &[
108 b"debug",
109 b"fallbackencoding",
110 b"quiet",
111 b"slash",
112 b"logtemplate",
113 b"message-output",
114 b"statuscopies",
115 b"style",
116 b"traceback",
117 b"verbose",
118 ];
119 return to_delete.contains(&item);
120 }
121 let sections_to_delete: &[&[u8]] =
122 &[b"defaults", b"commands", b"command-templates"];
123 sections_to_delete.contains(&section)
124 }
125
126 impl Config {
127 /// The configuration to use when printing configuration-loading errors
128 pub fn empty() -> Self {
129 Self {
130 layers: Vec::new(),
131 plain: PlainInfo::empty(),
132 }
133 }
134
135 /// Load system and user configuration from various files.
136 ///
137 /// This is also affected by some environment variables.
138 pub fn load_non_repo() -> Result<Self, ConfigError> {
139 let mut config = Self::empty();
140 let opt_rc_path = env::var_os("HGRCPATH");
141 // HGRCPATH replaces system config
142 if opt_rc_path.is_none() {
143 config.add_system_config()?
144 }
145
146 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
147 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
148 config.add_for_environment_variable("PAGER", b"pager", b"pager");
149
150 // These are set by `run-tests.py --rhg` to enable fallback for the
151 // entire test suite. Alternatives would be setting configuration
152 // through `$HGRCPATH` but some tests override that, or changing the
153 // `hg` shell alias to include `--config` but that disrupts tests that
154 // print command lines and check expected output.
155 config.add_for_environment_variable(
156 "RHG_ON_UNSUPPORTED",
157 b"rhg",
158 b"on-unsupported",
159 );
160 config.add_for_environment_variable(
161 "RHG_FALLBACK_EXECUTABLE",
162 b"rhg",
163 b"fallback-executable",
164 );
165
166 // HGRCPATH replaces user config
167 if opt_rc_path.is_none() {
168 config.add_user_config()?
169 }
170 if let Some(rc_path) = &opt_rc_path {
171 for path in env::split_paths(rc_path) {
172 if !path.as_os_str().is_empty() {
173 if path.is_dir() {
174 config.add_trusted_dir(&path)?
175 } else {
176 config.add_trusted_file(&path)?
177 }
178 }
179 }
180 }
181 Ok(config)
182 }
183
184 pub fn load_cli_args(
185 &mut self,
186 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
187 color_arg: Option<Vec<u8>>,
188 ) -> Result<(), ConfigError> {
189 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
190 self.layers.push(layer)
191 }
192 if let Some(arg) = color_arg {
193 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
194 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
195 self.layers.push(layer)
196 }
197 Ok(())
198 }
199
200 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
201 if let Some(entries) = std::fs::read_dir(path)
202 .when_reading_file(path)
203 .io_not_found_as_none()?
204 {
205 let mut file_paths = entries
206 .map(|result| {
207 result.when_reading_file(path).map(|entry| entry.path())
208 })
209 .collect::<Result<Vec<_>, _>>()?;
210 file_paths.sort();
211 for file_path in &file_paths {
212 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
213 self.add_trusted_file(file_path)?
214 }
215 }
216 }
217 Ok(())
218 }
219
220 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
221 if let Some(data) = std::fs::read(path)
222 .when_reading_file(path)
223 .io_not_found_as_none()?
224 {
225 self.layers.extend(ConfigLayer::parse(path, &data)?)
226 }
227 Ok(())
228 }
229
230 fn add_for_environment_variable(
231 &mut self,
232 var: &str,
233 section: &[u8],
234 key: &[u8],
235 ) {
236 if let Some(value) = env::var_os(var) {
237 let origin = layer::ConfigOrigin::Environment(var.into());
238 let mut layer = ConfigLayer::new(origin);
239 layer.add(
240 section.to_owned(),
241 key.to_owned(),
242 get_bytes_from_os_str(value),
243 None,
244 );
245 self.layers.push(layer)
246 }
247 }
248
249 #[cfg(unix)] // TODO: other platforms
250 fn add_system_config(&mut self) -> Result<(), ConfigError> {
251 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
252 let etc = prefix.join("etc").join("mercurial");
253 self.add_trusted_file(&etc.join("hgrc"))?;
254 self.add_trusted_dir(&etc.join("hgrc.d"))
255 };
256 let root = Path::new("/");
257 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
258 // instead? TODO: can this be a relative path?
259 let hg = crate::utils::current_exe()?;
260 // TODO: this order (per-installation then per-system) matches
261 // `systemrcpath()` in `mercurial/scmposix.py`, but
262 // `mercurial/helptext/config.txt` suggests it should be reversed
263 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
264 if installation_prefix != root {
265 add_for_prefix(installation_prefix)?
266 }
267 }
268 add_for_prefix(root)?;
269 Ok(())
270 }
271
272 #[cfg(unix)] // TODO: other plateforms
273 fn add_user_config(&mut self) -> Result<(), ConfigError> {
274 let opt_home = home::home_dir();
275 if let Some(home) = &opt_home {
276 self.add_trusted_file(&home.join(".hgrc"))?
277 }
278 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
279 if !darwin {
280 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
281 .map(PathBuf::from)
282 .or_else(|| opt_home.map(|home| home.join(".config")))
283 {
284 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
285 }
286 }
287 Ok(())
288 }
289
290 /// Loads in order, which means that the precedence is the same
291 /// as the order of `sources`.
292 pub fn load_from_explicit_sources(
293 sources: Vec<ConfigSource>,
294 ) -> Result<Self, ConfigError> {
295 let mut layers = vec![];
296
297 for source in sources.into_iter() {
298 match source {
299 ConfigSource::Parsed(c) => layers.push(c),
300 ConfigSource::AbsPath(c) => {
301 // TODO check if it should be trusted
302 // mercurial/ui.py:427
303 let data = match std::fs::read(&c) {
304 Err(_) => continue, // same as the python code
305 Ok(data) => data,
306 };
307 layers.extend(ConfigLayer::parse(&c, &data)?)
308 }
309 }
310 }
311
312 Ok(Config {
313 layers,
314 plain: PlainInfo::empty(),
315 })
316 }
317
318 /// Loads the per-repository config into a new `Config` which is combined
319 /// with `self`.
320 pub(crate) fn combine_with_repo(
321 &self,
322 repo_config_files: &[PathBuf],
323 ) -> Result<Self, ConfigError> {
324 let (cli_layers, other_layers) = self
325 .layers
326 .iter()
327 .cloned()
328 .partition(ConfigLayer::is_from_command_line);
329
330 let mut repo_config = Self {
331 layers: other_layers,
332 plain: PlainInfo::empty(),
333 };
334 for path in repo_config_files {
335 // TODO: check if this file should be trusted:
336 // `mercurial/ui.py:427`
337 repo_config.add_trusted_file(path)?;
338 }
339 repo_config.layers.extend(cli_layers);
340 Ok(repo_config)
341 }
342
343 pub fn apply_plain(&mut self, plain: PlainInfo) {
344 self.plain = plain;
345 }
346
347 fn get_parse<'config, T: 'config>(
348 &'config self,
349 section: &[u8],
350 item: &[u8],
351 expected_type: &'static str,
352 parse: impl Fn(&'config [u8]) -> Option<T>,
353 ) -> Result<Option<T>, ConfigValueParseError> {
354 match self.get_inner(section, item) {
355 Some((layer, v)) => match parse(&v.bytes) {
356 Some(b) => Ok(Some(b)),
357 None => Err(ConfigValueParseError {
358 origin: layer.origin.to_owned(),
359 line: v.line,
360 value: v.bytes.to_owned(),
361 section: section.to_owned(),
362 item: item.to_owned(),
363 expected_type,
364 }),
365 },
366 None => Ok(None),
367 }
368 }
369
370 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
371 /// Otherwise, returns an `Ok(value)` if found, or `None`.
372 pub fn get_str(
373 &self,
374 section: &[u8],
375 item: &[u8],
376 ) -> Result<Option<&str>, ConfigValueParseError> {
377 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
378 str::from_utf8(value).ok()
379 })
380 }
381
382 /// Returns an `Err` if the first value found is not a valid unsigned
383 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
384 pub fn get_u32(
385 &self,
386 section: &[u8],
387 item: &[u8],
388 ) -> Result<Option<u32>, ConfigValueParseError> {
389 self.get_parse(section, item, "valid integer", |value| {
390 str::from_utf8(value).ok()?.parse().ok()
391 })
392 }
393
394 /// Returns an `Err` if the first value found is not a valid file size
395 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
396 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
397 pub fn get_byte_size(
398 &self,
399 section: &[u8],
400 item: &[u8],
401 ) -> Result<Option<u64>, ConfigValueParseError> {
402 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
403 }
404
405 /// Returns an `Err` if the first value found is not a valid boolean.
406 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
407 /// found, or `None`.
408 pub fn get_option(
409 &self,
410 section: &[u8],
411 item: &[u8],
412 ) -> Result<Option<bool>, ConfigValueParseError> {
413 self.get_parse(section, item, "boolean", values::parse_bool)
414 }
415
416 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
417 /// if the value is not found, an `Err` if it's not a valid boolean.
418 pub fn get_bool(
419 &self,
420 section: &[u8],
421 item: &[u8],
422 ) -> Result<bool, ConfigValueParseError> {
423 Ok(self.get_option(section, item)?.unwrap_or(false))
424 }
425
426 /// Returns `true` if the extension is enabled, `false` otherwise
427 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
428 let value = self.get(b"extensions", extension);
429 match value {
430 Some(c) => !c.starts_with(b"!"),
431 None => false,
432 }
433 }
434
435 /// If there is an `item` value in `section`, parse and return a list of
436 /// byte strings.
437 pub fn get_list(
438 &self,
439 section: &[u8],
440 item: &[u8],
441 ) -> Option<Vec<Vec<u8>>> {
442 self.get(section, item).map(values::parse_list)
443 }
444
445 /// Returns the raw value bytes of the first one found, or `None`.
446 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
447 self.get_inner(section, item)
448 .map(|(_, value)| value.bytes.as_ref())
449 }
450
451 /// Returns the raw value bytes of the first one found, or `None`.
452 pub fn get_with_origin(
453 &self,
454 section: &[u8],
455 item: &[u8],
456 ) -> Option<(&[u8], &ConfigOrigin)> {
457 self.get_inner(section, item)
458 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
459 }
460
461 /// Returns the layer and the value of the first one found, or `None`.
462 fn get_inner(
463 &self,
464 section: &[u8],
465 item: &[u8],
466 ) -> Option<(&ConfigLayer, &ConfigValue)> {
467 // Filter out the config items that are hidden by [PLAIN].
468 // This differs from python hg where we delete them from the config.
469 let should_ignore = should_ignore(&self.plain, section, item);
470 for layer in self.layers.iter().rev() {
471 if !layer.trusted {
472 continue;
473 }
474 //The [PLAIN] config should not affect the defaults.
475 //
476 // However, PLAIN should also affect the "tweaked" defaults (unless
477 // "tweakdefault" is part of "HGPLAINEXCEPT").
478 //
479 // In practice the tweak-default layer is only added when it is
480 // relevant, so we can safely always take it into
481 // account here.
482 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
483 {
484 continue;
485 }
486 if let Some(v) = layer.get(section, item) {
487 return Some((layer, v));
488 }
489 }
490 None
491 }
492
493 /// Return all keys defined for the given section
494 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
495 self.layers
496 .iter()
497 .flat_map(|layer| layer.iter_keys(section))
498 .collect()
499 }
500
501 /// Returns whether any key is defined in the given section
502 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
503 self.layers
504 .iter()
505 .any(|layer| layer.has_non_empty_section(section))
506 }
507
508 /// Yields (key, value) pairs for everything in the given section
509 pub fn iter_section<'a>(
510 &'a self,
511 section: &'a [u8],
512 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
513 // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
514 // available:
515 // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
516 struct Peekable<I: Iterator> {
517 iter: I,
518 /// Remember a peeked value, even if it was None.
519 peeked: Option<Option<I::Item>>,
520 }
521
522 impl<I: Iterator> Peekable<I> {
523 fn new(iter: I) -> Self {
524 Self { iter, peeked: None }
525 }
526
527 fn next(&mut self) {
528 self.peeked = None
529 }
530
531 fn peek_mut(&mut self) -> Option<&mut I::Item> {
532 let iter = &mut self.iter;
533 self.peeked.get_or_insert_with(|| iter.next()).as_mut()
534 }
535 }
536
537 // Deduplicate keys redefined in multiple layers
538 let mut keys_already_seen = HashSet::new();
539 let mut key_is_new =
540 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
541 keys_already_seen.insert(key)
542 };
543 // This is similar to `flat_map` + `filter_map`, except with a single
544 // closure that owns `key_is_new` (and therefore the
545 // `keys_already_seen` set):
546 let mut layer_iters = Peekable::new(
547 self.layers
548 .iter()
549 .rev()
550 .map(move |layer| layer.iter_section(section)),
551 );
552 std::iter::from_fn(move || loop {
553 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
554 return Some(pair);
555 } else {
556 layer_iters.next();
557 }
558 })
559 }
560
561 /// Get raw values bytes from all layers (even untrusted ones) in order
562 /// of precedence.
563 #[cfg(test)]
564 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
565 let mut res = vec![];
566 for layer in self.layers.iter().rev() {
567 if let Some(v) = layer.get(section, item) {
568 res.push(v.bytes.as_ref());
569 }
570 }
571 res
572 }
573
574 // a config layer that's introduced by ui.tweakdefaults
575 fn tweakdefaults_layer() -> ConfigLayer {
576 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
577
578 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
579 layer.add(
580 section[..].into(),
581 item[..].into(),
582 value[..].into(),
583 None,
584 );
585 };
586 // duplication of [tweakrc] from [ui.py]
587 add(b"ui", b"rollback", b"False");
588 add(b"ui", b"statuscopies", b"yes");
589 add(b"ui", b"interface", b"curses");
590 add(b"ui", b"relative-paths", b"yes");
591 add(b"commands", b"grep.all-files", b"True");
592 add(b"commands", b"update.check", b"noconflict");
593 add(b"commands", b"status.verbose", b"True");
594 add(b"commands", b"resolve.explicit-re-merge", b"True");
595 add(b"git", b"git", b"1");
596 add(b"git", b"showfunc", b"1");
597 add(b"git", b"word-diff", b"1");
598 layer
599 }
600
601 // introduce the tweaked defaults as implied by ui.tweakdefaults
602 pub fn tweakdefaults(&mut self) {
603 self.layers.insert(0, Config::tweakdefaults_layer());
604 }
605 }
606
607 #[cfg(test)]
608 mod tests {
609 use super::*;
610 use pretty_assertions::assert_eq;
611 use std::fs::File;
612 use std::io::Write;
613
614 #[test]
615 fn test_include_layer_ordering() {
616 let tmpdir = tempfile::tempdir().unwrap();
617 let tmpdir_path = tmpdir.path();
618 let mut included_file =
619 File::create(&tmpdir_path.join("included.rc")).unwrap();
620
621 included_file.write_all(b"[section]\nitem=value1").unwrap();
622 let base_config_path = tmpdir_path.join("base.rc");
623 let mut config_file = File::create(&base_config_path).unwrap();
624 let data =
625 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
626 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
627 config_file.write_all(data).unwrap();
628
629 let sources = vec![ConfigSource::AbsPath(base_config_path)];
630 let config = Config::load_from_explicit_sources(sources)
631 .expect("expected valid config");
632
633 let (_, value) = config.get_inner(b"section", b"item").unwrap();
634 assert_eq!(
635 value,
636 &ConfigValue {
637 bytes: b"value2".to_vec(),
638 line: Some(4)
639 }
640 );
641
642 let value = config.get(b"section", b"item").unwrap();
643 assert_eq!(value, b"value2",);
644 assert_eq!(
645 config.get_all(b"section", b"item"),
646 [b"value2", b"value1", b"value0"]
647 );
648
649 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
650 assert_eq!(
651 config.get_byte_size(b"section2", b"size").unwrap(),
652 Some(1024 + 512)
653 );
654 assert!(config.get_u32(b"section2", b"not-count").is_err());
655 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
656 }
657 }
General Comments 0
You need to be logged in to leave comments. Login now