shell_rs/
touch.rs

1// Copyright (c) 2021 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by Apache-2.0 License that can be found
3// in the LICENSE file.
4
5use std::path::{Path, PathBuf};
6use std::time::SystemTime;
7
8use crate::error::Error;
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct Options {
12    /// Change the access time.
13    pub update_access: bool,
14
15    /// Change the modification time.
16    pub update_modification: bool,
17
18    /// Do not create any file.
19    pub no_create: bool,
20
21    /// Affect symbolic link instead of referenced file.
22    pub no_dereference: bool,
23
24    /// Use it instead of current time.
25    pub date: Option<SystemTime>,
26
27    /// Use this file's times instead of current time.
28    pub reference_file: Option<PathBuf>,
29}
30
31impl Default for Options {
32    fn default() -> Self {
33        Self {
34            update_access: true,
35            update_modification: true,
36            no_create: false,
37            no_dereference: false,
38            date: None,
39            reference_file: None,
40        }
41    }
42}
43
44/// Change file timestamps
45pub fn touch<P: AsRef<Path>>(file: P, options: &Options) -> Result<(), Error> {
46    let mut new_times = [nc::timespec_t::default(), nc::timespec_t::default()];
47    if let Some(ref ref_file) = options.reference_file {
48        let fd = unsafe { nc::openat(nc::AT_FDCWD, ref_file, nc::O_RDONLY, 0)? };
49        let mut statbuf = nc::stat_t::default();
50        unsafe { nc::fstat(fd, &mut statbuf)? };
51        unsafe { nc::close(fd)? };
52        new_times[0].tv_sec = statbuf.st_atime as isize;
53        new_times[0].tv_nsec = statbuf.st_atime_nsec as isize;
54        new_times[1].tv_sec = statbuf.st_mtime as isize;
55        new_times[1].tv_nsec = statbuf.st_mtime_nsec as isize;
56    } else {
57        let new_time = if let Some(date) = options.date {
58            date
59        } else {
60            SystemTime::now()
61        };
62        let duration = new_time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
63        new_times[0].tv_sec = duration.as_secs() as isize;
64        new_times[0].tv_nsec = (duration.as_nanos() % 1000) as isize;
65        new_times[1] = new_times[0];
66    }
67
68    let access = unsafe { nc::faccessat(nc::AT_FDCWD, file.as_ref(), nc::R_OK | nc::W_OK) };
69    if access.is_err() && options.no_create {
70        return access.map_err(Into::into);
71    }
72
73    let fd = unsafe {
74        nc::openat(
75            nc::AT_FDCWD,
76            file.as_ref(),
77            nc::O_WRONLY | nc::O_CREAT,
78            0o644,
79        )?
80    };
81
82    let mut statbuf = nc::stat_t::default();
83    unsafe { nc::fstat(fd, &mut statbuf)? };
84    unsafe { nc::close(fd)? };
85
86    if !options.update_access {
87        new_times[0].tv_sec = statbuf.st_atime as isize;
88        new_times[0].tv_nsec = statbuf.st_atime_nsec as isize;
89    }
90    if !options.update_modification {
91        new_times[1].tv_sec = statbuf.st_mtime as isize;
92        new_times[1].tv_nsec = statbuf.st_mtime_nsec as isize;
93    }
94
95    let flags = if options.no_dereference {
96        nc::AT_SYMLINK_NOFOLLOW
97    } else {
98        0
99    };
100    unsafe { nc::utimensat(nc::AT_FDCWD, file.as_ref(), &new_times, flags).map_err(Into::into) }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_touch() {
109        let file = "/tmp/touch.shell-rs";
110        assert!(touch(file, &Options::default()).is_ok());
111        assert!(touch(
112            file,
113            &Options {
114                no_create: true,
115                ..Options::default()
116            }
117        )
118        .is_ok());
119
120        assert!(touch(
121            file,
122            &Options {
123                reference_file: Some(PathBuf::from("/etc/passwd")),
124                ..Options::default()
125            }
126        )
127        .is_ok());
128    }
129}