Skip to content

Commit 47f5abf

Browse files
committed
signed_duration: fix a panicking bug in TryFrom<SignedDuration> for std::time::Duration
Fixes #526
1 parent c3d960e commit 47f5abf

File tree

3 files changed

+75
-11
lines changed

3 files changed

+75
-11
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# CHANGELOG
22

3+
0.2.22 (2026-02-28)
4+
===================
5+
This release includes a bug fix where fallible conversions from signed
6+
durations to unsigned durations could panic in some cases.
7+
8+
Bug fixes:
9+
10+
* [#526](https://github.com/BurntSushi/jiff/issues/526):
11+
Fix a panicking bug that occurs for
12+
`std::time::Duration::try_from(SignedDuration::new(0, -1))`.
13+
14+
315
0.2.21 (2026-02-22)
416
===================
517
This release contains a performance improvement and a bug fix for

src/signed_duration.rs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,33 @@ impl core::fmt::Debug for SignedDuration {
24952495
}
24962496
}
24972497

2498+
/// Fallibly converts a [`std::time::Duration`] to a `SignedDuration`.
2499+
///
2500+
/// # Errors
2501+
///
2502+
/// This fails when the duration's second component exceeds `i64::MAX`.
2503+
///
2504+
/// # Examples
2505+
///
2506+
/// ```
2507+
/// use std::time::Duration;
2508+
///
2509+
/// use jiff::SignedDuration;
2510+
///
2511+
/// let dur = Duration::new(5, 123_000_000);
2512+
/// let sdur = SignedDuration::try_from(dur)?;
2513+
/// assert_eq!(sdur, SignedDuration::new(5, 123_000_000));
2514+
///
2515+
/// let dur = Duration::new(i64::MAX as u64, 999_999_999);
2516+
/// let sdur = SignedDuration::try_from(dur)?;
2517+
/// assert_eq!(sdur, SignedDuration::new(i64::MAX, 999_999_999));
2518+
///
2519+
/// // Some failure cases:
2520+
/// assert!(SignedDuration::try_from(Duration::new(i64::MAX as u64 + 1, 0)).is_err());
2521+
/// assert!(SignedDuration::try_from(Duration::new(u64::MAX, 0)).is_err());
2522+
///
2523+
/// # Ok::<(), Box<dyn std::error::Error>>(())
2524+
/// ```
24982525
impl TryFrom<Duration> for SignedDuration {
24992526
type Error = Error;
25002527

@@ -2507,16 +2534,40 @@ impl TryFrom<Duration> for SignedDuration {
25072534
}
25082535
}
25092536

2537+
/// Fallibly converts a `SignedDuration` to a [`std::time::Duration`].
2538+
///
2539+
/// # Errors
2540+
///
2541+
/// This fails when the signed duration is negative.
2542+
///
2543+
/// # Examples
2544+
///
2545+
/// ```
2546+
/// use std::time::Duration;
2547+
///
2548+
/// use jiff::SignedDuration;
2549+
///
2550+
/// let sdur = SignedDuration::new(5, 123_000_000);
2551+
/// let dur = Duration::try_from(sdur)?;
2552+
/// assert_eq!(dur, Duration::new(5, 123_000_000));
2553+
///
2554+
/// // Some failure cases:
2555+
/// assert!(Duration::try_from(SignedDuration::new(-5, 0)).is_err());
2556+
/// assert!(Duration::try_from(SignedDuration::new(-5, -1)).is_err());
2557+
/// assert!(Duration::try_from(SignedDuration::new(0, -1)).is_err());
2558+
///
2559+
/// # Ok::<(), Box<dyn std::error::Error>>(())
2560+
/// ```
25102561
impl TryFrom<SignedDuration> for Duration {
25112562
type Error = Error;
25122563

25132564
fn try_from(sd: SignedDuration) -> Result<Duration, Error> {
25142565
let secs = u64::try_from(sd.as_secs())
2515-
.map_err(|_| SpecialBoundsError::UnsignedDurationSeconds)?;
2516-
// Guaranteed to succeed because the above only succeeds
2517-
// when `sd` is non-negative. And when `sd` is non-negative,
2518-
// we are guaranteed that 0<=nanos<=999,999,999.
2519-
let nanos = u32::try_from(sd.subsec_nanos()).unwrap();
2566+
.map_err(|_| SpecialBoundsError::SignedToUnsignedDuration)?;
2567+
// This could still be negative in the case where
2568+
// `sd.as_secs()` is zero.
2569+
let nanos = u32::try_from(sd.subsec_nanos())
2570+
.map_err(|_| SpecialBoundsError::SignedToUnsignedDuration)?;
25202571
Ok(Duration::new(secs, nanos))
25212572
}
25222573
}

src/util/b.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ pub(crate) enum SpecialBoundsError {
710710
UnixNanoseconds,
711711
SignedDurationFloatOutOfRangeF32,
712712
SignedDurationFloatOutOfRangeF64,
713-
UnsignedDurationSeconds,
713+
SignedToUnsignedDuration,
714714
}
715715

716716
impl core::fmt::Display for SpecialBoundsError {
@@ -723,11 +723,12 @@ impl core::fmt::Display for SpecialBoundsError {
723723
UnixMicroseconds::MIN as i128 * (NANOS_PER_MICRO as i128),
724724
UnixMicroseconds::MAX as i128 * (NANOS_PER_MICRO as i128),
725725
),
726-
UnsignedDurationSeconds => (
727-
"unsigned duration seconds",
728-
u64::MIN as i128,
729-
u64::MAX as i128,
730-
),
726+
SignedToUnsignedDuration => {
727+
return f.write_str(
728+
"negative signed durations cannot be converted \
729+
to an unsigned duration",
730+
);
731+
}
731732
SignedDurationFloatOutOfRangeF32 => {
732733
return write!(
733734
f,

0 commit comments

Comments
 (0)