bindy/bind9/duration.rs
1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! Duration parsing for Go-style duration strings.
5//!
6//! Supports parsing duration strings in Go format (e.g., "720h", "30d", "4w") into
7//! Rust `std::time::Duration`. Validates bounds according to RNDC key rotation requirements.
8
9use anyhow::{bail, Context, Result};
10use std::time::Duration;
11
12use crate::constants::{MAX_ROTATION_INTERVAL_HOURS, MIN_ROTATION_INTERVAL_HOURS};
13
14const SECONDS_PER_HOUR: u64 = 3600;
15const SECONDS_PER_DAY: u64 = 86400;
16const SECONDS_PER_WEEK: u64 = 604_800;
17
18/// Parse a Go-style duration string into a Rust `Duration`.
19///
20/// Supported units:
21/// - `h` (hours): "720h" = 30 days
22/// - `d` (days): "30d" = 30 days
23/// - `w` (weeks): "4w" = 28 days
24///
25/// # Constraints
26///
27/// - **Minimum:** 1 hour (`MIN_ROTATION_INTERVAL_HOURS`)
28/// - **Maximum:** 8760 hours / 365 days (`MAX_ROTATION_INTERVAL_HOURS`)
29///
30/// These bounds ensure keys are rotated frequently enough for security compliance
31/// but not so frequently as to cause operational issues.
32///
33/// # Examples
34///
35/// ```
36/// use bindy::bind9::duration::parse_duration;
37/// use std::time::Duration;
38///
39/// // Parse hours
40/// assert_eq!(parse_duration("24h").unwrap(), Duration::from_secs(86400));
41///
42/// // Parse days
43/// assert_eq!(parse_duration("30d").unwrap(), Duration::from_secs(2_592_000));
44///
45/// // Parse weeks
46/// assert_eq!(parse_duration("4w").unwrap(), Duration::from_secs(2_419_200));
47///
48/// // Invalid formats return errors
49/// assert!(parse_duration("").is_err());
50/// assert!(parse_duration("10").is_err()); // Missing unit
51/// assert!(parse_duration("10x").is_err()); // Invalid unit
52/// ```
53///
54/// # Errors
55///
56/// Returns an error if:
57/// - The format is invalid (missing unit, non-numeric value)
58/// - The duration is below the minimum (1h)
59/// - The duration is above the maximum (8760h / 365d / 52w)
60///
61/// # Panics
62///
63/// May panic if the `MIN_ROTATION_INTERVAL_HOURS` or `MAX_ROTATION_INTERVAL_HOURS`
64/// constants overflow when multiplied by `SECONDS_PER_HOUR`. This should never
65/// happen with the current constant values.
66pub fn parse_duration(duration_str: &str) -> Result<Duration> {
67 // Validate non-empty
68 if duration_str.is_empty() {
69 bail!("Duration string cannot be empty");
70 }
71
72 // Find where digits end and unit begins
73 let split_pos = duration_str
74 .chars()
75 .position(|c| !c.is_ascii_digit())
76 .context("Duration must end with a unit (h, d, or w)")?;
77
78 // Split into value and unit
79 let (value_str, unit) = duration_str.split_at(split_pos);
80
81 // Parse numeric value
82 let value: u64 = value_str
83 .parse()
84 .context("Duration value must be a positive integer")?;
85
86 // Convert to seconds based on unit
87 let seconds = match unit {
88 "h" => value
89 .checked_mul(SECONDS_PER_HOUR)
90 .context("Duration value too large (overflow)")?,
91 "d" => value
92 .checked_mul(SECONDS_PER_DAY)
93 .context("Duration value too large (overflow)")?,
94 "w" => value
95 .checked_mul(SECONDS_PER_WEEK)
96 .context("Duration value too large (overflow)")?,
97 _ => {
98 bail!("Unsupported duration unit '{unit}'. Use 'h' (hours), 'd' (days), or 'w' (weeks)")
99 }
100 };
101
102 // Validate bounds
103 let min_seconds = MIN_ROTATION_INTERVAL_HOURS
104 .checked_mul(SECONDS_PER_HOUR)
105 .expect("MIN_ROTATION_INTERVAL_HOURS constant overflow");
106
107 let max_seconds = MAX_ROTATION_INTERVAL_HOURS
108 .checked_mul(SECONDS_PER_HOUR)
109 .expect("MAX_ROTATION_INTERVAL_HOURS constant overflow");
110
111 if seconds < min_seconds {
112 bail!(
113 "Duration '{duration_str}' is below minimum of {MIN_ROTATION_INTERVAL_HOURS}h (1 hour)"
114 );
115 }
116
117 if seconds > max_seconds {
118 bail!(
119 "Duration '{duration_str}' exceeds maximum of {MAX_ROTATION_INTERVAL_HOURS}h (365 days)"
120 );
121 }
122
123 Ok(Duration::from_secs(seconds))
124}
125
126#[cfg(test)]
127#[path = "duration_tests.rs"]
128mod duration_tests;