diff --git a/backend/services/sidecar_install.py b/backend/services/sidecar_install.py index 8cae571f..3a242ea9 100644 --- a/backend/services/sidecar_install.py +++ b/backend/services/sidecar_install.py @@ -1273,8 +1273,8 @@ def _download_and_extract(job: dict, url: str, dest: Path, work_root: Path, env_ with tempfile.TemporaryDirectory(dir=str(root)) as tmp_dir: with tarfile.open(tmp_tar, "r:gz") as tf: try: - tf.extractall(tmp_dir, filter="data") # stdlib safe-extract (3.11.4+) - except TypeError: # pragma: no cover — pre-filter= interpreters + tf.extractall(tmp_dir, filter=_no_links_data_filter) # 3.11.4+ + except (TypeError, AttributeError): # pragma: no cover — no filter= / data_filter _safe_extract_members(tf, tmp_dir) entries = [p for p in Path(tmp_dir).iterdir() if p.is_dir()] if len(entries) != 1: @@ -1345,6 +1345,21 @@ def _run_post_install(spec: SidecarSpec, job: dict, py: Path, checkout: Path) -> ) +def _no_links_data_filter(member: "tarfile.TarInfo", path: str): + """The stdlib "data" filter, except that links are skipped, not fatal. + + The "data" filter raises on a link to an absolute path, and the pinned + Matcha-TTS tarball (a CosyVoice submodule) ships one: ``data`` points at + its author's own training-data folder. That aborted the whole fetch. No + installer here uses a link from a source tree, symlinks need privileges + on Windows, and the pre-3.11.4 path below already drops them, so both + paths now behave the same. + """ + if member.issym() or member.islnk(): + return None + return tarfile.data_filter(member, path) + + def _safe_extract_members(tf: "tarfile.TarFile", dest: str) -> None: """Tar-slip-guarded extraction for interpreters without ``extractall(filter="data")`` (Python < 3.11.4). diff --git a/tests/test_sidecar_install.py b/tests/test_sidecar_install.py index 38071c8d..77b307a2 100644 --- a/tests/test_sidecar_install.py +++ b/tests/test_sidecar_install.py @@ -1455,3 +1455,36 @@ def test_cosyvoice_post_install_code_compiles_for_any_checkout_path(): spec = si.get_spec("cosyvoice") compile(si._expand(spec.post_install_code, Path("C:/Program Files/x y/CosyVoice")), "", "exec") + + + +def _tarball_with_absolute_symlink() -> bytes: + """Shaped like the pinned Matcha-TTS tarball: a package plus a `data` link + to a folder on its author's machine.""" + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tf: + data = b"x = 1\n" + info = tarfile.TarInfo("Matcha-abc/matcha/__init__.py") + info.size = len(data) + tf.addfile(info, io.BytesIO(data)) + link = tarfile.TarInfo("Matcha-abc/data") + link.type = tarfile.SYMTYPE + link.linkname = "/home/someone/Projects/Grad-TTS/data" + tf.addfile(link) + return buf.getvalue() + + +def test_a_tarball_with_an_absolute_link_still_extracts(monkeypatch, tmp_path): + """The stdlib "data" filter raises AbsoluteLinkError on such a link, which + aborted the Matcha-TTS fetch and with it the CosyVoice install.""" + import httpx + + monkeypatch.setattr(httpx, "stream", lambda method, url, **kw: _FakeStream(_tarball_with_absolute_symlink())) + dest = tmp_path / "third_party" / "Matcha-TTS" + dest.parent.mkdir(parents=True) + job = si._new_job("fake-side") + + si._download_and_extract(job, "https://example.test/m.tar.gz", dest, tmp_path, "OMNIVOICE_FAKE_SIDE_DIR") + + assert (dest / "matcha" / "__init__.py").is_file() + assert not (dest / "data").exists() and not (dest / "data").is_symlink()