@@ -2655,6 +2655,74 @@ class LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
26552655 compression = zipfile .ZIP_LZMA
26562656
26572657
2658+
2659+ class MonkeypatchedDecompressorTests (unittest .TestCase ):
2660+ # Some third-party projects monkey-patch _get_decompressor() to add
2661+ # additional compression schemes. This can break at any time as the
2662+ # internal compressor objects change.
2663+ # To protect users, we try to keep this case working.
2664+ # See also: GH-156002 and GH-113767.
2665+ COMPRESSION = 99
2666+
2667+ class Compressor :
2668+ """Compressor with only the original BZ2Compressor API"""
2669+ def compress (self , data ):
2670+ return data .swapcase ()
2671+
2672+ def flush (self ):
2673+ return b''
2674+
2675+ class Decompressor :
2676+ """Decompressor with only the 3.3+ BZ2Decompressor API"""
2677+ eof = False
2678+
2679+ def decompress (self , data ):
2680+ return data .swapcase ()
2681+
2682+ def setUp (self ):
2683+ orig_check_compression = zipfile ._check_compression
2684+ orig_get_compressor = zipfile ._get_compressor
2685+ orig_get_decompressor = zipfile ._get_decompressor
2686+
2687+ def check_compression (compression ):
2688+ if compression != self .COMPRESSION :
2689+ orig_check_compression (compression )
2690+
2691+ def get_compressor (compress_type , compresslevel = None ):
2692+ if compress_type == self .COMPRESSION :
2693+ return self .Compressor ()
2694+ return orig_get_compressor (compress_type , compresslevel )
2695+
2696+ def get_decompressor (compress_type ):
2697+ if compress_type == self .COMPRESSION :
2698+ return self .Decompressor ()
2699+ return orig_get_decompressor (compress_type )
2700+
2701+ self .enterContext (mock .patch .object (
2702+ zipfile , '_check_compression' , check_compression ))
2703+ self .enterContext (mock .patch .object (
2704+ zipfile , '_get_compressor' , get_compressor ))
2705+ self .enterContext (mock .patch .object (
2706+ zipfile , '_get_decompressor' , get_decompressor ))
2707+
2708+ def test_roundtrip_monkeypatched_decompressor (self ):
2709+ data = bytes (range (256 )) * 8
2710+ buf = io .BytesIO ()
2711+ with zipfile .ZipFile (buf , "w" , compression = self .COMPRESSION ) as zf :
2712+ zf .writestr ("member" , data )
2713+ self .assertIn (data .swapcase (), buf .getvalue ())
2714+ with zipfile .ZipFile (io .BytesIO (buf .getvalue ())) as zf :
2715+ self .assertEqual (zf .read ("member" ), data )
2716+ with zf .open ("member" ) as f :
2717+ self .assertEqual (f .read (100 ), data [:100 ])
2718+ self .assertEqual (f .read1 (100 ), data [100 :200 ])
2719+ f .seek (- 100 , os .SEEK_END )
2720+ self .assertEqual (f .read (), data [- 100 :])
2721+ # Rewinding past the read buffer re-creates the decompressor.
2722+ f .seek (0 )
2723+ self .assertEqual (f .read (), data )
2724+
2725+
26582726class AbstractBadCrcTests :
26592727 def test_testzip_with_bad_crc (self ):
26602728 """Tests that files with bad CRCs return their name from testzip."""
0 commit comments