@@ -2626,6 +2626,74 @@ class LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
26262626 compression = zipfile .ZIP_LZMA
26272627
26282628
2629+
2630+ class MonkeypatchedDecompressorTests (unittest .TestCase ):
2631+ # Some third-party projects monkey-patch _get_decompressor() to add
2632+ # additional compression schemes. This can break at any time as the
2633+ # internal compressor objects change.
2634+ # To protect users, we try to keep this case working.
2635+ # See also: GH-156002 and GH-113767.
2636+ COMPRESSION = 99
2637+
2638+ class Compressor :
2639+ """Compressor with only the original BZ2Compressor API"""
2640+ def compress (self , data ):
2641+ return data .swapcase ()
2642+
2643+ def flush (self ):
2644+ return b''
2645+
2646+ class Decompressor :
2647+ """Decompressor with only the 3.3+ BZ2Decompressor API"""
2648+ eof = False
2649+
2650+ def decompress (self , data ):
2651+ return data .swapcase ()
2652+
2653+ def setUp (self ):
2654+ orig_check_compression = zipfile ._check_compression
2655+ orig_get_compressor = zipfile ._get_compressor
2656+ orig_get_decompressor = zipfile ._get_decompressor
2657+
2658+ def check_compression (compression ):
2659+ if compression != self .COMPRESSION :
2660+ orig_check_compression (compression )
2661+
2662+ def get_compressor (compress_type , compresslevel = None ):
2663+ if compress_type == self .COMPRESSION :
2664+ return self .Compressor ()
2665+ return orig_get_compressor (compress_type , compresslevel )
2666+
2667+ def get_decompressor (compress_type ):
2668+ if compress_type == self .COMPRESSION :
2669+ return self .Decompressor ()
2670+ return orig_get_decompressor (compress_type )
2671+
2672+ self .enterContext (mock .patch .object (
2673+ zipfile , '_check_compression' , check_compression ))
2674+ self .enterContext (mock .patch .object (
2675+ zipfile , '_get_compressor' , get_compressor ))
2676+ self .enterContext (mock .patch .object (
2677+ zipfile , '_get_decompressor' , get_decompressor ))
2678+
2679+ def test_roundtrip_monkeypatched_decompressor (self ):
2680+ data = bytes (range (256 )) * 8
2681+ buf = io .BytesIO ()
2682+ with zipfile .ZipFile (buf , "w" , compression = self .COMPRESSION ) as zf :
2683+ zf .writestr ("member" , data )
2684+ self .assertIn (data .swapcase (), buf .getvalue ())
2685+ with zipfile .ZipFile (io .BytesIO (buf .getvalue ())) as zf :
2686+ self .assertEqual (zf .read ("member" ), data )
2687+ with zf .open ("member" ) as f :
2688+ self .assertEqual (f .read (100 ), data [:100 ])
2689+ self .assertEqual (f .read1 (100 ), data [100 :200 ])
2690+ f .seek (- 100 , os .SEEK_END )
2691+ self .assertEqual (f .read (), data [- 100 :])
2692+ # Rewinding past the read buffer re-creates the decompressor.
2693+ f .seek (0 )
2694+ self .assertEqual (f .read (), data )
2695+
2696+
26292697class AbstractBadCrcTests :
26302698 def test_testzip_with_bad_crc (self ):
26312699 """Tests that files with bad CRCs return their name from testzip."""
0 commit comments