From 7cba340de143de0478a16ee26b03ee96d37da848 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Mon, 3 Aug 2026 14:15:00 +0200 Subject: [PATCH] Unify hash request validation --- core/services/hash_service.py | 51 +++++++++++++++++------------------ tests/test_hash_routes.py | 34 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/core/services/hash_service.py b/core/services/hash_service.py index f3424c1..3b9d76b 100644 --- a/core/services/hash_service.py +++ b/core/services/hash_service.py @@ -376,6 +376,20 @@ class HashService: return "", "file is outside configured model directories" return normalized_path, "" + async def _get_validated_hash_file_request(self, request): + """Read and validate the file request shared by hash endpoints.""" + data = await request.json() + normalized_path, error = self.resolve_hash_file_request(data) + if not error: + return normalized_path, None + + status = 400 if error == "file_path is required" else ( + 403 + if error == "file is outside configured model directories" + else 404 + ) + return "", self.web.json_response({"error": error}, status=status) + def write_calculated_hash_metadata( self, normalized_path, @@ -545,20 +559,11 @@ class HashService: async def calculate_file_hash(self, request): """Calculate SHA256 for a local model and persist sidecar metadata.""" - data = await request.json() - - normalized_path, error = self.resolve_hash_file_request(data) - if error == "file_path is required": - return self.web.json_response( - {"error": "file_path is required"}, status=400 - ) - if error: - status = ( - 403 - if error == "file is outside configured model directories" - else 404 - ) - return self.web.json_response({"error": error}, status=status) + normalized_path, error_response = await self._get_validated_hash_file_request( + request + ) + if error_response: + return error_response sha256, sha256_source = self.calculate_sha256_with_progress(normalized_path) if not sha256: @@ -587,19 +592,11 @@ class HashService: async def calculate_file_hash_start(self, request): """Start SHA256 calculation in a background thread.""" - data = await request.json() - normalized_path, error = self.resolve_hash_file_request(data) - if error == "file_path is required": - return self.web.json_response( - {"error": "file_path is required"}, status=400 - ) - if error: - status = ( - 403 - if error == "file is outside configured model directories" - else 404 - ) - return self.web.json_response({"error": error}, status=status) + normalized_path, error_response = await self._get_validated_hash_file_request( + request + ) + if error_response: + return error_response self.hash_tracker.cleanup() progress_id = f"hash_{uuid.uuid4().hex}" diff --git a/tests/test_hash_routes.py b/tests/test_hash_routes.py index fc11825..d6a9ef2 100644 --- a/tests/test_hash_routes.py +++ b/tests/test_hash_routes.py @@ -411,6 +411,40 @@ async def test_calculate_file_hash_writes_metadata_and_reports_source(): assert written["metadata"]["file_name"] == "old" +@pytest.mark.asyncio +@pytest.mark.parametrize( + "route", + [ + "/model_resolver/calculate-file-hash", + "/model_resolver/calculate-file-hash/start", + ], +) +async def test_hash_calculation_routes_share_file_request_validation(route): + handlers, values = _build_hash_routes() + handler = handlers[("POST", route)] + + response = await handler(_request({})) + assert response.status == 400 + assert json.loads(response.text) == {"error": "file_path is required"} + + with tempfile.TemporaryDirectory() as temp_dir: + missing_path = os.path.join(temp_dir, "missing.safetensors") + response = await handler(_request({"file_path": missing_path})) + assert response.status == 404 + assert json.loads(response.text) == {"error": "file does not exist"} + + outside_path = os.path.join(temp_dir, "outside.safetensors") + with open(outside_path, "wb") as model_file: + model_file.write(b"model") + values["is_path_in_configured_model_roots"].return_value = False + response = await handler(_request({"file_path": outside_path})) + + assert response.status == 403 + assert json.loads(response.text) == { + "error": "file is outside configured model directories" + } + + def test_hash_service_reports_header_source_and_cancellation(): sha256 = "e" * 64 _, values = _build_hash_routes()