Skip to content

Restore symlink mtime on Linux after extracting an entry - #381

Merged
weichsel merged 1 commit into
weichsel:developmentfrom
odrobnik:fix/linux-symlink-mtime
May 17, 2026
Merged

weichsel merged 1 commit into
weichsel:developmentfrom
odrobnik:fix/linux-symlink-mtime

Conversation

@odrobnik

@odrobnik odrobnik commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Linux extraction quietly drops the modification date of every symlink entry today. The fix is to call the existing setSymlinkModificationDate helper from the Linux branch instead of taking the blanket Apple-only no-op.

Background

FileManager.setAttributes(_:ofItemAtURL:traverseLink:) splits on platform:

#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
    try self.setSymlinkPermissions(posixPermissions, ofItemAtURL: url)
    try self.setSymlinkModificationDate(modificationDate, ofItemAtURL: url)
#else
    // Since non-Darwin POSIX platforms ignore permissions on symlinks and
    // swift-corelibs-foundation currently doesn't support setting the
    // modification date, this codepath is currently a no-op on these
    // platforms.
    return
#endif

The comment is half right:

  • Permissions — yes, Linux ignores symlink permission bits at the kernel level, regardless of what lchmod(2) does, so leaving the permission half off matches reality.
  • Modification date — but lutimes(2) writes to the symlink itself rather than chasing the target, and Glibc has shipped it since 2.6. The existing setSymlinkModificationDate already calls it correctly. The only reason it wasn't running on Linux is that the entire call site was Apple-gated. The "swift-corelibs-foundation doesn't support …" half of the comment is true for FileManager.setAttributes(_:ofItemAtPath:), but ZIPFoundation is going straight to libc anyway, so it doesn't depend on that.

So Linux extraction loses symlink mtimes on every archive, which sometimes matters for tooling that walks extracted trees by timestamp.

Change

Add a #elseif os(Linux) branch to the call site that calls only setSymlinkModificationDate. No definition changes — setSymlinkModificationDate itself was already correct and accessible, just unused.

#if os(macOS) || os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
    // setSymlinkPermissions + setSymlinkModificationDate
#elseif os(Linux)
    // setSymlinkModificationDate only (Linux ignores symlink permission bits)
#else
    // Bionic / Windows: still a no-op
    return
#endif

Bionic falls through to the no-op because its lutimes is __INTRODUCED_IN(26) (not safe to lean on at low NDK targets) and Windows has no equivalent symlink-targeted utimes call. Both can be revisited in a follow-up — out of scope here.

Validation

Built clean on macOS (Swift 6.3, Xcode 26) — Build complete!. Full swift test run shows the same 2 pre-existing failures the unmodified development branch has (testCreateZIP64ArchiveWithLargeSize — a Version enum mirror-string format mismatch unrelated to anything here); all other 129 tests pass.

I don't have a Linux box wired up to run the test target on, but the code path is straightforward — setSymlinkModificationDate already had test coverage on Apple platforms that exercises exactly this lutimes call. Happy to add a Linux-only XCTest if you'd prefer (the attributes round-trip would just need to assert on the extracted symlink's Date instead of returning early).

Out of scope

  • Symlink permission preservation on Linux. Drop entirely if you want the comment to be precise — the kernel ignores it, so there's nothing to preserve.
  • Bionic + Windows symlink mtime. Bionic's utimensat(AT_FDCWD, path, &times, AT_SYMLINK_NOFOLLOW) is the obvious workaround for Android once a maintainer is happy bumping the supported NDK floor; Windows would need a SetFileInformationByHandle call against an opened reparse-point handle.

Related: weichsel/ZIPFoundation#380 is my Android compile fix. This PR is independent and doesn't depend on it landing first.

The symlink path in `FileManager.setAttributes(_:ofItemAtURL:traverseLink:)`
has been an Apple-only no-op since the file's `#if` was added: every
non-Darwin platform took the `return`, dropping both stored permission
bits *and* the stored modification date for symlink entries.

Permission bits genuinely don't apply on Linux — the kernel ignores
them on symlinks no matter what `lchmod` does — so leaving that
half of the codepath off matches reality.

The modification date is a different story. `lutimes(2)` writes to
the symlink itself rather than chasing the link, has shipped in Glibc
since 2.6, and the existing `setSymlinkModificationDate` helper
already calls it correctly. The only reason it wasn't being invoked
on Linux is that the call site was Apple-gated.

Add a Linux branch that calls `setSymlinkModificationDate` (and only
that) so extracting a ZIP archive on Linux preserves stored symlink
mtimes — same fidelity Apple platforms already get. Bionic and
Windows fall through to the existing no-op path; Bionic ships
`lutimes` only behind `__INTRODUCED_IN(26)`, and Windows has no
equivalent symlink-targeted utimes call, so leave them alone here.

Comment on the `#else` branch is updated to reflect what's actually
left out (Bionic + Windows, not "non-Darwin POSIX" generally).
// Since non-Darwin POSIX platforms ignore permissions on symlinks and swift-corelibs-foundation
// currently doesn't support setting the modification date, this codepath is currently a no-op
// on these platforms.
// Bionic and Windows lack a fully equivalent symlink-targeted

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just say "Other platforms" here (vs. Windows & Bionic)

// since 2.6. swift-corelibs-foundation's `setAttributes` doesn't
// round-trip `.modificationDate` for symlinks today, so we go
// straight to the libc syscall here to preserve mtime parity with
// Apple platforms when extracting.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good.
Can you please move the
testInvalidSymlinkCompressionMethodErrorConditions
testSymlinkModificationDateTransferErrorConditions
tests from the darwinOnlyTests to the allTests list. They are also relevant on Linux now.

@weichsel
weichsel merged commit aecfce2 into weichsel:development May 17, 2026
10 checks passed
@odrobnik
odrobnik deleted the fix/linux-symlink-mtime branch June 13, 2026 08:38
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.

2 participants