@@ -1080,6 +1080,159 @@ def test_generated_valid_zip64_extra(self):
1080
1080
self .assertEqual (zinfo .header_offset , expected_header_offset )
1081
1081
self .assertEqual (zf .read (zinfo ), expected_content )
1082
1082
1083
+ def test_force_zip64 (self ):
1084
+ """Test that forcing zip64 extensions correctly notes this in the zip file"""
1085
+
1086
+ # GH-103861 describes an issue where forcing a small file to use zip64
1087
+ # extensions would add a zip64 extra record, but not change the data
1088
+ # sizes to 0xFFFFFFFF to indicate to the extractor that the zip64
1089
+ # record should be read. Additionally, it would not set the required
1090
+ # version to indicate that zip64 extensions are required to extract it.
1091
+ # This test replicates the situation and reads the raw data to specifically ensure:
1092
+ # - The required extract version is always >= ZIP64_VERSION
1093
+ # - The compressed and uncompressed size in the file headers are both
1094
+ # 0xFFFFFFFF (ie. point to zip64 record)
1095
+ # - The zip64 record is provided and has the correct sizes in it
1096
+ # Other aspects of the zip are checked as well, but verifying the above is the main goal.
1097
+ # Because this is hard to verify by parsing the data as a zip, the raw
1098
+ # bytes are checked to ensure that they line up with the zip spec.
1099
+ # The spec for this can be found at: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
1100
+ # The relevent sections for this test are:
1101
+ # - 4.3.7 for local file header
1102
+ # - 4.5.3 for zip64 extra field
1103
+
1104
+ data = io .BytesIO ()
1105
+ with zipfile .ZipFile (data , mode = "w" , allowZip64 = True ) as zf :
1106
+ with zf .open ("text.txt" , mode = "w" , force_zip64 = True ) as zi :
1107
+ zi .write (b"_" )
1108
+
1109
+ zipdata = data .getvalue ()
1110
+
1111
+ # pull out and check zip information
1112
+ (
1113
+ header , vers , os , flags , comp , csize , usize , fn_len ,
1114
+ ex_total_len , filename , ex_id , ex_len , ex_usize , ex_csize , cd_sig
1115
+ ) = struct .unpack ("<4sBBHH8xIIHH8shhQQx4s" , zipdata [:63 ])
1116
+
1117
+ self .assertEqual (header , b"PK\x03 \x04 " ) # local file header
1118
+ self .assertGreaterEqual (vers , zipfile .ZIP64_VERSION ) # requires zip64 to extract
1119
+ self .assertEqual (os , 0 ) # compatible with MS-DOS
1120
+ self .assertEqual (flags , 0 ) # no flags
1121
+ self .assertEqual (comp , 0 ) # compression method = stored
1122
+ self .assertEqual (csize , 0xFFFFFFFF ) # sizes are in zip64 extra
1123
+ self .assertEqual (usize , 0xFFFFFFFF )
1124
+ self .assertEqual (fn_len , 8 ) # filename len
1125
+ self .assertEqual (ex_total_len , 20 ) # size of extra records
1126
+ self .assertEqual (ex_id , 1 ) # Zip64 extra record
1127
+ self .assertEqual (ex_len , 16 ) # 16 bytes of data
1128
+ self .assertEqual (ex_usize , 1 ) # uncompressed size
1129
+ self .assertEqual (ex_csize , 1 ) # compressed size
1130
+ self .assertEqual (cd_sig , b"PK\x01 \x02 " ) # ensure the central directory header is next
1131
+
1132
+ z = zipfile .ZipFile (io .BytesIO (zipdata ))
1133
+ zinfos = z .infolist ()
1134
+ self .assertEqual (len (zinfos ), 1 )
1135
+ self .assertGreaterEqual (zinfos [0 ].extract_version , zipfile .ZIP64_VERSION ) # requires zip64 to extract
1136
+
1137
+ def test_unseekable_zip_unknown_filesize (self ):
1138
+ """Test that creating a zip with/without seeking will raise a RuntimeError if zip64 was required but not used"""
1139
+
1140
+ def make_zip (fp ):
1141
+ with zipfile .ZipFile (fp , mode = "w" , allowZip64 = True ) as zf :
1142
+ with zf .open ("text.txt" , mode = "w" , force_zip64 = False ) as zi :
1143
+ zi .write (b"_" * (zipfile .ZIP64_LIMIT + 1 ))
1144
+
1145
+ self .assertRaises (RuntimeError , make_zip , io .BytesIO ())
1146
+ self .assertRaises (RuntimeError , make_zip , Unseekable (io .BytesIO ()))
1147
+
1148
+ def test_zip64_required_not_allowed_fail (self ):
1149
+ """Test that trying to add a large file to a zip that doesn't allow zip64 extensions fails on add"""
1150
+ def make_zip (fp ):
1151
+ with zipfile .ZipFile (fp , mode = "w" , allowZip64 = False ) as zf :
1152
+ # pretend zipfile.ZipInfo.from_file was used to get the name and filesize
1153
+ info = zipfile .ZipInfo ("text.txt" )
1154
+ info .file_size = zipfile .ZIP64_LIMIT + 1
1155
+ zf .open (info , mode = "w" )
1156
+
1157
+ self .assertRaises (zipfile .LargeZipFile , make_zip , io .BytesIO ())
1158
+ self .assertRaises (zipfile .LargeZipFile , make_zip , Unseekable (io .BytesIO ()))
1159
+
1160
+ def test_unseekable_zip_known_filesize (self ):
1161
+ """Test that creating a zip without seeking will use zip64 extensions if the file size is provided up-front"""
1162
+
1163
+ # This test ensures that the zip will use a zip64 data descriptor (same
1164
+ # as a regular data descriptor except the sizes are 8 bytes instead of
1165
+ # 4) record to communicate the size of a file if the zip is being
1166
+ # written to an unseekable stream.
1167
+ # Because this sort of thing is hard to verify by parsing the data back
1168
+ # in as a zip, this test looks at the raw bytes created to ensure that
1169
+ # the correct data has been generated.
1170
+ # The spec for this can be found at: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
1171
+ # The relevent sections for this test are:
1172
+ # - 4.3.7 for local file header
1173
+ # - 4.3.9 for the data descriptor
1174
+ # - 4.5.3 for zip64 extra field
1175
+
1176
+ file_size = zipfile .ZIP64_LIMIT + 1
1177
+
1178
+ def make_zip (fp ):
1179
+ with zipfile .ZipFile (fp , mode = "w" , allowZip64 = True ) as zf :
1180
+ # pretend zipfile.ZipInfo.from_file was used to get the name and filesize
1181
+ info = zipfile .ZipInfo ("text.txt" )
1182
+ info .file_size = file_size
1183
+ with zf .open (info , mode = "w" , force_zip64 = False ) as zi :
1184
+ zi .write (b"_" * file_size )
1185
+ return fp
1186
+
1187
+ # check seekable file information
1188
+ seekable_data = make_zip (io .BytesIO ()).getvalue ()
1189
+ (
1190
+ header , vers , os , flags , comp , csize , usize , fn_len ,
1191
+ ex_total_len , filename , ex_id , ex_len , ex_usize , ex_csize ,
1192
+ cd_sig
1193
+ ) = struct .unpack ("<4sBBHH8xIIHH8shhQQ{}x4s" .format (file_size ), seekable_data [:62 + file_size ])
1194
+
1195
+ self .assertEqual (header , b"PK\x03 \x04 " ) # local file header
1196
+ self .assertGreaterEqual (vers , zipfile .ZIP64_VERSION ) # requires zip64 to extract
1197
+ self .assertEqual (os , 0 ) # compatible with MS-DOS
1198
+ self .assertEqual (flags , 0 ) # no flags set
1199
+ self .assertEqual (comp , 0 ) # compression method = stored
1200
+ self .assertEqual (csize , 0xFFFFFFFF ) # sizes are in zip64 extra
1201
+ self .assertEqual (usize , 0xFFFFFFFF )
1202
+ self .assertEqual (fn_len , 8 ) # filename len
1203
+ self .assertEqual (ex_total_len , 20 ) # size of extra records
1204
+ self .assertEqual (ex_id , 1 ) # Zip64 extra record
1205
+ self .assertEqual (ex_len , 16 ) # 16 bytes of data
1206
+ self .assertEqual (ex_usize , file_size ) # uncompressed size
1207
+ self .assertEqual (ex_csize , file_size ) # compressed size
1208
+ self .assertEqual (cd_sig , b"PK\x01 \x02 " ) # ensure the central directory header is next
1209
+
1210
+ # check unseekable file information
1211
+ unseekable_data = make_zip (Unseekable (io .BytesIO ())).fp .getvalue ()
1212
+ (
1213
+ header , vers , os , flags , comp , csize , usize , fn_len ,
1214
+ ex_total_len , filename , ex_id , ex_len , ex_usize , ex_csize ,
1215
+ dd_header , dd_usize , dd_csize , cd_sig
1216
+ ) = struct .unpack ("<4sBBHH8xIIHH8shhQQ{}x4s4xQQ4s" .format (file_size ), unseekable_data [:86 + file_size ])
1217
+
1218
+ self .assertEqual (header , b"PK\x03 \x04 " ) # local file header
1219
+ self .assertGreaterEqual (vers , zipfile .ZIP64_VERSION ) # requires zip64 to extract
1220
+ self .assertEqual (os , 0 ) # compatible with MS-DOS
1221
+ self .assertEqual ("{:b}" .format (flags ), "1000" ) # streaming flag set
1222
+ self .assertEqual (comp , 0 ) # compression method = stored
1223
+ self .assertEqual (csize , 0xFFFFFFFF ) # sizes are in zip64 extra
1224
+ self .assertEqual (usize , 0xFFFFFFFF )
1225
+ self .assertEqual (fn_len , 8 ) # filename len
1226
+ self .assertEqual (ex_total_len , 20 ) # size of extra records
1227
+ self .assertEqual (ex_id , 1 ) # Zip64 extra record
1228
+ self .assertEqual (ex_len , 16 ) # 16 bytes of data
1229
+ self .assertEqual (ex_usize , 0 ) # uncompressed size - 0 to defer to data descriptor
1230
+ self .assertEqual (ex_csize , 0 ) # compressed size - 0 to defer to data descriptor
1231
+ self .assertEqual (dd_header , b"PK\07 \x08 " ) # data descriptor
1232
+ self .assertEqual (dd_usize , file_size ) # file size (8 bytes because zip64)
1233
+ self .assertEqual (dd_csize , file_size ) # compressed size (8 bytes because zip64)
1234
+ self .assertEqual (cd_sig , b"PK\x01 \x02 " ) # ensure the central directory header is next
1235
+
1083
1236
1084
1237
@requires_zlib ()
1085
1238
class DeflateTestZip64InSmallFiles (AbstractTestZip64InSmallFiles ,
0 commit comments