|
9 | 9 | import json |
10 | 10 | from dataclasses import dataclass |
11 | 11 | from pathlib import Path |
| 12 | +from urllib.parse import urlparse |
12 | 13 | from urllib.request import Request, urlopen |
13 | 14 |
|
14 | 15 |
|
@@ -43,8 +44,7 @@ def load_sources(manifest_path: Path) -> list[Source]: |
43 | 44 |
|
44 | 45 | if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest): |
45 | 46 | raise ValueError(f"sources[{index}].sha256 must be 64 lowercase hex characters") |
46 | | - if "/refs/heads/" in url or url.endswith(("/main", "/master")): |
47 | | - raise ValueError(f"sources[{index}].url must be pinned to an immutable commit") |
| 47 | + _validate_immutable_url(url, f"sources[{index}].url") |
48 | 48 |
|
49 | 49 | sources.append( |
50 | 50 | Source( |
@@ -91,6 +91,24 @@ def check(sources: list[Source], root: Path) -> None: |
91 | 91 | raise ValueError("generated templates are out of date: " + ", ".join(drift)) |
92 | 92 |
|
93 | 93 |
|
| 94 | +def _validate_immutable_url(url: str, path: str) -> None: |
| 95 | + parsed = urlparse(url) |
| 96 | + if parsed.scheme != "https": |
| 97 | + raise ValueError(f"{path} must use https") |
| 98 | + |
| 99 | + if parsed.hostname == "raw.githubusercontent.com": |
| 100 | + parts = [part for part in parsed.path.split("/") if part] |
| 101 | + if len(parts) < 4: |
| 102 | + raise ValueError(f"{path} is not a valid raw GitHub file URL") |
| 103 | + revision = parts[2] |
| 104 | + if len(revision) != 40 or any( |
| 105 | + char not in "0123456789abcdefABCDEF" for char in revision |
| 106 | + ): |
| 107 | + raise ValueError(f"{path} must pin a 40-character Git commit SHA") |
| 108 | + elif "/refs/heads/" in parsed.path: |
| 109 | + raise ValueError(f"{path} must not reference a mutable branch") |
| 110 | + |
| 111 | + |
94 | 112 | def _safe_destination(root: Path, destination: Path) -> Path: |
95 | 113 | if destination.is_absolute() or ".." in destination.parts: |
96 | 114 | raise ValueError(f"unsafe destination: {destination}") |
|
0 commit comments