Skip to content

Commit 392dc17

Browse files
committed
feat: make timestamp args of utimensat and futimens optional
1 parent 3d3e6b9 commit 392dc17

File tree

4 files changed

+147
-13
lines changed

4 files changed

+147
-13
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ usage.
1717

1818
As an example of what Nix provides, examine the differences between what is
1919
exposed by libc and nix for the
20-
[gethostname](https://man7.org/linux/man-pages/man2/gethostname.2.html) system
20+
[gethostname](https://pubs.opengroup.org/onlinepubs/9699919799/functions/gethostname.html) system
2121
call:
2222

2323
```rust,ignore

src/sys/stat.rs

+61-4
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,69 @@ pub fn lutimes<P: ?Sized + NixPath>(
402402
Errno::result(res).map(drop)
403403
}
404404

405+
/// Timestamp update operation specifier used in [`utimensat`] and [`futimens`].
406+
#[cfg(not(target_os = "redox"))] // Redox does not support `UTIME_OMIT` and `UTIME_NOW`
407+
#[derive(Copy, Clone, Debug)]
408+
pub enum TimestampSpec {
409+
/// Leave the timestamp unchanged.
410+
Omit,
411+
/// Update the timestamp to `Now`
412+
Now,
413+
/// Update the timestamp to the specific value specified in `TimeSpec`.
414+
Set(TimeSpec),
415+
}
416+
417+
/// Construct a new [`libc::timespec`] instance according to the value of [`TimestampSpec`].
418+
///
419+
/// This is used in the implementations of [`futimens`] and [`utimensat`]. As an
420+
/// end user, you may NOT find this trait implementation useful.
421+
///
422+
/// [`timespec`]: libc::timespec
423+
#[cfg(not(target_os = "redox"))] // Redox does not support `UTIME_OMIT` and `UTIME_NOW`
424+
impl From<&TimestampSpec> for libc::timespec {
425+
fn from(spec: &TimestampSpec) -> Self {
426+
let mut default_value = super::time::zero_init_timespec();
427+
428+
match spec {
429+
TimestampSpec::Omit => default_value.tv_nsec = libc::UTIME_OMIT as _,
430+
TimestampSpec::Now => default_value.tv_nsec = libc::UTIME_NOW as _,
431+
TimestampSpec::Set(time) => {
432+
default_value.tv_sec = time.tv_sec();
433+
default_value.tv_nsec = time.tv_nsec();
434+
}
435+
}
436+
437+
default_value
438+
}
439+
}
440+
441+
442+
/// Change the access and modification times of the file specified by a file
443+
/// descriptor.
444+
///
445+
/// # References
446+
///
447+
/// [futimens(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html).
448+
#[cfg(not(target_os = "redox"))] // Redox does not support `UTIME_OMIT` and `UTIME_NOW`
449+
#[inline]
450+
pub fn futimens(
451+
fd: RawFd,
452+
atime: &TimestampSpec,
453+
mtime: &TimestampSpec,
454+
) -> Result<()> {
455+
let times = [atime.into(), mtime.into()];
456+
let res = unsafe { libc::futimens(fd, &times[0]) };
457+
458+
Errno::result(res).map(drop)
459+
}
460+
405461
/// Change the access and modification times of the file specified by a file descriptor.
406462
///
407463
/// # References
408464
///
409465
/// [futimens(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html).
410466
#[inline]
467+
#[cfg(target_os = "redox")]
411468
pub fn futimens(fd: RawFd, atime: &TimeSpec, mtime: &TimeSpec) -> Result<()> {
412469
let times: [libc::timespec; 2] = [*atime.as_ref(), *mtime.as_ref()];
413470
let res = unsafe { libc::futimens(fd, &times[0]) };
@@ -439,20 +496,20 @@ pub enum UtimensatFlags {
439496
/// # References
440497
///
441498
/// [utimensat(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/utimens.html).
442-
#[cfg(not(target_os = "redox"))]
499+
#[cfg(not(target_os = "redox"))] // Redox does not support `utimensat(2)` syscall
443500
#[cfg_attr(docsrs, doc(cfg(all())))]
444501
pub fn utimensat<P: ?Sized + NixPath>(
445502
dirfd: Option<RawFd>,
446503
path: &P,
447-
atime: &TimeSpec,
448-
mtime: &TimeSpec,
504+
atime: &TimestampSpec,
505+
mtime: &TimestampSpec,
449506
flag: UtimensatFlags,
450507
) -> Result<()> {
451508
let atflag = match flag {
452509
UtimensatFlags::FollowSymlink => AtFlags::empty(),
453510
UtimensatFlags::NoFollowSymlink => AtFlags::AT_SYMLINK_NOFOLLOW,
454511
};
455-
let times: [libc::timespec; 2] = [*atime.as_ref(), *mtime.as_ref()];
512+
let times = [atime.into(), mtime.into()];
456513
let res = path.with_nix_path(|cstr| unsafe {
457514
libc::utimensat(
458515
at_rawfd(dirfd),

src/sys/time.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::convert::From;
66
use std::time::Duration;
77
use std::{cmp, fmt, ops};
88

9-
const fn zero_init_timespec() -> timespec {
9+
pub(crate) const fn zero_init_timespec() -> timespec {
1010
// `std::mem::MaybeUninit::zeroed()` is not yet a const fn
1111
// (https://github.com/rust-lang/rust/issues/91850) so we will instead initialize an array of
1212
// the appropriate size to zero and then transmute it to a timespec value.

test/test_stat.rs

+84-7
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ use nix::sys::stat::utimensat;
3232
#[cfg(not(target_os = "redox"))]
3333
use nix::sys::stat::FchmodatFlags;
3434
use nix::sys::stat::Mode;
35-
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
36-
use nix::sys::stat::UtimensatFlags;
3735
#[cfg(not(target_os = "redox"))]
3836
use nix::sys::stat::{self};
3937
use nix::sys::stat::{fchmod, stat};
4038
#[cfg(not(target_os = "redox"))]
4139
use nix::sys::stat::{fchmodat, mkdirat};
4240
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
4341
use nix::sys::stat::{futimens, utimes};
42+
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
43+
use nix::sys::stat::{TimestampSpec, UtimensatFlags};
4444

4545
#[cfg(not(any(target_os = "netbsd", target_os = "redox")))]
4646
use nix::sys::stat::FileStat;
@@ -275,10 +275,47 @@ fn test_futimens() {
275275
let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty())
276276
.unwrap();
277277

278-
futimens(fd, &TimeSpec::seconds(10), &TimeSpec::seconds(20)).unwrap();
278+
futimens(
279+
fd,
280+
&TimestampSpec::Set(TimeSpec::seconds(10)),
281+
&TimestampSpec::Set(TimeSpec::seconds(20)),
282+
)
283+
.unwrap();
279284
assert_times_eq(10, 20, &fs::metadata(&fullpath).unwrap());
280285
}
281286

287+
#[test]
288+
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
289+
fn test_futimens_unchanged() {
290+
let tempdir = tempfile::tempdir().unwrap();
291+
let fullpath = tempdir.path().join("file");
292+
drop(File::create(&fullpath).unwrap());
293+
let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty())
294+
.unwrap();
295+
296+
let old_atime = fs::metadata(fullpath.as_path())
297+
.unwrap()
298+
.accessed()
299+
.unwrap();
300+
let old_mtime = fs::metadata(fullpath.as_path())
301+
.unwrap()
302+
.modified()
303+
.unwrap();
304+
305+
futimens(fd, &TimestampSpec::Omit, &TimestampSpec::Omit).unwrap();
306+
307+
let new_atime = fs::metadata(fullpath.as_path())
308+
.unwrap()
309+
.accessed()
310+
.unwrap();
311+
let new_mtime = fs::metadata(fullpath.as_path())
312+
.unwrap()
313+
.modified()
314+
.unwrap();
315+
assert_eq!(old_atime, new_atime);
316+
assert_eq!(old_mtime, new_mtime);
317+
}
318+
282319
#[test]
283320
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
284321
fn test_utimensat() {
@@ -295,8 +332,8 @@ fn test_utimensat() {
295332
utimensat(
296333
Some(dirfd),
297334
filename,
298-
&TimeSpec::seconds(12345),
299-
&TimeSpec::seconds(678),
335+
&TimestampSpec::Set(TimeSpec::seconds(12345)),
336+
&TimestampSpec::Set(TimeSpec::seconds(678)),
300337
UtimensatFlags::FollowSymlink,
301338
)
302339
.unwrap();
@@ -307,14 +344,54 @@ fn test_utimensat() {
307344
utimensat(
308345
None,
309346
filename,
310-
&TimeSpec::seconds(500),
311-
&TimeSpec::seconds(800),
347+
&TimestampSpec::Set(TimeSpec::seconds(500)),
348+
&TimestampSpec::Set(TimeSpec::seconds(800)),
312349
UtimensatFlags::FollowSymlink,
313350
)
314351
.unwrap();
315352
assert_times_eq(500, 800, &fs::metadata(&fullpath).unwrap());
316353
}
317354

355+
#[test]
356+
#[cfg(not(any(target_os = "redox", target_os = "haiku")))]
357+
fn test_utimensat_unchanged() {
358+
let _dr = crate::DirRestore::new();
359+
let tempdir = tempfile::tempdir().unwrap();
360+
let filename = "foo.txt";
361+
let fullpath = tempdir.path().join(filename);
362+
drop(File::create(&fullpath).unwrap());
363+
let dirfd =
364+
fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty())
365+
.unwrap();
366+
367+
let old_atime = fs::metadata(fullpath.as_path())
368+
.unwrap()
369+
.accessed()
370+
.unwrap();
371+
let old_mtime = fs::metadata(fullpath.as_path())
372+
.unwrap()
373+
.modified()
374+
.unwrap();
375+
utimensat(
376+
Some(dirfd),
377+
filename,
378+
&TimestampSpec::Omit,
379+
&TimestampSpec::Omit,
380+
UtimensatFlags::NoFollowSymlink,
381+
)
382+
.unwrap();
383+
let new_atime = fs::metadata(fullpath.as_path())
384+
.unwrap()
385+
.accessed()
386+
.unwrap();
387+
let new_mtime = fs::metadata(fullpath.as_path())
388+
.unwrap()
389+
.modified()
390+
.unwrap();
391+
assert_eq!(old_atime, new_atime);
392+
assert_eq!(old_mtime, new_mtime);
393+
}
394+
318395
#[test]
319396
#[cfg(not(target_os = "redox"))]
320397
fn test_mkdirat_success_path() {

0 commit comments

Comments
 (0)