Skip to content

Commit 8cb9c44

Browse files
committed
Eliminate overflow in Duration constructors
1 parent d0bab6a commit 8cb9c44

File tree

5 files changed

+92
-17
lines changed

5 files changed

+92
-17
lines changed

src/duration.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,18 @@ impl Duration {
227227
/// assert_eq!(Duration::new(1, 2_000_000_000), 3.seconds());
228228
/// ```
229229
pub const fn new(mut seconds: i64, mut nanoseconds: i32) -> Self {
230-
seconds += nanoseconds as i64 / 1_000_000_000;
230+
seconds = expect_opt!(
231+
seconds.checked_add(nanoseconds as i64 / 1_000_000_000),
232+
"overflow constructing `time::Duration`"
233+
);
231234
nanoseconds %= 1_000_000_000;
232235

233236
if seconds > 0 && nanoseconds < 0 {
237+
// `seconds` cannot overflow here because it is positive.
234238
seconds -= 1;
235239
nanoseconds += 1_000_000_000;
236240
} else if seconds < 0 && nanoseconds > 0 {
241+
// `seconds` cannot overflow here because it is negative.
237242
seconds += 1;
238243
nanoseconds -= 1_000_000_000;
239244
}
@@ -249,7 +254,10 @@ impl Duration {
249254
/// assert_eq!(Duration::weeks(1), 604_800.seconds());
250255
/// ```
251256
pub const fn weeks(weeks: i64) -> Self {
252-
Self::seconds(weeks * 604_800)
257+
Self::seconds(expect_opt!(
258+
weeks.checked_mul(604_800),
259+
"overflow constructing `time::Duration`"
260+
))
253261
}
254262

255263
/// Create a new `Duration` with the given number of days. Equivalent to
@@ -260,7 +268,10 @@ impl Duration {
260268
/// assert_eq!(Duration::days(1), 86_400.seconds());
261269
/// ```
262270
pub const fn days(days: i64) -> Self {
263-
Self::seconds(days * 86_400)
271+
Self::seconds(expect_opt!(
272+
days.checked_mul(86_400),
273+
"overflow constructing `time::Duration`"
274+
))
264275
}
265276

266277
/// Create a new `Duration` with the given number of hours. Equivalent to
@@ -271,7 +282,10 @@ impl Duration {
271282
/// assert_eq!(Duration::hours(1), 3_600.seconds());
272283
/// ```
273284
pub const fn hours(hours: i64) -> Self {
274-
Self::seconds(hours * 3_600)
285+
Self::seconds(expect_opt!(
286+
hours.checked_mul(3_600),
287+
"overflow constructing `time::Duration`"
288+
))
275289
}
276290

277291
/// Create a new `Duration` with the given number of minutes. Equivalent to
@@ -282,7 +296,10 @@ impl Duration {
282296
/// assert_eq!(Duration::minutes(1), 60.seconds());
283297
/// ```
284298
pub const fn minutes(minutes: i64) -> Self {
285-
Self::seconds(minutes * 60)
299+
Self::seconds(expect_opt!(
300+
minutes.checked_mul(60),
301+
"overflow constructing `time::Duration`"
302+
))
286303
}
287304

288305
/// Create a new `Duration` with the given number of seconds.
@@ -303,6 +320,9 @@ impl Duration {
303320
/// assert_eq!(Duration::seconds_f64(-0.5), -0.5.seconds());
304321
/// ```
305322
pub fn seconds_f64(seconds: f64) -> Self {
323+
if seconds > i64::MAX as f64 || seconds < i64::MIN as f64 {
324+
crate::expect_failed("overflow constructing `time::Duration`");
325+
}
306326
Self::new_unchecked(seconds as _, ((seconds % 1.) * 1_000_000_000.) as _)
307327
}
308328

@@ -314,6 +334,9 @@ impl Duration {
314334
/// assert_eq!(Duration::seconds_f32(-0.5), (-0.5).seconds());
315335
/// ```
316336
pub fn seconds_f32(seconds: f32) -> Self {
337+
if seconds > i64::MAX as f32 || seconds < i64::MIN as f32 {
338+
crate::expect_failed("overflow constructing `time::Duration`");
339+
}
317340
Self::new_unchecked(seconds as _, ((seconds % 1.) * 1_000_000_000.) as _)
318341
}
319342

@@ -364,10 +387,14 @@ impl Duration {
364387
/// As the input range cannot be fully mapped to the output, this should only be used where it's
365388
/// known to result in a valid value.
366389
pub(crate) const fn nanoseconds_i128(nanoseconds: i128) -> Self {
367-
Self::new_unchecked(
368-
(nanoseconds / 1_000_000_000) as _,
369-
(nanoseconds % 1_000_000_000) as _,
370-
)
390+
let seconds = nanoseconds / 1_000_000_000;
391+
let nanoseconds = nanoseconds % 1_000_000_000;
392+
393+
if seconds > i64::MAX as i128 || seconds < i64::MIN as i128 {
394+
crate::expect_failed("overflow constructing `time::Duration`");
395+
}
396+
397+
Self::new_unchecked(seconds as _, nanoseconds as _)
371398
}
372399
// endregion constructors
373400

src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,18 @@ macro_rules! const_try_opt {
283283
}
284284
};
285285
}
286+
287+
/// Try to unwrap an expression, panicking if not possible.
288+
///
289+
/// This is similar to `$e.expect($message)`, but is usable in `const` contexts.
290+
macro_rules! expect_opt {
291+
($e:expr, $message:literal) => {
292+
match $e {
293+
Some(value) => value,
294+
None => crate::expect_failed($message),
295+
}
296+
};
297+
}
286298
// endregion macros
287299

288300
mod date;
@@ -334,3 +346,11 @@ pub use crate::weekday::Weekday;
334346

335347
/// An alias for [`std::result::Result`] with a generic error from the time crate.
336348
pub type Result<T> = core::result::Result<T, Error>;
349+
350+
/// This is a separate function to reduce the code size of `expect_opt!`.
351+
#[inline(never)]
352+
#[cold]
353+
#[track_caller]
354+
const fn expect_failed(message: &str) -> ! {
355+
panic!("{}", message)
356+
}

tests/integration/duration.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ fn new() {
8282
assert_eq!(Duration::new(1, -1_400_000_000), (-400).milliseconds());
8383
assert_eq!(Duration::new(2, -1_400_000_000), 600.milliseconds());
8484
assert_eq!(Duration::new(3, -1_400_000_000), 1_600.milliseconds());
85+
86+
assert_panic!(Duration::new(i64::MAX, 1_000_000_000));
87+
assert_panic!(Duration::new(i64::MIN, -1_000_000_000));
8588
}
8689

8790
#[test]
@@ -90,6 +93,9 @@ fn weeks() {
9093
assert_eq!(Duration::weeks(2), (2 * 604_800).seconds());
9194
assert_eq!(Duration::weeks(-1), (-604_800).seconds());
9295
assert_eq!(Duration::weeks(-2), (2 * -604_800).seconds());
96+
97+
assert_panic!(Duration::weeks(i64::MAX));
98+
assert_panic!(Duration::weeks(i64::MIN));
9399
}
94100

95101
#[test]
@@ -106,6 +112,9 @@ fn days() {
106112
assert_eq!(Duration::days(2), (2 * 86_400).seconds());
107113
assert_eq!(Duration::days(-1), (-86_400).seconds());
108114
assert_eq!(Duration::days(-2), (2 * -86_400).seconds());
115+
116+
assert_panic!(Duration::days(i64::MAX));
117+
assert_panic!(Duration::days(i64::MIN));
109118
}
110119

111120
#[test]
@@ -122,6 +131,9 @@ fn hours() {
122131
assert_eq!(Duration::hours(2), (2 * 3_600).seconds());
123132
assert_eq!(Duration::hours(-1), (-3_600).seconds());
124133
assert_eq!(Duration::hours(-2), (2 * -3_600).seconds());
134+
135+
assert_panic!(Duration::hours(i64::MAX));
136+
assert_panic!(Duration::hours(i64::MIN));
125137
}
126138

127139
#[test]
@@ -138,6 +150,9 @@ fn minutes() {
138150
assert_eq!(Duration::minutes(2), (2 * 60).seconds());
139151
assert_eq!(Duration::minutes(-1), (-60).seconds());
140152
assert_eq!(Duration::minutes(-2), (2 * -60).seconds());
153+
154+
assert_panic!(Duration::minutes(i64::MAX));
155+
assert_panic!(Duration::minutes(i64::MIN));
141156
}
142157

143158
#[test]
@@ -168,6 +183,9 @@ fn whole_seconds() {
168183
fn seconds_f64() {
169184
assert_eq!(Duration::seconds_f64(0.5), 0.5.seconds());
170185
assert_eq!(Duration::seconds_f64(-0.5), (-0.5).seconds());
186+
187+
assert_panic!(Duration::seconds_f64(f64::MAX));
188+
assert_panic!(Duration::seconds_f64(f64::MIN));
171189
}
172190

173191
#[test]
@@ -185,6 +203,9 @@ fn as_seconds_f64() {
185203
fn seconds_f32() {
186204
assert_eq!(Duration::seconds_f32(0.5), 0.5.seconds());
187205
assert_eq!(Duration::seconds_f32(-0.5), (-0.5).seconds());
206+
207+
assert_panic!(Duration::seconds_f32(f32::MAX));
208+
assert_panic!(Duration::seconds_f32(f32::MIN));
188209
}
189210

190211
#[test]
@@ -600,6 +621,9 @@ fn std_sub_assign_overflow() {
600621
fn mul_int() {
601622
assert_eq!(1.seconds() * 2, 2.seconds());
602623
assert_eq!(1.seconds() * -2, (-2).seconds());
624+
625+
assert_panic!(Duration::MAX * 2);
626+
assert_panic!(Duration::MIN * 2);
603627
}
604628

605629
#[test]

tests/integration/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ macro_rules! modifier {
7171
(@value $field:ident $value:expr) => ($value);
7272
}
7373

74+
/// Assert that the given expression panics.
75+
macro_rules! assert_panic {
76+
($($x:tt)*) => {
77+
assert!(std::panic::catch_unwind(|| {
78+
$($x)*
79+
})
80+
.is_err())
81+
}
82+
}
83+
7484
mod date;
7585
mod derives;
7686
mod duration;

tests/integration/offset_date_time.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,8 @@ fn to_offset() {
5151

5252
#[test]
5353
fn to_offset_panic() {
54-
assert!(
55-
std::panic::catch_unwind(|| { PrimitiveDateTime::MAX.assume_utc().to_offset(offset!(+1)) })
56-
.is_err()
57-
);
58-
assert!(
59-
std::panic::catch_unwind(|| { PrimitiveDateTime::MIN.assume_utc().to_offset(offset!(-1)) })
60-
.is_err()
61-
);
54+
assert_panic!(PrimitiveDateTime::MAX.assume_utc().to_offset(offset!(+1)));
55+
assert_panic!(PrimitiveDateTime::MIN.assume_utc().to_offset(offset!(-1)));
6256
}
6357

6458
#[test]

0 commit comments

Comments
 (0)