diff --git a/crates/vm/src/convert/try_from.rs b/crates/vm/src/convert/try_from.rs index 4f921e9c5de..e950ec3e19e 100644 --- a/crates/vm/src/convert/try_from.rs +++ b/crates/vm/src/convert/try_from.rs @@ -126,10 +126,7 @@ impl TryFromObject for std::time::Duration { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { if let Some(float) = obj.downcast_ref::() { let f = float.to_f64(); - if f < 0.0 { - return Err(vm.new_value_error("negative duration")); - } - Ok(Self::from_secs_f64(f)) + duration_from_f64_floor(f, vm) } else if let Some(int) = obj.try_index_opt(vm) { let int = int?; let bigint = int.as_bigint(); @@ -149,3 +146,32 @@ impl TryFromObject for std::time::Duration { } } } + +fn duration_from_f64_floor(f: f64, vm: &VirtualMachine) -> PyResult { + const NANOS_PER_SEC_U128: u128 = 1_000_000_000; + const NANOS_PER_SEC: f64 = NANOS_PER_SEC_U128 as f64; + // Maximum duration representable by (secs, nanos) with u64 seconds and <1e9 nanoseconds. + const MAX_TOTAL_NANOS: u128 = + ((u64::MAX as u128) * NANOS_PER_SEC_U128) + (NANOS_PER_SEC_U128 - 1); + + if !f.is_finite() { + return Err(vm.new_value_error("value out of range")); + } + if f < 0.0 { + return Err(vm.new_value_error("negative duration")); + } + + let total_nanos = (f * NANOS_PER_SEC).floor(); + if total_nanos.is_sign_negative() { + return Err(vm.new_value_error("value out of range")); + } + let total_nanos_u128 = total_nanos as u128; + if total_nanos_u128 > MAX_TOTAL_NANOS { + return Err(vm.new_value_error("value out of range")); + } + + let secs = (total_nanos_u128 / NANOS_PER_SEC_U128) as u64; + let nanos = (total_nanos_u128 % NANOS_PER_SEC_U128) as u32; + + Ok(std::time::Duration::new(secs, nanos)) +}