From 121b582f46be3b3079587b0be07811710a6c82df Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Thu, 18 Jun 2026 04:18:18 +0200 Subject: [PATCH] fix(arxiv): handle legacy single-file gzipped source (no tar wrapper) Old arXiv submissions (e.g. legacy math/* papers) are served as a bare gzipped .tex rather than a .tar.gz, which tarfile.open rejected with 'invalid header'. Fall back to gunzipping the body directly when the tar parse fails, so R-C covers those papers too (recovers math/0001176 + math/0306167). Adds a regression test. Co-Authored-By: Claude Opus 4.8 --- codex/sources/arxiv.py | 16 ++++++++++++---- tests/sources/test_arxiv.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/codex/sources/arxiv.py b/codex/sources/arxiv.py index dd61961..7c1a803 100644 --- a/codex/sources/arxiv.py +++ b/codex/sources/arxiv.py @@ -8,6 +8,7 @@ Provides: from __future__ import annotations +import gzip import io import logging import tarfile @@ -157,12 +158,19 @@ def fetch_source(arxiv_id: str) -> str | None: # Inline \input/\include so multi-file projects yield the full body, # not just the primary file's skeleton of include directives (R-C). return flatten_inputs(files[primary_name], files) - except tarfile.TarError as exc: - logger.warning("Failed to open tar archive for %s: %s", arxiv_id, exc) + except tarfile.TarError: + # Old arXiv submissions are served as a single bare gzipped .tex with no + # tar wrapper (e.g. legacy math/* papers) — gunzip the body directly. + try: + single = gzip.decompress(raw).decode("utf-8", errors="replace") + except (OSError, EOFError) as exc: + logger.warning("Failed to read arXiv source for %s: %s", arxiv_id, exc) + return None + if "\\" in single[:5000]: # looks like LaTeX (has backslash commands) + return single + logger.debug("arXiv source for %s is not LaTeX", arxiv_id) return None - return None # unreachable but satisfies type checker - def fetch_pdf_url(arxiv_id: str) -> str: """Return the canonical PDF URL for an arXiv paper. diff --git a/tests/sources/test_arxiv.py b/tests/sources/test_arxiv.py index f62ff82..db376c1 100644 --- a/tests/sources/test_arxiv.py +++ b/tests/sources/test_arxiv.py @@ -86,6 +86,30 @@ def test_fetch_source_flattens_multifile(monkeypatch: pytest.MonkeyPatch) -> Non assert "\\input" not in result and "\\include" not in result +def test_fetch_source_bare_gzip_single_file(monkeypatch: pytest.MonkeyPatch) -> None: + """Legacy arXiv submissions are served as a single bare gzipped .tex (no tar + wrapper); fetch_source must gunzip the body directly rather than fail.""" + import gzip + + latex = b"\\documentstyle[a4]{article}\n\\section{Intro}\nOld single-file paper body." + gz_bytes = gzip.compress(latex) + + def mock_get( + url: str, + *, + timeout: int = 60, + follow_redirects: bool = True, + ) -> httpx.Response: + return httpx.Response(200, content=gz_bytes) + + monkeypatch.setattr(httpx, "get", mock_get) + + result = arxiv.fetch_source("math/0001176") + + assert result is not None + assert "Old single-file paper body." in result + + # --------------------------------------------------------------------------- # fetch_source — 404 → None # ---------------------------------------------------------------------------