Spaces:
Running
Running
Download _utils/_zip_artifact.py from scikit-plots/ai: direct link, hf CLI and curl.
- Browser
- Download file 26.7 kB
-
https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_zip_artifact.py
- Command line
-
hf download hf://spaces/scikit-plots/ai/_utils/_zip_artifact.py
-
curl -L -o _zip_artifact.py https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_zip_artifact.py
26.7 kB
| # Authors: The scikit-plots developers | |
| # SPDX-License-Identifier: BSD-3-Clause | |
| """ | |
| Server-authoritative ZIP edit artifact orchestration. | |
| Run 139 keeps model/provider output outside archive authority. A caller must | |
| supply three independently validated things: | |
| * the exact source ZIP generation (size + SHA-256), | |
| * an explicit set of existing paths authorized for modification, and | |
| * a bounded proposal whose replacement parts are independently size/hash bound. | |
| The proposal can name only authorized paths. The source archive remains the | |
| final tree authority because all mutation is delegated to ``_zip_workspace``. | |
| No provider identifier, model metadata, credential, URL, or model-selected | |
| archive structure participates in the rewrite contract. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import re | |
| from dataclasses import dataclass | |
| from typing import Any, BinaryIO, Mapping | |
| from starlette.datastructures import FormData, UploadFile | |
| from starlette.formparsers import MultiPartException, MultiPartParser | |
| from starlette.requests import Request | |
| from ._zip_workspace import ( | |
| DEFAULT_ZIP_WORKSPACE_LIMITS, | |
| ZipWorkspaceArtifact, | |
| ZipWorkspaceError, | |
| ZipWorkspaceLimits, | |
| rewrite_zip_workspace, | |
| validate_zip_workspace_file_path, | |
| ) | |
| ZIP_EDIT_CONTRACT = "scikitplot-zip-edit-v1" | |
| ZIP_EDIT_RECEIPT_CONTRACT = "scikitplot-zip-edit-receipt-v1" | |
| ZIP_EDIT_MANIFEST_FIELD = "manifest" | |
| ZIP_EDIT_SOURCE_FIELD = "archive" | |
| ZIP_EDIT_REPLACEMENT_PREFIX = "replacement:" | |
| ZIP_EDIT_MAX_MANIFEST_BYTES = 512 * 1024 | |
| ZIP_EDIT_MAX_AUTHORIZED_PATHS = 4096 | |
| ZIP_EDIT_MAX_REPLACEMENTS = 256 | |
| # Browser capability discovery must learn the same byte ceilings enforced by | |
| # the workspace rather than duplicating them in UI code. These aliases are | |
| # intentionally derived from the immutable default limit object. | |
| ZIP_EDIT_MAX_SOURCE_BYTES = DEFAULT_ZIP_WORKSPACE_LIMITS.max_source_bytes | |
| ZIP_EDIT_MAX_ENTRY_BYTES = DEFAULT_ZIP_WORKSPACE_LIMITS.max_entry_uncompressed_bytes | |
| ZIP_EDIT_MAX_REPLACEMENT_TOTAL_BYTES = ( | |
| DEFAULT_ZIP_WORKSPACE_LIMITS.max_replacement_total_bytes | |
| ) | |
| ZIP_EDIT_REQUEST_OVERHEAD_BYTES = 2 * 1024 * 1024 | |
| ZIP_EDIT_MAX_REQUEST_BYTES = ( | |
| DEFAULT_ZIP_WORKSPACE_LIMITS.max_source_bytes | |
| + DEFAULT_ZIP_WORKSPACE_LIMITS.max_replacement_total_bytes | |
| + ZIP_EDIT_REQUEST_OVERHEAD_BYTES | |
| ) | |
| _HASH_CHUNK_BYTES = 1024 * 1024 | |
| _ID_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,47}$") | |
| _SHA256_RE = re.compile(r"^[0-9a-f]{64}$") | |
| _LIFECYCLE_ID_RE = re.compile(r"^[0-9a-f]{32}$") | |
| _CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]") | |
| class ZipArtifactError(ValueError): | |
| """ZIP edit artifact request failed before an artifact could be returned.""" | |
| def __init__(self, code: str, message: str) -> None: | |
| super().__init__(message) | |
| self.code = str(code or "ZIP_EDIT_INVALID") | |
| class ZipEditSource: | |
| size: int | |
| sha256: str | |
| class ZipEditReplacement: | |
| id: str | |
| path: str | |
| size: int | |
| sha256: str | |
| provider_artifact_id: str = "" | |
| class ZipEditManifest: | |
| source: ZipEditSource | |
| authorized_paths: tuple[str, ...] | |
| replacements: tuple[ZipEditReplacement, ...] | |
| class ZipEditReceipt: | |
| """Bounded public receipt; authorized/proposed file bodies are never copied.""" | |
| source_sha256: str | |
| output_sha256: str | |
| entry_count: int | |
| authorized_count: int | |
| applied_count: int | |
| unchanged_count: int | |
| tree_preserved: bool | |
| unchanged_content_preserved: bool | |
| metadata_preserved: bool | |
| provider_artifact_ids: tuple[str, ...] = () | |
| def as_dict(self) -> dict[str, object]: | |
| out: dict[str, object] = { | |
| "contract": ZIP_EDIT_RECEIPT_CONTRACT, | |
| "source_sha256": self.source_sha256, | |
| "output_sha256": self.output_sha256, | |
| "entry_count": self.entry_count, | |
| "authorized_count": self.authorized_count, | |
| "applied_count": self.applied_count, | |
| "unchanged_count": self.unchanged_count, | |
| "tree_preserved": self.tree_preserved, | |
| "unchanged_content_preserved": self.unchanged_content_preserved, | |
| "metadata_preserved": self.metadata_preserved, | |
| } | |
| if self.provider_artifact_ids: | |
| out["provider_artifact_ids"] = list(self.provider_artifact_ids) | |
| return out | |
| class ZipEditArtifact: | |
| file: BinaryIO | |
| filename: str | |
| receipt: ZipEditReceipt | |
| def close(self) -> None: | |
| self.file.close() | |
| class ParsedZipEditRequest: | |
| manifest: ZipEditManifest | |
| source: UploadFile | |
| replacements: dict[str, UploadFile] | |
| wire_body_bytes: int | |
| wire_body_sha256: str | |
| async def close(self) -> None: | |
| rows = [self.source, *self.replacements.values()] | |
| for upload in rows: | |
| try: # ruff: ignore[suppressible-exception] | |
| await upload.close() | |
| except Exception: # ruff: ignore[blind-except, try-except-in-loop] | |
| pass | |
| class _ZipEditMultiPartParser(MultiPartParser): | |
| """Multipart parser with role-aware active byte ceilings.""" | |
| def __init__( | |
| self, | |
| *args, | |
| limits: ZipWorkspaceLimits, | |
| **kwargs, | |
| ) -> None: | |
| super().__init__(*args, **kwargs) | |
| self._limits = limits | |
| self._current_file_bytes = 0 | |
| self._replacement_bytes = 0 | |
| self._source_bytes = 0 | |
| self._seen_manifest = False | |
| self._seen_source = False | |
| self._seen_replacement_ids: set[str] = set() | |
| def on_part_begin(self) -> None: | |
| super().on_part_begin() | |
| self._current_file_bytes = 0 | |
| def on_headers_finished(self) -> None: | |
| super().on_headers_finished() | |
| field = self._current_part.field_name | |
| if self._current_part.file is None: | |
| if field != ZIP_EDIT_MANIFEST_FIELD: | |
| raise MultiPartException( | |
| "Unsupported ZIP edit multipart metadata field." | |
| ) | |
| if self._seen_manifest: | |
| raise MultiPartException("Duplicate ZIP edit manifest field.") | |
| self._seen_manifest = True | |
| return | |
| if field == ZIP_EDIT_SOURCE_FIELD: | |
| if self._seen_source: | |
| raise MultiPartException("Duplicate ZIP edit archive field.") | |
| self._seen_source = True | |
| return | |
| if field.startswith(ZIP_EDIT_REPLACEMENT_PREFIX): | |
| rid = field[len(ZIP_EDIT_REPLACEMENT_PREFIX) :] | |
| if _ID_RE.fullmatch(rid): | |
| if rid in self._seen_replacement_ids: | |
| raise MultiPartException("Duplicate ZIP edit replacement field.") | |
| self._seen_replacement_ids.add(rid) | |
| return | |
| raise MultiPartException("Unsupported ZIP edit multipart file field.") | |
| def on_part_data(self, data: bytes, start: int, end: int) -> None: | |
| nbytes = max(0, end - start) | |
| if self._current_part.file is not None: | |
| self._current_file_bytes += nbytes | |
| field = self._current_part.field_name | |
| if field == ZIP_EDIT_SOURCE_FIELD: | |
| self._source_bytes += nbytes | |
| if self._current_file_bytes > self._limits.max_source_bytes: | |
| raise MultiPartException( | |
| "ZIP source exceeded configured byte limit." | |
| ) | |
| elif field.startswith(ZIP_EDIT_REPLACEMENT_PREFIX): | |
| self._replacement_bytes += nbytes | |
| if self._current_file_bytes > self._limits.max_entry_uncompressed_bytes: | |
| raise MultiPartException( | |
| "ZIP replacement exceeded configured per-entry byte limit." | |
| ) | |
| if self._replacement_bytes > self._limits.max_replacement_total_bytes: | |
| raise MultiPartException( | |
| "ZIP replacements exceeded configured aggregate byte limit." | |
| ) | |
| else: | |
| raise MultiPartException("Unsupported ZIP edit multipart file field.") | |
| super().on_part_data(data, start, end) | |
| async def _bounded_stream(request: Request, maximum: int, stats: dict[str, object]): | |
| total = 0 | |
| digest = hashlib.sha256() | |
| content_length = request.headers.get("content-length") | |
| if content_length: | |
| try: | |
| if int(content_length) > maximum: | |
| raise MultiPartException( | |
| "ZIP edit request exceeded configured byte limit." | |
| ) | |
| except ValueError as exc: | |
| raise MultiPartException("Invalid ZIP edit Content-Length.") from exc | |
| async for chunk in request.stream(): | |
| total += len(chunk) | |
| if total > maximum: | |
| raise MultiPartException("ZIP edit request exceeded configured byte limit.") | |
| digest.update(chunk) | |
| yield chunk | |
| stats["wire_body_bytes"] = total | |
| stats["wire_body_sha256"] = digest.hexdigest() | |
| def _object(value: Any, *, field: str, allowed: set[str]) -> dict[str, Any]: | |
| if not isinstance(value, dict): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", f"{field} must be an object" | |
| ) | |
| unknown = set(value) - allowed | |
| if unknown: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| f"unsupported {field} field(s): " + ", ".join(sorted(unknown)), | |
| ) | |
| return value | |
| def _positive_size(value: Any, *, field: str, maximum: int) -> int: | |
| if ( | |
| isinstance(value, bool) | |
| or not isinstance(value, int) | |
| or value < 0 | |
| or value > maximum | |
| ): | |
| raise ZipArtifactError("ZIP_EDIT_MANIFEST_INVALID", f"{field} is invalid") | |
| return value | |
| def _sha256(value: Any, *, field: str) -> str: | |
| text = str(value or "").strip().lower() | |
| if not _SHA256_RE.fullmatch(text): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", f"{field} must be a SHA-256 hex digest" | |
| ) | |
| return text | |
| def _provider_artifact_id(value: Any) -> str: | |
| text = str(value or "").strip().lower() | |
| if text and not _LIFECYCLE_ID_RE.fullmatch(text): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "provider artifact lifecycle id is invalid" | |
| ) | |
| return text | |
| def _safe_edit_path(value: Any, *, limits: ZipWorkspaceLimits) -> tuple[str, str]: | |
| if not isinstance(value, str): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "ZIP edit paths must be strings" | |
| ) | |
| try: | |
| path = validate_zip_workspace_file_path(value, limits=limits) | |
| except ZipWorkspaceError as exc: | |
| raise ZipArtifactError("ZIP_EDIT_MANIFEST_INVALID", str(exc)) from exc | |
| # Canonical key is request-only duplicate protection. The workspace still | |
| # requires exact source-entry spelling when the replacement is applied. | |
| import unicodedata # ruff: ignore[import-outside-top-level] | |
| canonical = unicodedata.normalize("NFC", path).casefold() | |
| return path, canonical | |
| def parse_zip_edit_manifest( # ruff: ignore[too-many-branches] | |
| value: bytes | str | Mapping[str, Any], | |
| *, | |
| limits: ZipWorkspaceLimits = DEFAULT_ZIP_WORKSPACE_LIMITS, | |
| ) -> ZipEditManifest: | |
| """Parse a strict model-neutral authorization/proposal manifest.""" | |
| if isinstance(value, Mapping): | |
| raw = dict(value) | |
| try: | |
| encoded = json.dumps(raw, ensure_ascii=False, separators=(",", ":")).encode( | |
| "utf-8" | |
| ) | |
| except (TypeError, ValueError, UnicodeError) as exc: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "ZIP edit manifest must be JSON-compatible" | |
| ) from exc | |
| if len(encoded) > ZIP_EDIT_MAX_MANIFEST_BYTES: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_TOO_LARGE", | |
| "ZIP edit manifest exceeds configured limit", | |
| ) | |
| else: | |
| if isinstance(value, bytes): | |
| if len(value) > ZIP_EDIT_MAX_MANIFEST_BYTES: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_TOO_LARGE", | |
| "ZIP edit manifest exceeds configured limit", | |
| ) | |
| try: | |
| value = value.decode("utf-8") | |
| except UnicodeDecodeError as exc: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "ZIP edit manifest must be UTF-8" | |
| ) from exc | |
| elif not isinstance(value, str): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "ZIP edit manifest must be JSON" | |
| ) | |
| if len(value.encode("utf-8")) > ZIP_EDIT_MAX_MANIFEST_BYTES: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_TOO_LARGE", | |
| "ZIP edit manifest exceeds configured limit", | |
| ) | |
| try: | |
| raw = json.loads(value) | |
| except (json.JSONDecodeError, TypeError, ValueError) as exc: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "ZIP edit manifest must be valid JSON" | |
| ) from exc | |
| root = _object( | |
| raw, | |
| field="manifest", | |
| allowed={"contract", "source", "authorization", "proposal"}, | |
| ) | |
| if root.get("contract") != ZIP_EDIT_CONTRACT: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", f"contract must be {ZIP_EDIT_CONTRACT!r}" | |
| ) | |
| source_raw = _object(root.get("source"), field="source", allowed={"size", "sha256"}) | |
| source = ZipEditSource( | |
| size=_positive_size( | |
| source_raw.get("size"), field="source.size", maximum=limits.max_source_bytes | |
| ), | |
| sha256=_sha256(source_raw.get("sha256"), field="source.sha256"), | |
| ) | |
| auth_raw = _object( | |
| root.get("authorization"), field="authorization", allowed={"paths"} | |
| ) | |
| auth_value = auth_raw.get("paths") | |
| if not isinstance(auth_value, list) or not auth_value: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "authorization.paths must be a non-empty array" | |
| ) | |
| if len(auth_value) > min(ZIP_EDIT_MAX_AUTHORIZED_PATHS, limits.max_entries): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", "authorization.paths exceeds configured count" | |
| ) | |
| authorized: list[str] = [] | |
| auth_canonical: set[str] = set() | |
| for value in auth_value: | |
| path, canonical = _safe_edit_path(value, limits=limits) | |
| if canonical in auth_canonical: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| "authorization.paths contains duplicate or aliased paths", | |
| ) | |
| auth_canonical.add(canonical) | |
| authorized.append(path) | |
| proposal_raw = _object( | |
| root.get("proposal"), field="proposal", allowed={"replacements"} | |
| ) | |
| proposal_value = proposal_raw.get("replacements") | |
| if not isinstance(proposal_value, list) or not proposal_value: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| "proposal.replacements must be a non-empty array", | |
| ) | |
| if len(proposal_value) > ZIP_EDIT_MAX_REPLACEMENTS: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| "proposal.replacements exceeds configured count", | |
| ) | |
| replacements: list[ZipEditReplacement] = [] | |
| seen_ids: set[str] = set() | |
| seen_paths: set[str] = set() | |
| authorized_exact = set(authorized) | |
| for row in proposal_value: | |
| item = _object( | |
| row, | |
| field="proposal replacement", | |
| allowed={"id", "path", "size", "sha256", "provider_artifact_id"}, | |
| ) | |
| rid = str(item.get("id") or "") | |
| if not _ID_RE.fullmatch(rid) or rid in seen_ids: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| "proposal replacement id is invalid or duplicate", | |
| ) | |
| seen_ids.add(rid) | |
| path, canonical = _safe_edit_path(item.get("path"), limits=limits) | |
| if canonical in seen_paths: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_MANIFEST_INVALID", | |
| "proposal contains duplicate or aliased paths", | |
| ) | |
| seen_paths.add(canonical) | |
| if path not in authorized_exact: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_NOT_AUTHORIZED", | |
| "proposal path is not present in authorization.paths", | |
| ) | |
| replacements.append( | |
| ZipEditReplacement( | |
| id=rid, | |
| path=path, | |
| size=_positive_size( | |
| item.get("size"), | |
| field="proposal replacement size", | |
| maximum=limits.max_entry_uncompressed_bytes, | |
| ), | |
| sha256=_sha256(item.get("sha256"), field="proposal replacement sha256"), | |
| provider_artifact_id=_provider_artifact_id( | |
| item.get("provider_artifact_id") | |
| ), | |
| ) | |
| ) | |
| if sum(row.size for row in replacements) > limits.max_replacement_total_bytes: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_TOO_LARGE", | |
| "proposal replacement bytes exceed configured aggregate limit", | |
| ) | |
| return ZipEditManifest( | |
| source=source, | |
| authorized_paths=tuple(authorized), | |
| replacements=tuple(replacements), | |
| ) | |
| async def _measure_upload(upload: UploadFile, *, maximum: int) -> tuple[int, str]: | |
| digest = hashlib.sha256() | |
| total = 0 | |
| await upload.seek(0) | |
| while True: | |
| chunk = await upload.read(_HASH_CHUNK_BYTES) | |
| if not chunk: | |
| break | |
| total += len(chunk) | |
| if total > maximum: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_TOO_LARGE", "ZIP edit upload exceeds configured byte limit" | |
| ) | |
| digest.update(chunk) | |
| await upload.seek(0) | |
| return total, digest.hexdigest() | |
| async def parse_zip_edit_request( # ruff: ignore[too-many-branches] | |
| request: Request, | |
| *, | |
| limits: ZipWorkspaceLimits = DEFAULT_ZIP_WORKSPACE_LIMITS, | |
| max_request_bytes: int = ZIP_EDIT_MAX_REQUEST_BYTES, | |
| ) -> ParsedZipEditRequest: | |
| """Parse and independently bind every multipart byte generation.""" | |
| content_type = request.headers.get("content-type", "").lower() | |
| if not content_type.startswith("multipart/form-data"): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", "ZIP edit request must use multipart/form-data" | |
| ) | |
| stats: dict[str, object] = {} | |
| parser = _ZipEditMultiPartParser( | |
| headers=request.headers, | |
| stream=_bounded_stream(request, max_request_bytes, stats), | |
| max_files=ZIP_EDIT_MAX_REPLACEMENTS + 1, | |
| max_fields=1, | |
| max_part_size=ZIP_EDIT_MAX_MANIFEST_BYTES, | |
| limits=limits, | |
| ) | |
| try: | |
| form: FormData = await parser.parse() | |
| except ZipArtifactError: | |
| raise | |
| except MultiPartException as exc: | |
| text = str(exc) | |
| code = ( | |
| "ZIP_EDIT_TOO_LARGE" | |
| if "limit" in text.lower() or "exceeded" in text.lower() | |
| else "ZIP_EDIT_INVALID" | |
| ) | |
| raise ZipArtifactError(code, text) from exc | |
| uploads_seen = [ | |
| value for _key, value in form.multi_items() if isinstance(value, UploadFile) | |
| ] | |
| try: | |
| manifest_values = form.getlist(ZIP_EDIT_MANIFEST_FIELD) | |
| if len(manifest_values) != 1 or not isinstance(manifest_values[0], str): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", | |
| "multipart request must contain exactly one manifest field", | |
| ) | |
| manifest = parse_zip_edit_manifest(manifest_values[0], limits=limits) | |
| source: UploadFile | None = None | |
| replacements: dict[str, UploadFile] = {} | |
| for key, value in form.multi_items(): | |
| if key == ZIP_EDIT_MANIFEST_FIELD: | |
| continue | |
| if not isinstance(value, UploadFile): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", "unsupported ZIP edit multipart field" | |
| ) | |
| if key == ZIP_EDIT_SOURCE_FIELD: | |
| if source is not None: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", | |
| "ZIP edit request contains duplicate archive fields", | |
| ) | |
| source = value | |
| continue | |
| if not key.startswith(ZIP_EDIT_REPLACEMENT_PREFIX): | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", "unsupported ZIP edit multipart file field" | |
| ) | |
| rid = key[len(ZIP_EDIT_REPLACEMENT_PREFIX) :] | |
| if not _ID_RE.fullmatch(rid) or rid in replacements: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", | |
| "ZIP edit replacement multipart id is invalid or duplicate", | |
| ) | |
| replacements[rid] = value | |
| if source is None: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", "ZIP edit request is missing the archive file" | |
| ) | |
| expected_ids = {row.id for row in manifest.replacements} | |
| if set(replacements) != expected_ids: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_INVALID", | |
| "replacement multipart parts do not match proposal ids", | |
| ) | |
| source_size, source_sha = await _measure_upload( | |
| source, maximum=limits.max_source_bytes | |
| ) | |
| if source_size != manifest.source.size or source_sha != manifest.source.sha256: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_SOURCE_MISMATCH", | |
| "source ZIP does not match manifest generation", | |
| ) | |
| by_id = {row.id: row for row in manifest.replacements} | |
| for rid, upload in replacements.items(): | |
| descriptor = by_id[rid] | |
| actual_size, actual_sha = await _measure_upload( | |
| upload, maximum=limits.max_entry_uncompressed_bytes | |
| ) | |
| if actual_size != descriptor.size or actual_sha != descriptor.sha256: | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_REPLACEMENT_MISMATCH", | |
| "replacement bytes do not match proposal descriptor", | |
| ) | |
| return ParsedZipEditRequest( | |
| manifest=manifest, | |
| source=source, | |
| replacements=replacements, | |
| wire_body_bytes=int(stats.get("wire_body_bytes", 0)), | |
| wire_body_sha256=str(stats.get("wire_body_sha256", "")), | |
| ) | |
| except Exception: | |
| for upload in uploads_seen: | |
| try: # ruff: ignore[suppressible-exception] | |
| await upload.close() | |
| except Exception: # ruff: ignore[blind-except, try-except-in-loop] | |
| pass | |
| raise | |
| def _safe_output_filename(filename: str | None) -> str: | |
| raw = str(filename or "project.zip").replace("\\", "/").split("/")[-1] | |
| raw = _CONTROL_RE.sub("", raw).strip() | |
| stem = raw[:-4] if raw.lower().endswith(".zip") else raw | |
| stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._-")[:120] | |
| if not stem: | |
| stem = "project" | |
| return f"{stem}.modified.zip" | |
| def build_zip_edit_artifact( | |
| parsed: ParsedZipEditRequest, | |
| *, | |
| limits: ZipWorkspaceLimits = DEFAULT_ZIP_WORKSPACE_LIMITS, | |
| ) -> ZipEditArtifact: | |
| """Apply one verified authorization/proposal set to the complete source ZIP.""" | |
| by_id = {row.id: row for row in parsed.manifest.replacements} | |
| replacements: dict[str, BinaryIO] = { | |
| by_id[rid].path: upload.file for rid, upload in parsed.replacements.items() | |
| } | |
| try: | |
| workspace: ZipWorkspaceArtifact = rewrite_zip_workspace( | |
| parsed.source.file, | |
| replacements, | |
| authorized_paths=parsed.manifest.authorized_paths, | |
| expected_source_sha256=parsed.manifest.source.sha256, | |
| expected_source_size=parsed.manifest.source.size, | |
| replacement_expectations={ | |
| row.path: (row.size, row.sha256) for row in parsed.manifest.replacements | |
| }, | |
| limits=limits, | |
| ) | |
| except ZipWorkspaceError as exc: | |
| message = str(exc) | |
| if message.startswith("ZIP source generation does not match expected"): | |
| raise ZipArtifactError("ZIP_EDIT_SOURCE_MISMATCH", message) from exc | |
| if message == "ZIP replacement generation does not match expected identity": | |
| raise ZipArtifactError("ZIP_EDIT_REPLACEMENT_MISMATCH", message) from exc | |
| raise ZipArtifactError("ZIP_EDIT_WORKSPACE_REJECTED", message) from exc | |
| if workspace.receipt.source_sha256 != parsed.manifest.source.sha256: | |
| workspace.close() | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_SOURCE_MISMATCH", "workspace source generation changed" | |
| ) | |
| if set(workspace.receipt.changed_paths) != { | |
| row.path for row in parsed.manifest.replacements | |
| }: | |
| workspace.close() | |
| raise ZipArtifactError( | |
| "ZIP_EDIT_VERIFICATION_FAILED", "workspace changed-path receipt mismatch" | |
| ) | |
| receipt = ZipEditReceipt( | |
| source_sha256=workspace.receipt.source_sha256, | |
| output_sha256=workspace.receipt.output_sha256, | |
| entry_count=workspace.receipt.entry_count, | |
| authorized_count=len(parsed.manifest.authorized_paths), | |
| applied_count=len(parsed.manifest.replacements), | |
| unchanged_count=workspace.receipt.unchanged_count, | |
| tree_preserved=workspace.receipt.tree_preserved, | |
| unchanged_content_preserved=workspace.receipt.unchanged_content_preserved, | |
| metadata_preserved=workspace.receipt.metadata_preserved, | |
| ) | |
| return ZipEditArtifact( | |
| file=workspace.file, | |
| filename=_safe_output_filename(parsed.source.filename), | |
| receipt=receipt, | |
| ) | |
| def encode_zip_edit_receipt_header(receipt: ZipEditReceipt) -> str: | |
| """Encode a path-free receipt small enough for a browser-visible response header.""" | |
| return json.dumps( | |
| receipt.as_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True | |
| ) | |
| __all__ = ( | |
| "ZIP_EDIT_CONTRACT", | |
| "ZIP_EDIT_MAX_ENTRY_BYTES", | |
| "ZIP_EDIT_MAX_REPLACEMENT_TOTAL_BYTES", | |
| "ZIP_EDIT_MAX_REQUEST_BYTES", | |
| "ZIP_EDIT_MAX_SOURCE_BYTES", | |
| "ZIP_EDIT_RECEIPT_CONTRACT", | |
| "ParsedZipEditRequest", | |
| "ZipArtifactError", | |
| "ZipEditArtifact", | |
| "ZipEditManifest", | |
| "ZipEditReceipt", | |
| "build_zip_edit_artifact", | |
| "encode_zip_edit_receipt_header", | |
| "parse_zip_edit_manifest", | |
| "parse_zip_edit_request", | |
| ) | |