#[cfg(not(unix))]
fn main() {
eprintln!("touch: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::ffi::CString;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::process;
#[cfg(unix)]
const TOOL_NAME: &str = "touch";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg(unix)]
enum TimeTarget {
Both,
AccessOnly,
ModifyOnly,
}
#[derive(Clone, Copy)]
#[cfg(unix)]
struct TimePair {
atime_sec: i64,
atime_nsec: i64,
mtime_sec: i64,
mtime_nsec: i64,
}
#[cfg(unix)]
fn current_time() -> (i64, i64) {
let mut tv = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
unsafe {
libc::clock_gettime(libc::CLOCK_REALTIME, &mut tv);
}
(tv.tv_sec, tv.tv_nsec)
}
#[cfg(unix)]
fn get_file_times(path: &str) -> Result<TimePair, std::io::Error> {
use std::os::unix::fs::MetadataExt;
let meta = fs::symlink_metadata(path)?;
Ok(TimePair {
atime_sec: meta.atime(),
atime_nsec: meta.atime_nsec(),
mtime_sec: meta.mtime(),
mtime_nsec: meta.mtime_nsec(),
})
}
#[cfg(unix)]
fn parse_touch_timestamp(s: &str) -> Result<(i64, i64), String> {
let (main_part, seconds) = if let Some(dot_pos) = s.rfind('.') {
let secs_str = &s[dot_pos + 1..];
if secs_str.len() != 2 {
return Err(format!("invalid date format '{}'", s));
}
let secs: u32 = secs_str
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
if secs > 60 {
return Err(format!("invalid date format '{}'", s));
}
(&s[..dot_pos], secs)
} else {
(s, 0u32)
};
let (year, month, day, hour, minute) = match main_part.len() {
8 => {
let now = current_time();
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
unsafe {
libc::localtime_r(&now.0, &mut tm);
}
let year = tm.tm_year + 1900;
let month: u32 = main_part[0..2]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let day: u32 = main_part[2..4]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let hour: u32 = main_part[4..6]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let minute: u32 = main_part[6..8]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
(year, month, day, hour, minute)
}
10 => {
let yy: i32 = main_part[0..2]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let year = if yy >= 69 { 1900 + yy } else { 2000 + yy };
let month: u32 = main_part[2..4]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let day: u32 = main_part[4..6]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let hour: u32 = main_part[6..8]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let minute: u32 = main_part[8..10]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
(year, month, day, hour, minute)
}
12 => {
let year: i32 = main_part[0..4]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let month: u32 = main_part[4..6]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let day: u32 = main_part[6..8]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let hour: u32 = main_part[8..10]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
let minute: u32 = main_part[10..12]
.parse()
.map_err(|_| format!("invalid date format '{}'", s))?;
(year, month, day, hour, minute)
}
_ => return Err(format!("invalid date format '{}'", s)),
};
if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || minute > 59 {
return Err(format!("invalid date format '{}'", s));
}
let epoch = mktime_local(year, month, day, hour, minute, seconds)?;
Ok((epoch, 0))
}
#[cfg(unix)]
fn mktime_local(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<i64, String> {
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
tm.tm_year = year - 1900;
tm.tm_mon = month as i32 - 1;
tm.tm_mday = day as i32;
tm.tm_hour = hour as i32;
tm.tm_min = minute as i32;
tm.tm_sec = second as i32;
tm.tm_isdst = -1;
unsafe {
#[cfg(target_os = "linux")]
{
*libc::__errno_location() = 0;
}
#[cfg(target_os = "macos")]
{
*libc::__error() = 0;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
*libc::__errno_location() = 0;
}
}
let t = unsafe { libc::mktime(&mut tm) };
if t == -1 {
let errno = unsafe {
#[cfg(target_os = "linux")]
{
*libc::__errno_location()
}
#[cfg(target_os = "macos")]
{
*libc::__error()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
*libc::__errno_location()
}
};
if errno != 0 {
return Err("invalid time value".to_string());
}
}
Ok(t)
}
#[cfg(unix)]
fn parse_date_string(s: &str) -> Result<(i64, i64), String> {
parse_date_string_with_base(s, None)
}
#[cfg(unix)]
fn parse_date_string_with_base(
s: &str,
base_time: Option<(i64, i64)>,
) -> Result<(i64, i64), String> {
let trimmed = s.trim();
let base = || -> (i64, i64) { base_time.unwrap_or_else(current_time) };
if trimmed.eq_ignore_ascii_case("now") {
let (sec, nsec) = base();
return Ok((sec, nsec));
}
if trimmed.eq_ignore_ascii_case("yesterday") {
let (sec, _) = base();
return Ok((sec - 86400, 0));
}
if trimmed.eq_ignore_ascii_case("tomorrow") {
let (sec, _) = base();
return Ok((sec + 86400, 0));
}
if let Some(rest) = trimmed.strip_suffix(" ago") {
let rest = rest.trim();
if let Some(num_str) = rest
.strip_suffix(" days")
.or_else(|| rest.strip_suffix(" day"))
&& let Ok(n) = num_str.trim().parse::<i64>()
{
let (sec, _) = base();
return Ok((sec - n * 86400, 0));
}
}
if let Some(num_str) = trimmed
.strip_suffix(" days")
.or_else(|| trimmed.strip_suffix(" day"))
&& let Ok(n) = num_str.trim().parse::<i64>()
{
let (sec, _) = base();
return Ok((sec + n * 86400, 0));
}
if let Some(rest) = trimmed.strip_prefix('@') {
if let Some(dot_pos) = rest.find('.') {
let sec_str = &rest[..dot_pos];
let nsec_str = &rest[dot_pos + 1..];
let sec: i64 = sec_str
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let nsec_padded = format!("{:0<9}", nsec_str);
let nsec: i64 = nsec_padded[..9]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
return Ok((sec, nsec));
}
let sec: i64 = rest.parse().map_err(|_| format!("invalid date '{}'", s))?;
return Ok((sec, 0));
}
let normalized = trimmed.replace('T', " ");
let parts: Vec<&str> = normalized.splitn(2, ' ').collect();
let date_part = parts[0];
let date_fields: Vec<&str> = date_part.split('-').collect();
if date_fields.len() != 3 {
return Err(format!("invalid date '{}'", s));
}
let year: i32 = date_fields[0]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let month: u32 = date_fields[1]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let day: u32 = date_fields[2]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let (hour, minute, second, nsec, tz_offset_secs) = if parts.len() > 1 {
let time_part = parts[1].trim();
let (time_no_tz, tz_secs) = extract_tz_offset(time_part);
let (time_str, frac_nsec) = if let Some(dot_pos) = time_no_tz.find('.') {
let frac_str = &time_no_tz[dot_pos + 1..];
let padded = format!("{:0<9}", frac_str);
let ns: i64 = padded[..9]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
(&time_no_tz[..dot_pos], ns)
} else {
(time_no_tz.as_str(), 0i64)
};
let time_fields: Vec<&str> = time_str.split(':').collect();
if time_fields.len() < 2 || time_fields.len() > 3 {
return Err(format!("invalid date '{}'", s));
}
let h: u32 = time_fields[0]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let m: u32 = time_fields[1]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?;
let sec: u32 = if time_fields.len() == 3 {
time_fields[2]
.parse()
.map_err(|_| format!("invalid date '{}'", s))?
} else {
0
};
(h, m, sec, frac_nsec, tz_secs)
} else {
(0, 0, 0, 0, None)
};
if !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| hour > 23
|| minute > 59
|| second > 60
{
return Err(format!("invalid date '{}'", s));
}
if let Some(tz_secs) = tz_offset_secs {
let epoch_utc = mktime_utc(year, month, day, hour, minute, second)?;
Ok((epoch_utc - tz_secs as i64, nsec))
} else {
let epoch = mktime_local(year, month, day, hour, minute, second)?;
Ok((epoch, nsec))
}
}
#[cfg(unix)]
fn extract_tz_offset(time_str: &str) -> (String, Option<i32>) {
let trimmed = time_str.trim();
if let Some(stripped) = trimmed.strip_suffix('Z') {
return (stripped.trim().to_string(), Some(0));
}
if let Some(space_pos) = trimmed.rfind(' ') {
let suffix = &trimmed[space_pos + 1..];
if (suffix.starts_with('+') || suffix.starts_with('-')) && suffix.len() >= 5 {
let sign: i32 = if suffix.starts_with('+') { 1 } else { -1 };
let digits = &suffix[1..];
let (hh, mm) = if digits.contains(':') {
let parts: Vec<&str> = digits.split(':').collect();
if parts.len() == 2 {
if let (Ok(h), Ok(m)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
(h, m)
} else {
return (trimmed.to_string(), None);
}
} else {
return (trimmed.to_string(), None);
}
} else if digits.len() == 4 {
if let (Ok(h), Ok(m)) = (digits[..2].parse::<i32>(), digits[2..].parse::<i32>()) {
(h, m)
} else {
return (trimmed.to_string(), None);
}
} else {
return (trimmed.to_string(), None);
};
let offset = sign * (hh * 3600 + mm * 60);
return (trimmed[..space_pos].trim().to_string(), Some(offset));
}
}
(trimmed.to_string(), None)
}
#[cfg(unix)]
fn mktime_utc(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<i64, String> {
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
tm.tm_year = year - 1900;
tm.tm_mon = month as i32 - 1;
tm.tm_mday = day as i32;
tm.tm_hour = hour as i32;
tm.tm_min = minute as i32;
tm.tm_sec = second as i32;
tm.tm_isdst = 0;
let t = unsafe { libc::timegm(&mut tm) };
if t == -1 {
return Err("invalid time value".to_string());
}
Ok(t)
}
#[cfg(unix)]
fn set_file_times(
path: &str,
target: TimeTarget,
sec: i64,
nsec: i64,
no_deref: bool,
time_pair: Option<&TimePair>,
) -> Result<(), std::io::Error> {
let current = get_file_times(path).unwrap_or(TimePair {
atime_sec: sec,
atime_nsec: nsec,
mtime_sec: sec,
mtime_nsec: nsec,
});
let atime = match target {
TimeTarget::Both | TimeTarget::AccessOnly => {
if let Some(tp) = time_pair {
libc::timespec {
tv_sec: tp.atime_sec,
tv_nsec: tp.atime_nsec,
}
} else {
libc::timespec {
tv_sec: sec,
tv_nsec: nsec,
}
}
}
TimeTarget::ModifyOnly => libc::timespec {
tv_sec: current.atime_sec,
tv_nsec: current.atime_nsec,
},
};
let mtime = match target {
TimeTarget::Both | TimeTarget::ModifyOnly => {
if let Some(tp) = time_pair {
libc::timespec {
tv_sec: tp.mtime_sec,
tv_nsec: tp.mtime_nsec,
}
} else {
libc::timespec {
tv_sec: sec,
tv_nsec: nsec,
}
}
}
TimeTarget::AccessOnly => libc::timespec {
tv_sec: current.mtime_sec,
tv_nsec: current.mtime_nsec,
},
};
let times = [atime, mtime];
let c_path = CString::new(path).map_err(|_| std::io::Error::other("invalid path"))?;
let flags = if no_deref {
libc::AT_SYMLINK_NOFOLLOW
} else {
0
};
let ret = unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times.as_ptr(), flags) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(unix)]
fn is_long_opt_prefix(arg: &str, long_opt: &str) -> bool {
if let Some(eq_pos) = arg.find('=') {
long_opt.starts_with(&arg[..eq_pos]) && eq_pos >= 4 } else {
long_opt.starts_with(arg) && arg.len() >= 4 && arg.starts_with("--")
}
}
#[cfg(unix)]
fn long_opt_value(arg: &str, _long_opt: &str) -> Option<String> {
arg.find('=').map(|pos| arg[pos + 1..].to_string())
}
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let mut target = TimeTarget::Both;
let mut no_create = false;
let mut no_deref = false;
let mut date_str: Option<String> = None;
let mut reference: Option<String> = None;
let mut stamp: Option<String> = None;
let mut files: Vec<String> = Vec::new();
let mut saw_dashdash = false;
let args: Vec<String> = std::env::args().skip(1).collect();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if saw_dashdash {
files.push(arg.clone());
i += 1;
continue;
}
match arg.as_str() {
"--help" => {
print_help();
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-a" => target = TimeTarget::AccessOnly,
"-m" => target = TimeTarget::ModifyOnly,
"-c" | "--no-create" => no_create = true,
"-h" | "--no-dereference" => no_deref = true,
"-d" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'd'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
date_str = Some(args[i].clone());
}
"-r" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'r'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
reference = Some(args[i].clone());
}
"-t" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 't'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
stamp = Some(args[i].clone());
}
"--" => saw_dashdash = true,
_ if is_long_opt_prefix(arg, "--date") => {
if let Some(val) = long_opt_value(arg, "--date") {
date_str = Some(val);
} else {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'date'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
date_str = Some(args[i].clone());
}
}
_ if is_long_opt_prefix(arg, "--reference") => {
if let Some(val) = long_opt_value(arg, "--reference") {
reference = Some(val);
} else {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'reference'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
reference = Some(args[i].clone());
}
}
_ if is_long_opt_prefix(arg, "--time") => {
let val = if let Some(v) = long_opt_value(arg, "--time") {
v
} else {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'time'", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
args[i].clone()
};
match val.as_str() {
"access" | "atime" | "use" => target = TimeTarget::AccessOnly,
"modify" | "mtime" => target = TimeTarget::ModifyOnly,
_ => {
eprintln!("{}: invalid argument '{}' for '--time'", TOOL_NAME, val);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
_ if arg.starts_with("-d") && arg.len() > 2 => {
date_str = Some(arg[2..].to_string());
}
_ if arg.starts_with("-r") && arg.len() > 2 => {
reference = Some(arg[2..].to_string());
}
_ if arg.starts_with("-t") && arg.len() > 2 => {
stamp = Some(arg[2..].to_string());
}
_ if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") => {
for ch in arg[1..].chars() {
match ch {
'a' => target = TimeTarget::AccessOnly,
'm' => target = TimeTarget::ModifyOnly,
'c' => no_create = true,
'h' => no_deref = true,
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
_ => files.push(arg.clone()),
}
i += 1;
}
if files.is_empty() {
eprintln!("{}: missing file operand", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let parsed_stamp = stamp.as_deref().map(parse_touch_timestamp);
if let Some(Err(_)) = &parsed_stamp {
eprintln!(
"{}: invalid date format '{}'",
TOOL_NAME,
stamp.as_deref().unwrap()
);
process::exit(1);
}
let ref_times = if let Some(ref r) = reference {
match get_file_times(r) {
Ok(tp) => Some(tp),
Err(e) => {
eprintln!(
"{}: failed to get attributes of '{}': {}",
TOOL_NAME,
r,
coreutils_rs::common::io_error_msg(&e)
);
process::exit(1);
}
}
} else {
None
};
let parsed_date = if let (Some(date_s), Some(tp)) = (date_str.as_deref(), &ref_times) {
Some(parse_date_string_with_base(
date_s,
Some((tp.mtime_sec, tp.mtime_nsec)),
))
} else {
date_str.as_deref().map(parse_date_string)
};
if let Some(Err(_)) = &parsed_date {
eprintln!(
"{}: invalid date format '{}'",
TOOL_NAME,
date_str.as_deref().unwrap()
);
process::exit(1);
}
let (ts_sec, ts_nsec, use_ref_pair) = if let Some(Ok((sec, nsec))) = parsed_date {
(sec, nsec, false)
} else if let Some(ref tp) = ref_times {
match target {
TimeTarget::Both | TimeTarget::AccessOnly => (tp.atime_sec, tp.atime_nsec, true),
TimeTarget::ModifyOnly => (tp.mtime_sec, tp.mtime_nsec, true),
}
} else if let Some(Ok((sec, nsec))) = parsed_stamp {
(sec, nsec, false)
} else {
let t = current_time();
(t.0, t.1, false)
};
let mut exit_code = 0;
for file in &files {
if file.ends_with('/') {
let base = file.trim_end_matches('/');
if !base.is_empty() {
match fs::symlink_metadata(base) {
Ok(meta) if !meta.is_dir() => {
if !no_create {
eprintln!(
"{}: setting times of '{}': Not a directory",
TOOL_NAME, file
);
exit_code = 1;
}
continue;
}
Err(_) => {
if !no_create {
eprintln!(
"{}: cannot touch '{}': No such file or directory",
TOOL_NAME, file
);
exit_code = 1;
}
continue;
}
_ => {}
}
} else {
if !no_create {
eprintln!(
"{}: cannot touch '{}': No such file or directory",
TOOL_NAME, file
);
exit_code = 1;
}
continue;
}
}
if !no_deref && is_dangling_symlink(file) {
if no_create {
continue;
}
match fs::read_link(file) {
Ok(target_path) => {
let create_path = if target_path.is_absolute() {
target_path
} else {
let parent = std::path::Path::new(file)
.parent()
.unwrap_or(std::path::Path::new("."));
parent.join(target_path)
};
if let Err(e) = fs::File::create(&create_path) {
eprintln!(
"{}: cannot touch '{}': {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
exit_code = 1;
continue;
}
}
Err(e) => {
eprintln!(
"{}: cannot touch '{}': {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
exit_code = 1;
continue;
}
}
} else if !path_exists(file) {
if no_deref {
if !no_create {
eprintln!(
"{}: setting times of '{}': No such file or directory",
TOOL_NAME, file
);
exit_code = 1;
}
continue;
}
if no_create {
continue;
}
if let Err(e) = fs::File::create(file) {
eprintln!(
"{}: cannot touch '{}': {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
exit_code = 1;
continue;
}
}
let ref_pair = if use_ref_pair {
ref_times.as_ref()
} else {
None
};
if let Err(e) = set_file_times(file, target, ts_sec, ts_nsec, no_deref, ref_pair) {
eprintln!(
"{}: setting times of '{}': {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
exit_code = 1;
}
}
if exit_code != 0 {
process::exit(exit_code);
}
}
#[cfg(unix)]
fn path_exists(path: &str) -> bool {
fs::symlink_metadata(path).is_ok()
}
#[cfg(unix)]
fn is_dangling_symlink(path: &str) -> bool {
if let Ok(meta) = fs::symlink_metadata(path)
&& meta.file_type().is_symlink()
{
return fs::metadata(path).is_err();
}
false
}
#[cfg(unix)]
fn print_help() {
println!("Usage: {} [OPTION]... FILE...", TOOL_NAME);
println!("Update the access and modification times of each FILE to the current time.");
println!();
println!("A FILE argument that does not exist is created empty, unless -c is supplied.");
println!();
println!(" -a change only the access time");
println!(" -c, --no-create do not create any files");
println!(" -d, --date=STRING parse STRING and use it instead of current time");
println!(" -h, --no-dereference affect each symbolic link instead of any referenced");
println!(" file (useful only on systems that can change the");
println!(" timestamps of a symlink)");
println!(" -m change only the modification time");
println!(" -r, --reference=FILE use this file's times instead of current time");
println!(" -t STAMP use [[CC]YY]MMDDhhmm[.ss] instead of current time");
println!(" --time=WORD change the specified time:");
println!(" WORD is access, atime, or use: equivalent to -a");
println!(" WORD is modify or mtime: equivalent to -m");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
}
#[cfg(all(test, unix))]
mod tests {
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("ftouch");
Command::new(path)
}
#[test]
fn test_create_new_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("newfile.txt");
assert!(!file.exists());
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_no_create_flag() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("nocreate.txt");
assert!(!file.exists());
let output = cmd().args(["-c", file.to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
assert!(!file.exists());
}
#[test]
fn test_update_existing_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("existing.txt");
fs::write(&file, "content").unwrap();
let output = cmd()
.args(["-t", "200001010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
let old_mtime = meta.mtime();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert!(meta.mtime() > old_mtime, "mtime should be updated");
}
#[test]
fn test_access_time_only() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("atime.txt");
fs::write(&file, "data").unwrap();
let output = cmd()
.args(["-t", "200001010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
let old_mtime = meta.mtime();
let output = cmd()
.args(["-a", "-t", "202301010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert_eq!(meta.mtime(), old_mtime, "mtime should be unchanged");
assert!(meta.atime() > old_mtime, "atime should be updated");
}
#[test]
fn test_modification_time_only() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("mtime.txt");
fs::write(&file, "data").unwrap();
let output = cmd()
.args(["-t", "200001010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
let old_atime = meta.atime();
let output = cmd()
.args(["-m", "-t", "202301010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert_eq!(meta.atime(), old_atime, "atime should be unchanged");
assert!(meta.mtime() > old_atime, "mtime should be updated");
}
#[test]
fn test_reference_file() {
let dir = tempfile::tempdir().unwrap();
let ref_file = dir.path().join("ref.txt");
let target = dir.path().join("target.txt");
fs::write(&ref_file, "ref").unwrap();
fs::write(&target, "target").unwrap();
let output = cmd()
.args(["-t", "200506071234", ref_file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let ref_meta = fs::metadata(&ref_file).unwrap();
let output = cmd()
.args(["-r", ref_file.to_str().unwrap(), target.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let target_meta = fs::metadata(&target).unwrap();
assert_eq!(
target_meta.atime(),
ref_meta.atime(),
"atime should match reference"
);
assert_eq!(
target_meta.mtime(),
ref_meta.mtime(),
"mtime should match reference"
);
}
#[test]
fn test_t_timestamp_ccyymmddhhmmss() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("stamp.txt");
let output = cmd()
.args(["-t", "202306151230.45", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert!(meta.mtime() > 1686700000, "mtime should be in 2023");
assert!(meta.mtime() < 1686900000, "mtime should be in June 2023");
}
#[test]
fn test_t_timestamp_mmddhhm() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("stamp2.txt");
let output = cmd()
.args(["-t", "01150800", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_d_date_string_iso() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("date.txt");
let output = cmd()
.args(["-d", "2023-01-15", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert!(meta.mtime() > 1673600000);
assert!(meta.mtime() < 1673900000);
}
#[test]
fn test_d_date_string_iso_with_time() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("datetime.txt");
let output = cmd()
.args(["-d", "2023-01-15 10:30:00", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_d_epoch() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("epoch.txt");
let output = cmd()
.args(["-d", "@1000000000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert_eq!(meta.mtime(), 1000000000);
}
#[test]
fn test_d_now() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("now.txt");
let output = cmd()
.args(["-d", "now", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_missing_file_operand() {
let output = cmd().output().unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("missing file operand"));
}
#[test]
fn test_multiple_files() {
let dir = tempfile::tempdir().unwrap();
let f1 = dir.path().join("multi1.txt");
let f2 = dir.path().join("multi2.txt");
let output = cmd()
.args([f1.to_str().unwrap(), f2.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(f1.exists());
assert!(f2.exists());
}
#[test]
fn test_time_word_access() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("timeword.txt");
fs::write(&file, "data").unwrap();
let output = cmd()
.args(["-t", "200001010000", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
let old_mtime = meta.mtime();
let output = cmd()
.args([
"--time=access",
"-t",
"202301010000",
file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success());
let meta = fs::metadata(&file).unwrap();
assert_eq!(meta.mtime(), old_mtime, "mtime should be unchanged");
}
#[test]
fn test_matches_gnu_create() {
let dir = tempfile::tempdir().unwrap();
let gnu_file = dir.path().join("gnu_touch.txt");
let our_file = dir.path().join("our_touch.txt");
let gnu = Command::new("touch")
.arg(gnu_file.to_str().unwrap())
.output();
if let Ok(gnu) = gnu {
let ours = cmd().arg(our_file.to_str().unwrap()).output().unwrap();
assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
assert!(
gnu_file.exists() == our_file.exists(),
"File creation mismatch"
);
}
}
#[test]
fn test_matches_gnu_no_create() {
let dir = tempfile::tempdir().unwrap();
let gnu_file = dir.path().join("gnu_nc.txt");
let our_file = dir.path().join("our_nc.txt");
let gnu = Command::new("touch")
.args(["-c", gnu_file.to_str().unwrap()])
.output();
if let Ok(gnu) = gnu {
let ours = cmd()
.args(["-c", our_file.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
assert!(!gnu_file.exists());
assert!(!our_file.exists());
}
}
#[test]
fn test_parse_date_string_iso() {
let (sec, _nsec) = super::parse_date_string("2023-01-15").unwrap();
assert!(sec > 1673600000);
assert!(sec < 1673900000);
}
#[test]
fn test_parse_date_string_epoch() {
let (sec, nsec) = super::parse_date_string("@1000000000").unwrap();
assert_eq!(sec, 1000000000);
assert_eq!(nsec, 0);
}
#[test]
fn test_parse_date_string_epoch_frac() {
let (sec, nsec) = super::parse_date_string("@1000000000.5").unwrap();
assert_eq!(sec, 1000000000);
assert_eq!(nsec, 500000000);
}
#[test]
fn test_parse_date_string_now() {
let (sec, _) = super::parse_date_string("now").unwrap();
assert!(sec > 1600000000);
}
#[test]
fn test_parse_date_string_invalid() {
assert!(super::parse_date_string("not-a-date").is_err());
}
#[test]
fn test_parse_touch_timestamp_ccyymmddhhmm() {
let (sec, _) = super::parse_touch_timestamp("202301151030").unwrap();
assert!(sec > 1673600000);
}
#[test]
fn test_parse_touch_timestamp_with_seconds() {
let (sec, _) = super::parse_touch_timestamp("202301151030.45").unwrap();
assert!(sec > 1673600000);
}
#[test]
fn test_parse_touch_timestamp_invalid() {
assert!(super::parse_touch_timestamp("abc").is_err());
}
#[test]
fn test_iso_t_format() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("isot.txt");
let output = cmd()
.args(["-d", "2023-06-15T10:30:00", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_long_date_flag() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("longdate.txt");
let output = cmd()
.args(["--date=2023-01-15", file.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
assert!(file.exists());
}
#[test]
fn test_long_reference_flag() {
let dir = tempfile::tempdir().unwrap();
let ref_file = dir.path().join("ref_long.txt");
let target = dir.path().join("target_long.txt");
fs::write(&ref_file, "ref").unwrap();
fs::write(&target, "target").unwrap();
let output = cmd()
.args([
&format!("--reference={}", ref_file.to_str().unwrap()),
target.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success());
}
}