Skip to content

Commit 082a48a

Browse files
authored
Preserve signed filesystem timestamps in os.utime (#9047)
Extract the Python 3.14-compatible signed timestamp conversion from #8954, preserving floor rounding, float-or-index argument rules and signed divmod remainders. Reuse the existing Windows file-time helper rather than the mega patch range restriction, and keep the 3.14 argument diagnostics. Add three concise parser regressions to the existing os snippet without native filesystem range assumptions. Assisted-by: OpenAI Codex:model identifier unavailable
1 parent 64061cd commit 082a48a

5 files changed

Lines changed: 122 additions & 30 deletions

File tree

‎crates/host_env/src/os.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,20 @@ pub fn seek_fd(
605605
unsafe { suppress_iph!(libc::lseek(fd.as_raw(), position, how)) }.check_libc_neg()
606606
}
607607

608+
#[cfg(windows)]
609+
pub type FileTimeSeconds = libc::time64_t;
610+
#[cfg(any(unix, target_os = "wasi"))]
611+
#[cfg_attr(target_env = "musl", allow(deprecated))]
612+
pub type FileTimeSeconds = libc::time_t;
613+
614+
/// Seconds and nanoseconds passed to filesystem timestamp operations.
615+
#[cfg(any(unix, windows, target_os = "wasi"))]
616+
#[derive(Clone, Copy)]
617+
pub struct FileTime {
618+
pub seconds: FileTimeSeconds,
619+
pub nanoseconds: libc::c_long,
620+
}
621+
608622
#[cfg(windows)]
609623
fn filetime_from_duration(duration: Duration) -> FILETIME {
610624
let intervals = ((duration.as_secs() as i64 + 11644473600) * 10_000_000)

‎crates/host_env/src/posix.rs‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -762,12 +762,12 @@ pub fn fchmod(fd: BorrowedFd<'_>, mode: u32) -> std::io::Result<()> {
762762
#[cfg(target_os = "redox")]
763763
pub fn utimes(
764764
path: &Path,
765-
acc: core::time::Duration,
766-
modif: core::time::Duration,
765+
acc: crate::os::FileTime,
766+
modif: crate::os::FileTime,
767767
) -> std::io::Result<()> {
768-
let tv = |d: core::time::Duration| libc::timeval {
769-
tv_sec: d.as_secs() as _,
770-
tv_usec: d.subsec_micros() as _,
768+
let tv = |time: crate::os::FileTime| libc::timeval {
769+
tv_sec: time.seconds,
770+
tv_usec: (time.nanoseconds / 1_000) as _,
771771
};
772772
nix::sys::stat::utimes(path, &tv(acc).into(), &tv(modif).into()).map_err(std::io::Error::from)
773773
}
@@ -776,13 +776,13 @@ pub fn utimes(
776776
pub fn set_file_times_at(
777777
dir_fd: i32,
778778
path: &CStr,
779-
access: core::time::Duration,
780-
modified: core::time::Duration,
779+
access: crate::os::FileTime,
780+
modified: crate::os::FileTime,
781781
follow_symlinks: bool,
782782
) -> std::io::Result<()> {
783-
let ts = |d: core::time::Duration| libc::timespec {
784-
tv_sec: d.as_secs() as _,
785-
tv_nsec: d.subsec_nanos() as _,
783+
let ts = |time: crate::os::FileTime| libc::timespec {
784+
tv_sec: time.seconds,
785+
tv_nsec: time.nanoseconds as _,
786786
};
787787
let times = [ts(access), ts(modified)];
788788
let ret = unsafe {

‎crates/host_env/src/posix_wasi.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloc::ffi::CString;
2-
use core::{ffi::CStr, time::Duration};
2+
use core::ffi::CStr;
33
use rustix::fd::AsFd;
44
use std::{ffi::OsStr, io, path::Path};
55

@@ -14,13 +14,13 @@ pub fn stat_fd(fd: crate::crt_fd::Borrowed<'_>) -> io::Result<crate::fileutils::
1414
pub fn set_file_times_at(
1515
dir_fd: i32,
1616
path: &CStr,
17-
access: Duration,
18-
modified: Duration,
17+
access: crate::os::FileTime,
18+
modified: crate::os::FileTime,
1919
follow_symlinks: bool,
2020
) -> io::Result<()> {
21-
let ts = |d: Duration| libc::timespec {
22-
tv_sec: d.as_secs() as _,
23-
tv_nsec: d.subsec_nanos() as _,
21+
let ts = |time: crate::os::FileTime| libc::timespec {
22+
tv_sec: time.seconds,
23+
tv_nsec: time.nanoseconds as _,
2424
};
2525
let times = [ts(access), ts(modified)];
2626
unsafe {

‎crates/vm/src/stdlib/os.rs‎

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,9 @@ pub(super) mod _os {
206206
#[cfg(any(unix, windows))]
207207
use crate::utils::ToCString;
208208
use crate::{
209-
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
209+
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
210210
builtins::{
211-
PyBytesRef, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, PyTypeRef,
211+
PyBytesRef, PyFloat, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, PyTypeRef,
212212
},
213213
class::PyClassDef,
214214
common::lock::{OnceCell, PyRwLock},
@@ -225,11 +225,12 @@ pub(super) mod _os {
225225
},
226226
vm::VirtualMachine,
227227
};
228+
use core::hint::cold_path;
228229
#[cfg(not(windows))]
229230
use core::marker::PhantomData;
230-
use core::{hint::cold_path, time::Duration};
231231
use crossbeam_utils::atomic::AtomicCell;
232232
use rustpython_common::wtf8::Wtf8Buf;
233+
use rustpython_host_env::os::{FileTime, FileTimeSeconds};
233234
#[cfg(windows)]
234235
use rustpython_host_env::{nt as host_nt, windows::ToWideString};
235236

@@ -1838,6 +1839,56 @@ pub(super) mod _os {
18381839
follow_symlinks: FollowSymlinks,
18391840
}
18401841

1842+
fn file_time_seconds(obj: &PyObject, vm: &VirtualMachine) -> PyResult<FileTimeSeconds> {
1843+
obj.try_index(vm)
1844+
.and_then(|value| value.try_to_primitive(vm))
1845+
.map_err(|err| {
1846+
if err.fast_isinstance(vm.ctx.exceptions.overflow_error) {
1847+
vm.new_overflow_error("timestamp out of range for platform time_t")
1848+
} else {
1849+
err
1850+
}
1851+
})
1852+
}
1853+
1854+
fn file_time(obj: &PyObject, vm: &VirtualMachine) -> PyResult<FileTime> {
1855+
if let Some(float) = obj.downcast_ref::<PyFloat>() {
1856+
let value = float.to_f64();
1857+
if value.is_nan() {
1858+
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
1859+
}
1860+
let mut seconds = value.trunc();
1861+
let mut nanoseconds = (value.fract() * 1_000_000_000.0).floor();
1862+
if nanoseconds < 0.0 {
1863+
nanoseconds += 1_000_000_000.0;
1864+
seconds -= 1.0;
1865+
}
1866+
let minimum = FileTimeSeconds::MIN as f64;
1867+
// The signed maximum rounds up when represented by f64.
1868+
if !(minimum..-minimum).contains(&seconds) {
1869+
return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1870+
}
1871+
Ok(FileTime {
1872+
seconds: seconds as FileTimeSeconds,
1873+
nanoseconds: nanoseconds as libc::c_long,
1874+
})
1875+
} else {
1876+
let seconds = match file_time_seconds(obj, vm) {
1877+
Err(err) if err.fast_isinstance(vm.ctx.exceptions.type_error) => {
1878+
return Err(vm.new_type_error(format!(
1879+
"argument must be int or float, not {}",
1880+
obj.class().fully_qualified_name(vm)?
1881+
)));
1882+
}
1883+
result => result?,
1884+
};
1885+
Ok(FileTime {
1886+
seconds,
1887+
nanoseconds: 0,
1888+
})
1889+
}
1890+
}
1891+
18411892
#[pyfunction]
18421893
fn utime(args: UtimeArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
18431894
let parse_tup = |tup: &Py<PyTuple>| -> Option<(PyObjectRef, PyObjectRef)> {
@@ -1852,13 +1903,13 @@ pub(super) mod _os {
18521903
let (a, m) = parse_tup(&t).ok_or_else(|| {
18531904
vm.new_type_error("utime: 'times' must be either a tuple of two ints or None")
18541905
})?;
1855-
(a.try_into_value(vm)?, m.try_into_value(vm)?)
1906+
(file_time(&a, vm)?, file_time(&m, vm)?)
18561907
}
18571908
(None, Some(ns)) => {
18581909
let (a, m) = parse_tup(&ns)
18591910
.ok_or_else(|| vm.new_type_error("utime: 'ns' must be a tuple of two ints"))?;
18601911
let ns_in_sec: PyObjectRef = vm.ctx.new_int(1_000_000_000).into();
1861-
let ns_to_dur = |obj: PyObjectRef| {
1912+
let ns_to_time = |obj: PyObjectRef| {
18621913
let divmod = vm._divmod(&obj, &ns_in_sec)?;
18631914
let (div, rem) = divmod
18641915
.downcast_ref::<PyTuple>()
@@ -1870,16 +1921,31 @@ pub(super) mod _os {
18701921
divmod.class().name()
18711922
))
18721923
})?;
1873-
let secs = div.try_index(vm)?.try_to_primitive(vm)?;
1874-
let ns = rem.try_index(vm)?.try_to_primitive(vm)?;
1875-
Ok(Duration::new(secs, ns))
1924+
let seconds = file_time_seconds(&div, vm)?;
1925+
let rem = rem.try_index(vm)?;
1926+
let nanoseconds = rem.try_to_primitive::<libc::c_long>(vm).map_err(|err| {
1927+
if err.fast_isinstance(vm.ctx.exceptions.overflow_error) {
1928+
vm.new_overflow_error("Python int too large to convert to C long")
1929+
} else {
1930+
err
1931+
}
1932+
})?;
1933+
Ok(FileTime {
1934+
seconds,
1935+
nanoseconds,
1936+
})
18761937
};
1877-
// TODO: do validation to make sure this doesn't.. underflow?
1878-
(ns_to_dur(a)?, ns_to_dur(m)?)
1938+
(ns_to_time(a)?, ns_to_time(m)?)
18791939
}
18801940
(None, None) => {
18811941
let now = SystemTime::now();
18821942
let now = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
1943+
let now = FileTime {
1944+
seconds: now.as_secs().try_into().map_err(|_| {
1945+
vm.new_overflow_error("timestamp out of range for platform time_t")
1946+
})?,
1947+
nanoseconds: now.subsec_nanos() as libc::c_long,
1948+
};
18831949
(now, now)
18841950
}
18851951
(Some(_), Some(_)) => {
@@ -1893,8 +1959,8 @@ pub(super) mod _os {
18931959

18941960
fn utime_impl(
18951961
path: OsPath,
1896-
acc: Duration,
1897-
modif: Duration,
1962+
acc: FileTime,
1963+
modif: FileTime,
18981964
dir_fd: DirFd<'_, { UTIME_DIR_FD as usize }>,
18991965
_follow_symlinks: FollowSymlinks,
19001966
vm: &VirtualMachine,
@@ -1934,8 +2000,15 @@ pub(super) mod _os {
19342000
));
19352001
}
19362002

1937-
crate::host_env::os::set_file_times(&path, acc, modif)
1938-
.map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
2003+
let wide_path = path.to_wide_cstring(vm)?;
2004+
host_nt::set_file_times(
2005+
&wide_path,
2006+
acc.seconds,
2007+
acc.nanoseconds.into(),
2008+
modif.seconds,
2009+
modif.nanoseconds.into(),
2010+
)
2011+
.map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
19392012
}
19402013
}
19412014

‎extra_tests/snippets/stdlib_os.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
145145
assert os.write(fd, CONTENT2) == len(CONTENT2)
146146
os.close(fd)
147147

148+
# A valid negative access time must reach the invalid modification time.
149+
for atime in (-1, -0.25):
150+
assert_raises(TypeError, os.utime, fname, (atime, object()))
151+
assert_raises(TypeError, os.utime, fname, ns=(-1, object()))
152+
148153
fd = os.open(fname, os.O_WRONLY | os.O_APPEND)
149154
assert os.write(fd, CONTENT3) == len(CONTENT3)
150155
os.close(fd)

0 commit comments

Comments
 (0)