@@ -2765,6 +2765,73 @@ class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
27652765 compression = zipfile .ZIP_ZSTANDARD
27662766
27672767
2768+ class MonkeypatchedDecompressorTests (unittest .TestCase ):
2769+ # Some third-party projects monkey-patch _get_decompressor() to add
2770+ # additional compression schemes. This can break at any time as the
2771+ # internal compressor objects change.
2772+ # To protect users, we try to keep this case working.
2773+ # See also: GH-156002 and GH-113767.
2774+ COMPRESSION = 99
2775+
2776+ class Compressor :
2777+ """Compressor with only the original BZ2Compressor API"""
2778+ def compress (self , data ):
2779+ return data .swapcase ()
2780+
2781+ def flush (self ):
2782+ return b''
2783+
2784+ class Decompressor :
2785+ """Decompressor with only the 3.3+ BZ2Decompressor API"""
2786+ eof = False
2787+
2788+ def decompress (self , data ):
2789+ return data .swapcase ()
2790+
2791+ def setUp (self ):
2792+ orig_check_compression = zipfile ._check_compression
2793+ orig_get_compressor = zipfile ._get_compressor
2794+ orig_get_decompressor = zipfile ._get_decompressor
2795+
2796+ def check_compression (compression ):
2797+ if compression != self .COMPRESSION :
2798+ orig_check_compression (compression )
2799+
2800+ def get_compressor (compress_type , compresslevel = None ):
2801+ if compress_type == self .COMPRESSION :
2802+ return self .Compressor ()
2803+ return orig_get_compressor (compress_type , compresslevel )
2804+
2805+ def get_decompressor (compress_type ):
2806+ if compress_type == self .COMPRESSION :
2807+ return self .Decompressor ()
2808+ return orig_get_decompressor (compress_type )
2809+
2810+ self .enterContext (mock .patch .object (
2811+ zipfile , '_check_compression' , check_compression ))
2812+ self .enterContext (mock .patch .object (
2813+ zipfile , '_get_compressor' , get_compressor ))
2814+ self .enterContext (mock .patch .object (
2815+ zipfile , '_get_decompressor' , get_decompressor ))
2816+
2817+ def test_roundtrip_monkeypatched_decompressor (self ):
2818+ data = bytes (range (256 )) * 8
2819+ buf = io .BytesIO ()
2820+ with zipfile .ZipFile (buf , "w" , compression = self .COMPRESSION ) as zf :
2821+ zf .writestr ("member" , data )
2822+ self .assertIn (data .swapcase (), buf .getvalue ())
2823+ with zipfile .ZipFile (io .BytesIO (buf .getvalue ())) as zf :
2824+ self .assertEqual (zf .read ("member" ), data )
2825+ with zf .open ("member" ) as f :
2826+ self .assertEqual (f .read (100 ), data [:100 ])
2827+ self .assertEqual (f .read1 (100 ), data [100 :200 ])
2828+ f .seek (- 100 , os .SEEK_END )
2829+ self .assertEqual (f .read (), data [- 100 :])
2830+ # Rewinding past the read buffer re-creates the decompressor.
2831+ f .seek (0 )
2832+ self .assertEqual (f .read (), data )
2833+
2834+
27682835class AbstractBadCrcTests :
27692836 def test_testzip_with_bad_crc (self ):
27702837 """Tests that files with bad CRCs return their name from testzip."""
0 commit comments