2323import os
2424import shutil
2525import sys
26+ from collections .abc import Iterator , Mapping
2627from tempfile import mkstemp
28+ from typing import Any
2729from zipfile import ZipFile
2830
2931from FTB .ProgramConfiguration import ProgramConfiguration
3840 signature_checks ,
3941)
4042
41- __all__ = []
43+ __all__ : list [ str ] = []
4244__version__ = 0.1
4345__date__ = "2014-10-01"
44- __updated__ = "2025 -04-08 "
46+ __updated__ = "2026 -04-23 "
4547
4648
4749class Collector (Reporter ):
4850 @remote_checks
4951 @signature_checks
50- def refresh (self ):
52+ def refresh (self ) -> None :
5153 """
5254 Refresh signatures by contacting the server, downloading new signatures
5355 and invalidating old ones.
@@ -68,12 +70,13 @@ def refresh(self):
6870 os .remove (zipFileName )
6971
7072 @signature_checks
71- def refreshFromZip (self , zipFileName ) :
73+ def refreshFromZip (self , zipFileName : str ) -> None :
7274 """
7375 Refresh signatures from a local zip file, adding new signatures
7476 and invalidating old ones. (This is a non-standard use case;
7577 you probably want to use refresh() instead.)
7678 """
79+ assert self .sigCacheDir is not None
7780 with ZipFile (zipFileName , "r" ) as zipFile :
7881 if zipFile .testzip ():
7982 raise InvalidDataError (f"Bad CRC for downloaded zipfile { zipFileName } " )
@@ -94,12 +97,12 @@ def refreshFromZip(self, zipFileName):
9497 @remote_checks
9598 def submit (
9699 self ,
97- crashInfo ,
98- testCase = None ,
99- testCaseQuality = 0 ,
100- testCaseSize = None ,
101- metaData = None ,
102- ):
100+ crashInfo : CrashInfo ,
101+ testCase : str | None = None ,
102+ testCaseQuality : int = 0 ,
103+ testCaseSize : int | None = None ,
104+ metaData : Mapping [ str , Any ] | None = None ,
105+ ) -> Any :
103106 """
104107 Submit the given crash information and an optional testcase/metadata
105108 to the server for processing and storage.
@@ -131,7 +134,7 @@ def submit(
131134
132135 # Serialize our crash information, testcase and metadata into a dictionary to
133136 # POST
134- data = {}
137+ data : dict [ str , Any ] = {}
135138
136139 data ["rawStdout" ] = os .linesep .join (crashInfo .rawStdout )
137140 data ["rawStderr" ] = os .linesep .join (crashInfo .rawStderr )
@@ -154,6 +157,7 @@ def submit(
154157 if testcase_ext :
155158 data ["testcase_ext" ] = testcase_ext
156159
160+ assert crashInfo .configuration is not None
157161 data ["platform" ] = crashInfo .configuration .platform
158162 data ["product" ] = crashInfo .configuration .product
159163 data ["os" ] = crashInfo .configuration .os
@@ -165,7 +169,7 @@ def submit(
165169 data ["tool" ] = self .tool
166170
167171 if crashInfo .configuration .metadata or metaData :
168- aggrMetaData = {}
172+ aggrMetaData : dict [ str , Any ] = {}
169173
170174 if crashInfo .configuration .metadata :
171175 aggrMetaData .update (crashInfo .configuration .metadata )
@@ -184,7 +188,7 @@ def submit(
184188 return self .post (url , data ).json ()
185189
186190 @signature_checks
187- def search (self , crashInfo ) :
191+ def search (self , crashInfo : CrashInfo ) -> tuple [ str | None , dict [ str , Any ] | None ] :
188192 """
189193 Searches within the local signature cache directory for a signature matching the
190194 given crash.
@@ -196,7 +200,7 @@ def search(self, crashInfo):
196200 @return: Tuple containing filename of the signature and metadata matching, or
197201 None if no match.
198202 """
199-
203+ assert self . sigCacheDir is not None
200204 cachedSigFiles = os .listdir (self .sigCacheDir )
201205
202206 for sigFile in cachedSigFiles :
@@ -210,7 +214,7 @@ def search(self, crashInfo):
210214 crashSig = CrashSignature (sigData )
211215 if crashSig .matches (crashInfo ):
212216 metadataFile = sigFile .replace (".signature" , ".metadata" )
213- metadata = None
217+ metadata : dict [ str , Any ] | None = None
214218 if os .path .exists (metadataFile ):
215219 with open (metadataFile ) as m :
216220 metadata = json .loads (m .read ())
@@ -222,11 +226,11 @@ def search(self, crashInfo):
222226 @signature_checks
223227 def generate (
224228 self ,
225- crashInfo ,
226- forceCrashAddress = None ,
227- forceCrashInstruction = None ,
228- numFrames = None ,
229- ):
229+ crashInfo : CrashInfo ,
230+ forceCrashAddress : bool = False ,
231+ forceCrashInstruction : bool = False ,
232+ numFrames : int = 8 ,
233+ ) -> str | None :
230234 """
231235 Generates a signature in the local cache directory. It will be deleted when
232236 L{refresh} is called on the same local cache directory.
@@ -257,7 +261,7 @@ def generate(
257261 return self .__store_signature_hashed (sig )
258262
259263 @remote_checks
260- def download (self , crashId ) :
264+ def download (self , crashId : int ) -> tuple [ str , dict [ str , Any ]] | None :
261265 """
262266 Download the testcase for the specified crashId.
263267
@@ -300,7 +304,7 @@ def download(self, crashId):
300304 return (local_filename , resp_json )
301305
302306 @remote_checks
303- def download_all (self , bucketId ) :
307+ def download_all (self , bucketId : int ) -> Iterator [ str ] :
304308 """
305309 Download all testcases for the specified bucketId.
306310
@@ -310,8 +314,10 @@ def download_all(self, bucketId):
310314 @rtype: generator
311315 @return: generator of filenames where tests were stored.
312316 """
313- params = {"query" : json .dumps ({"op" : "OR" , "bucket" : bucketId })}
314- next_url = (
317+ params : dict [str , str ] | None = {
318+ "query" : json .dumps ({"op" : "OR" , "bucket" : bucketId })
319+ }
320+ next_url : str | None = (
315321 f"{ self .serverProtocol } ://{ self .serverHost } :{ self .serverPort } "
316322 "/crashmanager/rest/crashes/"
317323 )
@@ -350,7 +356,7 @@ def download_all(self, bucketId):
350356
351357 yield local_filename
352358
353- def __store_signature_hashed (self , signature ) :
359+ def __store_signature_hashed (self , signature : CrashSignature ) -> str :
354360 """
355361 Store a signature, using the sha1 hash hex representation as filename.
356362
@@ -361,19 +367,17 @@ def __store_signature_hashed(self, signature):
361367 @return: Name of the file that the signature was written to
362368
363369 """
370+ assert self .sigCacheDir is not None
364371 h = hashlib .new ("sha1" )
365- if str is bytes :
366- h .update (str (signature ))
367- else :
368- h .update (str (signature ).encode ("utf-8" ))
372+ h .update (str (signature ).encode ("utf-8" ))
369373 sigfile = os .path .join (self .sigCacheDir , h .hexdigest () + ".signature" )
370374 with open (sigfile , "w" ) as f :
371375 f .write (str (signature ))
372376
373377 return sigfile
374378
375379 @staticmethod
376- def read_testcase (testCase ) :
380+ def read_testcase (testCase : str ) -> tuple [ bytes , bool ] :
377381 """
378382 Read a testcase file, return the content and indicate if it is binary or not.
379383
@@ -394,7 +398,7 @@ def read_testcase(testCase):
394398 return (testCaseData , isBinary )
395399
396400
397- def main (args = None ):
401+ def main (args : list [ str ] | None = None ) -> int :
398402 """Command line options."""
399403 sentry_init ()
400404
@@ -686,7 +690,7 @@ def main(args=None):
686690 if opts .testcase :
687691 (testCaseData , isBinary ) = Collector .read_testcase (opts .testcase )
688692 if not isBinary :
689- crashInfo .testcase = testCaseData
693+ crashInfo .testcase = testCaseData . decode ( "utf-8" )
690694
691695 serverauthtoken = None
692696 if opts .serverauthtokenfile :
@@ -708,23 +712,26 @@ def main(args=None):
708712 return 0
709713
710714 if opts .submit :
715+ assert crashInfo is not None
711716 testcase = opts .testcase
712717 collector .submit (
713718 crashInfo , testcase , opts .testcasequality , opts .testcasesize , metadata
714719 )
715720 return 0
716721
717722 if opts .search :
718- (sig , metadata ) = collector .search (crashInfo )
723+ assert crashInfo is not None
724+ (sig , sigMetadata ) = collector .search (crashInfo )
719725 if sig is None :
720726 print ("No match found" , file = sys .stderr )
721727 return 3
722728 print (sig )
723- if metadata :
724- print (json .dumps (metadata , indent = 4 ))
729+ if sigMetadata :
730+ print (json .dumps (sigMetadata , indent = 4 ))
725731 return 0
726732
727733 if opts .generate :
734+ assert crashInfo is not None
728735 sigFile = collector .generate (
729736 crashInfo , opts .forcecrashaddr , opts .forcecrashinst , opts .numframes
730737 )
@@ -738,8 +745,9 @@ def main(args=None):
738745 return 0
739746
740747 if opts .autosubmit :
748+ assert configuration is not None
741749 runner = AutoRunner .fromBinaryArgs (opts .rargs [0 ], opts .rargs [1 :], env = env )
742- if runner .run ():
750+ if runner .run (): # type: ignore[attr-defined]
743751 crashInfo = runner .getCrashInfo (configuration )
744752 collector .submit (
745753 crashInfo , testcase , opts .testcasequality , opts .testcasesize , metadata
@@ -752,10 +760,11 @@ def main(args=None):
752760 return 1
753761
754762 if opts .download :
755- ( retFile , retJSON ) = collector .download (opts .download )
756- if not retFile :
763+ downloadResult = collector .download (opts .download )
764+ if downloadResult is None :
757765 print ("Specified crash entry does not have a testcase" , file = sys .stderr )
758766 return 1
767+ retFile , retJSON = downloadResult
759768
760769 if retJSON .get ("args" ):
761770 args = json .loads (retJSON ["args" ])
0 commit comments