checksums.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago
| 1 | """Checksum helpers for release artifacts (§QR.7.3).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import hashlib |
| 6 | from pathlib import Path |
| 7 | |
| 8 | |
| 9 | def sha256_file(path: Path, *, chunk_size: int = 1024 * 1024) -> str: |
| 10 | """Return lowercase hex SHA-256 of ``path`` using single-pass streaming.""" |
| 11 | digest = hashlib.sha256() |
| 12 | with path.open("rb") as handle: |
| 13 | while True: |
| 14 | chunk = handle.read(chunk_size) |
| 15 | if not chunk: |
| 16 | break |
| 17 | digest.update(chunk) |
| 18 | return digest.hexdigest() |
| 19 | |
| 20 | |
| 21 | def format_sha256sums_line(sha256: str, filename: str) -> str: |
| 22 | """Return one ``SHA256SUMS.txt`` line (``sha256 filename``).""" |
| 23 | return f"{sha256.lower()} {filename}" |
| 24 | |
| 25 | |
| 26 | def write_sha256sums( |
| 27 | destination: Path, |
| 28 | entries: list[tuple[str, str]], |
| 29 | ) -> str: |
| 30 | """Write ``SHA256SUMS.txt`` content and return the text written. |
| 31 | |
| 32 | Parameters |
| 33 | ---------- |
| 34 | destination: |
| 35 | Output path (typically ``SHA256SUMS.txt``). |
| 36 | entries: |
| 37 | List of ``(sha256, filename)`` pairs in publish order. |
| 38 | """ |
| 39 | lines = [format_sha256sums_line(sha, name) for sha, name in entries] |
| 40 | text = "\n".join(lines) + ("\n" if lines else "") |
| 41 | destination.write_text(text, encoding="utf-8") |
| 42 | return text |
| 43 | |
| 44 | |
| 45 | def parse_sha256sums(text: str) -> dict[str, str]: |
| 46 | """Parse ``sha256 filename`` lines into ``{filename: sha256}``. |
| 47 | |
| 48 | Filenames may contain spaces; the separator is two spaces after the digest |
| 49 | (GNU ``sha256sum`` style), with a fallback to the first whitespace split |
| 50 | when no double-space is present. |
| 51 | """ |
| 52 | result: dict[str, str] = {} |
| 53 | for line in text.splitlines(): |
| 54 | stripped = line.strip() |
| 55 | if not stripped or stripped.startswith("#"): |
| 56 | continue |
| 57 | if " " in stripped: |
| 58 | sha256, filename = stripped.split(" ", 1) |
| 59 | else: |
| 60 | parts = stripped.split(None, 1) |
| 61 | if len(parts) < 2: |
| 62 | raise ValueError(f"invalid SHA256SUMS line: {line!r}") |
| 63 | sha256, filename = parts[0], parts[1] |
| 64 | result[filename.strip()] = sha256.strip().lower() |
| 65 | return result |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
3 days ago