Skip to content

Commit 88cf661

Browse files
authored
Merge pull request #7648 from drinkcat/parse_time-ebd
timeout: Use common parser to parse time duration
2 parents 295628a + edc213d commit 88cf661

File tree

3 files changed

+155
-25
lines changed

3 files changed

+155
-25
lines changed

src/uucore/src/lib/features/parser/num_parser.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,14 @@ fn construct_extended_big_decimal<'a>(
294294
scale: u64,
295295
exponent: BigInt,
296296
) -> Result<ExtendedBigDecimal, ExtendedParserError<'a, ExtendedBigDecimal>> {
297-
if digits == BigUint::zero() && negative {
298-
return Ok(ExtendedBigDecimal::MinusZero);
297+
if digits == BigUint::zero() {
298+
// Return return 0 if the digits are zero. In particular, we do not ever
299+
// return Overflow/Underflow errors in that case.
300+
return Ok(if negative {
301+
ExtendedBigDecimal::MinusZero
302+
} else {
303+
ExtendedBigDecimal::zero()
304+
});
299305
}
300306

301307
let sign = if negative { Sign::Minus } else { Sign::Plus };
@@ -712,6 +718,24 @@ mod tests {
712718
ExtendedBigDecimal::MinusZero
713719
))
714720
));
721+
722+
// But no Overflow/Underflow if the digits are 0.
723+
assert_eq!(
724+
ExtendedBigDecimal::extended_parse(&format!("0e{}", i64::MAX as u64 + 2)),
725+
Ok(ExtendedBigDecimal::zero()),
726+
);
727+
assert_eq!(
728+
ExtendedBigDecimal::extended_parse(&format!("-0.0e{}", i64::MAX as u64 + 3)),
729+
Ok(ExtendedBigDecimal::MinusZero)
730+
);
731+
assert_eq!(
732+
ExtendedBigDecimal::extended_parse(&format!("0.0000e{}", i64::MIN)),
733+
Ok(ExtendedBigDecimal::zero()),
734+
);
735+
assert_eq!(
736+
ExtendedBigDecimal::extended_parse(&format!("-0e{}", i64::MIN + 2)),
737+
Ok(ExtendedBigDecimal::MinusZero)
738+
);
715739
}
716740

717741
#[test]

src/uucore/src/lib/features/parser/parse_time.rs

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (vars) NANOS numstr
6+
// spell-checker:ignore (vars) NANOS numstr infinityh INFD nans nanh bigdecimal extendedbigdecimal
77
//! Parsing a duration from a string.
88
//!
99
//! Use the [`from_str`] function to parse a [`Duration`] from a string.
1010
11+
use crate::{
12+
display::Quotable,
13+
extendedbigdecimal::ExtendedBigDecimal,
14+
parser::num_parser::{ExtendedParser, ExtendedParserError},
15+
};
16+
use bigdecimal::BigDecimal;
17+
use num_traits::Signed;
18+
use num_traits::ToPrimitive;
19+
use num_traits::Zero;
1120
use std::time::Duration;
1221

13-
use crate::display::Quotable;
14-
1522
/// Parse a duration from a string.
1623
///
1724
/// The string may contain only a number, like "123" or "4.5", or it
@@ -26,9 +33,10 @@ use crate::display::Quotable;
2633
/// * "h" for hours,
2734
/// * "d" for days.
2835
///
29-
/// This function uses [`Duration::saturating_mul`] to compute the
30-
/// number of seconds, so it does not overflow. If overflow would have
31-
/// occurred, [`Duration::MAX`] is returned instead.
36+
/// This function does not overflow if large values are provided. If
37+
/// overflow would have occurred, [`Duration::MAX`] is returned instead.
38+
///
39+
/// If the value is smaller than 1 nanosecond, we return 1 nanosecond.
3240
///
3341
/// # Errors
3442
///
@@ -45,6 +53,10 @@ use crate::display::Quotable;
4553
/// assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
4654
/// ```
4755
pub fn from_str(string: &str) -> Result<Duration, String> {
56+
// TODO: Switch to Duration::NANOSECOND if that ever becomes stable
57+
// https://github.com/rust-lang/rust/issues/57391
58+
const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
59+
4860
let len = string.len();
4961
if len == 0 {
5062
return Err("empty string".to_owned());
@@ -58,27 +70,43 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
5870
'h' => (slice, 60 * 60),
5971
'd' => (slice, 60 * 60 * 24),
6072
val if !val.is_alphabetic() => (string, 1),
61-
_ => {
62-
if string == "inf" || string == "infinity" {
63-
("inf", 1)
64-
} else {
65-
return Err(format!("invalid time interval {}", string.quote()));
66-
}
67-
}
73+
_ => match string.to_ascii_lowercase().as_str() {
74+
"inf" | "infinity" => ("inf", 1),
75+
_ => return Err(format!("invalid time interval {}", string.quote())),
76+
},
77+
};
78+
let num = match ExtendedBigDecimal::extended_parse(numstr) {
79+
Ok(ebd) | Err(ExtendedParserError::Overflow(ebd)) => ebd,
80+
Err(ExtendedParserError::Underflow(_)) => return Ok(NANOSECOND_DURATION),
81+
_ => return Err(format!("invalid time interval {}", string.quote())),
6882
};
69-
let num = numstr
70-
.parse::<f64>()
71-
.map_err(|e| format!("invalid time interval {}: {}", string.quote(), e))?;
7283

73-
if num < 0. {
74-
return Err(format!("invalid time interval {}", string.quote()));
84+
// Allow non-negative durations (-0 is fine), and infinity.
85+
let num = match num {
86+
ExtendedBigDecimal::BigDecimal(bd) if !bd.is_negative() => bd,
87+
ExtendedBigDecimal::MinusZero => 0.into(),
88+
ExtendedBigDecimal::Infinity => return Ok(Duration::MAX),
89+
_ => return Err(format!("invalid time interval {}", string.quote())),
90+
};
91+
92+
// Pre-multiply times to avoid precision loss
93+
let num: BigDecimal = num * times;
94+
95+
// Transform to nanoseconds (9 digits after decimal point)
96+
let (nanos_bi, _) = num.with_scale(9).into_bigint_and_scale();
97+
98+
// If the value is smaller than a nanosecond, just return that.
99+
if nanos_bi.is_zero() && !num.is_zero() {
100+
return Ok(NANOSECOND_DURATION);
75101
}
76102

77103
const NANOS_PER_SEC: u32 = 1_000_000_000;
78-
let whole_secs = num.trunc();
79-
let nanos = (num.fract() * (NANOS_PER_SEC as f64)).trunc();
80-
let duration = Duration::new(whole_secs as u64, nanos as u32);
81-
Ok(duration.saturating_mul(times))
104+
let whole_secs: u64 = match (&nanos_bi / NANOS_PER_SEC).try_into() {
105+
Ok(whole_secs) => whole_secs,
106+
Err(_) => return Ok(Duration::MAX),
107+
};
108+
let nanos: u32 = (&nanos_bi % NANOS_PER_SEC).to_u32().unwrap();
109+
Ok(Duration::new(whole_secs, nanos))
82110
}
83111

84112
#[cfg(test)]
@@ -98,8 +126,49 @@ mod tests {
98126
}
99127

100128
#[test]
101-
fn test_saturating_mul() {
129+
fn test_overflow() {
130+
// u64 seconds overflow (in Duration)
102131
assert_eq!(from_str("9223372036854775808d"), Ok(Duration::MAX));
132+
// ExtendedBigDecimal overflow
133+
assert_eq!(from_str("1e92233720368547758080"), Ok(Duration::MAX));
134+
}
135+
136+
#[test]
137+
fn test_underflow() {
138+
// TODO: Switch to Duration::NANOSECOND if that ever becomes stable
139+
// https://github.com/rust-lang/rust/issues/57391
140+
const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
141+
142+
// ExtendedBigDecimal underflow
143+
assert_eq!(from_str("1e-92233720368547758080"), Ok(NANOSECOND_DURATION));
144+
// nanoseconds underflow (in Duration)
145+
assert_eq!(from_str("0.0000000001"), Ok(NANOSECOND_DURATION));
146+
assert_eq!(from_str("1e-10"), Ok(NANOSECOND_DURATION));
147+
assert_eq!(from_str("9e-10"), Ok(NANOSECOND_DURATION));
148+
assert_eq!(from_str("1e-9"), Ok(NANOSECOND_DURATION));
149+
assert_eq!(from_str("1.9e-9"), Ok(NANOSECOND_DURATION));
150+
assert_eq!(from_str("2e-9"), Ok(Duration::from_nanos(2)));
151+
}
152+
153+
#[test]
154+
fn test_zero() {
155+
assert_eq!(from_str("0e-9"), Ok(Duration::ZERO));
156+
assert_eq!(from_str("0e-100"), Ok(Duration::ZERO));
157+
assert_eq!(from_str("0e-92233720368547758080"), Ok(Duration::ZERO));
158+
assert_eq!(from_str("0.000000000000000000000"), Ok(Duration::ZERO));
159+
}
160+
161+
#[test]
162+
fn test_hex_float() {
163+
assert_eq!(
164+
from_str("0x1.1p-1"),
165+
Ok(Duration::from_secs_f64(0.53125f64))
166+
);
167+
assert_eq!(
168+
from_str("0x1.1p-1d"),
169+
Ok(Duration::from_secs_f64(0.53125f64 * 3600.0 * 24.0))
170+
);
171+
assert_eq!(from_str("0xfh"), Ok(Duration::from_secs(15 * 3600)));
103172
}
104173

105174
#[test]
@@ -127,12 +196,31 @@ mod tests {
127196
assert!(from_str("-1").is_err());
128197
}
129198

199+
#[test]
200+
fn test_infinity() {
201+
assert_eq!(from_str("inf"), Ok(Duration::MAX));
202+
assert_eq!(from_str("infinity"), Ok(Duration::MAX));
203+
assert_eq!(from_str("infinityh"), Ok(Duration::MAX));
204+
assert_eq!(from_str("INF"), Ok(Duration::MAX));
205+
assert_eq!(from_str("INFs"), Ok(Duration::MAX));
206+
}
207+
208+
#[test]
209+
fn test_nan() {
210+
assert!(from_str("nan").is_err());
211+
assert!(from_str("nans").is_err());
212+
assert!(from_str("-nanh").is_err());
213+
assert!(from_str("NAN").is_err());
214+
assert!(from_str("-NAN").is_err());
215+
}
216+
130217
/// Test that capital letters are not allowed in suffixes.
131218
#[test]
132219
fn test_no_capital_letters() {
133220
assert!(from_str("1S").is_err());
134221
assert!(from_str("1M").is_err());
135222
assert!(from_str("1H").is_err());
136223
assert!(from_str("1D").is_err());
224+
assert!(from_str("INFD").is_err());
137225
}
138226
}

tests/by-util/test_timeout.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ fn test_dont_overflow() {
125125
.no_output();
126126
}
127127

128+
#[test]
129+
fn test_dont_underflow() {
130+
new_ucmd!()
131+
.args(&[".0000000001", "sleep", "1"])
132+
.fails_with_code(124)
133+
.no_output();
134+
new_ucmd!()
135+
.args(&["1e-100", "sleep", "1"])
136+
.fails_with_code(124)
137+
.no_output();
138+
// Unlike GNU coreutils, we underflow to 1ns for very short timeouts.
139+
// https://debbugs.gnu.org/cgi/bugreport.cgi?bug=77535
140+
new_ucmd!()
141+
.args(&["1e-18172487393827593258", "sleep", "1"])
142+
.fails_with_code(124)
143+
.no_output();
144+
}
145+
128146
#[test]
129147
fn test_negative_interval() {
130148
new_ucmd!()

0 commit comments

Comments
 (0)