Repository: pypa/auditwheel

Issue:
auditwheel crashes when SOURCE_DATE_EPOCH is less than 315532800
This likely arises as a consequence of the fact that auditwheel uses SOURCE_DATE_EPOCH to set ZIP timestamps, but ZIP uses DOS timestamps that cannot express times before unix epoch time 315532800. 

Minimal example to reproduce:
```shell
wget https://github.com/pypa/auditwheel/raw/refs/heads/main/tests/integration/arch-wheels/glibc/testsimple-0.0.1-cp313-cp313-linux_x86_64.whl
SOURCE_DATE_EPOCH=315532799 pipx run auditwheel==6.3.0 repair testsimple-0.0.1-cp313-cp313-linux_x86_64.whl
```

Traceback:
```
> SOURCE_DATE_EPOCH=315532799 pipx run auditwheel==6.3.0 repair testsimple-0.0.1-cp313-cp313-linux_x86_64.whl
⚠️  auditwheel is already on your PATH and installed at /home/user/.local/bin/auditwheel. Downloading
    and running anyway.
INFO:auditwheel.main_repair:Repairing testsimple-0.0.1-cp313-cp313-linux_x86_64.whl
INFO:auditwheel.wheeltools:Previous filename tags: linux_x86_64
INFO:auditwheel.wheeltools:New filename tags: manylinux_2_5_x86_64, manylinux1_x86_64
INFO:auditwheel.wheeltools:Previous WHEEL info tags: cp313-cp313-linux_x86_64
INFO:auditwheel.wheeltools:New WHEEL info tags: cp313-cp313-manylinux_2_5_x86_64, cp313-cp313-manylinux1_x86_64
Traceback (most recent call last):
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/bin/auditwheel", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/lib/python3.11/site-packages/auditwheel/main.py", line 53, in main
    result: int | None = args.func(args, p)
                         ^^^^^^^^^^^^^^^^^^
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/lib/python3.11/site-packages/auditwheel/main_repair.py", line 203, in execute
    out_wheel = repair_wheel(
                ^^^^^^^^^^^^^
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/lib/python3.11/site-packages/auditwheel/repair.py", line 60, in repair_wheel
    with InWheelCtx(wheel_path) as ctx:
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/lib/python3.11/site-packages/auditwheel/wheeltools.py", line 136, in __exit__
    dir2zip(self.name, self.out_wheel, self.zip_compression_level, date_time)
  File "/home/user/.local/pipx/.cache/98b64e7971674bc/lib/python3.11/site-packages/auditwheel/tools.py", line 160, in dir2zip
    z.writestr(zinfo, fp.read(), compresslevel=zip_compression_level)
  File "/usr/lib/python3.11/zipfile.py", line 1843, in writestr
    with self.open(zinfo, mode='w') as dest:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/zipfile.py", line 1556, in open
    return self._open_to_write(zinfo, force_zip64=force_zip64)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/zipfile.py", line 1657, in _open_to_write
    self.fp.write(zinfo.FileHeader(zip64))
                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/zipfile.py", line 477, in FileHeader
    header = struct.pack(structFileHeader, stringFileHeader,
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
struct.error: ushort format requires 0 <= number <= 65535
```

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pypa/auditwheel/pull/568?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.87%. Comparing base [(`8964d1e`)](https://app.codecov.io/gh/pypa/auditwheel/commit/8964d1e5e5daee875c324c2e0b73cc8f3e5af1c9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) to head [(`c01f204`)](https://app.codecov.io/gh/pypa/auditwheel/commit/c01f20448e4eed5b5dbd1587622c1eae28783b87?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #568      +/-   ##
==========================================
- Coverage   92.76%   89.87%   -2.89%     
==========================================
  Files          21       21              
  Lines        1755     1758       +3     
  Branches      331      332       +1     
==========================================
- Hits         1628     1580      -48     
- Misses         77      116      +39     
- Partials       50       62      +12     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pypa/auditwheel/pull/568?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Fix (patch):
diff --git a/src/auditwheel/tools.py b/src/auditwheel/tools.py
index 423cfa8..a84532e 100644
--- a/src/auditwheel/tools.py
+++ b/src/auditwheel/tools.py
@@ -142,6 +142,9 @@ def dir2zip(
         st = in_dir.stat()
         date_time = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc)
     date_time_args = date_time.timetuple()[:6]
+    if date_time_args < (1980, 1, 1, 0, 0, 0):
+        logger.warning("dir2zip, clipping timestamp to 1980-01-01")
+        date_time_args = (1980, 1, 1, 0, 0, 0)
     compression = zipfile.ZIP_DEFLATED
     with zipfile.ZipFile(zip_fname, "w", compression=compression) as z:
         for dname, _, files in walk(in_dir):


Test changes:
diff --git a/tests/integration/test_bundled_wheels.py b/tests/integration/test_bundled_wheels.py
index aa11ed1..28065a0 100644
--- a/tests/integration/test_bundled_wheels.py
+++ b/tests/integration/test_bundled_wheels.py
@@ -3,7 +3,6 @@ from __future__ import annotations
 import importlib
 import os
 import platform
-import subprocess
 import sys
 import zipfile
 from argparse import Namespace
@@ -138,29 +137,25 @@ def test_analyze_wheel_abi_static_exe(caplog):
         assert result.overall_policy.name == "manylinux_2_5_x86_64"
 
 
-@pytest.mark.skipif(platform.machine() != "x86_64", reason="only checked on x86_64")
-def test_wheel_source_date_epoch(tmp_path, monkeypatch):
-    wheel_build_path = tmp_path / "wheel"
-    subprocess.run(
-        [
-            sys.executable,
-            "-m",
-            "pip",
-            "wheel",
-            "--no-deps",
-            "-w",
-            wheel_build_path,
-            HERE / "sample_extension",
-        ],
-        check=True,
+@pytest.mark.parametrize(
+    "timestamp",
+    [
+        (0, 315532800),  # zip timestamp starts 1980-01-01, not 1970-01-01
+        (315532799, 315532800),  # zip timestamp starts 1980-01-01, not 1970-01-01
+        (315532801, 315532800),  # zip timestamp round odd seconds down to even seconds
+        (315532802, 315532802),
+        (650203201, 650203200),  # zip timestamp round odd seconds down to even seconds
+    ],
+)
+def test_wheel_source_date_epoch(timestamp, tmp_path, monkeypatch):
+    wheel_path = (
+        HERE / "arch-wheels/musllinux_1_2/testsimple-0.0.1-cp312-cp312-linux_x86_64.whl"
     )
-
-    wheel_path, *_ = list(wheel_build_path.glob("*.whl"))
     wheel_output_path = tmp_path / "out"
     args = Namespace(
         LIB_SDIR=".libs",
         ONLY_PLAT=False,
-        PLAT="manylinux_2_5_x86_64",
+        PLAT="auto",
         STRIP=False,
         UPDATE_TAGS=True,
         WHEEL_DIR=wheel_output_path,
@@ -173,7 +168,8 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch):
         prog="auditwheel",
         verbose=1,
     )
-    monkeypatch.setenv("SOURCE_DATE_EPOCH", "650203200")
+
+    monkeypatch.setenv("SOURCE_DATE_EPOCH", str(timestamp[0]))
     # patchelf might not be available as we aren't running in a manylinux container
     # here. We don't need need it in this test, so just patch it.
     with mock.patch("auditwheel.patcher._verify_patchelf"):
@@ -182,6 +178,5 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch):
     output_wheel, *_ = list(wheel_output_path.glob("*.whl"))
     with zipfile.ZipFile(output_wheel) as wheel_file:
         for file in wheel_file.infolist():
-            assert (
-                datetime(*file.date_time, tzinfo=timezone.utc).timestamp() == 650203200
-            )
+            file_date_time = datetime(*file.date_time, tzinfo=timezone.utc)
+            assert file_date_time.timestamp() == timestamp[1]