1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

touch: support obsolete POSIX timestamp argument

Support obsolete form of timestamp argument for old POSIX versions. In
summary, when older versions of POSIX are used and the first positional
argument looks like a date and time, then treat it as a timestamp
instead of as a filename. For example, before this commit

    _POSIX2_VERSION=199209
    POSIXLY_CORRECT=1
    touch 01010000 11111111

would create two files, `01010000` and `11111111`. After this commit,
the first argument is interpreted as a date and time (in this case,
midnight on January 1 of the current year) and that date and time are
set on the file named `11111111`.

Fixes #7180.
This commit is contained in:
Jeffrey Finkelstein 2025-02-03 20:24:45 -05:00
parent ee0d178f8c
commit 6dfa1f8276
2 changed files with 93 additions and 14 deletions

View file

@ -135,12 +135,57 @@ 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.
fn get_year(s: &str) -> u8 {
// Pre-condition: s.len() >= 2
let bytes = s.as_bytes();
let y1 = bytes[0] - b'0';
let y2 = bytes[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() {
if files.len() >= 2 {
let s = files[0];
if s.len() == 8 && all_digits(s) {
true
} else if s.len() == 10 && all_digits(s) {
let year = get_year(s);
(69..=99).contains(&year)
} else {
false
}
} else {
false
}
} else {
false
}
}
_ => false,
}
}
#[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,
@ -150,19 +195,23 @@ 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);
if is_first_filename_timestamp(reference, date.as_deref(), timestamp, &filenames) {
let head = filenames[0];
let tail = &filenames[1..];
timestamp = Some(head);
filenames = tail.to_vec();
}
let source = if let Some(reference) = reference {
Source::Reference(PathBuf::from(reference))
@ -172,9 +221,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
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),
@ -275,7 +331,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(

View file

@ -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(&["9001010000", "11111111"])
.succeeds()
.no_output();
assert!(at.file_exists("11111111"));
assert!(!at.file_exists("01010000"));
}