diff --git a/framework/py/flwr/cli/install.py b/framework/py/flwr/cli/install.py index 4a1bf2bf4f7a..6c2766f8eca6 100644 --- a/framework/py/flwr/cli/install.py +++ b/framework/py/flwr/cli/install.py @@ -228,7 +228,7 @@ def _verify_hashes(list_content: str, tmpdir: Path) -> bool: True if all file hashes match, False otherwise. """ for line in list_content.strip().split("\n"): - rel_path, hash_expected, _ = line.split(",") + rel_path, hash_expected, _ = line.rsplit(",", maxsplit=2) file_path = tmpdir / rel_path if not file_path.exists() or get_sha256_hash(file_path) != hash_expected: return False diff --git a/framework/py/flwr/cli/install_test.py b/framework/py/flwr/cli/install_test.py index 34f75ea1c1ab..b87741aeaa54 100644 --- a/framework/py/flwr/cli/install_test.py +++ b/framework/py/flwr/cli/install_test.py @@ -15,6 +15,7 @@ """Tests for Flower command line interface `install` command.""" +import hashlib import io import zipfile from pathlib import Path @@ -23,7 +24,7 @@ import pytest from .archive_utils import safe_extract_zip -from .install import install_from_fab +from .install import _verify_hashes, install_from_fab def _zip_bytes(entries: list[tuple[str, bytes]]) -> bytes: @@ -65,6 +66,17 @@ def test_safe_extract_zip_rejects_absolute_paths(tmp_path: Path) -> None: safe_extract_zip(zf, tmp_path) +def test_verify_hashes_accepts_comma_in_file_name(tmp_path: Path) -> None: + """Hash verification should preserve commas in manifest file paths.""" + file_name = "metrics,round=1.json" + file_content = b"{}" + (tmp_path / file_name).write_bytes(file_content) + file_hash = hashlib.sha256(file_content).hexdigest() + content_manifest = f"{file_name},{file_hash},{len(file_content) * 8}" + + assert _verify_hashes(content_manifest, tmp_path) + + def test_install_from_fab_rejects_zip_slip(tmp_path: Path) -> None: """install_from_fab should fail fast on zip-slip entries.""" fab_bytes = _zip_bytes(