1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Copyright (c) 2021 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
// Use of this source is governed by Apache-2.0 License that can be found
// in the LICENSE file.

use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::error::Error;

#[derive(Debug, Clone, PartialEq)]
pub struct Options {
    /// Change the access time.
    pub update_access: bool,

    /// Change the modification time.
    pub update_modification: bool,

    /// Do not create any file.
    pub no_create: bool,

    /// Affect symbolic link instead of referenced file.
    pub no_dereference: bool,

    /// Use it instead of current time.
    pub date: Option<SystemTime>,

    /// Use this file's times instead of current time.
    pub reference_file: Option<PathBuf>,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            update_access: true,
            update_modification: true,
            no_create: false,
            no_dereference: false,
            date: None,
            reference_file: None,
        }
    }
}

/// Change file timestamps
pub fn touch<P: AsRef<Path>>(file: P, options: &Options) -> Result<(), Error> {
    let mut new_times = [nc::timespec_t::default(), nc::timespec_t::default()];
    if let Some(ref ref_file) = options.reference_file {
        let fd = nc::openat(nc::AT_FDCWD, ref_file, nc::O_RDONLY, 0)?;
        let mut statbuf = nc::stat_t::default();
        nc::fstat(fd, &mut statbuf)?;
        nc::close(fd)?;
        new_times[0].tv_sec = statbuf.st_atime as isize;
        new_times[0].tv_nsec = statbuf.st_atime_nsec as isize;
        new_times[1].tv_sec = statbuf.st_mtime as isize;
        new_times[1].tv_nsec = statbuf.st_mtime_nsec as isize;
    } else {
        let new_time = if let Some(date) = options.date {
            date
        } else {
            SystemTime::now()
        };
        let duration = new_time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
        new_times[0].tv_sec = duration.as_secs() as isize;
        new_times[0].tv_nsec = (duration.as_nanos() % 1000) as isize;
        new_times[1] = new_times[0];
    }

    let access = nc::faccessat(nc::AT_FDCWD, file.as_ref(), nc::R_OK | nc::W_OK);
    if access.is_err() && options.no_create {
        return access.map_err(Into::into);
    }

    let fd = nc::openat(
        nc::AT_FDCWD,
        file.as_ref(),
        nc::O_WRONLY | nc::O_CREAT,
        0o644,
    )?;

    let mut statbuf = nc::stat_t::default();
    nc::fstat(fd, &mut statbuf)?;
    nc::close(fd)?;

    if !options.update_access {
        new_times[0].tv_sec = statbuf.st_atime as isize;
        new_times[0].tv_nsec = statbuf.st_atime_nsec as isize;
    }
    if !options.update_modification {
        new_times[1].tv_sec = statbuf.st_mtime as isize;
        new_times[1].tv_nsec = statbuf.st_mtime_nsec as isize;
    }

    let flags = if options.no_dereference {
        nc::AT_SYMLINK_NOFOLLOW
    } else {
        0
    };
    nc::utimensat(nc::AT_FDCWD, file.as_ref(), &new_times, flags).map_err(Into::into)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_touch() {
        let file = "/tmp/touch.shell-rs";
        assert!(touch(file, &Options::default()).is_ok());
        assert!(touch(
            file,
            &Options {
                no_create: true,
                ..Options::default()
            }
        )
        .is_ok());

        assert!(touch(
            file,
            &Options {
                reference_file: Some(PathBuf::from("/etc/passwd")),
                ..Options::default()
            }
        )
        .is_ok());
    }
}