Skip to content

Add -stc: store/restore the oldest of {atime, mtime, ctime} as creation time - #243

Open
amayer-glei wants to merge 2 commits into
ip7z:mainfrom
amayer-glei:stc
Open

Add -stc: store/restore the oldest of {atime, mtime, ctime} as creation time#243
amayer-glei wants to merge 2 commits into
ip7z:mainfrom
amayer-glei:stc

Conversation

@amayer-glei

Copy link
Copy Markdown

Branch layout (same change, three packagings)

This PR contains only the code changes — branch
stc: 2 commits on top of 26.00
(CLI flag, GUI checkbox). The same change is also published on the fork in two other
packagings, so you can pick whatever suits your contribution workflow:

  • i18nstc + the Lang/
    translation-pack update
    (id 4087, 325 files; also offered to the language-pack repo).
  • ghi18n + GitHub Actions CI
    (matrix x86/x64/arm64, -stc smoke test) with a rolling prerelease
    ci-gh carrying per-arch binary
    zips, so the feature can be tried without building.

Summary

This adds a new option, -stc, that remaps the creation time (ctime) slot to the
oldest of a file's three timestamps — min(atime, mtime, ctime) — both when storing
files into an archive and when restoring them to the filesystem.

  • Compression (a / u): the value written into the archive's CTime field becomes
    min(atime, mtime, ctime) of the source file (the real ctime is ignored as a value, but is
    still included in the min). atime/mtime slots are unaffected.
  • Extraction (x / e): the extracted file's creation time is set to
    min(atime, mtime, ctime) of the archived timestamps (whichever are defined). atime/mtime
    are restored normally.

The same single switch drives both directions (like -ssp / -stl), and it is off by
default
, so behavior is unchanged unless -stc is given.

Why

A file's creation time is often newer than its real age (e.g. after a copy or a
checkout), while the oldest of its timestamps best reflects "how old is this data, really."
This option lets an archive preserve that oldest-known time as the creation time, and restore
it on extraction — a fixed, always-min() special case of a possible future ffmpeg-style
-map i:o timestamp remap (-map is still free for that).

Usage

Command line

rem store: archive "Created" = oldest of the file's timestamps
7zz a -stc archive.7z file.txt

rem extract: filesystem creation time = oldest of the archived timestamps
7zz x -stc archive.7z -oout

-stc also forces CTime to be stored (equivalent to -mtc), so there is always a ctime
slot to hold the oldest value. atime/mtime are stored per their usual defaults / -mta/-mtm.

GUI (7zFM / 7zG)

Add to archiveOptions...Time group → new checkbox:

Set creation time to oldest of access/modification/creation time

The setting is persisted in the registry (next to "Do not change source files last access
time") and internally forces "Store creation time" when enabled.

Behavior details / edge cases

  • Works for 7z and any format whose handler stores a creation-time slot and honors the
    tc time option (e.g. zip/tar NTFS times). Tested with 7z.
  • On extraction the creation time becomes the oldest of the timestamps actually present in
    the archive; if only mtime was stored, creation time = mtime.
  • Repacked (unchanged) files in an update keep their existing archived times; only new/changed
    files get the remapped ctime.
  • Linux/macOS: the same remap is applied to the stream/property paths; note that on POSIX the
    "ctime" slot follows 7-Zip's existing convention (st_ctime).

Implementation

Both timestamp write-paths are remapped on compression, and a single choke point on extraction.

Shared / engine:

  • CPP/Windows/TimeUtils.h — new FiTime_Min3() helper (built on the existing Compare_FiTime).
  • CPP/7zip/Common/FileStreams.{h,cpp}CInFileStream::StoreOldestCTime; remap in
    GetProps (the solid/fast path, regular files) for Windows and Linux.
  • CPP/7zip/UI/Common/UpdateCallback.{h,cpp}CArchiveUpdateCallback::StoreOldestCTime;
    remap in GetProperty/GetRootProp (the property path, used for directories); flag passed to
    the input stream in GetStream.
  • CPP/7zip/UI/Common/Update.{h,cpp}CUpdateOptions::StoreOldestCTime, wired to the callback.
  • CPP/7zip/UI/Common/ArchiveExtractCallback.{h,cpp}CExtractNtOptions::OldestCTime;
    GetFiTimesCAM() takes the flag and rewrites CTime to the oldest defined time — one place
    covering files, directories and symlinks.

Command line (7zz / 7za):

  • CPP/7zip/UI/Common/ArchiveCommandLine.cppNKey::kOldestCTime + { "stc", SWFRM_SIMPLE }
    in kSwitchForms[] (positional alignment preserved); Parse2 handling for the extract branch
    (nt.OldestCTime) and the update branch (StoreOldestCTime + injected tc property to force
    ctime storage).
  • CPP/7zip/UI/Console/Main.cpp — help text line.

GUI:

  • CPP/7zip/UI/GUI/CompressDialogRes.h — new IDX_COMPRESS_OLDEST_CTIME.
  • CPP/7zip/UI/GUI/CompressOptionsDialog.rc — new checkbox; "Time" group box and dialog height
    grown by 20 px.
  • CPP/7zip/UI/GUI/CompressDialog.{h,cpp}CInfo::OldestCTime / CCompressDialog::OldestCTime
    and the SET_GUI_BOOL / SET_FINAL_BOOL_PAIRS / options-dialog wiring (mirrors PreserveATime).
  • CPP/7zip/UI/Common/ZipRegistry.{h,cpp} — registry persistence.
  • CPP/7zip/UI/GUI/UpdateGUI.cpp — maps the checkbox to CUpdateOptions::StoreOldestCTime and
    forces the tc property.

No changes to -Wall -WX were needed; no existing behavior is altered when the option is absent.

Localization

Only the GUI checkbox string is translatable; the CLI help line in Console/Main.cpp
is hardcoded English (the console 7zz help is not localized).

The translatable string is dialog ID IDX_COMPRESS_OLDEST_CTIME = 4087, registered in
kLangIDs_Options[], so LangSetDlgItems() substitutes it from Lang\<lang>.txt at runtime
(falling back to the English RC caption when absent).

The Lang/*.txt translation files are not part of this source tree; they are maintained in
the separate language pack repo (PopuriAO29/7zip-lang, 7-Zip 26.01) and feed the installer
(DOC/7zip.wxs). Upstream, 4087 was the next free id after the existing Time-group strings
(40824086) and before sec/ns (4090/4091), so there is no id collision.

The new string has been added at id 4087 to the language pack (see Lang/ and
Lang-updated.zip), inserted byte-exactly (UTF-8 BOM + CRLF preserved, one line added after the
4086 entry; 4086/4090/4091 verified unchanged):

  • Translated (31 languages + regional variants, using each file's own timestamp terminology):
    de, fr, es, it, pt, pt-br, nl, pl, cs, sk, sv, fi, ru, uk, ja, ko, zh-cn, zh-tw, tr, el, hu,
    ro, bg, sl, ca, gl, id, ar, fa, bs, va.
  • English seed (source string) was used for the en-* variants, the en.ttt template, and
    az / co / rue (which have a translated Time group but were not machine-guessed — these
    three still need a native speaker).
  • Languages that do not translate the Time dialog at all were left untouched (they fall back to
    the English RC caption for the whole group, including this checkbox).

English source (ID 4087):

4087
Set creation time to oldest of access/modification/creation time

German (as applied to all de-* files):

4087
Setze Erstellungszeit auf die älteste von Zugriffs-/Änderungs-/Erstellungszeit

Note for reviewers: the PopuriAO29/7zip-lang repo has a pre-existing .gitattributes
(eol=crlf) vs core.autocrlf mismatch, so a local git diff there shows whole-file
"renormalization" noise even on a pristine clone. The changes here add exactly one line per
file; run git add --renormalize (or diff with a matching EOL config) for a clean view.

CI

New workflow .github/workflows/build.yml (the tree previously had no CI):

  • Matrix x86 / x64 / arm64 on windows-latest (VS 2026windows-latest
    no longer ships VS2022, which was the first CI failure: vswhere [17.0,18.0) finds nothing →
    vcvarsall path empty → nmake 9009), one BuildBin.vs2026.cmd <arch> per leg, fail-fast: false.
    No 32-bit arm leg: the runner's VS2026 lacks the ARM32 VC++ component (vcvarsall amd64_arm
    fails within seconds — second CI failure), and official 7-Zip ships x86/x64/arm64 only.
  • Smoke test (x86/x64 legs only — ARM binaries can't run on the runner):
    7zz a -stc7zz l -slt asserts Created == Modified on the oldest date
    (DST-proof on purpose: 7-Zip's listing converts via FileTimeToLocalFileTime, which applies
    the current DST bias, so the absolute clock time in the listing is not stable across DST —
    comparing the two listing lines against each other is), then 7zz x -stc asserts the extracted
    CreationTime is exactly the oldest timestamp, plus 7zz t.
  • Artifacts: per-arch 7zip-<arch> upload of every bundle binary
    (7z.dll, 7zz.exe, 7za.exe, 7zr.exe, 7zFM.exe, lzma.exe, the codec DLLs, SFX modules).
    On pushes, a release job additionally republishes a rolling prerelease ci-<branch>
    with per-arch zips (gh release, contents: write scoped to that job only) — so the
    binaries are one click away on the fork's Releases page.
  • Reliability note: BuildBin.*.cmd does not propagate an nmake failure (popd resets
    ERRORLEVEL), so the workflow explicitly verifies key outputs exist after the build
    (7z.dll, 7zz.exe) and uses if-no-files-found: error on upload as a second net.

Validated locally with Docker: rhysd/actionlint0 errors; both PowerShell steps were
dry-run against the real x64 build outputs (smoke test passes, 13 artifacts collected).

Testing

Build (clean, all bundles, warnings-as-errors):

BuildBin.vs2026.cmd x64

7zz.exe, 7za.exe, 7z.dll, 7zFM.exe, lzma.exe, SFX modules all build with zero
errors/warnings.

Smoke tests (source file set to Creation=2020-01-03, Access=2020-01-02, Write=2020-01-01
← oldest):

Step Result
7zz a -stc7zz l -slt archive Created = 2020-01-01 (oldest), not real ctime 2020-01-03
archive with -mtc -mta -mtm (no remap), then 7zz x -stc extracted CreationTime = 2020-01-01 (oldest) ✅
same archive, 7zz x (no -stc) extracted CreationTime = 2020-01-03 (stored ctime) — default unchanged ✅

@amayer-glei

Copy link
Copy Markdown
Author

Why: Android>Windows 7z backups resets ctime to "now" 🫠

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.

1 participant