gabriel / musehub public
fix BREAKING task/fix-delta-compressed-base #1 / 1
gabriel · 164 days ago · Apr 13, 2026 · Diff

fix: decompress zlib-compressed delta bases before apply_delta

Old-wire-path objects were stored zlib-compressed in R2. When a new object is pushed as delta+zlib against such a base, the server fetched the raw R2 bytes (compressed) and passed them directly to apply_delta(). The delta was applied against wrong bytes — producing garbage — and hash verification was skipped for non-sha256:-prefixed IDs, so it passed.

Real-world failure: staging README showed '0!' after a one-line README edit was pushed as delta+zlib against the old zlib-compressed README.

Fix: - Move decompress_if_needed() to muse_contracts/compression.py (canonical home) - Apply decompress_if_needed() to base_raw before apply_delta() in wire_push_object_pack so deltas are always applied against plain bytes - Add POST /{owner}/{slug}/repair-object endpoint (owner/write-only) to replace a corrupt stored object with SHA-256-verified correct bytes - Update ui_tree.py and _ui_helpers.py to import from compression module

Tests: 45 pass — 18 new regression tests cover both delta base scenarios (content_cache path and storage path) and the repair endpoint.

sha256:aaf5f76dd4004e6d4a0ff70dd68960b118fb032175aef5cb3745e385d9084ebd sha
+40 ~24 −3 symbols
sha256:69dda09485a1e1f5eb78486b22daefe0b54511197804272addeec386e949cc66 snapshot
+40
symbols added
~24
symbols modified
−3
symbols removed
0
dead code introduced
Semantic Changes 67 symbols
+ _bare_sha256 function function _bare_sha256 L48–50
+ _compress_zlib function function _compress_zlib L53–54
+ _make_delta_stream function function _make_delta_stream L57–64
+ _mp function function _mp L67–68
+ _sha256_oid function function _sha256_oid L43–45
+ AsyncClient import import AsyncClient L32–32
+ AsyncSession import import AsyncSession L33–33
+ annotations import import annotations L24–24
+ db import import db L36–36
+ factory_create_repo import import factory_create_repo L35–35
+ hashlib import import hashlib L26–26
+ msgpack import import msgpack L30–30
+ pytest import import pytest L31–31
+ struct import import struct L27–27
+ zlib import import zlib L28–28 ← moved from musehub/api/routes/musehub/ui_tree.py
+ test_decompress_if_needed_binary_non_zlib_passthrough function function test_decompress_if_needed_binary_non_zlib_passthrough L108–112
+ test_decompress_if_needed_empty_bytes_passthrough function function test_decompress_if_needed_empty_bytes_passthrough L115–117
+ test_decompress_if_needed_full_readme_roundtrip function function test_decompress_if_needed_full_readme_roundtrip L134–138
+ test_decompress_if_needed_importable_from_compression_module function function test_decompress_if_needed_importable_from_compression_module L75–78
+ test_decompress_if_needed_plain_text_passthrough function function test_decompress_if_needed_plain_text_passthrough L102–105
+ test_decompress_if_needed_short_data_passthrough function function test_decompress_if_needed_short_data_passthrough L120–122
+ test_decompress_if_needed_truncated_zlib_returns_original function function test_decompress_if_needed_truncated_zlib_returns_original L125–131
+ test_decompress_if_needed_zlib_level_1_magic_78_01 function function test_decompress_if_needed_zlib_level_1_magic_78_01 L81–85
+ test_decompress_if_needed_zlib_level_6_magic_78_9c function function test_decompress_if_needed_zlib_level_6_magic_78_9c L88–92
+ test_decompress_if_needed_zlib_level_9_magic_78_da function function test_decompress_if_needed_zlib_level_9_magic_78_da L95–99
+ test_push_delta_plain_base_baseline function async_function test_push_delta_plain_base_baseline L146–183
+ test_push_delta_reconstructed_bytes_match_target function async_function test_push_delta_reconstructed_bytes_match_target L303–357
+ test_push_delta_zlib_base_in_content_cache_succeeds function async_function test_push_delta_zlib_base_in_content_cache_succeeds L191–241
+ test_push_delta_zlib_base_in_storage_succeeds function async_function test_push_delta_zlib_base_in_storage_succeeds L245–299
+ test_repair_object_accepts_sha256_prefixed_id function async_function test_repair_object_accepts_sha256_prefixed_id L436–472
+ test_repair_object_corrects_corrupted_stored_bytes function async_function test_repair_object_corrects_corrupted_stored_bytes L365–410
+ test_repair_object_is_idempotent function async_function test_repair_object_is_idempotent L476–514
+ test_repair_object_rejects_hash_mismatch function async_function test_repair_object_rejects_hash_mismatch L414–432
~ musehub/api/routes/musehub/ui_tree.py .py 1 symbol added, 3 symbols removed
− _ZLIB_MAGIC variable variable _ZLIB_MAGIC L39–39
− decompress_if_needed function function decompress_if_needed L42–54
− zlib import import zlib L11–11 → moved to tests/test_delta_compressed_base_regression.py
+ decompress_if_needed import import decompress_if_needed L19–19
~ musehub/api/routes/wire.py .py 2 symbols added, 12 symbols modified
+ _ZLIB_MAGIC variable variable _ZLIB_MAGIC L24–24
+ decompress_if_needed function function decompress_if_needed L27–46
~ musehub/services/musehub_wire.py .py 2 symbols added, 3 symbols modified
+ decompress_if_needed import import decompress_if_needed L81–81
+ wire_repair_object function async_function wire_repair_object L997–1072
← Older Oldest on task/fix-delta-compressed-base
All commits
Newer → Latest on task/fix-delta-compressed-base

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:aaf5f76dd4004e6d4a0ff70dd68960b118fb032175aef5cb3745e385d9084ebd --body "your comment"