rust: Add config parsing support for more value types
authorSimon Sapin <simon.sapin@octobus.net>
Tue, 16 Feb 2021 13:55:31 +0100
changeset 46636 305d74c262de
parent 46635 d2e61f00ee9d
child 46637 bc08c2331f99
rust: Add config parsing support for more value types * Rust `str` (ASCII or UTF-8) * Integer * Byte quantities Differential Revision: https://phab.mercurial-scm.org/D10008
rust/hg-core/src/config/config.rs
--- a/rust/hg-core/src/config/config.rs	Wed Feb 17 11:21:34 2021 +0100
+++ b/rust/hg-core/src/config/config.rs	Tue Feb 16 13:55:31 2021 +0100
@@ -15,6 +15,7 @@
 use format_bytes::{write_bytes, DisplayBytes};
 use std::env;
 use std::path::{Path, PathBuf};
+use std::str;
 
 use crate::errors::{HgResultExt, IoResultExt};
 
@@ -61,6 +62,32 @@
     }
 }
 
+pub fn parse_byte_size(value: &[u8]) -> Option<u64> {
+    let value = str::from_utf8(value).ok()?.to_ascii_lowercase();
+    const UNITS: &[(&str, u64)] = &[
+        ("g", 1 << 30),
+        ("gb", 1 << 30),
+        ("m", 1 << 20),
+        ("mb", 1 << 20),
+        ("k", 1 << 10),
+        ("kb", 1 << 10),
+        ("b", 1 << 0), // Needs to be last
+    ];
+    for &(unit, multiplier) in UNITS {
+        // TODO: use `value.strip_suffix(unit)` when we require Rust 1.45+
+        if value.ends_with(unit) {
+            let value_before_unit = &value[..value.len() - unit.len()];
+            let float: f64 = value_before_unit.trim().parse().ok()?;
+            if float >= 0.0 {
+                return Some((float * multiplier as f64).round() as u64);
+            } else {
+                return None;
+            }
+        }
+    }
+    value.parse().ok()
+}
+
 impl Config {
     /// Load system and user configuration from various files.
     ///
@@ -231,16 +258,14 @@
         Ok(repo_config)
     }
 
-    /// Returns an `Err` if the first value found is not a valid boolean.
-    /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
-    /// found, or `None`.
-    pub fn get_option(
-        &self,
+    fn get_parse<'config, T: 'config>(
+        &'config self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<Option<bool>, ConfigParseError> {
+        parse: impl Fn(&'config [u8]) -> Option<T>,
+    ) -> Result<Option<T>, ConfigParseError> {
         match self.get_inner(&section, &item) {
-            Some((layer, v)) => match parse_bool(&v.bytes) {
+            Some((layer, v)) => match parse(&v.bytes) {
                 Some(b) => Ok(Some(b)),
                 None => Err(ConfigParseError {
                     origin: layer.origin.to_owned(),
@@ -252,6 +277,50 @@
         }
     }
 
+    /// Returns an `Err` if the first value found is not a valid UTF-8 string.
+    /// Otherwise, returns an `Ok(value)` if found, or `None`.
+    pub fn get_str(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<Option<&str>, ConfigParseError> {
+        self.get_parse(section, item, |value| str::from_utf8(value).ok())
+    }
+
+    /// Returns an `Err` if the first value found is not a valid unsigned
+    /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
+    pub fn get_u32(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<Option<u32>, ConfigParseError> {
+        self.get_parse(section, item, |value| {
+            str::from_utf8(value).ok()?.parse().ok()
+        })
+    }
+
+    /// Returns an `Err` if the first value found is not a valid file size
+    /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
+    /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
+    pub fn get_byte_size(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<Option<u64>, ConfigParseError> {
+        self.get_parse(section, item, parse_byte_size)
+    }
+
+    /// Returns an `Err` if the first value found is not a valid boolean.
+    /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
+    /// found, or `None`.
+    pub fn get_option(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<Option<bool>, ConfigParseError> {
+        self.get_parse(section, item, parse_bool)
+    }
+
     /// Returns the corresponding boolean in the config. Returns `Ok(false)`
     /// if the value is not found, an `Err` if it's not a valid boolean.
     pub fn get_bool(
@@ -317,7 +386,8 @@
         let base_config_path = tmpdir_path.join("base.rc");
         let mut config_file = File::create(&base_config_path).unwrap();
         let data =
-            b"[section]\nitem=value0\n%include included.rc\nitem=value2";
+            b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
+              [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
         config_file.write_all(data).unwrap();
 
         let sources = vec![ConfigSource::AbsPath(base_config_path)];
@@ -339,5 +409,13 @@
             config.get_all(b"section", b"item"),
             [b"value2", b"value1", b"value0"]
         );
+
+        assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
+        assert_eq!(
+            config.get_byte_size(b"section2", b"size").unwrap(),
+            Some(1024 + 512)
+        );
+        assert!(config.get_u32(b"section2", b"not-count").is_err());
+        assert!(config.get_byte_size(b"section2", b"not-size").is_err());
     }
 }