33import re
44import uuid
55from collections .abc import Iterable , Mapping , Sequence , Set
6- from typing import TYPE_CHECKING , TypedDict , TypeGuard
6+ from typing import TYPE_CHECKING , NotRequired , TypedDict , TypeGuard , cast
77
88import jsonschema
99import orjson
1010from django .db import IntegrityError , router
11- from django .db .models import Case , Exists , IntegerField , Q , QuerySet , Value , When
11+ from django .db .models import Case , Exists , F , IntegerField , Q , QuerySet , Value , When
1212from django .http import Http404 , HttpResponse , StreamingHttpResponse
1313from rest_framework import status
1414from rest_framework .request import Request
@@ -422,18 +422,56 @@ def post(self, request: Request, project: Project) -> Response:
422422 return Response ({"associatedDsymFiles" : []})
423423
424424
425- def get_file_info (file ) -> tuple [str | None , str | None , list [str ]]:
425+ class AssembleRequestFile (TypedDict ):
426+ """One file entry from the DIF assemble request body."""
427+
428+ name : str
429+ chunks : list [str ]
430+ debug_id : NotRequired [str ]
431+
432+
433+ AssembleRequestPayload = dict [str , AssembleRequestFile ]
434+ """Mapping from file checksums to the corresponding assemble request payload."""
435+
436+
437+ def parse_assemble_request_payload (body : bytes ) -> AssembleRequestPayload :
438+ """Parse and validate the DIF assemble request body."""
439+ schema : dict [str , object ] = {
440+ "type" : "object" ,
441+ "patternProperties" : {
442+ "^[0-9a-f]{40}$" : {
443+ "type" : "object" ,
444+ "required" : ["name" , "chunks" ],
445+ "properties" : {
446+ "name" : {"type" : "string" },
447+ "debug_id" : {"type" : "string" },
448+ "chunks" : {
449+ "type" : "array" ,
450+ "items" : {"type" : "string" , "pattern" : "^[0-9a-f]{40}$" },
451+ },
452+ },
453+ "additionalProperties" : True ,
454+ }
455+ },
456+ "additionalProperties" : False ,
457+ }
458+ payload_obj : object = orjson .loads (body )
459+ jsonschema .validate (payload_obj , schema )
460+ return cast (AssembleRequestPayload , payload_obj )
461+
462+
463+ def get_file_info (file : AssembleRequestFile ) -> tuple [str , str | None , list [str ]]:
426464 """
427465 Extracts file information from one assemble payload.
428466 """
429- name = file . get ( "name" )
467+ name = file [ "name" ]
430468 debug_id = file .get ("debug_id" )
431- chunks = file . get ( "chunks" , [])
469+ chunks = file [ "chunks" ]
432470
433471 return name , debug_id , chunks
434472
435473
436- def batch_assemble (project , files ):
474+ def batch_assemble (project : Project , files : AssembleRequestPayload ):
437475 """
438476 Performs assembling in a batch fashion, issuing queries that span multiple files.
439477 """
@@ -477,7 +515,7 @@ def batch_assemble(project, files):
477515 )
478516
479517 for debug_file in existing_debug_files :
480- checksum = debug_file .checksum
518+ checksum = debug_file .nonnull_checksum
481519 file = files_to_check .pop (checksum )
482520 requested_debug_id = requested_debug_ids_by_checksum [checksum ]
483521
@@ -558,22 +596,22 @@ def batch_assemble(project, files):
558596 return file_response
559597
560598
561- def _get_requested_debug_id (file ) -> str | None :
599+ def _get_requested_debug_id (file : AssembleRequestFile ) -> str | None :
562600 """Returns the effective requested debug ID for one assemble payload.
563601
564602 This normalizes an explicit ``debug_id`` when present, or derives one from a
565603 ProGuard-style request name such as ``/proguard/mapping-<uuid>.txt``.
566604 """
567- return get_debug_id_from_dif_request (name = file . get ( "name" ) , debug_id = file .get ("debug_id" ))
605+ return get_debug_id_from_dif_request (name = file [ "name" ] , debug_id = file .get ("debug_id" ))
568606
569607
570- def _is_requested_proguard (file ) -> bool :
608+ def _is_requested_proguard (file : AssembleRequestFile ) -> bool :
571609 """Returns whether one assemble payload should be treated as a ProGuard request.
572610
573611 This is true only when the request's effective debug ID comes from a
574612 ProGuard-style filename, rather than merely from an explicit ``debug_id``.
575613 """
576- name = file . get ( "name" )
614+ name = file [ "name" ]
577615 requested_debug_id = _get_requested_debug_id (file )
578616 return (
579617 requested_debug_id is not None
@@ -583,7 +621,7 @@ def _is_requested_proguard(file) -> bool:
583621
584622def _is_proguard_reupload_clone_request (
585623 requested_debug_id : str | None ,
586- file ,
624+ file : AssembleRequestFile ,
587625 selected_debug_id : str | None ,
588626) -> TypeGuard [str ]:
589627 """Return whether the assemble request should clone a ProGuard debug file."""
@@ -595,6 +633,7 @@ def _is_proguard_reupload_clone_request(
595633
596634
597635class _DebugFileAnnotations (TypedDict ):
636+ nonnull_checksum : str
598637 requested_debug_id_match : int
599638 proguard_clone_source_match : int
600639
@@ -639,8 +678,11 @@ def _find_existing_debug_files(
639678 ProjectDebugFile .objects .filter (
640679 project_id = project .id ,
641680 checksum__in = checksums ,
681+ checksum__isnull = False ,
642682 )
643683 .annotate (
684+ # Mirror the filtered checksum into an annotated non-null field for type safety.
685+ nonnull_checksum = F ("checksum" ),
644686 requested_debug_id_match = _build_requested_debug_id_match_annotation (
645687 requested_debug_ids_by_checksum .items ()
646688 ),
@@ -740,29 +782,8 @@ def post(self, request: Request, project: Project) -> Response:
740782
741783 :auth: required
742784 """
743- schema = {
744- "type" : "object" ,
745- "patternProperties" : {
746- "^[0-9a-f]{40}$" : {
747- "type" : "object" ,
748- "required" : ["name" , "chunks" ],
749- "properties" : {
750- "name" : {"type" : "string" },
751- "debug_id" : {"type" : "string" },
752- "chunks" : {
753- "type" : "array" ,
754- "items" : {"type" : "string" , "pattern" : "^[0-9a-f]{40}$" },
755- },
756- },
757- "additionalProperties" : True ,
758- }
759- },
760- "additionalProperties" : False ,
761- }
762-
763785 try :
764- files = orjson .loads (request .body )
765- jsonschema .validate (files , schema )
786+ files = parse_assemble_request_payload (request .body )
766787 except jsonschema .ValidationError as e :
767788 return Response ({"error" : str (e ).splitlines ()[0 ]}, status = 400 )
768789 except Exception :
0 commit comments