Skip to content

Commit d86a7fb

Browse files
authored
Merge pull request #7274 from jfinkels/touch-obsolete-posix-args
touch: support obsolete POSIX timestamp argument
2 parents 2afab7c + 8642156 commit d86a7fb

File tree

2 files changed

+103
-15
lines changed

2 files changed

+103
-15
lines changed

src/uu/touch/src/touch.rs

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,62 @@ fn filetime_to_datetime(ft: &FileTime) -> Option<DateTime<Local>> {
135135
Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into())
136136
}
137137

138+
/// Whether all characters in the string are digits.
139+
fn all_digits(s: &str) -> bool {
140+
s.as_bytes().iter().all(u8::is_ascii_digit)
141+
}
142+
143+
/// Convert a two-digit year string to the corresponding number.
144+
///
145+
/// `s` must be of length two or more. The last two bytes of `s` are
146+
/// assumed to be the two digits of the year.
147+
fn get_year(s: &str) -> u8 {
148+
let bytes = s.as_bytes();
149+
let n = bytes.len();
150+
let y1 = bytes[n - 2] - b'0';
151+
let y2 = bytes[n - 1] - b'0';
152+
10 * y1 + y2
153+
}
154+
155+
/// Whether the first filename should be interpreted as a timestamp.
156+
fn is_first_filename_timestamp(
157+
reference: Option<&OsString>,
158+
date: Option<&str>,
159+
timestamp: &Option<String>,
160+
files: &[&String],
161+
) -> bool {
162+
match std::env::var("_POSIX2_VERSION") {
163+
Ok(s) if s == "199209" => {
164+
if timestamp.is_none() && reference.is_none() && date.is_none() && files.len() >= 2 {
165+
let s = files[0];
166+
all_digits(s)
167+
&& (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s))))
168+
} else {
169+
false
170+
}
171+
}
172+
_ => false,
173+
}
174+
}
175+
176+
/// Cycle the last two characters to the beginning of the string.
177+
///
178+
/// `s` must have length at least two.
179+
fn shr2(s: &str) -> String {
180+
let n = s.len();
181+
let (a, b) = s.split_at(n - 2);
182+
let mut result = String::with_capacity(n);
183+
result.push_str(b);
184+
result.push_str(a);
185+
result
186+
}
187+
138188
#[uucore::main]
139189
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
140190
let matches = uu_app().try_get_matches_from(args)?;
141191

142-
let files: Vec<InputFile> = matches
143-
.get_many::<OsString>(ARG_FILES)
192+
let mut filenames: Vec<&String> = matches
193+
.get_many::<String>(ARG_FILES)
144194
.ok_or_else(|| {
145195
USimpleError::new(
146196
1,
@@ -150,31 +200,46 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
150200
),
151201
)
152202
})?
153-
.map(|filename| {
154-
if filename == "-" {
155-
InputFile::Stdout
156-
} else {
157-
InputFile::Path(PathBuf::from(filename))
158-
}
159-
})
160203
.collect();
161204

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

164207
let reference = matches.get_one::<OsString>(options::sources::REFERENCE);
165-
let timestamp = matches.get_one::<String>(options::sources::TIMESTAMP);
208+
let date = matches
209+
.get_one::<String>(options::sources::DATE)
210+
.map(|date| date.to_owned());
211+
212+
let mut timestamp = matches
213+
.get_one::<String>(options::sources::TIMESTAMP)
214+
.map(|t| t.to_owned());
215+
216+
if is_first_filename_timestamp(reference, date.as_deref(), &timestamp, &filenames) {
217+
timestamp = if filenames[0].len() == 10 {
218+
Some(shr2(filenames[0]))
219+
} else {
220+
Some(filenames[0].to_string())
221+
};
222+
filenames = filenames[1..].to_vec();
223+
}
166224

167225
let source = if let Some(reference) = reference {
168226
Source::Reference(PathBuf::from(reference))
169227
} else if let Some(ts) = timestamp {
170-
Source::Timestamp(parse_timestamp(ts)?)
228+
Source::Timestamp(parse_timestamp(&ts)?)
171229
} else {
172230
Source::Now
173231
};
174232

175-
let date = matches
176-
.get_one::<String>(options::sources::DATE)
177-
.map(|date| date.to_owned());
233+
let files: Vec<InputFile> = filenames
234+
.into_iter()
235+
.map(|filename| {
236+
if filename == "-" {
237+
InputFile::Stdout
238+
} else {
239+
InputFile::Path(PathBuf::from(filename))
240+
}
241+
})
242+
.collect();
178243

179244
let opts = Options {
180245
no_create: matches.get_flag(options::NO_CREATE),
@@ -275,7 +340,6 @@ pub fn uu_app() -> Command {
275340
Arg::new(ARG_FILES)
276341
.action(ArgAction::Append)
277342
.num_args(1..)
278-
.value_parser(ValueParser::os_string())
279343
.value_hint(clap::ValueHint::AnyPath),
280344
)
281345
.group(

tests/by-util/test_touch.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,3 +917,27 @@ fn test_touch_reference_symlink_with_no_deref() {
917917
// Times should be taken from the symlink, not the destination
918918
assert_eq!((time, time), get_symlink_times(&at, arg));
919919
}
920+
921+
#[test]
922+
fn test_obsolete_posix_format() {
923+
let (at, mut ucmd) = at_and_ucmd!();
924+
ucmd.env("_POSIX2_VERSION", "199209")
925+
.env("POSIXLY_CORRECT", "1")
926+
.args(&["01010000", "11111111"])
927+
.succeeds()
928+
.no_output();
929+
assert!(at.file_exists("11111111"));
930+
assert!(!at.file_exists("01010000"));
931+
}
932+
933+
#[test]
934+
fn test_obsolete_posix_format_with_year() {
935+
let (at, mut ucmd) = at_and_ucmd!();
936+
ucmd.env("_POSIX2_VERSION", "199209")
937+
.env("POSIXLY_CORRECT", "1")
938+
.args(&["0101000090", "11111111"])
939+
.succeeds()
940+
.no_output();
941+
assert!(at.file_exists("11111111"));
942+
assert!(!at.file_exists("0101000090"));
943+
}

0 commit comments

Comments
 (0)