Skip to main content

dynamic_config/
units.rs

1//! Serde adapters for the two units configuration is always written in.
2//!
3//! `timeout = 30` is ambiguous — seconds? milliseconds? — and `max_body = 67108864`
4//! is unreadable. Both are usually written as `"30s"` and `"64MiB"`, which no
5//! stock `Deserialize` impl accepts. These adapters do, while still accepting a
6//! bare number so existing files keep working:
7//!
8//! ```
9//! use std::time::Duration;
10//! use serde::Deserialize;
11//!
12//! #[derive(Deserialize)]
13//! struct Config {
14//!     #[serde(with = "dynamic_config::duration")]
15//!     timeout: Duration,
16//!     #[serde(default, with = "dynamic_config::duration::option")]
17//!     grace: Option<Duration>,
18//!     #[serde(with = "dynamic_config::bytes")]
19//!     max_body: u64,
20//! }
21//! ```
22//!
23//! Both are strict about nonsense: an unknown unit or a bare `-` is an error
24//! naming what was expected, never a silent zero.
25
26use std::fmt;
27
28use serde::de::{self, Unexpected, Visitor};
29use serde::{Deserializer, Serializer};
30
31/// Splits `12kb` into `(12, "kb")`, tolerating whitespace between them.
32///
33/// Nothing here quotes the text it was given. What arrives is a configuration
34/// *value*, and a value that fails to parse as a duration is exactly the shape
35/// of a password pasted into the wrong field — the same reason
36/// `loader::origin` reduces figment's ``found string "hunter2"`` to "a string".
37/// The expected spelling is what a reader needs, and the loader has already
38/// named the key and the file.
39fn split_unit(text: &str) -> Result<(u64, &str), String> {
40    let text = text.trim();
41    let digits = text
42        .find(|character: char| !character.is_ascii_digit())
43        .unwrap_or(text.len());
44
45    if digits == 0 {
46        return Err("expected a number first, as in `30s`".to_owned());
47    }
48
49    let value = text[..digits]
50        .parse::<u64>()
51        .map_err(|_| "the number is too large for a 64-bit count".to_owned())?;
52
53    Ok((value, text[digits..].trim()))
54}
55
56// ---------------------------------------------------------------------------
57
58/// `Duration` from `"30s"`, `"1h30m"`, `"500ms"`, or a bare number of seconds.
59///
60/// Units: `ms`, `s`, `m`, `h`, `d`. Several may be concatenated, largest first
61/// by convention but in any order in practice — `"1h30m"` is 90 minutes.
62///
63/// ```
64/// use std::time::Duration;
65/// use serde::Deserialize;
66///
67/// #[derive(Deserialize)]
68/// struct Config {
69///     #[serde(with = "dynamic_config::duration")]
70///     timeout: Duration,
71///     #[serde(default, with = "dynamic_config::duration::option")]
72///     grace: Option<Duration>,
73/// }
74/// ```
75///
76/// Strict about nonsense: an unknown unit or a bare `-` is an error naming what
77/// was expected, never a silent zero.
78pub mod duration {
79    use super::*;
80    use std::time::Duration;
81
82    /// Parses a duration string.
83    ///
84    /// # Errors
85    ///
86    /// If a component has no unit, an unknown unit, or the total overflows.
87    pub fn parse(text: &str) -> Result<Duration, String> {
88        let text = text.trim();
89
90        if text.is_empty() {
91            return Err("an empty string is not a duration".to_owned());
92        }
93
94        let mut total = Duration::ZERO;
95        let mut rest = text;
96
97        while !rest.is_empty() {
98            let (value, tail) = split_unit(rest)?;
99
100            let boundary = tail
101                .find(|character: char| character.is_ascii_digit())
102                .unwrap_or(tail.len());
103            let (unit, tail) = tail.split_at(boundary);
104
105            let component = match unit.trim() {
106                "ms" => Duration::from_millis(value),
107                "s" => Duration::from_secs(value),
108                // `checked_mul`, as `bytes` below already does: this is
109                // arithmetic on config input, and the module's promise is
110                // "strict about nonsense" — a silent saturation to u64::MAX
111                // seconds is nonsense accepted quietly.
112                "m" => Duration::from_secs(checked(value, 60)?),
113                "h" => Duration::from_secs(checked(value, 60 * 60)?),
114                "d" => Duration::from_secs(checked(value, 24 * 60 * 60)?),
115                "" => return Err("a component has no unit; expected one of ms, s, m, h, d".into()),
116                // The unit is not echoed either: it is whatever followed the
117                // digits, so in `1234hunter2` it is the rest of the password.
118                _ => return Err("unknown duration unit; expected one of ms, s, m, h, d".into()),
119            };
120
121            total = total
122                .checked_add(component)
123                .ok_or_else(|| "the total is longer than a `Duration` can hold".to_owned())?;
124
125            rest = tail;
126        }
127
128        Ok(total)
129    }
130
131    /// One component's seconds, or the error the module promises on overflow.
132    fn checked(value: u64, seconds_per_unit: u64) -> Result<u64, String> {
133        value
134            .checked_mul(seconds_per_unit)
135            .ok_or_else(|| "a component overflows a 64-bit second count".to_owned())
136    }
137
138    struct DurationVisitor;
139
140    impl Visitor<'_> for DurationVisitor {
141        type Value = Duration;
142
143        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144            formatter.write_str("a duration such as \"30s\" or \"1h30m\", or a number of seconds")
145        }
146
147        fn visit_str<E: de::Error>(self, value: &str) -> Result<Duration, E> {
148            parse(value).map_err(E::custom)
149        }
150
151        fn visit_u64<E: de::Error>(self, value: u64) -> Result<Duration, E> {
152            Ok(Duration::from_secs(value))
153        }
154
155        // serde renders `Unexpected::Signed` with the number in it, and that
156        // one is fine to show: reaching here means `u64::try_from` refused it,
157        // so it is a negative count and not something anybody typed a
158        // credential into.
159        fn visit_i64<E: de::Error>(self, value: i64) -> Result<Duration, E> {
160            u64::try_from(value)
161                .map(Duration::from_secs)
162                .map_err(|_| E::invalid_value(Unexpected::Signed(value), &self))
163        }
164    }
165
166    /// `#[serde(with = "dynamic_config::duration")]`
167    ///
168    /// # Errors
169    ///
170    /// If the value is neither a number nor a parseable duration string.
171    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
172        deserializer.deserialize_any(DurationVisitor)
173    }
174
175    /// Round-trips as the millisecond count, so a written config stays valid.
176    ///
177    /// # Errors
178    ///
179    /// Whatever the serializer reports.
180    pub fn serialize<S: Serializer>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
181        serializer.serialize_str(&format!("{}ms", value.as_millis()))
182    }
183
184    /// The same, for `Option<Duration>`.
185    pub mod option {
186        use super::*;
187
188        struct OptionVisitor;
189
190        impl<'de> Visitor<'de> for OptionVisitor {
191            type Value = Option<Duration>;
192
193            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194                formatter.write_str("a duration, a number of seconds, or null")
195            }
196
197            fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
198                Ok(None)
199            }
200
201            fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
202                Ok(None)
203            }
204
205            fn visit_some<D: Deserializer<'de>>(
206                self,
207                deserializer: D,
208            ) -> Result<Self::Value, D::Error> {
209                super::deserialize(deserializer).map(Some)
210            }
211        }
212
213        /// `#[serde(with = "dynamic_config::duration::option")]`
214        ///
215        /// # Errors
216        ///
217        /// As [`duration::deserialize`](super::deserialize).
218        pub fn deserialize<'de, D: Deserializer<'de>>(
219            deserializer: D,
220        ) -> Result<Option<Duration>, D::Error> {
221            deserializer.deserialize_option(OptionVisitor)
222        }
223
224        /// # Errors
225        ///
226        /// Whatever the serializer reports.
227        pub fn serialize<S: Serializer>(
228            value: &Option<Duration>,
229            serializer: S,
230        ) -> Result<S::Ok, S::Error> {
231            match value {
232                Some(duration) => super::serialize(duration, serializer),
233                None => serializer.serialize_none(),
234            }
235        }
236    }
237}
238
239// ---------------------------------------------------------------------------
240
241/// A byte count from `"64MiB"`, `"1GB"`, or a bare number.
242///
243/// Both conventions are supported and they are *not* the same: `KiB`/`MiB`/`GiB`
244/// are powers of 1024, `KB`/`MB`/`GB` powers of 1000. A bare `K`/`M`/`G` is
245/// read as the binary form, which is what everyone means when they type it into
246/// a config file.
247///
248/// ```
249/// use serde::Deserialize;
250///
251/// #[derive(Deserialize)]
252/// struct Config {
253///     #[serde(with = "dynamic_config::bytes")]
254///     max_body: u64,
255/// }
256/// ```
257///
258/// Strict about nonsense: an unknown unit is an error naming what was expected,
259/// never a silent zero.
260pub mod bytes {
261    use super::*;
262
263    /// Parses a byte-size string.
264    ///
265    /// # Errors
266    ///
267    /// If the unit is unknown or the total overflows a `u64`.
268    pub fn parse(text: &str) -> Result<u64, String> {
269        let (value, unit) = split_unit(text)?;
270
271        let multiplier: u64 = match unit.trim().to_ascii_lowercase().as_str() {
272            "" | "b" => 1,
273            "k" | "kib" => 1 << 10,
274            "m" | "mib" => 1 << 20,
275            "g" | "gib" => 1 << 30,
276            "t" | "tib" => 1 << 40,
277            "kb" => 1_000,
278            "mb" => 1_000_000,
279            "gb" => 1_000_000_000,
280            "tb" => 1_000_000_000_000,
281            // Not echoed, as in `duration`: the unit is whatever followed
282            // the digits, which in a mistyped secret is the rest of it.
283            _ => {
284                return Err("unknown size unit; expected one of B, KiB, MiB, GiB, TiB, \
285                     KB, MB, GB, TB"
286                    .to_owned())
287            }
288        };
289
290        value
291            .checked_mul(multiplier)
292            .ok_or_else(|| "the size overflows a 64-bit byte count".to_owned())
293    }
294
295    struct BytesVisitor;
296
297    impl Visitor<'_> for BytesVisitor {
298        type Value = u64;
299
300        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
301            formatter.write_str("a size such as \"64MiB\", or a number of bytes")
302        }
303
304        fn visit_str<E: de::Error>(self, value: &str) -> Result<u64, E> {
305            parse(value).map_err(E::custom)
306        }
307
308        fn visit_u64<E: de::Error>(self, value: u64) -> Result<u64, E> {
309            Ok(value)
310        }
311
312        fn visit_i64<E: de::Error>(self, value: i64) -> Result<u64, E> {
313            u64::try_from(value).map_err(|_| E::invalid_value(Unexpected::Signed(value), &self))
314        }
315    }
316
317    /// `#[serde(with = "dynamic_config::bytes")]`
318    ///
319    /// # Errors
320    ///
321    /// If the value is neither a number nor a parseable size string.
322    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
323        deserializer.deserialize_any(BytesVisitor)
324    }
325
326    /// # Errors
327    ///
328    /// Whatever the serializer reports.
329    pub fn serialize<S: Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
330        serializer.serialize_u64(*value)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use std::time::Duration;
337
338    use super::{bytes, duration};
339
340    #[test]
341    fn every_duration_unit_parses() {
342        assert_eq!(
343            duration::parse("500ms").unwrap(),
344            Duration::from_millis(500)
345        );
346        assert_eq!(duration::parse("30s").unwrap(), Duration::from_secs(30));
347        assert_eq!(duration::parse("5m").unwrap(), Duration::from_secs(300));
348        assert_eq!(duration::parse("2h").unwrap(), Duration::from_secs(7_200));
349        assert_eq!(duration::parse("1d").unwrap(), Duration::from_secs(86_400));
350    }
351
352    #[test]
353    fn duration_components_add_up() {
354        assert_eq!(
355            duration::parse("1h30m").unwrap(),
356            Duration::from_secs(5_400)
357        );
358        assert_eq!(
359            duration::parse("1m 500ms").unwrap(),
360            Duration::from_millis(60_500)
361        );
362    }
363
364    #[test]
365    fn a_duration_without_a_unit_is_an_error_not_a_guess() {
366        // Ambiguous in a string: seconds or milliseconds? A bare *number* means
367        // seconds, but "30" as text is a mistake worth reporting.
368        let error = duration::parse("30").unwrap_err();
369
370        assert!(error.contains("no unit"), "{error}");
371    }
372
373    #[test]
374    fn an_unknown_duration_unit_lists_the_valid_ones() {
375        let error = duration::parse("30w").unwrap_err();
376
377        assert!(error.contains("unknown duration unit"), "{error}");
378        assert!(error.contains("ms, s, m, h, d"), "{error}");
379        // The unit itself is not quoted back: it is whatever followed the
380        // digits, which in a mistyped credential is the rest of it.
381        assert!(!error.contains("`w`"), "{error}");
382    }
383
384    #[test]
385    fn duration_rejects_empty_and_non_numeric_input() {
386        assert!(duration::parse("   ").is_err());
387        assert!(duration::parse("abc").is_err());
388        assert!(duration::parse("-5s").is_err());
389    }
390
391    #[test]
392    fn binary_and_decimal_size_units_differ() {
393        assert_eq!(bytes::parse("1KiB").unwrap(), 1_024);
394        assert_eq!(bytes::parse("1KB").unwrap(), 1_000);
395        assert_eq!(bytes::parse("64MiB").unwrap(), 64 * 1_024 * 1_024);
396        assert_eq!(bytes::parse("1GB").unwrap(), 1_000_000_000);
397    }
398
399    #[test]
400    fn a_bare_size_unit_is_binary() {
401        assert_eq!(bytes::parse("1M").unwrap(), 1 << 20);
402        assert_eq!(bytes::parse("512").unwrap(), 512);
403        assert_eq!(bytes::parse("512B").unwrap(), 512);
404    }
405
406    #[test]
407    fn size_units_are_case_insensitive() {
408        assert_eq!(
409            bytes::parse("64mib").unwrap(),
410            bytes::parse("64MiB").unwrap()
411        );
412        assert_eq!(bytes::parse("1gb").unwrap(), bytes::parse("1GB").unwrap());
413    }
414
415    #[test]
416    fn an_unknown_size_unit_lists_the_valid_ones() {
417        let error = bytes::parse("5PB").unwrap_err();
418
419        assert!(error.contains("unknown size unit"), "{error}");
420        assert!(error.contains("KiB, MiB"), "{error}");
421        assert!(
422            !error.contains("pb"),
423            "the unit is not quoted back: {error}"
424        );
425    }
426
427    #[test]
428    fn a_size_that_overflows_is_reported_rather_than_wrapped() {
429        let error = bytes::parse("100000000000TiB").unwrap_err();
430
431        assert!(error.contains("overflows"), "{error}");
432    }
433
434    #[test]
435    fn both_adapters_still_accept_a_bare_number() {
436        #[derive(serde::Deserialize)]
437        struct Config {
438            #[serde(with = "super::duration")]
439            timeout: Duration,
440            #[serde(with = "super::bytes")]
441            max_body: u64,
442        }
443
444        let config: Config =
445            serde_json::from_str(r#"{"timeout": 30, "max_body": 1048576}"#).unwrap();
446
447        assert_eq!(config.timeout, Duration::from_secs(30));
448        assert_eq!(config.max_body, 1_048_576);
449    }
450
451    #[test]
452    fn the_string_forms_deserialize_through_serde() {
453        #[derive(serde::Deserialize)]
454        struct Config {
455            #[serde(with = "super::duration")]
456            timeout: Duration,
457            #[serde(default, with = "super::duration::option")]
458            grace: Option<Duration>,
459            #[serde(with = "super::bytes")]
460            max_body: u64,
461        }
462
463        let config: Config =
464            serde_json::from_str(r#"{"timeout": "1h30m", "max_body": "64MiB"}"#).unwrap();
465
466        assert_eq!(config.timeout, Duration::from_secs(5_400));
467        assert_eq!(config.grace, None);
468        assert_eq!(config.max_body, 64 * 1_024 * 1_024);
469
470        let config: Config =
471            serde_json::from_str(r#"{"timeout": "1s", "grace": "250ms", "max_body": 1}"#).unwrap();
472
473        assert_eq!(config.grace, Some(Duration::from_millis(250)));
474    }
475
476    #[test]
477    fn a_duration_component_that_overflows_is_an_error_not_a_saturation() {
478        // Fits in u64, overflows when multiplied by 60.
479        let error = duration::parse("307445734561825861m").unwrap_err();
480
481        assert!(error.contains("overflows"), "{error}");
482    }
483}