Skip to content

Commit 1f5d8d4

Browse files
committed
Auto merge of #98246 - joshtriplett:times, r=m-ou-se
Support setting file accessed/modified timestamps Add `struct FileTimes` to contain the relevant file timestamps, since most platforms require setting all of them at once. (This also allows for future platform-specific extensions such as setting creation time.) Add `File::set_file_time` to set the timestamps for a `File`. Implement the `sys` backends for UNIX, macOS (which needs to fall back to `futimes` before macOS 10.13 because it lacks `futimens`), Windows, and WASI.
2 parents 25bb1c1 + f8061dd commit 1f5d8d4

File tree

9 files changed

+262
-3
lines changed

9 files changed

+262
-3
lines changed

library/std/src/fs.rs

+81
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ pub struct DirEntry(fs_imp::DirEntry);
184184
#[stable(feature = "rust1", since = "1.0.0")]
185185
pub struct OpenOptions(fs_imp::OpenOptions);
186186

187+
/// Representation of the various timestamps on a file.
188+
#[derive(Copy, Clone, Debug, Default)]
189+
#[unstable(feature = "file_set_times", issue = "98245")]
190+
pub struct FileTimes(fs_imp::FileTimes);
191+
187192
/// Representation of the various permissions on a file.
188193
///
189194
/// This module only currently provides one bit of information,
@@ -596,6 +601,58 @@ impl File {
596601
pub fn set_permissions(&self, perm: Permissions) -> io::Result<()> {
597602
self.inner.set_permissions(perm.0)
598603
}
604+
605+
/// Changes the timestamps of the underlying file.
606+
///
607+
/// # Platform-specific behavior
608+
///
609+
/// This function currently corresponds to the `futimens` function on Unix (falling back to
610+
/// `futimes` on macOS before 10.13) and the `SetFileTime` function on Windows. Note that this
611+
/// [may change in the future][changes].
612+
///
613+
/// [changes]: io#platform-specific-behavior
614+
///
615+
/// # Errors
616+
///
617+
/// This function will return an error if the user lacks permission to change timestamps on the
618+
/// underlying file. It may also return an error in other os-specific unspecified cases.
619+
///
620+
/// This function may return an error if the operating system lacks support to change one or
621+
/// more of the timestamps set in the `FileTimes` structure.
622+
///
623+
/// # Examples
624+
///
625+
/// ```no_run
626+
/// #![feature(file_set_times)]
627+
///
628+
/// fn main() -> std::io::Result<()> {
629+
/// use std::fs::{self, File, FileTimes};
630+
///
631+
/// let src = fs::metadata("src")?;
632+
/// let dest = File::options().write(true).open("dest")?;
633+
/// let times = FileTimes::new()
634+
/// .set_accessed(src.accessed()?)
635+
/// .set_modified(src.modified()?);
636+
/// dest.set_times(times)?;
637+
/// Ok(())
638+
/// }
639+
/// ```
640+
#[unstable(feature = "file_set_times", issue = "98245")]
641+
#[doc(alias = "futimens")]
642+
#[doc(alias = "futimes")]
643+
#[doc(alias = "SetFileTime")]
644+
pub fn set_times(&self, times: FileTimes) -> io::Result<()> {
645+
self.inner.set_times(times.0)
646+
}
647+
648+
/// Changes the modification time of the underlying file.
649+
///
650+
/// This is an alias for `set_times(FileTimes::new().set_modified(time))`.
651+
#[unstable(feature = "file_set_times", issue = "98245")]
652+
#[inline]
653+
pub fn set_modified(&self, time: SystemTime) -> io::Result<()> {
654+
self.set_times(FileTimes::new().set_modified(time))
655+
}
599656
}
600657

601658
// In addition to the `impl`s here, `File` also has `impl`s for
@@ -1252,6 +1309,30 @@ impl FromInner<fs_imp::FileAttr> for Metadata {
12521309
}
12531310
}
12541311

1312+
impl FileTimes {
1313+
/// Create a new `FileTimes` with no times set.
1314+
///
1315+
/// Using the resulting `FileTimes` in [`File::set_times`] will not modify any timestamps.
1316+
#[unstable(feature = "file_set_times", issue = "98245")]
1317+
pub fn new() -> Self {
1318+
Self::default()
1319+
}
1320+
1321+
/// Set the last access time of a file.
1322+
#[unstable(feature = "file_set_times", issue = "98245")]
1323+
pub fn set_accessed(mut self, t: SystemTime) -> Self {
1324+
self.0.set_accessed(t.into_inner());
1325+
self
1326+
}
1327+
1328+
/// Set the last modified time of a file.
1329+
#[unstable(feature = "file_set_times", issue = "98245")]
1330+
pub fn set_modified(mut self, t: SystemTime) -> Self {
1331+
self.0.set_modified(t.into_inner());
1332+
self
1333+
}
1334+
}
1335+
12551336
impl Permissions {
12561337
/// Returns `true` if these permissions describe a readonly (unwritable) file.
12571338
///

library/std/src/sys/unix/fs.rs

+88-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner};
2020
target_os = "watchos",
2121
))]
2222
use crate::sys::weak::syscall;
23-
#[cfg(target_os = "macos")]
23+
#[cfg(any(target_os = "android", target_os = "macos"))]
2424
use crate::sys::weak::weak;
2525

2626
use libc::{c_int, mode_t};
@@ -313,6 +313,9 @@ pub struct FilePermissions {
313313
mode: mode_t,
314314
}
315315

316+
#[derive(Copy, Clone)]
317+
pub struct FileTimes([libc::timespec; 2]);
318+
316319
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
317320
pub struct FileType {
318321
mode: mode_t,
@@ -507,6 +510,48 @@ impl FilePermissions {
507510
}
508511
}
509512

513+
impl FileTimes {
514+
pub fn set_accessed(&mut self, t: SystemTime) {
515+
self.0[0] = t.t.to_timespec().expect("Invalid system time");
516+
}
517+
518+
pub fn set_modified(&mut self, t: SystemTime) {
519+
self.0[1] = t.t.to_timespec().expect("Invalid system time");
520+
}
521+
}
522+
523+
struct TimespecDebugAdapter<'a>(&'a libc::timespec);
524+
525+
impl fmt::Debug for TimespecDebugAdapter<'_> {
526+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527+
f.debug_struct("timespec")
528+
.field("tv_sec", &self.0.tv_sec)
529+
.field("tv_nsec", &self.0.tv_nsec)
530+
.finish()
531+
}
532+
}
533+
534+
impl fmt::Debug for FileTimes {
535+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536+
f.debug_struct("FileTimes")
537+
.field("accessed", &TimespecDebugAdapter(&self.0[0]))
538+
.field("modified", &TimespecDebugAdapter(&self.0[1]))
539+
.finish()
540+
}
541+
}
542+
543+
impl Default for FileTimes {
544+
fn default() -> Self {
545+
// Redox doesn't appear to support `UTIME_OMIT`, so we stub it out here, and always return
546+
// an error in `set_times`.
547+
#[cfg(target_os = "redox")]
548+
let omit = libc::timespec { tv_sec: 0, tv_nsec: 0 };
549+
#[cfg(not(target_os = "redox"))]
550+
let omit = libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_OMIT as _ };
551+
Self([omit; 2])
552+
}
553+
}
554+
510555
impl FileType {
511556
pub fn is_dir(&self) -> bool {
512557
self.is(libc::S_IFDIR)
@@ -1029,6 +1074,48 @@ impl File {
10291074
cvt_r(|| unsafe { libc::fchmod(self.as_raw_fd(), perm.mode) })?;
10301075
Ok(())
10311076
}
1077+
1078+
pub fn set_times(&self, times: FileTimes) -> io::Result<()> {
1079+
cfg_if::cfg_if! {
1080+
if #[cfg(target_os = "redox")] {
1081+
// Redox doesn't appear to support `UTIME_OMIT`.
1082+
drop(times);
1083+
Err(io::const_io_error!(
1084+
io::ErrorKind::Unsupported,
1085+
"setting file times not supported",
1086+
))
1087+
} else if #[cfg(any(target_os = "android", target_os = "macos"))] {
1088+
// futimens requires macOS 10.13, and Android API level 19
1089+
cvt(unsafe {
1090+
weak!(fn futimens(c_int, *const libc::timespec) -> c_int);
1091+
match futimens.get() {
1092+
Some(futimens) => futimens(self.as_raw_fd(), times.0.as_ptr()),
1093+
#[cfg(target_os = "macos")]
1094+
None => {
1095+
fn ts_to_tv(ts: &libc::timespec) -> libc::timeval {
1096+
libc::timeval {
1097+
tv_sec: ts.tv_sec,
1098+
tv_usec: (ts.tv_nsec / 1000) as _
1099+
}
1100+
}
1101+
let timevals = [ts_to_tv(&times.0[0]), ts_to_tv(&times.0[1])];
1102+
libc::futimes(self.as_raw_fd(), timevals.as_ptr())
1103+
}
1104+
// futimes requires even newer Android.
1105+
#[cfg(target_os = "android")]
1106+
None => return Err(io::const_io_error!(
1107+
io::ErrorKind::Unsupported,
1108+
"setting file times requires Android API level >= 19",
1109+
)),
1110+
}
1111+
})?;
1112+
Ok(())
1113+
} else {
1114+
cvt(unsafe { libc::futimens(self.as_raw_fd(), times.0.as_ptr()) })?;
1115+
Ok(())
1116+
}
1117+
}
1118+
}
10321119
}
10331120

10341121
impl DirBuilder {

library/std/src/sys/unsupported/fs.rs

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub struct DirEntry(!);
1717
#[derive(Clone, Debug)]
1818
pub struct OpenOptions {}
1919

20+
#[derive(Copy, Clone, Debug, Default)]
21+
pub struct FileTimes {}
22+
2023
pub struct FilePermissions(!);
2124

2225
pub struct FileType(!);
@@ -86,6 +89,11 @@ impl fmt::Debug for FilePermissions {
8689
}
8790
}
8891

92+
impl FileTimes {
93+
pub fn set_accessed(&mut self, _t: SystemTime) {}
94+
pub fn set_modified(&mut self, _t: SystemTime) {}
95+
}
96+
8997
impl FileType {
9098
pub fn is_dir(&self) -> bool {
9199
self.0
@@ -237,6 +245,10 @@ impl File {
237245
pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> {
238246
self.0
239247
}
248+
249+
pub fn set_times(&self, _times: FileTimes) -> io::Result<()> {
250+
self.0
251+
}
240252
}
241253

242254
impl DirBuilder {

library/std/src/sys/wasi/fs.rs

+25
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ pub struct FilePermissions {
6363
readonly: bool,
6464
}
6565

66+
#[derive(Copy, Clone, Debug, Default)]
67+
pub struct FileTimes {
68+
accessed: Option<wasi::Timestamp>,
69+
modified: Option<wasi::Timestamp>,
70+
}
71+
6672
#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
6773
pub struct FileType {
6874
bits: wasi::Filetype,
@@ -112,6 +118,16 @@ impl FilePermissions {
112118
}
113119
}
114120

121+
impl FileTimes {
122+
pub fn set_accessed(&mut self, t: SystemTime) {
123+
self.accessed = Some(t.to_wasi_timestamp_or_panic());
124+
}
125+
126+
pub fn set_modified(&mut self, t: SystemTime) {
127+
self.modified = Some(t.to_wasi_timestamp_or_panic());
128+
}
129+
}
130+
115131
impl FileType {
116132
pub fn is_dir(&self) -> bool {
117133
self.bits == wasi::FILETYPE_DIRECTORY
@@ -459,6 +475,15 @@ impl File {
459475
unsupported()
460476
}
461477

478+
pub fn set_times(&self, times: FileTimes) -> io::Result<()> {
479+
self.fd.filestat_set_times(
480+
times.accessed.unwrap_or(0),
481+
times.modified.unwrap_or(0),
482+
times.accessed.map_or(0, |_| wasi::FSTFLAGS_ATIM)
483+
| times.modified.map_or(0, |_| wasi::FSTFLAGS_MTIM),
484+
)
485+
}
486+
462487
pub fn read_link(&self, file: &Path) -> io::Result<PathBuf> {
463488
read_link(&self.fd, file)
464489
}

library/std/src/sys/wasi/time.rs

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ impl SystemTime {
4747
SystemTime(Duration::from_nanos(ts))
4848
}
4949

50+
pub fn to_wasi_timestamp_or_panic(&self) -> wasi::Timestamp {
51+
self.0.as_nanos().try_into().expect("time does not fit in WASI timestamp")
52+
}
53+
5054
pub fn sub_time(&self, other: &SystemTime) -> Result<Duration, Duration> {
5155
self.0.checked_sub(other.0).ok_or_else(|| other.0 - self.0)
5256
}

library/std/src/sys/windows/c.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ pub struct SOCKADDR {
620620
}
621621

622622
#[repr(C)]
623-
#[derive(Copy, Clone)]
623+
#[derive(Copy, Clone, Debug, Default)]
624624
pub struct FILETIME {
625625
pub dwLowDateTime: DWORD,
626626
pub dwHighDateTime: DWORD,
@@ -888,6 +888,12 @@ extern "system" {
888888
pub fn GetSystemDirectoryW(lpBuffer: LPWSTR, uSize: UINT) -> UINT;
889889
pub fn RemoveDirectoryW(lpPathName: LPCWSTR) -> BOOL;
890890
pub fn SetFileAttributesW(lpFileName: LPCWSTR, dwFileAttributes: DWORD) -> BOOL;
891+
pub fn SetFileTime(
892+
hFile: BorrowedHandle<'_>,
893+
lpCreationTime: Option<&FILETIME>,
894+
lpLastAccessTime: Option<&FILETIME>,
895+
lpLastWriteTime: Option<&FILETIME>,
896+
) -> BOOL;
891897
pub fn SetLastError(dwErrCode: DWORD);
892898
pub fn GetCommandLineW() -> LPWSTR;
893899
pub fn GetTempPathW(nBufferLength: DWORD, lpBuffer: LPCWSTR) -> DWORD;

library/std/src/sys/windows/fs.rs

+31
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ pub struct FilePermissions {
8383
attrs: c::DWORD,
8484
}
8585

86+
#[derive(Copy, Clone, Debug, Default)]
87+
pub struct FileTimes {
88+
accessed: Option<c::FILETIME>,
89+
modified: Option<c::FILETIME>,
90+
}
91+
8692
#[derive(Debug)]
8793
pub struct DirBuilder;
8894

@@ -536,6 +542,21 @@ impl File {
536542
})?;
537543
Ok(())
538544
}
545+
546+
pub fn set_times(&self, times: FileTimes) -> io::Result<()> {
547+
let is_zero = |t: c::FILETIME| t.dwLowDateTime == 0 && t.dwHighDateTime == 0;
548+
if times.accessed.map_or(false, is_zero) || times.modified.map_or(false, is_zero) {
549+
return Err(io::const_io_error!(
550+
io::ErrorKind::InvalidInput,
551+
"Cannot set file timestamp to 0",
552+
));
553+
}
554+
cvt(unsafe {
555+
c::SetFileTime(self.as_handle(), None, times.accessed.as_ref(), times.modified.as_ref())
556+
})?;
557+
Ok(())
558+
}
559+
539560
/// Get only basic file information such as attributes and file times.
540561
fn basic_info(&self) -> io::Result<c::FILE_BASIC_INFO> {
541562
unsafe {
@@ -903,6 +924,16 @@ impl FilePermissions {
903924
}
904925
}
905926

927+
impl FileTimes {
928+
pub fn set_accessed(&mut self, t: SystemTime) {
929+
self.accessed = Some(t.into_inner());
930+
}
931+
932+
pub fn set_modified(&mut self, t: SystemTime) {
933+
self.modified = Some(t.into_inner());
934+
}
935+
}
936+
906937
impl FileType {
907938
fn new(attrs: c::DWORD, reparse_tag: c::DWORD) -> FileType {
908939
FileType { attributes: attrs, reparse_tag }

0 commit comments

Comments
 (0)