shell-rs 0.2.6

Rust reimplementation of common coreutils APIs
Documentation
// 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 = unsafe { nc::openat(nc::AT_FDCWD, ref_file, nc::O_RDONLY, 0)? };
        let mut statbuf = nc::stat_t::default();
        unsafe { nc::fstat(fd, &mut statbuf)? };
        unsafe { 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 = unsafe { 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 = unsafe {
        nc::openat(
            nc::AT_FDCWD,
            file.as_ref(),
            nc::O_WRONLY | nc::O_CREAT,
            0o644,
        )?
    };

    let mut statbuf = nc::stat_t::default();
    unsafe { nc::fstat(fd, &mut statbuf)? };
    unsafe { 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
    };
    unsafe { 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());
    }
}