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 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ Provides:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gzip
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import tarfile
|
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,
|
# Inline \input/\include so multi-file projects yield the full body,
|
||||||
# not just the primary file's skeleton of include directives (R-C).
|
# not just the primary file's skeleton of include directives (R-C).
|
||||||
return flatten_inputs(files[primary_name], files)
|
return flatten_inputs(files[primary_name], files)
|
||||||
except tarfile.TarError as exc:
|
except tarfile.TarError:
|
||||||
logger.warning("Failed to open tar archive for %s: %s", arxiv_id, exc)
|
# 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
|
||||||
|
|
||||||
return None # unreachable but satisfies type checker
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_pdf_url(arxiv_id: str) -> str:
|
def fetch_pdf_url(arxiv_id: str) -> str:
|
||||||
"""Return the canonical PDF URL for an arXiv paper.
|
"""Return the canonical PDF URL for an arXiv paper.
|
||||||
|
|||||||
@@ -86,6 +86,30 @@ def test_fetch_source_flattens_multifile(monkeypatch: pytest.MonkeyPatch) -> Non
|
|||||||
assert "\\input" not in result and "\\include" not in result
|
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
|
# fetch_source — 404 → None
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user