Skip to content

Commit

Permalink
Merge pull request uutils#7274 from jfinkels/touch-obsolete-posix-args
Browse files Browse the repository at this point in the history
touch: support obsolete POSIX timestamp argument
  • Loading branch information
cakebaker authored Feb 8, 2025
2 parents 2afab7c + 8642156 commit d86a7fb
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 15 deletions.
94 changes: 79 additions & 15 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,62 @@ fn filetime_to_datetime(ft: &FileTime) -> Option<DateTime<Local>> {
Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into())
}

/// Whether all characters in the string are digits.
fn all_digits(s: &str) -> bool {
s.as_bytes().iter().all(u8::is_ascii_digit)
}

/// Convert a two-digit year string to the corresponding number.
///
/// `s` must be of length two or more. The last two bytes of `s` are
/// assumed to be the two digits of the year.
fn get_year(s: &str) -> u8 {
let bytes = s.as_bytes();
let n = bytes.len();
let y1 = bytes[n - 2] - b'0';
let y2 = bytes[n - 1] - b'0';
10 * y1 + y2
}

/// Whether the first filename should be interpreted as a timestamp.
fn is_first_filename_timestamp(
reference: Option<&OsString>,
date: Option<&str>,
timestamp: &Option<String>,
files: &[&String],
) -> bool {
match std::env::var("_POSIX2_VERSION") {
Ok(s) if s == "199209" => {
if timestamp.is_none() && reference.is_none() && date.is_none() && files.len() >= 2 {
let s = files[0];
all_digits(s)
&& (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s))))
} else {
false
}
}
_ => false,
}
}

/// Cycle the last two characters to the beginning of the string.
///
/// `s` must have length at least two.
fn shr2(s: &str) -> String {
let n = s.len();
let (a, b) = s.split_at(n - 2);
let mut result = String::with_capacity(n);
result.push_str(b);
result.push_str(a);
result
}

#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;

let files: Vec<InputFile> = matches
.get_many::<OsString>(ARG_FILES)
let mut filenames: Vec<&String> = matches
.get_many::<String>(ARG_FILES)
.ok_or_else(|| {
USimpleError::new(
1,
Expand All @@ -150,31 +200,46 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
),
)
})?
.map(|filename| {
if filename == "-" {
InputFile::Stdout
} else {
InputFile::Path(PathBuf::from(filename))
}
})
.collect();

let no_deref = matches.get_flag(options::NO_DEREF);

let reference = matches.get_one::<OsString>(options::sources::REFERENCE);
let timestamp = matches.get_one::<String>(options::sources::TIMESTAMP);
let date = matches
.get_one::<String>(options::sources::DATE)
.map(|date| date.to_owned());

let mut timestamp = matches
.get_one::<String>(options::sources::TIMESTAMP)
.map(|t| t.to_owned());

if is_first_filename_timestamp(reference, date.as_deref(), &timestamp, &filenames) {
timestamp = if filenames[0].len() == 10 {
Some(shr2(filenames[0]))
} else {
Some(filenames[0].to_string())
};
filenames = filenames[1..].to_vec();
}

let source = if let Some(reference) = reference {
Source::Reference(PathBuf::from(reference))
} else if let Some(ts) = timestamp {
Source::Timestamp(parse_timestamp(ts)?)
Source::Timestamp(parse_timestamp(&ts)?)
} else {
Source::Now
};

let date = matches
.get_one::<String>(options::sources::DATE)
.map(|date| date.to_owned());
let files: Vec<InputFile> = filenames
.into_iter()
.map(|filename| {
if filename == "-" {
InputFile::Stdout
} else {
InputFile::Path(PathBuf::from(filename))
}
})
.collect();

let opts = Options {
no_create: matches.get_flag(options::NO_CREATE),
Expand Down Expand Up @@ -275,7 +340,6 @@ pub fn uu_app() -> Command {
Arg::new(ARG_FILES)
.action(ArgAction::Append)
.num_args(1..)
.value_parser(ValueParser::os_string())
.value_hint(clap::ValueHint::AnyPath),
)
.group(
Expand Down
24 changes: 24 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,3 +917,27 @@ fn test_touch_reference_symlink_with_no_deref() {
// Times should be taken from the symlink, not the destination
assert_eq!((time, time), get_symlink_times(&at, arg));
}

#[test]
fn test_obsolete_posix_format() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.env("_POSIX2_VERSION", "199209")
.env("POSIXLY_CORRECT", "1")
.args(&["01010000", "11111111"])
.succeeds()
.no_output();
assert!(at.file_exists("11111111"));
assert!(!at.file_exists("01010000"));
}

#[test]
fn test_obsolete_posix_format_with_year() {
let (at, mut ucmd) = at_and_ucmd!();
ucmd.env("_POSIX2_VERSION", "199209")
.env("POSIXLY_CORRECT", "1")
.args(&["0101000090", "11111111"])
.succeeds()
.no_output();
assert!(at.file_exists("11111111"));
assert!(!at.file_exists("0101000090"));
}

0 comments on commit d86a7fb

Please # to comment.