|
2 | 2 |
|
3 | 3 | from pathlib import Path |
4 | 4 | import os |
| 5 | +import shutil |
5 | 6 | import subprocess |
6 | 7 | import tarfile |
7 | 8 | import tempfile |
|
13 | 14 | from leanup.repo.mathlib_cache import normalize_lean_version, remove_path |
14 | 15 |
|
15 | 16 |
|
| 17 | +def has_parallel_gzip() -> bool: |
| 18 | + return shutil.which("pigz") is not None and shutil.which("tar") is not None |
| 19 | + |
| 20 | + |
| 21 | +def validate_archive_paths(archive: Path, target_dir: Path) -> None: |
| 22 | + target_dir = target_dir.resolve() |
| 23 | + with tarfile.open(archive, "r:gz") as tar: |
| 24 | + for member in tar.getmembers(): |
| 25 | + member_path = (target_dir / member.name).resolve() |
| 26 | + if not str(member_path).startswith(str(target_dir)): |
| 27 | + raise ValueError(f"Archive contains unsafe path: {member.name}") |
| 28 | + |
| 29 | + |
| 30 | +def extract_tar_gz(archive: Path, target_dir: Path) -> None: |
| 31 | + validate_archive_paths(archive, target_dir) |
| 32 | + if has_parallel_gzip(): |
| 33 | + subprocess.run(["tar", "-I", "pigz", "-xf", str(archive), "-C", str(target_dir)], check=True) |
| 34 | + return |
| 35 | + safe_extract(archive, target_dir) |
| 36 | + |
| 37 | + |
16 | 38 | def download_to(url: str, output_file: Path) -> Path: |
17 | 39 | output_file.parent.mkdir(parents=True, exist_ok=True) |
18 | 40 | with tempfile.NamedTemporaryFile(dir=output_file.parent, prefix=f".{output_file.name}.", suffix=".tmp", delete=False) as handle: |
@@ -61,14 +83,25 @@ def tar_directory(source_dir: Path, arcname: str, output_file: Path, exclude: se |
61 | 83 | with tempfile.NamedTemporaryFile(dir=output_file.parent, prefix=f".{output_file.name}.", suffix=".tmp", delete=False) as handle: |
62 | 84 | temp_output = Path(handle.name) |
63 | 85 | try: |
64 | | - with tarfile.open(temp_output, "w:gz", dereference=False) as tar: |
65 | | - if exclude: |
66 | | - for child in sorted(source_dir.iterdir()): |
67 | | - if child.name in exclude: |
68 | | - continue |
69 | | - tar.add(child, arcname=f"{arcname}/{child.name}", recursive=True) |
70 | | - else: |
71 | | - tar.add(source_dir, arcname=arcname, recursive=True) |
| 86 | + if exclude or not has_parallel_gzip(): |
| 87 | + with tarfile.open(temp_output, "w:gz", dereference=False) as tar: |
| 88 | + if exclude: |
| 89 | + for child in sorted(source_dir.iterdir()): |
| 90 | + if child.name in exclude: |
| 91 | + continue |
| 92 | + tar.add(child, arcname=f"{arcname}/{child.name}", recursive=True) |
| 93 | + else: |
| 94 | + tar.add(source_dir, arcname=arcname, recursive=True) |
| 95 | + else: |
| 96 | + subprocess.run( |
| 97 | + ["tar", "-I", "pigz", "-cf", str(temp_output), "-C", str(source_dir.parent), "--", source_dir.name], |
| 98 | + check=True, |
| 99 | + ) |
| 100 | + # Preserve the requested archive root name. External tar is only used when arcname equals source name or .lake. |
| 101 | + if arcname != source_dir.name: |
| 102 | + remove_path(temp_output) |
| 103 | + with tarfile.open(temp_output, "w:gz", dereference=False) as tar: |
| 104 | + tar.add(source_dir, arcname=arcname, recursive=True) |
72 | 105 | temp_output.replace(output_file) |
73 | 106 | return output_file |
74 | 107 | except Exception: |
@@ -117,9 +150,10 @@ def get_elan(server: str | None = None) -> Path: |
117 | 150 | def unpack_elan(archive: Path | None = None, target_home: Path | None = None) -> Path: |
118 | 151 | archive_path = archive or elan_archive_path() |
119 | 152 | target = target_home or elan_home() |
120 | | - with tempfile.TemporaryDirectory(prefix="leanup-elan-unpack-") as work: |
| 153 | + target.parent.mkdir(parents=True, exist_ok=True) |
| 154 | + with tempfile.TemporaryDirectory(prefix=".leanup-elan-unpack.", dir=target.parent) as work: |
121 | 155 | work_root = Path(work) |
122 | | - safe_extract(archive_path, work_root) |
| 156 | + extract_tar_gz(archive_path, work_root) |
123 | 157 | extracted = work_root / ".elan" |
124 | 158 | if not extracted.exists(): |
125 | 159 | raise ValueError(f"Archive does not contain .elan/: {archive_path}") |
@@ -160,9 +194,10 @@ def get_lean(version: str, server: str | None = None) -> Path: |
160 | 194 | def unpack_lean(version: str, archive: Path | None = None, target_home: Path | None = None) -> Path: |
161 | 195 | archive_path = archive or lean_archive_path(version) |
162 | 196 | home = target_home or elan_home() |
163 | | - with tempfile.TemporaryDirectory(prefix="leanup-lean-unpack-") as work: |
| 197 | + (home / "toolchains").mkdir(parents=True, exist_ok=True) |
| 198 | + with tempfile.TemporaryDirectory(prefix=".leanup-lean-unpack.", dir=home / "toolchains") as work: |
164 | 199 | work_root = Path(work) |
165 | | - safe_extract(archive_path, work_root) |
| 200 | + extract_tar_gz(archive_path, work_root) |
166 | 201 | toolchains_root = work_root / ".elan" / "toolchains" |
167 | 202 | candidates = [path for path in toolchains_root.iterdir() if path.is_dir()] if toolchains_root.exists() else [] |
168 | 203 | if len(candidates) != 1: |
|
0 commit comments