##// END OF EJS Templates
rust-config: fix incorrect coercion of null values to false...
Raphaël Gomès -
r51876:8343947a default
parent child Browse files
Show More
@@ -1,725 +1,725 b''
1 1 //! Code for parsing default Mercurial config items.
2 2 use itertools::Itertools;
3 3 use serde::Deserialize;
4 4
5 5 use crate::{errors::HgError, exit_codes, FastHashMap};
6 6
7 7 /// Corresponds to the structure of `mercurial/configitems.toml`.
8 8 #[derive(Debug, Deserialize)]
9 9 pub struct ConfigItems {
10 10 items: Vec<DefaultConfigItem>,
11 11 templates: FastHashMap<String, Vec<TemplateItem>>,
12 12 #[serde(rename = "template-applications")]
13 13 template_applications: Vec<TemplateApplication>,
14 14 }
15 15
16 16 /// Corresponds to a config item declaration in `mercurial/configitems.toml`.
17 17 #[derive(Clone, Debug, PartialEq, Deserialize)]
18 18 #[serde(try_from = "RawDefaultConfigItem")]
19 19 pub struct DefaultConfigItem {
20 20 /// Section of the config the item is in (e.g. `[merge-tools]`)
21 21 section: String,
22 22 /// Name of the item (e.g. `meld.gui`)
23 23 name: String,
24 24 /// Default value (can be dynamic, see [`DefaultConfigItemType`])
25 25 default: Option<DefaultConfigItemType>,
26 26 /// If the config option is generic (e.g. `merge-tools.*`), defines
27 27 /// the priority of this item relative to other generic items.
28 28 /// If we're looking for <pattern>, then all generic items within the same
29 29 /// section will be sorted by order of priority, and the first regex match
30 30 /// against `name` is returned.
31 31 #[serde(default)]
32 32 priority: Option<isize>,
33 33 /// Aliases, if any. Each alias is a tuple of `(section, name)` for each
34 34 /// option that is aliased to this one.
35 35 #[serde(default)]
36 36 alias: Vec<(String, String)>,
37 37 /// Whether the config item is marked as experimental
38 38 #[serde(default)]
39 39 experimental: bool,
40 40 /// The (possibly empty) docstring for the item
41 41 #[serde(default)]
42 42 documentation: String,
43 43 /// Whether the item is part of an in-core extension. This allows us to
44 44 /// hide them if the extension is not enabled, to preserve legacy
45 45 /// behavior.
46 46 #[serde(default)]
47 47 in_core_extension: Option<String>,
48 48 }
49 49
50 50 /// Corresponds to the raw (i.e. on disk) structure of config items. Used as
51 51 /// an intermediate step in deserialization.
52 52 #[derive(Clone, Debug, Deserialize)]
53 53 struct RawDefaultConfigItem {
54 54 section: String,
55 55 name: String,
56 56 default: Option<toml::Value>,
57 57 #[serde(rename = "default-type")]
58 58 default_type: Option<String>,
59 59 #[serde(default)]
60 60 priority: isize,
61 61 #[serde(default)]
62 62 generic: bool,
63 63 #[serde(default)]
64 64 alias: Vec<(String, String)>,
65 65 #[serde(default)]
66 66 experimental: bool,
67 67 #[serde(default)]
68 68 documentation: String,
69 69 #[serde(default)]
70 70 in_core_extension: Option<String>,
71 71 }
72 72
73 73 impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem {
74 74 type Error = HgError;
75 75
76 76 fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> {
77 77 Ok(Self {
78 78 section: value.section,
79 79 name: value.name,
80 80 default: raw_default_to_concrete(
81 81 value.default_type,
82 82 value.default,
83 83 )?,
84 84 priority: if value.generic {
85 85 Some(value.priority)
86 86 } else {
87 87 None
88 88 },
89 89 alias: value.alias,
90 90 experimental: value.experimental,
91 91 documentation: value.documentation,
92 92 in_core_extension: value.in_core_extension,
93 93 })
94 94 }
95 95 }
96 96
97 97 impl DefaultConfigItem {
98 98 fn is_generic(&self) -> bool {
99 99 self.priority.is_some()
100 100 }
101 101
102 102 pub fn in_core_extension(&self) -> Option<&str> {
103 103 self.in_core_extension.as_deref()
104 104 }
105 105
106 106 pub fn section(&self) -> &str {
107 107 self.section.as_ref()
108 108 }
109 109 }
110 110
111 111 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> {
112 112 type Error = HgError;
113 113
114 114 fn try_from(
115 115 value: &'a DefaultConfigItem,
116 116 ) -> Result<Option<&'a str>, Self::Error> {
117 117 match &value.default {
118 118 Some(default) => {
119 119 let err = HgError::abort(
120 120 format!(
121 121 "programming error: wrong query on config item '{}.{}'",
122 122 value.section,
123 123 value.name
124 124 ),
125 125 exit_codes::ABORT,
126 126 Some(format!(
127 127 "asked for '&str', type of default is '{}'",
128 128 default.type_str()
129 129 )),
130 130 );
131 131 match default {
132 132 DefaultConfigItemType::Primitive(toml::Value::String(
133 133 s,
134 134 )) => Ok(Some(s)),
135 135 _ => Err(err),
136 136 }
137 137 }
138 138 None => Ok(None),
139 139 }
140 140 }
141 141 }
142 142
143 143 impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a [u8]> {
144 144 type Error = HgError;
145 145
146 146 fn try_from(
147 147 value: &'a DefaultConfigItem,
148 148 ) -> Result<Option<&'a [u8]>, Self::Error> {
149 149 match &value.default {
150 150 Some(default) => {
151 151 let err = HgError::abort(
152 152 format!(
153 153 "programming error: wrong query on config item '{}.{}'",
154 154 value.section,
155 155 value.name
156 156 ),
157 157 exit_codes::ABORT,
158 158 Some(format!(
159 159 "asked for bytes, type of default is '{}', \
160 160 which cannot be interpreted as bytes",
161 161 default.type_str()
162 162 )),
163 163 );
164 164 match default {
165 165 DefaultConfigItemType::Primitive(p) => {
166 166 Ok(p.as_str().map(str::as_bytes))
167 167 }
168 168 _ => Err(err),
169 169 }
170 170 }
171 171 None => Ok(None),
172 172 }
173 173 }
174 174 }
175 175
176 176 impl TryFrom<&DefaultConfigItem> for Option<bool> {
177 177 type Error = HgError;
178 178
179 179 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
180 180 match &value.default {
181 181 Some(default) => {
182 182 let err = HgError::abort(
183 183 format!(
184 184 "programming error: wrong query on config item '{}.{}'",
185 185 value.section,
186 186 value.name
187 187 ),
188 188 exit_codes::ABORT,
189 189 Some(format!(
190 190 "asked for 'bool', type of default is '{}'",
191 191 default.type_str()
192 192 )),
193 193 );
194 194 match default {
195 195 DefaultConfigItemType::Primitive(
196 196 toml::Value::Boolean(b),
197 197 ) => Ok(Some(*b)),
198 198 _ => Err(err),
199 199 }
200 200 }
201 None => Ok(Some(false)),
201 None => Ok(None),
202 202 }
203 203 }
204 204 }
205 205
206 206 impl TryFrom<&DefaultConfigItem> for Option<u32> {
207 207 type Error = HgError;
208 208
209 209 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
210 210 match &value.default {
211 211 Some(default) => {
212 212 let err = HgError::abort(
213 213 format!(
214 214 "programming error: wrong query on config item '{}.{}'",
215 215 value.section,
216 216 value.name
217 217 ),
218 218 exit_codes::ABORT,
219 219 Some(format!(
220 220 "asked for 'u32', type of default is '{}'",
221 221 default.type_str()
222 222 )),
223 223 );
224 224 match default {
225 225 DefaultConfigItemType::Primitive(
226 226 toml::Value::Integer(b),
227 227 ) => {
228 228 Ok(Some((*b).try_into().expect("TOML integer to u32")))
229 229 }
230 230 _ => Err(err),
231 231 }
232 232 }
233 233 None => Ok(None),
234 234 }
235 235 }
236 236 }
237 237
238 238 impl TryFrom<&DefaultConfigItem> for Option<u64> {
239 239 type Error = HgError;
240 240
241 241 fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
242 242 match &value.default {
243 243 Some(default) => {
244 244 let err = HgError::abort(
245 245 format!(
246 246 "programming error: wrong query on config item '{}.{}'",
247 247 value.section,
248 248 value.name
249 249 ),
250 250 exit_codes::ABORT,
251 251 Some(format!(
252 252 "asked for 'u64', type of default is '{}'",
253 253 default.type_str()
254 254 )),
255 255 );
256 256 match default {
257 257 DefaultConfigItemType::Primitive(
258 258 toml::Value::Integer(b),
259 259 ) => {
260 260 Ok(Some((*b).try_into().expect("TOML integer to u64")))
261 261 }
262 262 _ => Err(err),
263 263 }
264 264 }
265 265 None => Ok(None),
266 266 }
267 267 }
268 268 }
269 269
270 270 /// Allows abstracting over more complex default values than just primitives.
271 271 /// The former `configitems.py` contained some dynamic code that is encoded
272 272 /// in this enum.
273 273 #[derive(Debug, PartialEq, Clone, Deserialize)]
274 274 pub enum DefaultConfigItemType {
275 275 /// Some primitive type (string, integer, boolean)
276 276 Primitive(toml::Value),
277 277 /// A dynamic value that will be given by the code at runtime
278 278 Dynamic,
279 279 /// An lazily-returned array (possibly only relevant in the Python impl)
280 280 /// Example: `lambda: [b"zstd", b"zlib"]`
281 281 Lambda(Vec<String>),
282 282 /// For now, a special case for `web.encoding` that points to the
283 283 /// `encoding.encoding` module in the Python impl so that local encoding
284 284 /// is correctly resolved at runtime
285 285 LazyModule(String),
286 286 ListType,
287 287 }
288 288
289 289 impl DefaultConfigItemType {
290 290 pub fn type_str(&self) -> &str {
291 291 match self {
292 292 DefaultConfigItemType::Primitive(primitive) => {
293 293 primitive.type_str()
294 294 }
295 295 DefaultConfigItemType::Dynamic => "dynamic",
296 296 DefaultConfigItemType::Lambda(_) => "lambda",
297 297 DefaultConfigItemType::LazyModule(_) => "lazy_module",
298 298 DefaultConfigItemType::ListType => "list_type",
299 299 }
300 300 }
301 301 }
302 302
303 303 /// Most of the fields are shared with [`DefaultConfigItem`].
304 304 #[derive(Debug, Clone, Deserialize)]
305 305 #[serde(try_from = "RawTemplateItem")]
306 306 struct TemplateItem {
307 307 suffix: String,
308 308 default: Option<DefaultConfigItemType>,
309 309 priority: Option<isize>,
310 310 #[serde(default)]
311 311 alias: Vec<(String, String)>,
312 312 #[serde(default)]
313 313 experimental: bool,
314 314 #[serde(default)]
315 315 documentation: String,
316 316 }
317 317
318 318 /// Corresponds to the raw (i.e. on disk) representation of a template item.
319 319 /// Used as an intermediate step in deserialization.
320 320 #[derive(Clone, Debug, Deserialize)]
321 321 struct RawTemplateItem {
322 322 suffix: String,
323 323 default: Option<toml::Value>,
324 324 #[serde(rename = "default-type")]
325 325 default_type: Option<String>,
326 326 #[serde(default)]
327 327 priority: isize,
328 328 #[serde(default)]
329 329 generic: bool,
330 330 #[serde(default)]
331 331 alias: Vec<(String, String)>,
332 332 #[serde(default)]
333 333 experimental: bool,
334 334 #[serde(default)]
335 335 documentation: String,
336 336 }
337 337
338 338 impl TemplateItem {
339 339 fn into_default_item(
340 340 self,
341 341 application: TemplateApplication,
342 342 ) -> DefaultConfigItem {
343 343 DefaultConfigItem {
344 344 section: application.section,
345 345 name: application
346 346 .prefix
347 347 .map(|prefix| format!("{}.{}", prefix, self.suffix))
348 348 .unwrap_or(self.suffix),
349 349 default: self.default,
350 350 priority: self.priority,
351 351 alias: self.alias,
352 352 experimental: self.experimental,
353 353 documentation: self.documentation,
354 354 in_core_extension: None,
355 355 }
356 356 }
357 357 }
358 358
359 359 impl TryFrom<RawTemplateItem> for TemplateItem {
360 360 type Error = HgError;
361 361
362 362 fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> {
363 363 Ok(Self {
364 364 suffix: value.suffix,
365 365 default: raw_default_to_concrete(
366 366 value.default_type,
367 367 value.default,
368 368 )?,
369 369 priority: if value.generic {
370 370 Some(value.priority)
371 371 } else {
372 372 None
373 373 },
374 374 alias: value.alias,
375 375 experimental: value.experimental,
376 376 documentation: value.documentation,
377 377 })
378 378 }
379 379 }
380 380
381 381 /// Transforms the on-disk string-based representation of complex default types
382 382 /// to the concrete [`DefaultconfigItemType`].
383 383 fn raw_default_to_concrete(
384 384 default_type: Option<String>,
385 385 default: Option<toml::Value>,
386 386 ) -> Result<Option<DefaultConfigItemType>, HgError> {
387 387 Ok(match default_type.as_deref() {
388 388 None => default.as_ref().map(|default| {
389 389 DefaultConfigItemType::Primitive(default.to_owned())
390 390 }),
391 391 Some("dynamic") => Some(DefaultConfigItemType::Dynamic),
392 392 Some("list_type") => Some(DefaultConfigItemType::ListType),
393 393 Some("lambda") => match &default {
394 394 Some(default) => Some(DefaultConfigItemType::Lambda(
395 395 default.to_owned().try_into().map_err(|e| {
396 396 HgError::abort(
397 397 e.to_string(),
398 398 exit_codes::ABORT,
399 399 Some("Check 'mercurial/configitems.toml'".into()),
400 400 )
401 401 })?,
402 402 )),
403 403 None => {
404 404 return Err(HgError::abort(
405 405 "lambda defined with no return value".to_string(),
406 406 exit_codes::ABORT,
407 407 Some("Check 'mercurial/configitems.toml'".into()),
408 408 ))
409 409 }
410 410 },
411 411 Some("lazy_module") => match &default {
412 412 Some(default) => {
413 413 Some(DefaultConfigItemType::LazyModule(match default {
414 414 toml::Value::String(module) => module.to_owned(),
415 415 _ => {
416 416 return Err(HgError::abort(
417 417 "lazy_module module name should be a string"
418 418 .to_string(),
419 419 exit_codes::ABORT,
420 420 Some("Check 'mercurial/configitems.toml'".into()),
421 421 ))
422 422 }
423 423 }))
424 424 }
425 425 None => {
426 426 return Err(HgError::abort(
427 427 "lazy_module should have a default value".to_string(),
428 428 exit_codes::ABORT,
429 429 Some("Check 'mercurial/configitems.toml'".into()),
430 430 ))
431 431 }
432 432 },
433 433 Some(invalid) => {
434 434 return Err(HgError::abort(
435 435 format!("invalid default_type '{}'", invalid),
436 436 exit_codes::ABORT,
437 437 Some("Check 'mercurial/configitems.toml'".into()),
438 438 ))
439 439 }
440 440 })
441 441 }
442 442
443 443 #[derive(Debug, Clone, Deserialize)]
444 444 struct TemplateApplication {
445 445 template: String,
446 446 section: String,
447 447 #[serde(default)]
448 448 prefix: Option<String>,
449 449 }
450 450
451 451 /// Represents the (dynamic) set of default core Mercurial config items from
452 452 /// `mercurial/configitems.toml`.
453 453 #[derive(Clone, Debug, Default)]
454 454 pub struct DefaultConfig {
455 455 /// Mapping of section -> (mapping of name -> item)
456 456 items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>,
457 457 }
458 458
459 459 impl DefaultConfig {
460 460 pub fn empty() -> DefaultConfig {
461 461 Self {
462 462 items: Default::default(),
463 463 }
464 464 }
465 465
466 466 /// Returns `Self`, given the contents of `mercurial/configitems.toml`
467 467 #[logging_timer::time("trace")]
468 468 pub fn from_contents(contents: &str) -> Result<Self, HgError> {
469 469 let mut from_file: ConfigItems =
470 470 toml::from_str(contents).map_err(|e| {
471 471 HgError::abort(
472 472 e.to_string(),
473 473 exit_codes::ABORT,
474 474 Some("Check 'mercurial/configitems.toml'".into()),
475 475 )
476 476 })?;
477 477
478 478 let mut flat_items = from_file.items;
479 479
480 480 for application in from_file.template_applications.drain(..) {
481 481 match from_file.templates.get(&application.template) {
482 482 None => return Err(
483 483 HgError::abort(
484 484 format!(
485 485 "template application refers to undefined template '{}'",
486 486 application.template
487 487 ),
488 488 exit_codes::ABORT,
489 489 Some("Check 'mercurial/configitems.toml'".into())
490 490 )
491 491 ),
492 492 Some(template_items) => {
493 493 for template_item in template_items {
494 494 flat_items.push(
495 495 template_item
496 496 .clone()
497 497 .into_default_item(application.clone()),
498 498 )
499 499 }
500 500 }
501 501 };
502 502 }
503 503
504 504 let items = flat_items.into_iter().fold(
505 505 FastHashMap::default(),
506 506 |mut acc, item| {
507 507 acc.entry(item.section.to_owned())
508 508 .or_insert_with(|| {
509 509 let mut section = FastHashMap::default();
510 510 section.insert(item.name.to_owned(), item.to_owned());
511 511 section
512 512 })
513 513 .insert(item.name.to_owned(), item);
514 514 acc
515 515 },
516 516 );
517 517
518 518 Ok(Self { items })
519 519 }
520 520
521 521 /// Return the default config item that matches `section` and `item`.
522 522 pub fn get(
523 523 &self,
524 524 section: &[u8],
525 525 item: &[u8],
526 526 ) -> Option<&DefaultConfigItem> {
527 527 // Core items must be valid UTF-8
528 528 let section = String::from_utf8_lossy(section);
529 529 let section_map = self.items.get(section.as_ref())?;
530 530 let item_name_lossy = String::from_utf8_lossy(item);
531 531 match section_map.get(item_name_lossy.as_ref()) {
532 532 Some(item) => Some(item),
533 533 None => {
534 534 for generic_item in section_map
535 535 .values()
536 536 .filter(|item| item.is_generic())
537 537 .sorted_by_key(|item| match item.priority {
538 538 Some(priority) => (priority, &item.name),
539 539 _ => unreachable!(),
540 540 })
541 541 {
542 542 if regex::bytes::Regex::new(&generic_item.name)
543 543 .expect("invalid regex in configitems")
544 544 .is_match(item)
545 545 {
546 546 return Some(generic_item);
547 547 }
548 548 }
549 549 None
550 550 }
551 551 }
552 552 }
553 553 }
554 554
555 555 #[cfg(test)]
556 556 mod tests {
557 557 use crate::config::config_items::{
558 558 DefaultConfigItem, DefaultConfigItemType,
559 559 };
560 560
561 561 use super::DefaultConfig;
562 562
563 563 #[test]
564 564 fn test_config_read() {
565 565 let contents = r#"
566 566 [[items]]
567 567 section = "alias"
568 568 name = "abcd.*"
569 569 default = 3
570 570 generic = true
571 571 priority = -1
572 572
573 573 [[items]]
574 574 section = "alias"
575 575 name = ".*"
576 576 default-type = "dynamic"
577 577 generic = true
578 578
579 579 [[items]]
580 580 section = "cmdserver"
581 581 name = "track-log"
582 582 default-type = "lambda"
583 583 default = [ "chgserver", "cmdserver", "repocache",]
584 584
585 585 [[items]]
586 586 section = "chgserver"
587 587 name = "idletimeout"
588 588 default = 3600
589 589
590 590 [[items]]
591 591 section = "cmdserver"
592 592 name = "message-encodings"
593 593 default-type = "list_type"
594 594
595 595 [[items]]
596 596 section = "web"
597 597 name = "encoding"
598 598 default-type = "lazy_module"
599 599 default = "encoding.encoding"
600 600
601 601 [[items]]
602 602 section = "command-templates"
603 603 name = "graphnode"
604 604 alias = [["ui", "graphnodetemplate"]]
605 605 documentation = """This is a docstring.
606 606 This is another line \
607 607 but this is not."""
608 608
609 609 [[items]]
610 610 section = "censor"
611 611 name = "policy"
612 612 default = "abort"
613 613 experimental = true
614 614
615 615 [[template-applications]]
616 616 template = "diff-options"
617 617 section = "commands"
618 618 prefix = "revert.interactive"
619 619
620 620 [[template-applications]]
621 621 template = "diff-options"
622 622 section = "diff"
623 623
624 624 [templates]
625 625 [[templates.diff-options]]
626 626 suffix = "nodates"
627 627 default = false
628 628
629 629 [[templates.diff-options]]
630 630 suffix = "showfunc"
631 631 default = false
632 632
633 633 [[templates.diff-options]]
634 634 suffix = "unified"
635 635 "#;
636 636 let res = DefaultConfig::from_contents(contents);
637 637 let config = match res {
638 638 Ok(config) => config,
639 639 Err(e) => panic!("{}", e),
640 640 };
641 641 let expected = DefaultConfigItem {
642 642 section: "censor".into(),
643 643 name: "policy".into(),
644 644 default: Some(DefaultConfigItemType::Primitive("abort".into())),
645 645 priority: None,
646 646 alias: vec![],
647 647 experimental: true,
648 648 documentation: "".into(),
649 649 in_core_extension: None,
650 650 };
651 651 assert_eq!(config.get(b"censor", b"policy"), Some(&expected));
652 652
653 653 // Test generic priority. The `.*` pattern is wider than `abcd.*`, but
654 654 // `abcd.*` has priority, so it should match first.
655 655 let expected = DefaultConfigItem {
656 656 section: "alias".into(),
657 657 name: "abcd.*".into(),
658 658 default: Some(DefaultConfigItemType::Primitive(3.into())),
659 659 priority: Some(-1),
660 660 alias: vec![],
661 661 experimental: false,
662 662 documentation: "".into(),
663 663 in_core_extension: None,
664 664 };
665 665 assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected));
666 666
667 667 //... but if it doesn't, we should fallback to `.*`
668 668 let expected = DefaultConfigItem {
669 669 section: "alias".into(),
670 670 name: ".*".into(),
671 671 default: Some(DefaultConfigItemType::Dynamic),
672 672 priority: Some(0),
673 673 alias: vec![],
674 674 experimental: false,
675 675 documentation: "".into(),
676 676 in_core_extension: None,
677 677 };
678 678 assert_eq!(config.get(b"alias", b"something"), Some(&expected));
679 679
680 680 let expected = DefaultConfigItem {
681 681 section: "chgserver".into(),
682 682 name: "idletimeout".into(),
683 683 default: Some(DefaultConfigItemType::Primitive(3600.into())),
684 684 priority: None,
685 685 alias: vec![],
686 686 experimental: false,
687 687 documentation: "".into(),
688 688 in_core_extension: None,
689 689 };
690 690 assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected));
691 691
692 692 let expected = DefaultConfigItem {
693 693 section: "cmdserver".into(),
694 694 name: "track-log".into(),
695 695 default: Some(DefaultConfigItemType::Lambda(vec![
696 696 "chgserver".into(),
697 697 "cmdserver".into(),
698 698 "repocache".into(),
699 699 ])),
700 700 priority: None,
701 701 alias: vec![],
702 702 experimental: false,
703 703 documentation: "".into(),
704 704 in_core_extension: None,
705 705 };
706 706 assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected));
707 707
708 708 let expected = DefaultConfigItem {
709 709 section: "command-templates".into(),
710 710 name: "graphnode".into(),
711 711 default: None,
712 712 priority: None,
713 713 alias: vec![("ui".into(), "graphnodetemplate".into())],
714 714 experimental: false,
715 715 documentation:
716 716 "This is a docstring.\nThis is another line but this is not."
717 717 .into(),
718 718 in_core_extension: None,
719 719 };
720 720 assert_eq!(
721 721 config.get(b"command-templates", b"graphnode"),
722 722 Some(&expected)
723 723 );
724 724 }
725 725 }
@@ -1,811 +1,810 b''
1 1 // config.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 //! Mercurial config parsing and interfaces.
11 11
12 12 pub mod config_items;
13 13 mod layer;
14 14 mod plain_info;
15 15 mod values;
16 16 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
17 17 use lazy_static::lazy_static;
18 18 pub use plain_info::PlainInfo;
19 19
20 20 use self::config_items::DefaultConfig;
21 21 use self::config_items::DefaultConfigItem;
22 22 use self::layer::ConfigLayer;
23 23 use self::layer::ConfigValue;
24 24 use crate::errors::HgError;
25 25 use crate::errors::{HgResultExt, IoResultExt};
26 26 use crate::utils::files::get_bytes_from_os_str;
27 27 use format_bytes::{write_bytes, DisplayBytes};
28 28 use std::collections::HashSet;
29 29 use std::env;
30 30 use std::fmt;
31 31 use std::path::{Path, PathBuf};
32 32 use std::str;
33 33
34 34 lazy_static! {
35 35 static ref DEFAULT_CONFIG: Result<DefaultConfig, HgError> = {
36 36 DefaultConfig::from_contents(include_str!(
37 37 "../../../../mercurial/configitems.toml"
38 38 ))
39 39 };
40 40 }
41 41
42 42 /// Holds the config values for the current repository
43 43 /// TODO update this docstring once we support more sources
44 44 #[derive(Clone)]
45 45 pub struct Config {
46 46 layers: Vec<layer::ConfigLayer>,
47 47 plain: PlainInfo,
48 48 }
49 49
50 50 impl DisplayBytes for Config {
51 51 fn display_bytes(
52 52 &self,
53 53 out: &mut dyn std::io::Write,
54 54 ) -> std::io::Result<()> {
55 55 for (index, layer) in self.layers.iter().rev().enumerate() {
56 56 write_bytes!(
57 57 out,
58 58 b"==== Layer {} (trusted: {}) ====\n{}",
59 59 index,
60 60 if layer.trusted {
61 61 &b"yes"[..]
62 62 } else {
63 63 &b"no"[..]
64 64 },
65 65 layer
66 66 )?;
67 67 }
68 68 Ok(())
69 69 }
70 70 }
71 71
72 72 pub enum ConfigSource {
73 73 /// Absolute path to a config file
74 74 AbsPath(PathBuf),
75 75 /// Already parsed (from the CLI, env, Python resources, etc.)
76 76 Parsed(layer::ConfigLayer),
77 77 }
78 78
79 79 #[derive(Debug)]
80 80 pub struct ConfigValueParseErrorDetails {
81 81 pub origin: ConfigOrigin,
82 82 pub line: Option<usize>,
83 83 pub section: Vec<u8>,
84 84 pub item: Vec<u8>,
85 85 pub value: Vec<u8>,
86 86 pub expected_type: &'static str,
87 87 }
88 88
89 89 // boxed to avoid very large Result types
90 90 pub type ConfigValueParseError = Box<ConfigValueParseErrorDetails>;
91 91
92 92 impl fmt::Display for ConfigValueParseError {
93 93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 94 // TODO: add origin and line number information, here and in
95 95 // corresponding python code
96 96 write!(
97 97 f,
98 98 "config error: {}.{} is not a {} ('{}')",
99 99 String::from_utf8_lossy(&self.section),
100 100 String::from_utf8_lossy(&self.item),
101 101 self.expected_type,
102 102 String::from_utf8_lossy(&self.value)
103 103 )
104 104 }
105 105 }
106 106
107 107 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
108 108 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
109 109 // duplication with [_applyconfig] in [ui.py],
110 110 if !plain.is_plain() {
111 111 return false;
112 112 }
113 113 if section == b"alias" {
114 114 return plain.plainalias();
115 115 }
116 116 if section == b"revsetalias" {
117 117 return plain.plainrevsetalias();
118 118 }
119 119 if section == b"templatealias" {
120 120 return plain.plaintemplatealias();
121 121 }
122 122 if section == b"ui" {
123 123 let to_delete: &[&[u8]] = &[
124 124 b"debug",
125 125 b"fallbackencoding",
126 126 b"quiet",
127 127 b"slash",
128 128 b"logtemplate",
129 129 b"message-output",
130 130 b"statuscopies",
131 131 b"style",
132 132 b"traceback",
133 133 b"verbose",
134 134 ];
135 135 return to_delete.contains(&item);
136 136 }
137 137 let sections_to_delete: &[&[u8]] =
138 138 &[b"defaults", b"commands", b"command-templates"];
139 139 sections_to_delete.contains(&section)
140 140 }
141 141
142 142 impl Config {
143 143 /// The configuration to use when printing configuration-loading errors
144 144 pub fn empty() -> Self {
145 145 Self {
146 146 layers: Vec::new(),
147 147 plain: PlainInfo::empty(),
148 148 }
149 149 }
150 150
151 151 /// Load system and user configuration from various files.
152 152 ///
153 153 /// This is also affected by some environment variables.
154 154 pub fn load_non_repo() -> Result<Self, ConfigError> {
155 155 let mut config = Self::empty();
156 156 let opt_rc_path = env::var_os("HGRCPATH");
157 157 // HGRCPATH replaces system config
158 158 if opt_rc_path.is_none() {
159 159 config.add_system_config()?
160 160 }
161 161
162 162 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
163 163 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
164 164 config.add_for_environment_variable("PAGER", b"pager", b"pager");
165 165
166 166 // These are set by `run-tests.py --rhg` to enable fallback for the
167 167 // entire test suite. Alternatives would be setting configuration
168 168 // through `$HGRCPATH` but some tests override that, or changing the
169 169 // `hg` shell alias to include `--config` but that disrupts tests that
170 170 // print command lines and check expected output.
171 171 config.add_for_environment_variable(
172 172 "RHG_ON_UNSUPPORTED",
173 173 b"rhg",
174 174 b"on-unsupported",
175 175 );
176 176 config.add_for_environment_variable(
177 177 "RHG_FALLBACK_EXECUTABLE",
178 178 b"rhg",
179 179 b"fallback-executable",
180 180 );
181 181
182 182 // HGRCPATH replaces user config
183 183 if opt_rc_path.is_none() {
184 184 config.add_user_config()?
185 185 }
186 186 if let Some(rc_path) = &opt_rc_path {
187 187 for path in env::split_paths(rc_path) {
188 188 if !path.as_os_str().is_empty() {
189 189 if path.is_dir() {
190 190 config.add_trusted_dir(&path)?
191 191 } else {
192 192 config.add_trusted_file(&path)?
193 193 }
194 194 }
195 195 }
196 196 }
197 197 Ok(config)
198 198 }
199 199
200 200 pub fn load_cli_args(
201 201 &mut self,
202 202 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
203 203 color_arg: Option<Vec<u8>>,
204 204 ) -> Result<(), ConfigError> {
205 205 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
206 206 self.layers.push(layer)
207 207 }
208 208 if let Some(arg) = color_arg {
209 209 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
210 210 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
211 211 self.layers.push(layer)
212 212 }
213 213 Ok(())
214 214 }
215 215
216 216 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
217 217 if let Some(entries) = std::fs::read_dir(path)
218 218 .when_reading_file(path)
219 219 .io_not_found_as_none()?
220 220 {
221 221 let mut file_paths = entries
222 222 .map(|result| {
223 223 result.when_reading_file(path).map(|entry| entry.path())
224 224 })
225 225 .collect::<Result<Vec<_>, _>>()?;
226 226 file_paths.sort();
227 227 for file_path in &file_paths {
228 228 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
229 229 self.add_trusted_file(file_path)?
230 230 }
231 231 }
232 232 }
233 233 Ok(())
234 234 }
235 235
236 236 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
237 237 if let Some(data) = std::fs::read(path)
238 238 .when_reading_file(path)
239 239 .io_not_found_as_none()?
240 240 {
241 241 self.layers.extend(ConfigLayer::parse(path, &data)?)
242 242 }
243 243 Ok(())
244 244 }
245 245
246 246 fn add_for_environment_variable(
247 247 &mut self,
248 248 var: &str,
249 249 section: &[u8],
250 250 key: &[u8],
251 251 ) {
252 252 if let Some(value) = env::var_os(var) {
253 253 let origin = layer::ConfigOrigin::Environment(var.into());
254 254 let mut layer = ConfigLayer::new(origin);
255 255 layer.add(
256 256 section.to_owned(),
257 257 key.to_owned(),
258 258 get_bytes_from_os_str(value),
259 259 None,
260 260 );
261 261 self.layers.push(layer)
262 262 }
263 263 }
264 264
265 265 #[cfg(unix)] // TODO: other platforms
266 266 fn add_system_config(&mut self) -> Result<(), ConfigError> {
267 267 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
268 268 let etc = prefix.join("etc").join("mercurial");
269 269 self.add_trusted_file(&etc.join("hgrc"))?;
270 270 self.add_trusted_dir(&etc.join("hgrc.d"))
271 271 };
272 272 let root = Path::new("/");
273 273 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
274 274 // instead? TODO: can this be a relative path?
275 275 let hg = crate::utils::current_exe()?;
276 276 // TODO: this order (per-installation then per-system) matches
277 277 // `systemrcpath()` in `mercurial/scmposix.py`, but
278 278 // `mercurial/helptext/config.txt` suggests it should be reversed
279 279 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
280 280 if installation_prefix != root {
281 281 add_for_prefix(installation_prefix)?
282 282 }
283 283 }
284 284 add_for_prefix(root)?;
285 285 Ok(())
286 286 }
287 287
288 288 #[cfg(unix)] // TODO: other plateforms
289 289 fn add_user_config(&mut self) -> Result<(), ConfigError> {
290 290 let opt_home = home::home_dir();
291 291 if let Some(home) = &opt_home {
292 292 self.add_trusted_file(&home.join(".hgrc"))?
293 293 }
294 294 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
295 295 if !darwin {
296 296 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
297 297 .map(PathBuf::from)
298 298 .or_else(|| opt_home.map(|home| home.join(".config")))
299 299 {
300 300 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
301 301 }
302 302 }
303 303 Ok(())
304 304 }
305 305
306 306 /// Loads in order, which means that the precedence is the same
307 307 /// as the order of `sources`.
308 308 pub fn load_from_explicit_sources(
309 309 sources: Vec<ConfigSource>,
310 310 ) -> Result<Self, ConfigError> {
311 311 let mut layers = vec![];
312 312
313 313 for source in sources.into_iter() {
314 314 match source {
315 315 ConfigSource::Parsed(c) => layers.push(c),
316 316 ConfigSource::AbsPath(c) => {
317 317 // TODO check if it should be trusted
318 318 // mercurial/ui.py:427
319 319 let data = match std::fs::read(&c) {
320 320 Err(_) => continue, // same as the python code
321 321 Ok(data) => data,
322 322 };
323 323 layers.extend(ConfigLayer::parse(&c, &data)?)
324 324 }
325 325 }
326 326 }
327 327
328 328 Ok(Config {
329 329 layers,
330 330 plain: PlainInfo::empty(),
331 331 })
332 332 }
333 333
334 334 /// Loads the per-repository config into a new `Config` which is combined
335 335 /// with `self`.
336 336 pub(crate) fn combine_with_repo(
337 337 &self,
338 338 repo_config_files: &[PathBuf],
339 339 ) -> Result<Self, ConfigError> {
340 340 let (cli_layers, other_layers) = self
341 341 .layers
342 342 .iter()
343 343 .cloned()
344 344 .partition(ConfigLayer::is_from_command_line);
345 345
346 346 let mut repo_config = Self {
347 347 layers: other_layers,
348 348 plain: PlainInfo::empty(),
349 349 };
350 350 for path in repo_config_files {
351 351 // TODO: check if this file should be trusted:
352 352 // `mercurial/ui.py:427`
353 353 repo_config.add_trusted_file(path)?;
354 354 }
355 355 repo_config.layers.extend(cli_layers);
356 356 Ok(repo_config)
357 357 }
358 358
359 359 pub fn apply_plain(&mut self, plain: PlainInfo) {
360 360 self.plain = plain;
361 361 }
362 362
363 363 /// Returns the default value for the given config item, if any.
364 364 pub fn get_default(
365 365 &self,
366 366 section: &[u8],
367 367 item: &[u8],
368 368 ) -> Result<Option<&DefaultConfigItem>, HgError> {
369 369 let default_config = DEFAULT_CONFIG.as_ref().map_err(|e| {
370 370 HgError::abort(
371 371 e.to_string(),
372 372 crate::exit_codes::ABORT,
373 373 Some("`mercurial/configitems.toml` is not valid".into()),
374 374 )
375 375 })?;
376 376 let default_opt = default_config.get(section, item);
377 377 Ok(default_opt.filter(|default| {
378 378 default
379 379 .in_core_extension()
380 380 .map(|extension| {
381 381 // Only return the default for an in-core extension item
382 382 // if said extension is enabled
383 383 self.is_extension_enabled(extension.as_bytes())
384 384 })
385 385 .unwrap_or(true)
386 386 }))
387 387 }
388 388
389 389 /// Return the config item that corresponds to a section + item, a function
390 390 /// to parse from the raw bytes to the expected type (which is passed as
391 391 /// a string only to make debugging easier).
392 392 /// Used by higher-level methods like `get_bool`.
393 393 ///
394 394 /// `fallback_to_default` controls whether the default value (if any) is
395 395 /// returned if nothing is found.
396 396 fn get_parse<'config, T: 'config>(
397 397 &'config self,
398 398 section: &[u8],
399 399 item: &[u8],
400 400 expected_type: &'static str,
401 401 parse: impl Fn(&'config [u8]) -> Option<T>,
402 402 fallback_to_default: bool,
403 403 ) -> Result<Option<T>, HgError>
404 404 where
405 405 Option<T>: TryFrom<&'config DefaultConfigItem, Error = HgError>,
406 406 {
407 407 match self.get_inner(section, item) {
408 408 Some((layer, v)) => match parse(&v.bytes) {
409 409 Some(b) => Ok(Some(b)),
410 410 None => Err(Box::new(ConfigValueParseErrorDetails {
411 411 origin: layer.origin.to_owned(),
412 412 line: v.line,
413 413 value: v.bytes.to_owned(),
414 414 section: section.to_owned(),
415 415 item: item.to_owned(),
416 416 expected_type,
417 417 })
418 418 .into()),
419 419 },
420 420 None => {
421 421 if !fallback_to_default {
422 422 return Ok(None);
423 423 }
424 424 match self.get_default(section, item)? {
425 425 Some(default) => {
426 426 // Defaults are TOML values, so they're not in the same
427 427 // shape as in the config files.
428 428 // First try to convert directly to the expected type
429 429 let as_t = default.try_into();
430 430 match as_t {
431 431 Ok(t) => Ok(t),
432 432 Err(e) => {
433 433 // If it fails, it means that...
434 434 let as_bytes: Result<Option<&[u8]>, _> =
435 435 default.try_into();
436 436 match as_bytes {
437 437 Ok(bytes_opt) => {
438 438 if let Some(bytes) = bytes_opt {
439 439 // ...we should be able to parse it
440 440 return Ok(parse(bytes));
441 441 }
442 442 Err(e)
443 443 }
444 444 Err(_) => Err(e),
445 445 }
446 446 }
447 447 }
448 448 }
449 449 None => {
450 450 self.print_devel_warning(section, item)?;
451 451 Ok(None)
452 452 }
453 453 }
454 454 }
455 455 }
456 456 }
457 457
458 458 fn print_devel_warning(
459 459 &self,
460 460 section: &[u8],
461 461 item: &[u8],
462 462 ) -> Result<(), HgError> {
463 463 let warn_all = self.get_bool(b"devel", b"all-warnings")?;
464 464 let warn_specific = self.get_bool(b"devel", b"warn-config-unknown")?;
465 465 if !warn_all || !warn_specific {
466 466 // We technically shouldn't print anything here since it's not
467 467 // the concern of `hg-core`.
468 468 //
469 469 // We're printing directly to stderr since development warnings
470 470 // are not on by default and surfacing this to consumer crates
471 471 // (like `rhg`) would be more difficult, probably requiring
472 472 // something à la `log` crate.
473 473 //
474 474 // TODO maybe figure out a way of exposing a "warnings" channel
475 475 // that consumer crates can hook into. It would be useful for
476 476 // all other warnings that `hg-core` could expose.
477 477 eprintln!(
478 478 "devel-warn: accessing unregistered config item: '{}.{}'",
479 479 String::from_utf8_lossy(section),
480 480 String::from_utf8_lossy(item),
481 481 );
482 482 }
483 483 Ok(())
484 484 }
485 485
486 486 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
487 487 /// Otherwise, returns an `Ok(value)` if found, or `None`.
488 488 pub fn get_str(
489 489 &self,
490 490 section: &[u8],
491 491 item: &[u8],
492 492 ) -> Result<Option<&str>, HgError> {
493 493 self.get_parse(
494 494 section,
495 495 item,
496 496 "ASCII or UTF-8 string",
497 497 |value| str::from_utf8(value).ok(),
498 498 true,
499 499 )
500 500 }
501 501
502 502 /// Same as `get_str`, but doesn't fall back to the default `configitem`
503 503 /// if not defined in the user config.
504 504 pub fn get_str_no_default(
505 505 &self,
506 506 section: &[u8],
507 507 item: &[u8],
508 508 ) -> Result<Option<&str>, HgError> {
509 509 self.get_parse(
510 510 section,
511 511 item,
512 512 "ASCII or UTF-8 string",
513 513 |value| str::from_utf8(value).ok(),
514 514 false,
515 515 )
516 516 }
517 517
518 518 /// Returns an `Err` if the first value found is not a valid unsigned
519 519 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
520 520 pub fn get_u32(
521 521 &self,
522 522 section: &[u8],
523 523 item: &[u8],
524 524 ) -> Result<Option<u32>, HgError> {
525 525 self.get_parse(
526 526 section,
527 527 item,
528 528 "valid integer",
529 529 |value| str::from_utf8(value).ok()?.parse().ok(),
530 530 true,
531 531 )
532 532 }
533 533
534 534 /// Returns an `Err` if the first value found is not a valid file size
535 535 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
536 536 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
537 537 pub fn get_byte_size(
538 538 &self,
539 539 section: &[u8],
540 540 item: &[u8],
541 541 ) -> Result<Option<u64>, HgError> {
542 542 self.get_parse(
543 543 section,
544 544 item,
545 545 "byte quantity",
546 546 values::parse_byte_size,
547 547 true,
548 548 )
549 549 }
550 550
551 551 /// Returns an `Err` if the first value found is not a valid boolean.
552 552 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
553 553 /// found, or `None`.
554 554 pub fn get_option(
555 555 &self,
556 556 section: &[u8],
557 557 item: &[u8],
558 558 ) -> Result<Option<bool>, HgError> {
559 559 self.get_parse(section, item, "boolean", values::parse_bool, true)
560 560 }
561 561
562 562 /// Same as `get_option`, but doesn't fall back to the default `configitem`
563 563 /// if not defined in the user config.
564 564 pub fn get_option_no_default(
565 565 &self,
566 566 section: &[u8],
567 567 item: &[u8],
568 568 ) -> Result<Option<bool>, HgError> {
569 569 self.get_parse(section, item, "boolean", values::parse_bool, false)
570 570 }
571 571
572 572 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
573 573 /// if the value is not found, an `Err` if it's not a valid boolean.
574 574 pub fn get_bool(
575 575 &self,
576 576 section: &[u8],
577 577 item: &[u8],
578 578 ) -> Result<bool, HgError> {
579 579 Ok(self.get_option(section, item)?.unwrap_or(false))
580 580 }
581 581
582 582 /// Same as `get_bool`, but doesn't fall back to the default `configitem`
583 583 /// if not defined in the user config.
584 584 pub fn get_bool_no_default(
585 585 &self,
586 586 section: &[u8],
587 587 item: &[u8],
588 588 ) -> Result<bool, HgError> {
589 589 Ok(self.get_option_no_default(section, item)?.unwrap_or(false))
590 590 }
591 591
592 592 /// Returns `true` if the extension is enabled, `false` otherwise
593 593 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
594 594 let value = self.get(b"extensions", extension);
595 595 match value {
596 596 Some(c) => !c.starts_with(b"!"),
597 597 None => false,
598 598 }
599 599 }
600 600
601 601 /// If there is an `item` value in `section`, parse and return a list of
602 602 /// byte strings.
603 603 pub fn get_list(
604 604 &self,
605 605 section: &[u8],
606 606 item: &[u8],
607 607 ) -> Option<Vec<Vec<u8>>> {
608 608 self.get(section, item).map(values::parse_list)
609 609 }
610 610
611 611 /// Returns the raw value bytes of the first one found, or `None`.
612 612 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
613 613 self.get_inner(section, item)
614 614 .map(|(_, value)| value.bytes.as_ref())
615 615 }
616 616
617 617 /// Returns the raw value bytes of the first one found, or `None`.
618 618 pub fn get_with_origin(
619 619 &self,
620 620 section: &[u8],
621 621 item: &[u8],
622 622 ) -> Option<(&[u8], &ConfigOrigin)> {
623 623 self.get_inner(section, item)
624 624 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
625 625 }
626 626
627 627 /// Returns the layer and the value of the first one found, or `None`.
628 628 fn get_inner(
629 629 &self,
630 630 section: &[u8],
631 631 item: &[u8],
632 632 ) -> Option<(&ConfigLayer, &ConfigValue)> {
633 633 // Filter out the config items that are hidden by [PLAIN].
634 634 // This differs from python hg where we delete them from the config.
635 635 let should_ignore = should_ignore(&self.plain, section, item);
636 636 for layer in self.layers.iter().rev() {
637 637 if !layer.trusted {
638 638 continue;
639 639 }
640 640 //The [PLAIN] config should not affect the defaults.
641 641 //
642 642 // However, PLAIN should also affect the "tweaked" defaults (unless
643 643 // "tweakdefault" is part of "HGPLAINEXCEPT").
644 644 //
645 645 // In practice the tweak-default layer is only added when it is
646 646 // relevant, so we can safely always take it into
647 647 // account here.
648 648 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
649 649 {
650 650 continue;
651 651 }
652 652 if let Some(v) = layer.get(section, item) {
653 653 return Some((layer, v));
654 654 }
655 655 }
656 656 None
657 657 }
658 658
659 659 /// Return all keys defined for the given section
660 660 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
661 661 self.layers
662 662 .iter()
663 663 .flat_map(|layer| layer.iter_keys(section))
664 664 .collect()
665 665 }
666 666
667 667 /// Returns whether any key is defined in the given section
668 668 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
669 669 self.layers
670 670 .iter()
671 671 .any(|layer| layer.has_non_empty_section(section))
672 672 }
673 673
674 674 /// Yields (key, value) pairs for everything in the given section
675 675 pub fn iter_section<'a>(
676 676 &'a self,
677 677 section: &'a [u8],
678 678 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
679 679 // Deduplicate keys redefined in multiple layers
680 680 let mut keys_already_seen = HashSet::new();
681 681 let mut key_is_new =
682 682 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
683 683 keys_already_seen.insert(key)
684 684 };
685 685 // This is similar to `flat_map` + `filter_map`, except with a single
686 686 // closure that owns `key_is_new` (and therefore the
687 687 // `keys_already_seen` set):
688 688 let mut layer_iters = self
689 689 .layers
690 690 .iter()
691 691 .rev()
692 692 .map(move |layer| layer.iter_section(section))
693 693 .peekable();
694 694 std::iter::from_fn(move || loop {
695 695 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
696 696 return Some(pair);
697 697 } else {
698 698 layer_iters.next();
699 699 }
700 700 })
701 701 }
702 702
703 703 /// Get raw values bytes from all layers (even untrusted ones) in order
704 704 /// of precedence.
705 705 #[cfg(test)]
706 706 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
707 707 let mut res = vec![];
708 708 for layer in self.layers.iter().rev() {
709 709 if let Some(v) = layer.get(section, item) {
710 710 res.push(v.bytes.as_ref());
711 711 }
712 712 }
713 713 res
714 714 }
715 715
716 716 // a config layer that's introduced by ui.tweakdefaults
717 717 fn tweakdefaults_layer() -> ConfigLayer {
718 718 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
719 719
720 720 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
721 721 layer.add(
722 722 section[..].into(),
723 723 item[..].into(),
724 724 value[..].into(),
725 725 None,
726 726 );
727 727 };
728 728 // duplication of [tweakrc] from [ui.py]
729 729 add(b"ui", b"rollback", b"False");
730 730 add(b"ui", b"statuscopies", b"yes");
731 731 add(b"ui", b"interface", b"curses");
732 732 add(b"ui", b"relative-paths", b"yes");
733 733 add(b"commands", b"grep.all-files", b"True");
734 734 add(b"commands", b"update.check", b"noconflict");
735 735 add(b"commands", b"status.verbose", b"True");
736 736 add(b"commands", b"resolve.explicit-re-merge", b"True");
737 737 add(b"git", b"git", b"1");
738 738 add(b"git", b"showfunc", b"1");
739 739 add(b"git", b"word-diff", b"1");
740 740 layer
741 741 }
742 742
743 743 // introduce the tweaked defaults as implied by ui.tweakdefaults
744 744 pub fn tweakdefaults(&mut self) {
745 745 self.layers.insert(0, Config::tweakdefaults_layer());
746 746 }
747 747 }
748 748
749 749 #[cfg(test)]
750 750 mod tests {
751 751 use super::*;
752 752 use pretty_assertions::assert_eq;
753 753 use std::fs::File;
754 754 use std::io::Write;
755 755
756 756 #[test]
757 757 fn test_include_layer_ordering() {
758 758 let tmpdir = tempfile::tempdir().unwrap();
759 759 let tmpdir_path = tmpdir.path();
760 760 let mut included_file =
761 761 File::create(&tmpdir_path.join("included.rc")).unwrap();
762 762
763 763 included_file.write_all(b"[section]\nitem=value1").unwrap();
764 764 let base_config_path = tmpdir_path.join("base.rc");
765 765 let mut config_file = File::create(&base_config_path).unwrap();
766 766 let data =
767 767 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
768 768 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
769 769 config_file.write_all(data).unwrap();
770 770
771 771 let sources = vec![ConfigSource::AbsPath(base_config_path)];
772 772 let config = Config::load_from_explicit_sources(sources)
773 773 .expect("expected valid config");
774 774
775 775 let (_, value) = config.get_inner(b"section", b"item").unwrap();
776 776 assert_eq!(
777 777 value,
778 778 &ConfigValue {
779 779 bytes: b"value2".to_vec(),
780 780 line: Some(4)
781 781 }
782 782 );
783 783
784 784 let value = config.get(b"section", b"item").unwrap();
785 785 assert_eq!(value, b"value2",);
786 786 assert_eq!(
787 787 config.get_all(b"section", b"item"),
788 788 [b"value2", b"value1", b"value0"]
789 789 );
790 790
791 791 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
792 792 assert_eq!(
793 793 config.get_byte_size(b"section2", b"size").unwrap(),
794 794 Some(1024 + 512)
795 795 );
796 796 assert!(config.get_u32(b"section2", b"not-count").is_err());
797 797 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
798 798 }
799 799
800 800 #[test]
801 801 fn test_default_parse() {
802 802 let config = Config::load_from_explicit_sources(vec![])
803 803 .expect("expected valid config");
804 804 let ret = config.get_byte_size(b"cmdserver", b"max-log-size");
805 805 assert!(ret.is_ok(), "{:?}", ret);
806 806
807 807 let ret = config.get_byte_size(b"ui", b"formatted");
808 // FIXME should be `is_none()`
809 assert!(ret.unwrap().is_some());
808 assert!(ret.unwrap().is_none());
810 809 }
811 810 }
General Comments 0
You need to be logged in to leave comments. Login now