55import pickle
66import stat
77import sys
8+ import time
89import unittest
910from zipp .compat .overlay import zipfile
1011
@@ -586,7 +587,11 @@ def test_getinfo_missing(self, alpharep):
586587
587588 def test_malformed_paths (self ):
588589 """
589- Path should handle malformed paths.
590+ Path should handle malformed paths gracefully.
591+
592+ Paths with leading slashes are not visible.
593+
594+ Paths with dots are treated like regular files.
590595 """
591596 data = io .BytesIO ()
592597 zf = zipfile .ZipFile (data , "w" )
@@ -595,15 +600,71 @@ def test_malformed_paths(self):
595600 zf .writestr ("../parent.txt" , b"content" )
596601 zf .filename = ''
597602 root = zipfile .Path (zf )
598- assert list (map (str , root .iterdir ())) == [
599- 'one-slash.txt' ,
600- 'two-slash.txt' ,
601- 'parent.txt' ,
602- ]
603+ assert list (map (str , root .iterdir ())) == ['../' ]
604+ assert root .joinpath ('..' ).joinpath ('parent.txt' ).read_bytes () == b'content'
605+
606+ def test_unsupported_names (self ):
607+ """
608+ Path segments with special characters are readable.
609+
610+ On some platforms or file systems, characters like
611+ ``:`` and ``?`` are not allowed, but they are valid
612+ in the zip file.
613+ """
614+ data = io .BytesIO ()
615+ zf = zipfile .ZipFile (data , "w" )
616+ zf .writestr ("path?" , b"content" )
617+ zf .writestr ("V: NMS.flac" , b"fLaC..." )
618+ zf .filename = ''
619+ root = zipfile .Path (zf )
620+ contents = root .iterdir ()
621+ assert next (contents ).name == 'path?'
622+ assert next (contents ).name == 'V: NMS.flac'
623+ assert root .joinpath ('V: NMS.flac' ).read_bytes () == b"fLaC..."
624+
625+ def test_backslash_not_separator (self ):
626+ """
627+ In a zip file, backslashes are not separators.
628+ """
629+ data = io .BytesIO ()
630+ zf = zipfile .ZipFile (data , "w" )
631+ zf .writestr (DirtyZipInfo .for_name ("foo\\ bar" , zf ), b"content" )
632+ zf .filename = ''
633+ root = zipfile .Path (zf )
634+ (first ,) = root .iterdir ()
635+ assert not first .is_dir ()
636+ assert first .name == 'foo\\ bar'
603637
604638 @pass_alpharep
605639 def test_interface (self , alpharep ):
606640 from .compat .py310 import Traversable
607641
608642 zf = zipfile .Path (alpharep )
609643 assert isinstance (zf , Traversable )
644+
645+
646+ class DirtyZipInfo (zipfile .ZipInfo ):
647+ """
648+ Bypass name sanitization.
649+ """
650+
651+ def __init__ (self , filename , * args , ** kwargs ):
652+ super ().__init__ (filename , * args , ** kwargs )
653+ self .filename = filename
654+
655+ @classmethod
656+ def for_name (cls , name , archive ):
657+ """
658+ Construct the same way that ZipFile.writestr does.
659+
660+ TODO: extract this functionality and re-use
661+ """
662+ self = cls (filename = name , date_time = time .localtime (time .time ())[:6 ])
663+ self .compress_type = archive .compression
664+ self .compress_level = archive .compresslevel
665+ if self .filename .endswith ('/' ): # pragma: no cover
666+ self .external_attr = 0o40775 << 16 # drwxrwxr-x
667+ self .external_attr |= 0x10 # MS-DOS directory flag
668+ else :
669+ self .external_attr = 0o600 << 16 # ?rw-------
670+ return self
0 commit comments