From e95fa2d6caa3087457485f84556e61f30b68c768 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 29 Jul 2026 13:09:26 +0500 Subject: [PATCH 1/2] fix: use chunked read for integration and preset manifest hash Replace unbounded fh.read() with chunked iteration to prevent excessive memory allocation on large or corrupted manifest files. Applies to both integrations/catalog.py and presets/__init__.py get_hash() methods. --- src/specify_cli/integrations/catalog.py | 5 ++++- src/specify_cli/presets/__init__.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/integrations/catalog.py b/src/specify_cli/integrations/catalog.py index 1794caad83..c90d6eca4d 100644 --- a/src/specify_cli/integrations/catalog.py +++ b/src/specify_cli/integrations/catalog.py @@ -841,5 +841,8 @@ def tools(self) -> List[Dict[str, Any]]: def get_hash(self) -> str: """SHA-256 hash of the descriptor file.""" + h = hashlib.sha256() with open(self.path, "rb") as fh: - return f"sha256:{hashlib.sha256(fh.read()).hexdigest()}" + for chunk in iter(lambda: fh.read(8192), b""): + h.update(chunk) + return f"sha256:{h.hexdigest()}" diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index e2f6c089a4..ee053ea08d 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -452,8 +452,11 @@ def tags(self) -> List[str]: def get_hash(self) -> str: """Calculate SHA256 hash of manifest file.""" + h = hashlib.sha256() with open(self.path, 'rb') as f: - return f"sha256:{hashlib.sha256(f.read()).hexdigest()}" + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return f"sha256:{h.hexdigest()}" class PresetRegistry: From f59d4e9915336eab13f396eb3c9d8487b93fa611 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Fri, 31 Jul 2026 20:00:20 +0500 Subject: [PATCH 2/2] test: verify full hash value in get_hash() tests to cover chunked path The existing tests only checked the sha256: prefix, which would pass even if the chunked hash was broken. Now verify the complete hash matches hashlib.sha256(content).hexdigest() to exercise the multi-chunk path introduced by the chunked read change. --- tests/integrations/test_integration_catalog.py | 4 ++++ tests/test_presets.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index 4688c6a21e..c5ac6e34ae 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -642,6 +642,10 @@ def test_get_hash(self, tmp_path): desc = IntegrationDescriptor(p) h = desc.get_hash() assert h.startswith("sha256:") + import hashlib + content = p.read_bytes() + expected = f"sha256:{hashlib.sha256(content).hexdigest()}" + assert h == expected def test_tools_accessor(self, tmp_path): data = {**VALID_DESCRIPTOR, "requires": { diff --git a/tests/test_presets.py b/tests/test_presets.py index f4813eae13..993e968f31 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -337,7 +337,10 @@ def test_get_hash(self, pack_dir): manifest = PresetManifest(pack_dir / "preset.yml") hash_val = manifest.get_hash() assert hash_val.startswith("sha256:") - assert len(hash_val) > 10 + import hashlib + content = (pack_dir / "preset.yml").read_bytes() + expected = f"sha256:{hashlib.sha256(content).hexdigest()}" + assert hash_val == expected def test_multiple_templates(self, temp_dir, valid_pack_data): """Test pack with multiple templates of different types."""