@@ -189,6 +189,10 @@ async def pull(self, ref: str) -> Bundle:
189189 for layer in result .manifest .layers :
190190 cached_blob = self ._cache .get_blob (layer .content_sha256 )
191191 if cached_blob is not None :
192+ if self ._config .verify_checksums :
193+ actual_sha = hashlib .sha256 (cached_blob ).hexdigest ()
194+ if actual_sha != layer .content_sha256 :
195+ raise IntegrityError (expected = layer .content_sha256 , actual = actual_sha )
192196 assets [layer .logical_path ] = Asset (
193197 asset_id = layer .asset_id ,
194198 logical_path = layer .logical_path ,
@@ -233,25 +237,33 @@ def _build_assets_from_pull(
233237 content = str (item .get ("contentText" , "" )).encode ()
234238 layer = layer_map .get (logical_path )
235239
236- # Verify checksum against the resolve manifest
237- if layer and self ._config .verify_checksums :
238- actual_sha = hashlib .sha256 (content ).hexdigest ()
239- if actual_sha != layer .content_sha256 :
240- raise IntegrityError (expected = layer .content_sha256 , actual = actual_sha )
240+ # Always compute actual hash — never cache under an unverified claimed hash
241+ actual_sha = hashlib .sha256 (content ).hexdigest ()
241242
242- content_sha256 = layer .content_sha256 if layer else hashlib .sha256 (content ).hexdigest ()
243- self ._cache .put_blob (content_sha256 , content )
243+ if layer and self ._config .verify_checksums and actual_sha != layer .content_sha256 :
244+ raise IntegrityError (expected = layer .content_sha256 , actual = actual_sha )
245+
246+ self ._cache .put_blob (actual_sha , content )
244247
245248 media_type = str (item .get ("mediaType" ) or "" ) or (layer .media_type if layer else None )
246249 assets [logical_path ] = Asset (
247250 asset_id = layer .asset_id if layer else logical_path ,
248251 logical_path = logical_path ,
249252 asset_type = AssetType (str (item ["assetType" ])),
250253 content = content ,
251- content_sha256 = content_sha256 ,
254+ content_sha256 = actual_sha ,
252255 size_bytes = layer .size_bytes if layer else len (content ),
253256 media_type = media_type or None ,
254257 )
258+
259+ # Enforce manifest completeness — all expected layers must be present
260+ missing = set (layer_map .keys ()) - set (assets .keys ())
261+ if missing :
262+ raise IntegrityError (
263+ expected = f"all { len (layer_map )} manifest layers" ,
264+ actual = f"missing { len (missing )} layers: { ', ' .join (sorted (missing ))} " ,
265+ )
266+
255267 return assets
256268
257269 async def _pull_version (self , namespace : str , slug : str , version : str ) -> dict [str , object ]:
0 commit comments