Skip to content

Commit df2d61c

Browse files
committed
tail: support hex parsing
1 parent c90bca4 commit df2d61c

File tree

4 files changed

+84
-20
lines changed

4 files changed

+84
-20
lines changed

src/uu/tail/src/args.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
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 (ToDO) kqueue Signum fundu
6+
// spell-checker:ignore (ToDO) kqueue Signum
77

88
use crate::paths::Input;
99
use crate::{Quotable, parse, platform};
1010
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
11-
use fundu::{DurationParser, SaturatingInto};
1211
use same_file::Handle;
1312
use std::ffi::OsString;
1413
use std::io::IsTerminal;
1514
use std::time::Duration;
1615
use uucore::error::{UResult, USimpleError, UUsageError};
1716
use uucore::parser::parse_size::{ParseSizeError, parse_size_u64};
17+
use uucore::parser::parse_time;
1818
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
1919
use uucore::{format_usage, help_about, help_usage, show_warning};
2020

@@ -228,22 +228,15 @@ impl Settings {
228228
};
229229

230230
if let Some(source) = matches.get_one::<String>(options::SLEEP_INT) {
231-
// Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`:
232-
// * doesn't panic on errors like `Duration::from_secs_f64` would.
233-
// * no precision loss, rounding errors or other floating point problems.
234-
// * evaluates to `Duration::MAX` if the parsed number would have exceeded
235-
// `DURATION::MAX` or `infinity` was given
236-
// * not applied here but it supports customizable time units and provides better error
237-
// messages
238-
settings.sleep_sec = match DurationParser::without_time_units().parse(source) {
239-
Ok(duration) => SaturatingInto::<Duration>::saturating_into(duration),
231+
settings.sleep_sec = match parse_time::from_str_without_suffix(source) {
232+
Ok(duration) => duration,
240233
Err(_) => {
241234
return Err(UUsageError::new(
242235
1,
243236
format!("invalid number of seconds: '{source}'"),
244237
));
245238
}
246-
}
239+
};
247240
}
248241

249242
if let Some(s) = matches.get_one::<String>(options::MAX_UNCHANGED_STATS) {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,21 +459,30 @@ pub(crate) fn parse<'a>(
459459
let mut digits = BigUint::zero();
460460
let mut scale = 0u64;
461461
let mut exponent = BigInt::zero();
462+
463+
// track whether there are digits before the decimal point
462464
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
463465
chars.next();
464466
digits = digits * base as u8 + d;
465467
}
466468

467469
// Parse fractional/exponent part of the number for supported bases.
468470
if matches!(base, Base::Decimal | Base::Hexadecimal) && target != ParseTarget::Integral {
471+
let mut digit_after_decimal = false;
469472
// Parse the fractional part of the number if there can be one and the input contains
470473
// a '.' decimal separator.
471-
if matches!(chars.peek(), Some(&(_, '.'))) {
474+
if let Some(&(decimal_idx, '.')) = chars.peek() {
472475
chars.next();
473476
while let Some(d) = chars.peek().and_then(|&(_, c)| base.digit(c)) {
477+
digit_after_decimal = true;
474478
chars.next();
475479
(digits, scale) = (digits * base as u8 + d, scale + 1);
476480
}
481+
482+
// fail if there's only a decimal point
483+
if decimal_idx == 0 && !digit_after_decimal {
484+
return Err(ExtendedParserError::NotNumeric);
485+
}
477486
}
478487

479488
let exp_char = match base {

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

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ use std::time::Duration;
5252
/// assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
5353
/// ```
5454
pub fn from_str(string: &str) -> Result<Duration, String> {
55+
from_str_inner(
56+
string,
57+
&[('s', 1), ('m', 60), ('h', 60 * 60), ('d', 60 * 60 * 24)],
58+
)
59+
}
60+
61+
pub fn from_str_without_suffix(string: &str) -> Result<Duration, String> {
62+
from_str_inner(string, &[])
63+
}
64+
65+
fn from_str_inner(string: &str, allowed_suffixes: &[(char, u32)]) -> Result<Duration, String> {
5566
// TODO: Switch to Duration::NANOSECOND if that ever becomes stable
5667
// https://github.com/rust-lang/rust/issues/57391
5768
const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
@@ -60,11 +71,7 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
6071
if len == 0 {
6172
return Err(format!("invalid time interval {}", string.quote()));
6273
}
63-
let num = match num_parser::parse(
64-
string,
65-
ParseTarget::Duration,
66-
&[('s', 1), ('m', 60), ('h', 60 * 60), ('d', 60 * 60 * 24)],
67-
) {
74+
let num = match num_parser::parse(string, ParseTarget::Duration, allowed_suffixes) {
6875
Ok(ebd) | Err(ExtendedParserError::Overflow(ebd)) => ebd,
6976
Err(ExtendedParserError::Underflow(_)) => return Ok(NANOSECOND_DURATION),
7077
_ => {
@@ -100,17 +107,19 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
100107
#[cfg(test)]
101108
mod tests {
102109

103-
use crate::parser::parse_time::from_str;
110+
use crate::parser::parse_time::{from_str, from_str_without_suffix};
104111
use std::time::Duration;
105112

106113
#[test]
107114
fn test_no_units() {
108115
assert_eq!(from_str("123"), Ok(Duration::from_secs(123)));
116+
assert_eq!(from_str_without_suffix("123"), Ok(Duration::from_secs(123)));
109117
}
110118

111119
#[test]
112120
fn test_units() {
113121
assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
122+
assert!(from_str_without_suffix("2d").is_err());
114123
}
115124

116125
#[test]
@@ -119,6 +128,10 @@ mod tests {
119128
assert_eq!(from_str("9223372036854775808d"), Ok(Duration::MAX));
120129
// ExtendedBigDecimal overflow
121130
assert_eq!(from_str("1e92233720368547758080"), Ok(Duration::MAX));
131+
assert_eq!(
132+
from_str_without_suffix("1e92233720368547758080"),
133+
Ok(Duration::MAX)
134+
);
122135
}
123136

124137
#[test]
@@ -136,6 +149,22 @@ mod tests {
136149
assert_eq!(from_str("1e-9"), Ok(NANOSECOND_DURATION));
137150
assert_eq!(from_str("1.9e-9"), Ok(NANOSECOND_DURATION));
138151
assert_eq!(from_str("2e-9"), Ok(Duration::from_nanos(2)));
152+
153+
// ExtendedBigDecimal underflow
154+
assert_eq!(
155+
from_str_without_suffix("1e-92233720368547758080"),
156+
Ok(NANOSECOND_DURATION)
157+
);
158+
// nanoseconds underflow (in Duration)
159+
assert_eq!(
160+
from_str_without_suffix("0.0000000001"),
161+
Ok(NANOSECOND_DURATION)
162+
);
163+
assert_eq!(from_str_without_suffix("1e-10"), Ok(NANOSECOND_DURATION));
164+
assert_eq!(from_str_without_suffix("9e-10"), Ok(NANOSECOND_DURATION));
165+
assert_eq!(from_str_without_suffix("1e-9"), Ok(NANOSECOND_DURATION));
166+
assert_eq!(from_str_without_suffix("1.9e-9"), Ok(NANOSECOND_DURATION));
167+
assert_eq!(from_str_without_suffix("2e-9"), Ok(Duration::from_nanos(2)));
139168
}
140169

141170
#[test]
@@ -144,6 +173,17 @@ mod tests {
144173
assert_eq!(from_str("0e-100"), Ok(Duration::ZERO));
145174
assert_eq!(from_str("0e-92233720368547758080"), Ok(Duration::ZERO));
146175
assert_eq!(from_str("0.000000000000000000000"), Ok(Duration::ZERO));
176+
177+
assert_eq!(from_str_without_suffix("0e-9"), Ok(Duration::ZERO));
178+
assert_eq!(from_str_without_suffix("0e-100"), Ok(Duration::ZERO));
179+
assert_eq!(
180+
from_str_without_suffix("0e-92233720368547758080"),
181+
Ok(Duration::ZERO)
182+
);
183+
assert_eq!(
184+
from_str_without_suffix("0.000000000000000000000"),
185+
Ok(Duration::ZERO)
186+
);
147187
}
148188

149189
#[test]
@@ -152,6 +192,10 @@ mod tests {
152192
from_str("0x1.1p-1"),
153193
Ok(Duration::from_secs_f64(0.53125f64))
154194
);
195+
assert_eq!(
196+
from_str_without_suffix("0x1.1p-1"),
197+
Ok(Duration::from_secs_f64(0.53125f64))
198+
);
155199
assert_eq!(
156200
from_str("0x1.1p-1d"),
157201
Ok(Duration::from_secs_f64(0.53125f64 * 3600.0 * 24.0))
@@ -162,26 +206,37 @@ mod tests {
162206
#[test]
163207
fn test_error_empty() {
164208
assert!(from_str("").is_err());
209+
assert!(from_str_without_suffix("").is_err());
165210
}
166211

167212
#[test]
168213
fn test_error_invalid_unit() {
169214
assert!(from_str("123X").is_err());
215+
assert!(from_str_without_suffix("123X").is_err());
170216
}
171217

172218
#[test]
173219
fn test_error_multi_bytes_characters() {
174220
assert!(from_str("10€").is_err());
221+
assert!(from_str_without_suffix("10€").is_err());
175222
}
176223

177224
#[test]
178225
fn test_error_invalid_magnitude() {
179226
assert!(from_str("12abc3s").is_err());
227+
assert!(from_str_without_suffix("12abc3s").is_err());
228+
}
229+
230+
#[test]
231+
fn test_error_only_point() {
232+
assert!(from_str(".").is_err());
233+
assert!(from_str_without_suffix(".").is_err());
180234
}
181235

182236
#[test]
183237
fn test_negative() {
184238
assert!(from_str("-1").is_err());
239+
assert!(from_str_without_suffix("-1").is_err());
185240
}
186241

187242
#[test]
@@ -191,6 +246,10 @@ mod tests {
191246
assert_eq!(from_str("infinityh"), Ok(Duration::MAX));
192247
assert_eq!(from_str("INF"), Ok(Duration::MAX));
193248
assert_eq!(from_str("INFs"), Ok(Duration::MAX));
249+
250+
assert_eq!(from_str_without_suffix("inf"), Ok(Duration::MAX));
251+
assert_eq!(from_str_without_suffix("infinity"), Ok(Duration::MAX));
252+
assert_eq!(from_str_without_suffix("INF"), Ok(Duration::MAX));
194253
}
195254

196255
#[test]
@@ -200,6 +259,10 @@ mod tests {
200259
assert!(from_str("-nanh").is_err());
201260
assert!(from_str("NAN").is_err());
202261
assert!(from_str("-NAN").is_err());
262+
263+
assert!(from_str_without_suffix("nan").is_err());
264+
assert!(from_str_without_suffix("NAN").is_err());
265+
assert!(from_str_without_suffix("-NAN").is_err());
203266
}
204267

205268
/// Test that capital letters are not allowed in suffixes.

tests/by-util/test_tail.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4425,7 +4425,6 @@ fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same
44254425
}
44264426

44274427
#[rstest]
4428-
#[case::exponent_exceed_float_max("1.0e100000")]
44294428
#[case::underscore_delimiter("1_000")]
44304429
#[case::only_point(".")]
44314430
#[case::space_in_primes("' '")]

0 commit comments

Comments
 (0)