@@ -650,6 +650,66 @@ def cmd_list(args: argparse.Namespace) -> None:
650650 print (v )
651651
652652
653+ def cmd_verify (args : argparse .Namespace ) -> None :
654+ """Verify fixture file integrity against manifest sha256 hashes."""
655+ store = _parse_store (args .store )
656+ versions = store .list_versions ()
657+
658+ if not versions :
659+ _info ("no versions found" )
660+ sys .exit (1 )
661+
662+ _info (f"verifying { len (versions )} version(s) in { store .display_name ()} ..." )
663+
664+ total_ok = 0
665+ errors : list [str ] = []
666+
667+ for version in versions :
668+ _info (f"\n --- v{ version } ---" )
669+ manifest = _read_manifest (store , version )
670+ if manifest is None :
671+ msg = f"v{ version } : manifest not found"
672+ _info (f" FAIL: { msg } " )
673+ errors .append (msg )
674+ continue
675+
676+ prefix = manifest .pop ("_prefix" , f"v{ version } /arrays" )
677+
678+ for entry in manifest ["fixtures" ]:
679+ name = entry ["name" ]
680+ expected_hash = entry .get ("sha256" )
681+ data = store .read (f"{ prefix } /{ name } " )
682+
683+ if data is None :
684+ msg = f"v{ version } /{ name } : file missing from store"
685+ _info (f" FAIL: { msg } " )
686+ errors .append (msg )
687+ continue
688+
689+ if expected_hash is None :
690+ msg = f"v{ version } /{ name } : no sha256 in manifest"
691+ _info (f" FAIL: { msg } " )
692+ errors .append (msg )
693+ continue
694+
695+ actual_hash = hashlib .sha256 (data ).hexdigest ()
696+ if actual_hash != expected_hash :
697+ msg = f"v{ version } /{ name } : sha256 mismatch expected={ expected_hash [:12 ]} actual={ actual_hash [:12 ]} "
698+ _info (f" FAIL: { msg } " )
699+ errors .append (msg )
700+ else :
701+ _info (f" { name } : ok ({ len (data )} bytes)" )
702+ total_ok += 1
703+
704+ _info (f"\n result: { total_ok } ok, { len (errors )} failed" )
705+ if errors :
706+ for e in errors :
707+ _info (f" { e } " )
708+ sys .exit (1 )
709+ else :
710+ _info ("all fixtures verified." )
711+
712+
653713def cmd_validate_manifest (args : argparse .Namespace ) -> None :
654714 """Check that manifests are additive-only across all versions."""
655715 store = _parse_store (args .store )
@@ -904,6 +964,26 @@ def main() -> None:
904964 )
905965 p .add_argument ("version" , nargs = "?" , help = "Show manifest for this version" )
906966
967+ # -- verify --
968+ p = sub .add_parser (
969+ "verify" ,
970+ help = "Verify fixture file integrity against manifest sha256 hashes" ,
971+ description = (
972+ "Download every fixture file for every version and verify its\n "
973+ "SHA-256 hash matches the manifest. Also checks that all files\n "
974+ "listed in manifests are present in the store."
975+ ),
976+ epilog = (
977+ "examples:\n "
978+ " uv run compat.py verify\n "
979+ " uv run compat.py verify --store /tmp/store"
980+ ),
981+ formatter_class = argparse .RawDescriptionHelpFormatter ,
982+ )
983+ p .add_argument (
984+ "--store" , default = DEFAULT_STORE , help = "Store spec (default: %(default)s)"
985+ )
986+
907987 # -- validate-manifest --
908988 p = sub .add_parser (
909989 "validate-manifest" ,
@@ -930,6 +1010,7 @@ def main() -> None:
9301010 "publish" : cmd_publish ,
9311011 "check" : cmd_check ,
9321012 "list" : cmd_list ,
1013+ "verify" : cmd_verify ,
9331014 "validate-manifest" : cmd_validate_manifest ,
9341015 }
9351016 commands [args .command ](args )
0 commit comments