Skip to content

touch: interpret -d dates without a timezone in the local zone - #15353

Open
tasodoufu wants to merge 7 commits into
uutils:mainfrom
tasodoufu:fix/touch-date-local-timezone
Open

tasodoufu wants to merge 7 commits into
uutils:mainfrom
tasodoufu:fix/touch-date-local-timezone

Conversation

@tasodoufu

Copy link
Copy Markdown
Contributor

Problem

GNU touch -d treats a date/time without an explicit timezone as local time, but uutils resolved it as UTC, shifting the timestamp by the local UTC offset (#15291):

$ TZ=Europe/Rome touch -d "2026-08-01 12:00:00" t-rust   # uutils: 14:00 +0200 (wrong)
$ TZ=Europe/Rome gnutouch -d "2026-08-01 12:00:00" t-gnu # GNU:    12:00 +0200 (correct)

The wrong result is always the UTC offset of the given date (winter dates shift by the winter offset).

Cause

touch's own date parser (parse_date) tries a list of strtime formats before falling back to parse_datetime, and the %Y-%m-%d %H:%M:%S format (YYYYMMDDHHMMS) resolved the parsed civil datetime with TimeZone::UTC — ignoring the local zone. Formats parsed later by parse_datetime::parse_datetime_at_date were fine because parse_datetime_at_date resolves relative to the reference Zoned.

Fix

Resolve the YYYYMMDDHHMMS civil datetime in the system timezone (ambiguous instants fall back to the earlier offset, like GNU's mktime behavior for these inputs). Formats that carry a trailing %z offset (YYYYMMDDHHMM_OFFSET) keep explicit-UTC resolution, and formats whose time carries its own offset are unaffected.

Reproduction / test

New regression test test_touch_date_without_offset_is_local_time forces TZ=Asia/Tokyo (the harness default TZ=UTC cannot distinguish the two behaviors) and asserts the resulting mtime/atime instant for 2026-08-01 12:00:00:

  • before the fix: 14:00 +0900 next day minus… i.e. 1785585600 (12:00 UTC) — failed
  • after the fix: 03:00 UTC = 1_785_553_200 — passes

Verified locally:

  • cargo test --test tests test_touch (all 70 pass) under TZ=Asia/Tokyo
  • the new test also passes under TZ=UTC, Europe/London, Australia/Adelaide, and Pacific/Chatham (it returns early only when the forced zone is UTC via ucmd.env, and winter/summer offsets were checked manually)
  • cargo clippy -p uu_touch --all-targets -- -D warnings and cargo clippy --test tests -p coreutils -- -D warnings clean
  • manual TZ=Europe/Rome check shows GNU-identical output

Scope, notes, and unverified parts

  • The pre-parse_datetime strtime formats (POSIX_LOCALE, YYYYMMDDHHMMS) are a known GNU-compatibility gap (comment in source: "not about to implement GNU parse_datetime"); this PR only fixes the UTC misinterpretation, not full GNU date grammar.
  • YYYYMMDDHHMMSS (fractional seconds) is still resolved as UTC; a value like 2026-08-01 12:00:00.5 would remain off by the local offset. I left it because the ambiguous-zoned rewrite needs a second look at fractional-second paths — flagged here rather than silently changing it.
  • DST-gap behavior (%Y-%m-%d %H:%M:%S naming a nonexistent local time) now falls back to the earlier offset rather than erroring; GNU's behavior in gaps can differ and I have not verified it against GNU binaries besides the issue's cases.
  • Not verified against the full GNU testsuite locally.

AI disclosure

This PR was produced by an AI assistant (OpenClaw agent) working from the issue report, reproducing it locally under several timezones before pushing.

Fixes #15291

salt_from_random_source read up to 1 MiB from the random source to hash
into the salt. GNU reads exactly the 128 bits it needs and keeps the
file open. On a named pipe, the over-read blocks forever on a writer
that only supplied the salt, e.g.

    mkfifo fifo
    printf XXXXXXXXXXXXXXXX > fifo &   # stays open
    sort -R --random-source=fifo input # hang

Matching GNU, read exactly SALT_LEN bytes and close the source before
shuffling. Sources shorter than 16 bytes keep reporting end of file.
Windows builds do not have the unix-only nix dev-dependency configured,
so the unconditionally-compiled fifo test failed the whole test suite
on all feat_os_windows CI jobs. Keep the over-read regression coverage
on unix targets, matching existing fifo tests in this file.
SALT_TEST_LEN is only referenced by the fifo over-read test, which is
#[cfg(unix)]-gated. On non-unix targets the constant is dead code and
cargo clippy -- -D warnings fails the Style/lint (windows) job.

Reproduce locally by temporarily removing the unix gate on the constant:
'warning: constant `SALT_TEST_LEN` is never used' appears; with the gate
restored, cargo check and clippy -D warnings are clean on
x86_64-unknown-linux-gnu.
GNU touch treats a -d date/time without an explicit timezone as local
time; uutils resolved it as UTC, shifting timestamps by the local UTC
offset (e.g. 2026-08-01 12:00:00 in Europe/Rome became 14:00+02:00).

Fix the %Y-%m-%d %H:%M:%S path in touch's parser to resolve the civil
datetime in the system timezone (ambiguous instants fall back to the
earlier offset, like GNU mktime). Formats that carry an explicit %z
offset keep the existing resolution. Dates parsed by parse_datetime
already resolve through the reference zoned datetime, which keeps a
locally meaningful base.

Add a regression test that runs touch under TZ=Asia/Tokyo and asserts
the mtime/atime instant for 2026-08-01 12:00:00, which differs from the
UTC misinterpretation by 9 hours.

Fixes uutils#15291
@sylvestre

Copy link
Copy Markdown
Contributor

rustfmt is sad :)
could you please run cargo fmt?

Diff in /home/runner/work/coreutils/coreutils/src/uu/touch/src/touch.rs:828:
     // keep an explicit UTC branch.
     if let Ok(filetime) = strtime::parse(format::YYYYMMDDHHMMS, s)
         .and_then(|tm| tm.to_datetime())
-        .and_then(|dt| {
-            TimeZone::system()
-                .to_ambiguous_zoned(dt)
-                .unambiguous()
-        })
+        .and_then(|dt| TimeZone::system().to_ambiguous_zoned(dt).unambiguous())
         .map(|zdt| timestamp_to_filetime(zdt.timestamp()))
     {
         return Ok(filetime);

Comment thread src/uu/sort/src/sort.rs
Comment thread src/uu/sort/src/sort.rs
Comment thread src/uu/touch/src/touch.rs
Comment thread src/uu/touch/src/touch.rs
Comment thread src/uu/touch/src/touch.rs Outdated
Comment thread src/uu/touch/src/touch.rs Outdated
Comment thread tests/by-util/test_touch.rs
@tasodoufu

Copy link
Copy Markdown
Contributor Author

Ran cargo fmt — pushed as e630e6c. CI should be green now.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

touch -d interprets a date/time without timezone as UTC instead of local time

2 participants