chtypes 2.0.0.dev12__py3-none-any.whl → 2.0.0.dev13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,7 @@ from chtypes._ocifetch._layout import (
75
75
  from chtypes._ocifetch._lock import Lock, LockPin, load_lock, new_lock, save_lock
76
76
  from chtypes._ocifetch._oci import (
77
77
  Descriptor,
78
+ copy_blob_verified,
78
79
  fetch_blob_bytes,
79
80
  fetch_blob_to_path,
80
81
  fetch_manifest_by_digest,
@@ -86,6 +87,8 @@ from chtypes._ocifetch._oci import (
86
87
  resolve_platform_manifest,
87
88
  spelling_components,
88
89
  validate_spelling,
90
+ verify_body_matches_digest,
91
+ verify_json_descriptor,
89
92
  version_within_request,
90
93
  )
91
94
  from chtypes._ocifetch._referrers import ReferrerListing, discover_referrers, read_listing
@@ -1346,6 +1349,7 @@ def _verify_and_install_from_local_blobs(
1346
1349
  trusted_keys: tuple[TrustedKey, ...],
1347
1350
  visible_only: bool = False,
1348
1351
  referrers: dict[str, list[tuple[str, dict]]] | None = None,
1352
+ manifest_size: int | None = None,
1349
1353
  ) -> VerifiedRecord | None:
1350
1354
  """docs/guides/fetch-v1.md §1/§6: "A pre-seeded layout has entries in
1351
1355
  `index.json` with no corresponding `unpacked/` directory yet; the first
@@ -1357,7 +1361,14 @@ def _verify_and_install_from_local_blobs(
1357
1361
  `source_root/blobs/sha256/*`, never the network. Returns `None` — never
1358
1362
  raises — for a missing, unreadable or untrusted entry, the same
1359
1363
  tolerant shape `_find_verified_signature` uses for a network referrer:
1360
- a bad pre-seed is "nothing usable here," not a crash."""
1364
+ a bad pre-seed is "nothing usable here," not a crash.
1365
+
1366
+ The manifest blob is checked against `manifest_digest` (and against
1367
+ `manifest_size`, the index descriptor's size, when the caller has one)
1368
+ before it is parsed, and the layer blob against the manifest's layer
1369
+ descriptor (size and sha256) before it is decompressed: the same
1370
+ checks a download gets (§7.3), so no byte the signature does not cover
1371
+ reaches the install."""
1361
1372
  blobs_dir = source_root / "blobs" / "sha256"
1362
1373
  if not blobs_dir.is_dir():
1363
1374
  return None
@@ -1366,7 +1377,15 @@ def _verify_and_install_from_local_blobs(
1366
1377
  if not manifest_path.is_file():
1367
1378
  return None
1368
1379
  try:
1369
- manifest_doc = json.loads(manifest_path.read_bytes())
1380
+ manifest_bytes = manifest_path.read_bytes()
1381
+ verify_body_matches_digest(manifest_bytes, manifest_digest, what="pre-seeded manifest")
1382
+ if manifest_size is not None:
1383
+ verify_json_descriptor(
1384
+ manifest_bytes,
1385
+ Descriptor(media_type="", digest=manifest_digest, size=manifest_size),
1386
+ what="pre-seeded manifest",
1387
+ )
1388
+ manifest_doc = json.loads(manifest_bytes)
1370
1389
  layer_desc = manifest_layer_descriptor(manifest_doc)
1371
1390
  except (ValueError, OSError, FetchError):
1372
1391
  return None
@@ -1415,9 +1434,18 @@ def _verify_and_install_from_local_blobs(
1415
1434
  scratch = _scratch_root(cache_write_root)
1416
1435
  tmp_dir = tempfile.mkdtemp(dir=str(scratch))
1417
1436
  try:
1437
+ # Size and sha256 against the layer descriptor BEFORE decompressing
1438
+ # (§7.3), on a copy in scratch, which is then the file unpacked.
1439
+ verified_layer = os.path.join(tmp_dir, "layer")
1440
+ copy_blob_verified(
1441
+ str(layer_path),
1442
+ layer_desc,
1443
+ verified_layer,
1444
+ what=f"pre-seeded layer {layer_desc.digest}",
1445
+ )
1418
1446
  unpacked_dir = os.path.join(tmp_dir, "unpacked")
1419
1447
  unpack_tar_zst(
1420
- str(layer_path),
1448
+ verified_layer,
1421
1449
  unpacked_dir,
1422
1450
  library_name=predicate["library"],
1423
1451
  library_sha256=predicate["library_sha256"],
@@ -1469,11 +1497,15 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
1469
1497
  candidates: list[str] = []
1470
1498
  candidate_layers: set[str] = set()
1471
1499
  manifests_read: dict[str, bytes] = {}
1500
+ sizes: dict[str, int] = {}
1472
1501
  for m in index_doc.get("manifests") or []:
1473
1502
  digest = m.get("digest")
1474
1503
  if not digest or digest in already_verified:
1475
1504
  continue
1476
1505
  candidates.append(digest)
1506
+ size = m.get("size")
1507
+ if isinstance(size, int) and not isinstance(size, bool):
1508
+ sizes[digest] = size
1477
1509
  try:
1478
1510
  manifest_hex = parse_digest(digest)
1479
1511
  manifest_bytes = (blobs_dir / manifest_hex).read_bytes()
@@ -1495,6 +1527,7 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
1495
1527
  trusted_keys=trusted_keys,
1496
1528
  visible_only=True,
1497
1529
  referrers=referrers,
1530
+ manifest_size=sizes.get(digest),
1498
1531
  )
1499
1532
  if record is not None:
1500
1533
  already_verified.add(digest)
chtypes/_ocifetch/_oci.py CHANGED
@@ -26,6 +26,7 @@ from chtypes._ocifetch._http import (
26
26
 
27
27
  __all__ = [
28
28
  "Descriptor",
29
+ "copy_blob_verified",
29
30
  "fetch_blob_bytes",
30
31
  "fetch_blob_to_path",
31
32
  "fetch_manifest_by_digest",
@@ -143,6 +144,46 @@ def verify_body_matches_digest(body: bytes, digest: str, *, what: str) -> None:
143
144
  )
144
145
 
145
146
 
147
+ def copy_blob_verified(src_path: str, descriptor: Descriptor, dest_path: str, *, what: str) -> None:
148
+ """Copy a LOCAL blob file (a pre-seeded layout's `blobs/sha256/<hex>`)
149
+ to `dest_path`, streaming, and check the copy's size and sha256 against
150
+ `descriptor` BEFORE the caller does anything else with it — the local
151
+ counterpart of `fetch_blob_to_path`, with the same §7.3 rule ("check
152
+ size and sha256 before decompressing"). The caller unpacks the copy it
153
+ verified, never the source file, so the bytes checked are the bytes
154
+ used. Reads at most `descriptor.size + 1` bytes; removes `dest_path` on
155
+ any failure."""
156
+ expected = parse_digest(descriptor.digest)
157
+ hasher = hashlib.sha256()
158
+ size = 0
159
+ try:
160
+ with open(src_path, "rb") as src, open(dest_path, "wb") as dst:
161
+ while size <= descriptor.size:
162
+ chunk = src.read(min(1 << 20, descriptor.size + 1 - size))
163
+ if not chunk:
164
+ break
165
+ hasher.update(chunk)
166
+ dst.write(chunk)
167
+ size += len(chunk)
168
+ if size > descriptor.size:
169
+ raise ArtifactCorruptError(
170
+ f"{what}: larger than the descriptor's size {descriptor.size}"
171
+ )
172
+ if size != descriptor.size:
173
+ raise ArtifactCorruptError(
174
+ f"{what}: size {size} disagreed with the descriptor's {descriptor.size}"
175
+ )
176
+ actual = hasher.hexdigest()
177
+ if actual != expected:
178
+ raise ArtifactCorruptError(f"{what}: sha256 {actual} disagreed with {expected}")
179
+ except BaseException:
180
+ try:
181
+ os.unlink(dest_path)
182
+ except OSError:
183
+ pass
184
+ raise
185
+
186
+
146
187
  def resolve_platform_manifest(index_doc: dict, platform_key: str) -> Descriptor:
147
188
  """Pick the one manifest descriptor in an OCI image index matching
148
189
  `platform_key` (e.g. "linux-arm64"), refusing a duplicate or absent
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chtypes
3
- Version: 2.0.0.dev12
3
+ Version: 2.0.0.dev13
4
4
  Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
5
5
  Project-URL: Homepage, https://github.com/wave-rf/chtypes
6
6
  Project-URL: Repository, https://github.com/wave-rf/chtypes
@@ -22,7 +22,7 @@ chtypes/_ocifetch/__init__.py,sha256=Nwmn1_JqkRSy-1q8182RT5m4HJfbdJgr54it7_TKvBg
22
22
  chtypes/_ocifetch/_channel.py,sha256=1MuIj5O8N1d8OdTN4Rx4zf3kc2Nh1hjx4o5BIE6JgmA,20484
23
23
  chtypes/_ocifetch/_constants.py,sha256=Gk-h6UkPg5dvNlhkr8z-UY4Y866unUPOSpE8ZwJvoJw,6287
24
24
  chtypes/_ocifetch/_dsse.py,sha256=nYkjmRefmVTonH1KUADNNUl0J9sh2C3fg-0WEyURjcc,9838
25
- chtypes/_ocifetch/_ensure.py,sha256=3d00ZmJY7qjBQAsyEL6g3nikiHnaMm7BzoKTxn-EGts,79685
25
+ chtypes/_ocifetch/_ensure.py,sha256=0v-tGzC4P0oLtADN-YBHzAMWaqOc2bYincxwUAhWtyg,81190
26
26
  chtypes/_ocifetch/_errors.py,sha256=SE0zcTUr1fSvSH7HiLGjAInYvD_im8_SyeZNf-VKzXI,7594
27
27
  chtypes/_ocifetch/_faults.py,sha256=HpoB9tJW7aLRiJdCFnspzkwhpFATZ6f6hGi49Mbfnhs,7609
28
28
  chtypes/_ocifetch/_goldens.py,sha256=8MicgeJSRd4WZkaEsdujRnOa7twQvbT-wfSMPZaD-ms,4191
@@ -30,15 +30,15 @@ chtypes/_ocifetch/_hold.py,sha256=AmXKLFWdPEmZ5fuxKbTKbzYn-Gue9AyMfYTquQ0qvOs,48
30
30
  chtypes/_ocifetch/_http.py,sha256=3D8WZXAObw-IiZqLSh9GEys2EKOEE2i9CVfUqjLh3E0,29013
31
31
  chtypes/_ocifetch/_layout.py,sha256=ezEoVrsSMhvDhQ4U04hsGdu1-_ISZQ4V6zg4gGIChTQ,21300
32
32
  chtypes/_ocifetch/_lock.py,sha256=14_uex9qXdOht-5FSrSvFvd99-gB7DmI3bDuxXs4YU8,5530
33
- chtypes/_ocifetch/_oci.py,sha256=UdA8QGTh1O27DBYRf5UxsevjgI52IecQlEf9IO6Y0A4,13920
33
+ chtypes/_ocifetch/_oci.py,sha256=Xc0JEShGemdTlu2LviOBFSMTcjwCu9uP6jdG0qlKO80,15677
34
34
  chtypes/_ocifetch/_prune.py,sha256=gPToHNkOOVjCWA-lowJHnGpguqOFXklUOQs7PAGfhBQ,10203
35
35
  chtypes/_ocifetch/_referrers.py,sha256=SQ8BMVzzA1085azzfAA0Nw3GrHPjeu98smasKyS609A,10530
36
36
  chtypes/_ocifetch/_resolve.py,sha256=Qv8xFUdOR-Kb_JqmUeDiQ4_Xv2ETuNFpmoDckMp7eV8,11239
37
37
  chtypes/_ocifetch/_retired.py,sha256=mqnI9RA7UK0A5RthCeLlBeVAu5SHbtwKFwxsSZk5e6A,7018
38
38
  chtypes/_ocifetch/_revocation.py,sha256=DdKRjFp1eFAxVD7DZHK1bj3_sWVLlaEkVEVx6JyV2Zs,23710
39
39
  chtypes/_ocifetch/_unpack.py,sha256=Cn4-up63KL63L8pnkS5zwJ6VbbHPSigpCnmOdcHJ4RI,5609
40
- chtypes-2.0.0.dev12.dist-info/METADATA,sha256=3hHZvSuwop0hmBO2CrCF4szXxFd26F5plQWz3Jkf5oI,11365
41
- chtypes-2.0.0.dev12.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
42
- chtypes-2.0.0.dev12.dist-info/entry_points.txt,sha256=TdNcluWBR0VOt5mVFROsp-NiVPAdR3EHo0UsHlMC0r0,50
43
- chtypes-2.0.0.dev12.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
44
- chtypes-2.0.0.dev12.dist-info/RECORD,,
40
+ chtypes-2.0.0.dev13.dist-info/METADATA,sha256=W3xyv6dfQ9wI53cfWtQWioHnctgRuShzOSDZC5H3Jv4,11365
41
+ chtypes-2.0.0.dev13.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
42
+ chtypes-2.0.0.dev13.dist-info/entry_points.txt,sha256=TdNcluWBR0VOt5mVFROsp-NiVPAdR3EHo0UsHlMC0r0,50
43
+ chtypes-2.0.0.dev13.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
44
+ chtypes-2.0.0.dev13.dist-info/RECORD,,