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