zipremove 0.3.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipremove
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Extend `zipfile` with `remove`-related functionalities
5
5
  Home-page: https://github.com/danny0838/zipremove
6
6
  Author: Danny Lin
@@ -37,6 +37,7 @@ Dynamic: license-file
37
37
  ![Status](https://img.shields.io/pypi/status/zipremove)
38
38
  ![License](https://img.shields.io/github/license/danny0838/zipremove)
39
39
  [![Downloads](https://static.pepy.tech/personalized-badge/zipremove?period=month&left_text=Downloads)](https://pepy.tech/project/zipremove)
40
+ [![Pull request](https://img.shields.io/github/pulls/detail/state/python/cpython/134627)](https://github.com/python/cpython/pull/134627)
40
41
 
41
42
  This package extends `zipfile` with `remove`-related functionalities.
42
43
 
@@ -61,30 +62,32 @@ This package extends `zipfile` with `remove`-related functionalities.
61
62
 
62
63
  * `ZipFile.repack(removed=None, *, strict_descriptor=False[, chunk_size])`
63
64
 
64
- Rewrites the archive to remove stale local file entries, shrinking the ZIP
65
- file size.
65
+ Rewrites the archive to remove stale local file entries, shrinking its file
66
+ size.
66
67
 
67
68
  If *removed* is provided, it must be a sequence of `ZipInfo` objects
68
69
  representing removed entries; only their corresponding local file entries
69
70
  will be removed.
70
71
 
71
- If *removed* is not provided, local file entries no longer referenced in the
72
- central directory will be removed. The algorithm assumes that local file
73
- entries are stored consecutively:
72
+ If *removed* is not provided, the archive is scanned to identify and remove
73
+ local file entries that are no longer referenced in the central directory.
74
+ The algorithm assumes that local file entries (and the central directory,
75
+ which is mostly treated as the "last entry") are stored consecutively:
74
76
 
75
77
  1. Data before the first referenced entry is removed only when it appears to
76
78
  be a sequence of consecutive entries with no extra following bytes; extra
77
- preceeding bytes are preserved.
79
+ preceding bytes are preserved.
78
80
  2. Data between referenced entries is removed only when it appears to
79
81
  be a sequence of consecutive entries with no extra preceding bytes; extra
80
82
  following bytes are preserved.
81
-
82
- ``strict_descriptor=True`` can be provided to skip the slower scan for an
83
- unsigned data descriptor (deprecated in the latest ZIP specification and is
84
- only used by legacy tools) when checking for bytes resembling a valid local
85
- file entry. This improves performance, but may cause some stale local file
86
- entries to be preserved, as any entry using an unsigned descriptor cannot
87
- be detected.
83
+ 3. Entries must not overlap. If any entry's data overlaps with another, a
84
+ `BadZipFile` error is raised and no changes are made.
85
+
86
+ When scanning, setting `strict_descriptor=True` disables detection of any
87
+ entry using an unsigned data descriptor (deprecated in the ZIP specification
88
+ since version 6.3.0, released on 2006-09-29, and used only by some legacy
89
+ tools). This improves performance, but may cause some stale entries to be
90
+ preserved.
88
91
 
89
92
  *chunk_size* may be specified to control the buffer size when moving
90
93
  entry data (default is 1 MiB).
@@ -3,6 +3,7 @@
3
3
  ![Status](https://img.shields.io/pypi/status/zipremove)
4
4
  ![License](https://img.shields.io/github/license/danny0838/zipremove)
5
5
  [![Downloads](https://static.pepy.tech/personalized-badge/zipremove?period=month&left_text=Downloads)](https://pepy.tech/project/zipremove)
6
+ [![Pull request](https://img.shields.io/github/pulls/detail/state/python/cpython/134627)](https://github.com/python/cpython/pull/134627)
6
7
 
7
8
  This package extends `zipfile` with `remove`-related functionalities.
8
9
 
@@ -27,30 +28,32 @@ This package extends `zipfile` with `remove`-related functionalities.
27
28
 
28
29
  * `ZipFile.repack(removed=None, *, strict_descriptor=False[, chunk_size])`
29
30
 
30
- Rewrites the archive to remove stale local file entries, shrinking the ZIP
31
- file size.
31
+ Rewrites the archive to remove stale local file entries, shrinking its file
32
+ size.
32
33
 
33
34
  If *removed* is provided, it must be a sequence of `ZipInfo` objects
34
35
  representing removed entries; only their corresponding local file entries
35
36
  will be removed.
36
37
 
37
- If *removed* is not provided, local file entries no longer referenced in the
38
- central directory will be removed. The algorithm assumes that local file
39
- entries are stored consecutively:
38
+ If *removed* is not provided, the archive is scanned to identify and remove
39
+ local file entries that are no longer referenced in the central directory.
40
+ The algorithm assumes that local file entries (and the central directory,
41
+ which is mostly treated as the "last entry") are stored consecutively:
40
42
 
41
43
  1. Data before the first referenced entry is removed only when it appears to
42
44
  be a sequence of consecutive entries with no extra following bytes; extra
43
- preceeding bytes are preserved.
45
+ preceding bytes are preserved.
44
46
  2. Data between referenced entries is removed only when it appears to
45
47
  be a sequence of consecutive entries with no extra preceding bytes; extra
46
48
  following bytes are preserved.
47
-
48
- ``strict_descriptor=True`` can be provided to skip the slower scan for an
49
- unsigned data descriptor (deprecated in the latest ZIP specification and is
50
- only used by legacy tools) when checking for bytes resembling a valid local
51
- file entry. This improves performance, but may cause some stale local file
52
- entries to be preserved, as any entry using an unsigned descriptor cannot
53
- be detected.
49
+ 3. Entries must not overlap. If any entry's data overlaps with another, a
50
+ `BadZipFile` error is raised and no changes are made.
51
+
52
+ When scanning, setting `strict_descriptor=True` disables detection of any
53
+ entry using an unsigned data descriptor (deprecated in the ZIP specification
54
+ since version 6.3.0, released on 2006-09-29, and used only by some legacy
55
+ tools). This improves performance, but may cause some stale entries to be
56
+ preserved.
54
57
 
55
58
  *chunk_size* may be specified to control the buffer size when moving
56
59
  entry data (default is 1 MiB).
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = zipremove
3
- version = 0.3.0
3
+ version = 0.4.1
4
4
  author = Danny Lin
5
5
  author_email = danny0838@gmail.com
6
6
  url = https://github.com/danny0838/zipremove
@@ -15,6 +15,7 @@ from zipfile import (
15
15
  _FH_GENERAL_PURPOSE_FLAG_BITS,
16
16
  _FH_SIGNATURE,
17
17
  _FH_UNCOMPRESSED_SIZE,
18
+ LZMADecompressor,
18
19
  _get_decompressor,
19
20
  crc32,
20
21
  sizeFileHeader,
@@ -29,6 +30,12 @@ except NameError:
29
30
  # polyfill for Python < 3.14
30
31
  ZIP_ZSTANDARD = 93
31
32
 
33
+ try:
34
+ from zipfile import _MASK_ENCRYPTED
35
+ except ImportError:
36
+ # polyfill for Python < 3.11
37
+ _MASK_ENCRYPTED = 1 << 0
38
+
32
39
  try:
33
40
  from zipfile import _MASK_USE_DATA_DESCRIPTOR
34
41
  except ImportError:
@@ -50,18 +57,16 @@ except ImportError:
50
57
  return filename
51
58
 
52
59
  try:
53
- import zipfile
54
- zipfile.LZMADecompressor().unused_data
60
+ LZMADecompressor().unused_data
55
61
  except AttributeError:
56
62
  # polyfill to support LZMADecompressor().unused_data
57
- class LZMADecompressor(zipfile.LZMADecompressor):
58
- @property
59
- def unused_data(self):
60
- try:
61
- return self._decomp.unused_data
62
- except AttributeError:
63
- return b''
64
- zipfile.LZMADecompressor = LZMADecompressor
63
+ @property
64
+ def unused_data(self):
65
+ try:
66
+ return self._decomp.unused_data
67
+ except AttributeError:
68
+ return b''
69
+ LZMADecompressor.unused_data = unused_data
65
70
 
66
71
 
67
72
  class _ZipRepacker:
@@ -257,14 +262,11 @@ class _ZipRepacker:
257
262
  used_entry_size,
258
263
  )
259
264
 
260
- if used_entry_size < entry_size:
261
- stale_entry_size = self._validate_local_file_entry_sequence(
262
- fp,
263
- old_header_offset + used_entry_size,
264
- old_header_offset + entry_size,
265
- )
266
- else:
267
- stale_entry_size = 0
265
+ stale_entry_size = self._validate_local_file_entry_sequence(
266
+ fp,
267
+ old_header_offset + used_entry_size,
268
+ old_header_offset + entry_size,
269
+ )
268
270
 
269
271
  if stale_entry_size > 0:
270
272
  self._copy_bytes(
@@ -375,8 +377,8 @@ class _ZipRepacker:
375
377
  if pos > end_offset:
376
378
  return None
377
379
 
380
+ # parse zip64
378
381
  try:
379
- # parse zip64
380
382
  try:
381
383
  zinfo._decodeExtra(crc32(filename))
382
384
  except TypeError:
@@ -399,8 +401,11 @@ class _ZipRepacker:
399
401
 
400
402
  dd = self._scan_data_descriptor(fp, pos, end_offset, zip64)
401
403
  if dd is None and not self.strict_descriptor:
402
- dd = self._scan_data_descriptor_no_sig_by_decompression(
403
- fp, pos, end_offset, zip64, fheader[_FH_COMPRESSION_METHOD])
404
+ if zinfo.flag_bits & _MASK_ENCRYPTED:
405
+ dd = False
406
+ else:
407
+ dd = self._scan_data_descriptor_no_sig_by_decompression(
408
+ fp, pos, end_offset, zip64, fheader[_FH_COMPRESSION_METHOD])
404
409
  if dd is False:
405
410
  dd = self._scan_data_descriptor_no_sig(fp, pos, end_offset, zip64)
406
411
  if dd is None:
@@ -488,6 +493,7 @@ class _ZipRepacker:
488
493
  dd_fmt = '<LQQ' if zip64 else '<LLL'
489
494
  dd_size = struct.calcsize(dd_fmt)
490
495
 
496
+ # early return and prevent potential `fp.read(-1)`
491
497
  if end_offset - dd_size < offset:
492
498
  return None
493
499
 
@@ -608,15 +614,16 @@ class ZipFile(ZipFile):
608
614
 
609
615
  with self._lock:
610
616
  # get the zinfo
611
- # raise KeyError if arcname does not exist
612
617
  if isinstance(zinfo_or_arcname, ZipInfo):
613
618
  zinfo = zinfo_or_arcname
614
- if zinfo not in self.filelist:
615
- raise KeyError('There is no item %r in the archive' % zinfo)
616
619
  else:
620
+ # raise KeyError if arcname does not exist
617
621
  zinfo = self.getinfo(zinfo_or_arcname)
618
622
 
619
- self.filelist.remove(zinfo)
623
+ try:
624
+ self.filelist.remove(zinfo)
625
+ except ValueError:
626
+ raise KeyError('There is no item %r in the archive' % zinfo) from None
620
627
 
621
628
  try:
622
629
  del self.NameToInfo[zinfo.filename]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zipremove
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Extend `zipfile` with `remove`-related functionalities
5
5
  Home-page: https://github.com/danny0838/zipremove
6
6
  Author: Danny Lin
@@ -37,6 +37,7 @@ Dynamic: license-file
37
37
  ![Status](https://img.shields.io/pypi/status/zipremove)
38
38
  ![License](https://img.shields.io/github/license/danny0838/zipremove)
39
39
  [![Downloads](https://static.pepy.tech/personalized-badge/zipremove?period=month&left_text=Downloads)](https://pepy.tech/project/zipremove)
40
+ [![Pull request](https://img.shields.io/github/pulls/detail/state/python/cpython/134627)](https://github.com/python/cpython/pull/134627)
40
41
 
41
42
  This package extends `zipfile` with `remove`-related functionalities.
42
43
 
@@ -61,30 +62,32 @@ This package extends `zipfile` with `remove`-related functionalities.
61
62
 
62
63
  * `ZipFile.repack(removed=None, *, strict_descriptor=False[, chunk_size])`
63
64
 
64
- Rewrites the archive to remove stale local file entries, shrinking the ZIP
65
- file size.
65
+ Rewrites the archive to remove stale local file entries, shrinking its file
66
+ size.
66
67
 
67
68
  If *removed* is provided, it must be a sequence of `ZipInfo` objects
68
69
  representing removed entries; only their corresponding local file entries
69
70
  will be removed.
70
71
 
71
- If *removed* is not provided, local file entries no longer referenced in the
72
- central directory will be removed. The algorithm assumes that local file
73
- entries are stored consecutively:
72
+ If *removed* is not provided, the archive is scanned to identify and remove
73
+ local file entries that are no longer referenced in the central directory.
74
+ The algorithm assumes that local file entries (and the central directory,
75
+ which is mostly treated as the "last entry") are stored consecutively:
74
76
 
75
77
  1. Data before the first referenced entry is removed only when it appears to
76
78
  be a sequence of consecutive entries with no extra following bytes; extra
77
- preceeding bytes are preserved.
79
+ preceding bytes are preserved.
78
80
  2. Data between referenced entries is removed only when it appears to
79
81
  be a sequence of consecutive entries with no extra preceding bytes; extra
80
82
  following bytes are preserved.
81
-
82
- ``strict_descriptor=True`` can be provided to skip the slower scan for an
83
- unsigned data descriptor (deprecated in the latest ZIP specification and is
84
- only used by legacy tools) when checking for bytes resembling a valid local
85
- file entry. This improves performance, but may cause some stale local file
86
- entries to be preserved, as any entry using an unsigned descriptor cannot
87
- be detected.
83
+ 3. Entries must not overlap. If any entry's data overlaps with another, a
84
+ `BadZipFile` error is raised and no changes are made.
85
+
86
+ When scanning, setting `strict_descriptor=True` disables detection of any
87
+ entry using an unsigned data descriptor (deprecated in the ZIP specification
88
+ since version 6.3.0, released on 2006-09-29, and used only by some legacy
89
+ tools). This improves performance, but may cause some stale entries to be
90
+ preserved.
88
91
 
89
92
  *chunk_size* may be specified to control the buffer size when moving
90
93
  entry data (default is 1 MiB).
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import io
2
3
  import itertools
3
4
  import os
@@ -610,6 +611,25 @@ class AbstractRepackTests(RepackHelperMixin):
610
611
  with zipfile.ZipFile(TESTFN) as zh:
611
612
  self.assertIsNone(zh.testzip())
612
613
 
614
+ def test_repack_propagation(self):
615
+ """Should call internal API with adequate parameters."""
616
+ self._prepare_zip_from_test_files(TESTFN, self.test_files)
617
+
618
+ with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
619
+ with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp, \
620
+ mock.patch.object(zipfile, '_ZipRepacker', wraps=zipfile._ZipRepacker) as m_zr:
621
+ zh.repack()
622
+ m_zr.assert_called_once_with()
623
+ m_rp.assert_called_once_with(zh, None)
624
+
625
+ with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
626
+ zi = zh.remove(zh.infolist()[0])
627
+ with mock.patch.object(zipfile._ZipRepacker, 'repack') as m_rp, \
628
+ mock.patch.object(zipfile, '_ZipRepacker', wraps=zipfile._ZipRepacker) as m_zr:
629
+ zh.repack([zi], strict_descriptor=True, chunk_size=1024)
630
+ m_zr.assert_called_once_with(strict_descriptor=True, chunk_size=1024)
631
+ m_rp.assert_called_once_with(zh, [zi])
632
+
613
633
  def test_repack_bytes_before_first_file(self):
614
634
  """Should preserve random bytes before the first recorded local file entry."""
615
635
  for ii in ([], [0], [0, 1], [0, 1, 2]):
@@ -847,222 +867,6 @@ class AbstractRepackTests(RepackHelperMixin):
847
867
  with zipfile.ZipFile(TESTFN) as zh:
848
868
  self.assertIsNone(zh.testzip())
849
869
 
850
- @requires_zip64fix()
851
- def test_repack_zip64(self):
852
- """Should correctly handle file entries with zip64."""
853
- for ii in ([0], [0, 1], [1], [2]):
854
- with self.subTest(remove=ii):
855
- # calculate the expected results
856
- test_files = [data for j, data in enumerate(self.test_files) if j not in ii]
857
- expected_zinfos = self._prepare_zip_from_test_files(TESTFN, test_files, force_zip64=True)
858
- expected_size = os.path.getsize(TESTFN)
859
-
860
- # do the removal and check the result
861
- self._prepare_zip_from_test_files(TESTFN, self.test_files, force_zip64=True)
862
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
863
- for i in ii:
864
- zh.remove(self.test_files[i][0])
865
- zh.repack()
866
-
867
- # check infolist
868
- self.assertEqual(
869
- [ComparableZipInfo(zi) for zi in zh.infolist()],
870
- expected_zinfos,
871
- )
872
-
873
- # check file size
874
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
875
-
876
- # make sure the zip file is still valid
877
- with zipfile.ZipFile(TESTFN) as zh:
878
- self.assertIsNone(zh.testzip())
879
-
880
- def test_repack_data_descriptor(self):
881
- """Should correctly handle file entries using data descriptor."""
882
- for ii in ([0], [0, 1], [1], [2]):
883
- with self.subTest(remove=ii):
884
- # calculate the expected results
885
- test_files = [data for j, data in enumerate(self.test_files) if j not in ii]
886
- with open(TESTFN, 'wb') as fh:
887
- expected_zinfos = self._prepare_zip_from_test_files(Unseekable(fh), test_files)
888
- expected_size = os.path.getsize(TESTFN)
889
-
890
- # do the removal and check the result
891
- with open(TESTFN, 'wb') as fh:
892
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files)
893
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
894
- # make sure data descriptor bit is really set (by making zipfile unseekable)
895
- for zi in zh.infolist():
896
- self.assertTrue(zi.flag_bits & 8, f'data descriptor not used: {zi.filename}')
897
-
898
- for i in ii:
899
- zh.remove(self.test_files[i][0])
900
- zh.repack()
901
-
902
- # check infolist
903
- self.assertEqual(
904
- [ComparableZipInfo(zi) for zi in zh.infolist()],
905
- expected_zinfos,
906
- )
907
-
908
- # check file size
909
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
910
-
911
- # make sure the zip file is still valid
912
- with zipfile.ZipFile(TESTFN) as zh:
913
- self.assertIsNone(zh.testzip())
914
-
915
- @requires_zip64fix()
916
- def test_repack_data_descriptor_and_zip64(self):
917
- """Should correctly handle file entries using data descriptor and zip64."""
918
- for ii in ([0], [0, 1], [1], [2]):
919
- with self.subTest(remove=ii):
920
- # calculate the expected results
921
- test_files = [data for j, data in enumerate(self.test_files) if j not in ii]
922
- with open(TESTFN, 'wb') as fh:
923
- expected_zinfos = self._prepare_zip_from_test_files(Unseekable(fh), test_files, force_zip64=True)
924
- expected_size = os.path.getsize(TESTFN)
925
-
926
- # do the removal and check the result
927
- with open(TESTFN, 'wb') as fh:
928
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files, force_zip64=True)
929
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
930
- # make sure data descriptor bit is really set (by making zipfile unseekable)
931
- for zi in zh.infolist():
932
- self.assertTrue(zi.flag_bits & 8, f'data descriptor not used: {zi.filename}')
933
-
934
- for i in ii:
935
- zh.remove(self.test_files[i][0])
936
- zh.repack()
937
-
938
- # check infolist
939
- self.assertEqual(
940
- [ComparableZipInfo(zi) for zi in zh.infolist()],
941
- expected_zinfos,
942
- )
943
-
944
- # check file size
945
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
946
-
947
- # make sure the zip file is still valid
948
- with zipfile.ZipFile(TESTFN) as zh:
949
- self.assertIsNone(zh.testzip())
950
-
951
- def test_repack_data_descriptor_no_sig(self):
952
- """Should correctly handle file entries using data descriptor without signature."""
953
- for ii in ([0], [0, 1], [1], [2]):
954
- with self.subTest(remove=ii):
955
- # calculate the expected results
956
- test_files = [data for j, data in enumerate(self.test_files) if j not in ii]
957
- with open(TESTFN, 'wb') as fh:
958
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
959
- expected_zinfos = self._prepare_zip_from_test_files(Unseekable(fh), test_files)
960
- expected_size = os.path.getsize(TESTFN)
961
-
962
- # do the removal and check the result
963
- with open(TESTFN, 'wb') as fh:
964
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
965
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files)
966
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
967
- # make sure data descriptor bit is really set (by making zipfile unseekable)
968
- for zi in zh.infolist():
969
- self.assertTrue(zi.flag_bits & 8, f'data descriptor flag not set: {zi.filename}')
970
-
971
- for i in ii:
972
- zh.remove(self.test_files[i][0])
973
- zh.repack()
974
-
975
- # check infolist
976
- self.assertEqual(
977
- [ComparableZipInfo(zi) for zi in zh.infolist()],
978
- expected_zinfos,
979
- )
980
-
981
- # check file size
982
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
983
-
984
- # make sure the zip file is still valid
985
- with zipfile.ZipFile(TESTFN) as zh:
986
- self.assertIsNone(zh.testzip())
987
-
988
- def test_repack_data_descriptor_no_sig_strict(self):
989
- """Should skip data descriptor without signature when `strict_descriptor` is set."""
990
- for ii in ([0], [0, 1], [1], [2]):
991
- with self.subTest(remove=ii):
992
- # calculate the expected results
993
- with open(TESTFN, 'wb') as fh:
994
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
995
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files)
996
- with zipfile.ZipFile(TESTFN, 'a') as zh:
997
- for i in ii:
998
- zh.remove(self.test_files[i][0])
999
- expected_zinfos = [ComparableZipInfo(zi) for zi in zh.infolist()]
1000
- expected_size = os.path.getsize(TESTFN)
1001
-
1002
- # do the removal and check the result
1003
- with open(TESTFN, 'wb') as fh:
1004
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
1005
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files)
1006
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
1007
- # make sure data descriptor bit is really set (by making zipfile unseekable)
1008
- for zi in zh.infolist():
1009
- self.assertTrue(zi.flag_bits & 8, f'data descriptor flag not set: {zi.filename}')
1010
-
1011
- for i in ii:
1012
- zh.remove(self.test_files[i][0])
1013
- zh.repack(strict_descriptor=True)
1014
-
1015
- # check infolist
1016
- self.assertEqual(
1017
- [ComparableZipInfo(zi) for zi in zh.infolist()],
1018
- expected_zinfos,
1019
- )
1020
-
1021
- # check file size
1022
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
1023
-
1024
- # make sure the zip file is still valid
1025
- with zipfile.ZipFile(TESTFN) as zh:
1026
- self.assertIsNone(zh.testzip())
1027
-
1028
- @requires_zip64fix()
1029
- def test_repack_data_descriptor_no_sig_and_zip64(self):
1030
- """Should correctly handle file entries using data descriptor without signature and zip64."""
1031
- for ii in ([0], [0, 1], [1], [2]):
1032
- with self.subTest(remove=ii):
1033
- # calculate the expected results
1034
- test_files = [data for j, data in enumerate(self.test_files) if j not in ii]
1035
- with open(TESTFN, 'wb') as fh:
1036
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
1037
- expected_zinfos = self._prepare_zip_from_test_files(Unseekable(fh), test_files, force_zip64=True)
1038
- expected_size = os.path.getsize(TESTFN)
1039
-
1040
- # do the removal and check the result
1041
- with open(TESTFN, 'wb') as fh:
1042
- with mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig):
1043
- self._prepare_zip_from_test_files(Unseekable(fh), self.test_files, force_zip64=True)
1044
- with zipfile.ZipFile(TESTFN, 'a', self.compression) as zh:
1045
- # make sure data descriptor bit is really set (by making zipfile unseekable)
1046
- for zi in zh.infolist():
1047
- self.assertTrue(zi.flag_bits & 8, f'data descriptor flag not set: {zi.filename}')
1048
-
1049
- for i in ii:
1050
- zh.remove(self.test_files[i][0])
1051
- zh.repack()
1052
-
1053
- # check infolist
1054
- self.assertEqual(
1055
- [ComparableZipInfo(zi) for zi in zh.infolist()],
1056
- expected_zinfos,
1057
- )
1058
-
1059
- # check file size
1060
- self.assertEqual(os.path.getsize(TESTFN), expected_size)
1061
-
1062
- # make sure the zip file is still valid
1063
- with zipfile.ZipFile(TESTFN) as zh:
1064
- self.assertIsNone(zh.testzip())
1065
-
1066
870
  def test_repack_prepended_bytes(self):
1067
871
  for ii in ([], [0], [0, 1], [1], [2]):
1068
872
  with self.subTest(remove=ii):
@@ -1575,6 +1379,320 @@ class OtherRepackTests(unittest.TestCase):
1575
1379
  self.assertEqual(fz.read(), expected)
1576
1380
 
1577
1381
  class ZipRepackerTests(unittest.TestCase):
1382
+ def _generate_local_file_entry(self, arcname, raw_bytes,
1383
+ compression=zipfile.ZIP_STORED,
1384
+ force_zip64=False, dd=False, dd_sig=True):
1385
+ fz = io.BytesIO()
1386
+ f = Unseekable(fz) if dd else fz
1387
+ cm = (mock.patch.object(struct, 'pack', side_effect=struct_pack_no_dd_sig)
1388
+ if not dd_sig else contextlib.nullcontext())
1389
+ with zipfile.ZipFile(f, 'w', compression=compression) as zh:
1390
+ with cm:
1391
+ with zh.open(arcname, 'w', force_zip64=force_zip64) as fh:
1392
+ fh.write(raw_bytes)
1393
+ fz.seek(0)
1394
+ return fz.read()
1395
+
1396
+ def test_validate_local_file_entry_stored(self):
1397
+ self._test_validate_local_file_entry(method=zipfile.ZIP_STORED)
1398
+
1399
+ @requires_zlib()
1400
+ def test_validate_local_file_entry_zlib(self):
1401
+ self._test_validate_local_file_entry(method=zipfile.ZIP_DEFLATED)
1402
+
1403
+ @requires_bz2()
1404
+ def test_validate_local_file_entry_bz2(self):
1405
+ self._test_validate_local_file_entry(method=zipfile.ZIP_BZIP2)
1406
+
1407
+ @requires_lzma()
1408
+ def test_validate_local_file_entry_lzma(self):
1409
+ self._test_validate_local_file_entry(method=zipfile.ZIP_LZMA)
1410
+
1411
+ @requires_zstd()
1412
+ def test_validate_local_file_entry_zstd(self):
1413
+ self._test_validate_local_file_entry(method=zipfile.ZIP_ZSTANDARD)
1414
+
1415
+ def _test_validate_local_file_entry(self, method):
1416
+ repacker = zipfile._ZipRepacker()
1417
+
1418
+ # basic
1419
+ bytes_ = self._generate_local_file_entry(
1420
+ 'file.txt', b'dummy', compression=method)
1421
+ fz = io.BytesIO(bytes_)
1422
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1423
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1424
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1425
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1426
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1427
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1428
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1429
+ self.assertEqual(result, len(bytes_))
1430
+ m_sdd.assert_not_called()
1431
+ m_sddnsbd.assert_not_called()
1432
+ m_sddns.assert_not_called()
1433
+
1434
+ # offset
1435
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1436
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1437
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1438
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1439
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1440
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1441
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_) + 1)
1442
+ self.assertEqual(result, len(bytes_))
1443
+ m_sdd.assert_not_called()
1444
+ m_sddnsbd.assert_not_called()
1445
+ m_sddns.assert_not_called()
1446
+
1447
+ bytes_ = b'pre' + bytes_ + b'post'
1448
+ fz = io.BytesIO(bytes_)
1449
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1450
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1451
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1452
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1453
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1454
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1455
+ result = repacker._validate_local_file_entry(fz, 3, len(bytes_) - 4)
1456
+ self.assertEqual(result, len(bytes_) - 7)
1457
+ m_sdd.assert_not_called()
1458
+ m_sddnsbd.assert_not_called()
1459
+ m_sddns.assert_not_called()
1460
+
1461
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1462
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1463
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1464
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1465
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1466
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1467
+ result = repacker._validate_local_file_entry(fz, 3, len(bytes_))
1468
+ self.assertEqual(result, len(bytes_) - 7)
1469
+ m_sdd.assert_not_called()
1470
+ m_sddnsbd.assert_not_called()
1471
+ m_sddns.assert_not_called()
1472
+
1473
+ # return None if no match at given offset
1474
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1475
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1476
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1477
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1478
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1479
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1480
+ result = repacker._validate_local_file_entry(fz, 2, len(bytes_) - 4)
1481
+ self.assertEqual(result, None)
1482
+ m_sdd.assert_not_called()
1483
+ m_sddnsbd.assert_not_called()
1484
+ m_sddns.assert_not_called()
1485
+
1486
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1487
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1488
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1489
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1490
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1491
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1492
+ result = repacker._validate_local_file_entry(fz, 4, len(bytes_) - 4)
1493
+ self.assertEqual(result, None)
1494
+ m_sdd.assert_not_called()
1495
+ m_sddnsbd.assert_not_called()
1496
+ m_sddns.assert_not_called()
1497
+
1498
+ # return None if no sufficient header length
1499
+ bytes_ = self._generate_local_file_entry(
1500
+ 'file.txt', b'dummy', compression=method)
1501
+ bytes_ = bytes_[:29]
1502
+ fz = io.BytesIO(bytes_)
1503
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1504
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1505
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1506
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1507
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1508
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1509
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1510
+ self.assertEqual(result, None)
1511
+ m_sdd.assert_not_called()
1512
+ m_sddnsbd.assert_not_called()
1513
+ m_sddns.assert_not_called()
1514
+
1515
+ # data descriptor
1516
+ bytes_ = self._generate_local_file_entry(
1517
+ 'file.txt', b'dummy', compression=method, dd=True)
1518
+ fz = io.BytesIO(bytes_)
1519
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1520
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1521
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1522
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1523
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1524
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1525
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1526
+ self.assertEqual(result, len(bytes_))
1527
+ m_sdd.assert_called_once_with(fz, 38, len(bytes_), False)
1528
+ m_sddnsbd.assert_not_called()
1529
+ m_sddns.assert_not_called()
1530
+
1531
+ # data descriptor (unsigned)
1532
+ bytes_ = self._generate_local_file_entry(
1533
+ 'file.txt', b'dummy', compression=method, dd=True, dd_sig=False)
1534
+ fz = io.BytesIO(bytes_)
1535
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1536
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1537
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1538
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1539
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1540
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1541
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1542
+ self.assertEqual(result, len(bytes_))
1543
+ m_sdd.assert_called_once_with(fz, 38, len(bytes_), False)
1544
+ m_sddnsbd.assert_called_once_with(fz, 38, len(bytes_), False, method)
1545
+ if repacker._scan_data_descriptor_no_sig_by_decompression(fz, 38, len(bytes_), False, method):
1546
+ m_sddns.assert_not_called()
1547
+ else:
1548
+ m_sddns.assert_called_once_with(fz, 38, len(bytes_), False)
1549
+
1550
+ # return None for data descriptor (unsigned) if `strict_descriptor=True`
1551
+ repacker = zipfile._ZipRepacker(strict_descriptor=True)
1552
+ bytes_ = self._generate_local_file_entry(
1553
+ 'file.txt', b'dummy', compression=method, dd=True, dd_sig=False)
1554
+ fz = io.BytesIO(bytes_)
1555
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1556
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1557
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1558
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1559
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1560
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1561
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1562
+ self.assertEqual(result, None)
1563
+ m_sdd.assert_called_once_with(fz, 38, len(bytes_), False)
1564
+ m_sddnsbd.assert_not_called()
1565
+ m_sddns.assert_not_called()
1566
+
1567
+ @requires_zip64fix()
1568
+ def test_validate_local_file_entry_zip64_stored(self):
1569
+ self._test_validate_local_file_entry_zip64(method=zipfile.ZIP_STORED)
1570
+
1571
+ @requires_zip64fix()
1572
+ @requires_zlib()
1573
+ def test_validate_local_file_entry_zip64_zlib(self):
1574
+ self._test_validate_local_file_entry_zip64(method=zipfile.ZIP_DEFLATED)
1575
+
1576
+ @requires_zip64fix()
1577
+ @requires_bz2()
1578
+ def test_validate_local_file_entry_zip64_bz2(self):
1579
+ self._test_validate_local_file_entry_zip64(method=zipfile.ZIP_BZIP2)
1580
+
1581
+ @requires_zip64fix()
1582
+ @requires_lzma()
1583
+ def test_validate_local_file_entry_zip64_lzma(self):
1584
+ self._test_validate_local_file_entry_zip64(method=zipfile.ZIP_LZMA)
1585
+
1586
+ @requires_zip64fix()
1587
+ @requires_zstd()
1588
+ def test_validate_local_file_entry_zip64_zstd(self):
1589
+ self._test_validate_local_file_entry_zip64(method=zipfile.ZIP_ZSTANDARD)
1590
+
1591
+ def _test_validate_local_file_entry_zip64(self, method):
1592
+ repacker = zipfile._ZipRepacker()
1593
+
1594
+ # zip64
1595
+ bytes_ = self._generate_local_file_entry(
1596
+ 'file.txt', b'dummy', compression=method, force_zip64=True)
1597
+ fz = io.BytesIO(bytes_)
1598
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1599
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1600
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1601
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1602
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1603
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1604
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1605
+ self.assertEqual(result, len(bytes_))
1606
+ m_sdd.assert_not_called()
1607
+ m_sddnsbd.assert_not_called()
1608
+ m_sddns.assert_not_called()
1609
+
1610
+ # data descriptor + zip64
1611
+ bytes_ = self._generate_local_file_entry(
1612
+ 'file.txt', b'dummy', compression=method, force_zip64=True, dd=True)
1613
+ fz = io.BytesIO(bytes_)
1614
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1615
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1616
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1617
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1618
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1619
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1620
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1621
+ self.assertEqual(result, len(bytes_))
1622
+ m_sdd.assert_called_once_with(fz, 58, len(bytes_), True)
1623
+ m_sddnsbd.assert_not_called()
1624
+ m_sddns.assert_not_called()
1625
+
1626
+ # data descriptor (unsigned) + zip64
1627
+ bytes_ = self._generate_local_file_entry(
1628
+ 'file.txt', b'dummy', compression=method, force_zip64=True, dd=True, dd_sig=False)
1629
+ fz = io.BytesIO(bytes_)
1630
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1631
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1632
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1633
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1634
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1635
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1636
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1637
+ self.assertEqual(result, len(bytes_))
1638
+ m_sdd.assert_called_once_with(fz, 58, len(bytes_), True)
1639
+ m_sddnsbd.assert_called_once_with(fz, 58, len(bytes_), True, method)
1640
+ if repacker._scan_data_descriptor_no_sig_by_decompression(fz, 58, len(bytes_), True, method):
1641
+ m_sddns.assert_not_called()
1642
+ else:
1643
+ m_sddns.assert_called_once_with(fz, 58, len(bytes_), True)
1644
+
1645
+ # return None for data descriptor (unsigned) if `strict_descriptor=True`
1646
+ repacker = zipfile._ZipRepacker(strict_descriptor=True)
1647
+ bytes_ = self._generate_local_file_entry(
1648
+ 'file.txt', b'dummy', compression=method, force_zip64=True, dd=True, dd_sig=False)
1649
+ fz = io.BytesIO(bytes_)
1650
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1651
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1652
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1653
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1654
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1655
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1656
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1657
+ self.assertEqual(result, None)
1658
+ m_sdd.assert_called_once_with(fz, 58, len(bytes_), True)
1659
+ m_sddnsbd.assert_not_called()
1660
+ m_sddns.assert_not_called()
1661
+
1662
+ def test_validate_local_file_entry_encrypted(self):
1663
+ repacker = zipfile._ZipRepacker()
1664
+
1665
+ bytes_ = (
1666
+ b'PK\x03\x04'
1667
+ b'\x14\x00'
1668
+ b'\x09\x00'
1669
+ b'\x08\x00'
1670
+ b'\xAB\x28'
1671
+ b'\xD2\x5A'
1672
+ b'\x00\x00\x00\x00'
1673
+ b'\x00\x00\x00\x00'
1674
+ b'\x00\x00\x00\x00'
1675
+ b'\x08\x00'
1676
+ b'\x00\x00'
1677
+ b'file.txt'
1678
+ b'\x97\xF1\x83\x34\x9D\xC4\x8C\xD3\xED\x79\x8C\xA2\xBB\x49\xFF\x1B\x89'
1679
+ b'\x3F\xF2\xF4\x4F'
1680
+ b'\x11\x00\x00\x00'
1681
+ b'\x05\x00\x00\x00'
1682
+ )
1683
+ fz = io.BytesIO(bytes_)
1684
+ with mock.patch.object(repacker, '_scan_data_descriptor',
1685
+ wraps=repacker._scan_data_descriptor) as m_sdd, \
1686
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig_by_decompression',
1687
+ wraps=repacker._scan_data_descriptor_no_sig_by_decompression) as m_sddnsbd, \
1688
+ mock.patch.object(repacker, '_scan_data_descriptor_no_sig',
1689
+ wraps=repacker._scan_data_descriptor_no_sig) as m_sddns:
1690
+ result = repacker._validate_local_file_entry(fz, 0, len(bytes_))
1691
+ self.assertEqual(result, len(bytes_))
1692
+ m_sdd.assert_called_once_with(fz, 38, len(bytes_), False)
1693
+ m_sddnsbd.assert_not_called()
1694
+ m_sddns.assert_called_once_with(fz, 38, len(bytes_), False)
1695
+
1578
1696
  def test_iter_scan_signature(self):
1579
1697
  bytes_ = b'sig__sig__sig__sig'
1580
1698
  ln = len(bytes_)
@@ -1623,136 +1741,177 @@ class ZipRepackerTests(unittest.TestCase):
1623
1741
 
1624
1742
  def test_scan_data_descriptor(self):
1625
1743
  repacker = zipfile._ZipRepacker()
1626
- SIG = zipfile._DD_SIGNATURE
1744
+
1745
+ sig = zipfile._DD_SIGNATURE
1746
+ raw_bytes = comp_bytes = b'dummy'
1747
+ raw_len = comp_len = len(raw_bytes)
1748
+ raw_crc = zipfile.crc32(raw_bytes)
1627
1749
 
1628
1750
  # basic
1629
- bytes_ = b'dummy' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5)
1751
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1630
1752
  self.assertEqual(
1631
1753
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1632
- (0x4ff4f23f, 5, 5, 16),
1754
+ (raw_crc, comp_len, raw_len, 16),
1633
1755
  )
1634
1756
 
1635
1757
  # return None if no signature
1636
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1758
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1637
1759
  self.assertEqual(
1638
1760
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1639
1761
  None,
1640
1762
  )
1641
1763
 
1642
- # return None if not unpackable
1643
- bytes_ = struct.pack('<L', SIG)
1764
+ # return None if compressed size not match
1765
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len + 1, raw_len)
1644
1766
  self.assertEqual(
1645
1767
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1646
1768
  None,
1647
1769
  )
1648
1770
 
1649
- # return None if compressed size not match
1650
- bytes_ = b'dumm' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5)
1771
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len - 1, raw_len)
1772
+ self.assertEqual(
1773
+ repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1774
+ None,
1775
+ )
1776
+
1777
+ bytes_ = b'1' + comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1778
+ self.assertEqual(
1779
+ repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1780
+ None,
1781
+ )
1782
+
1783
+ bytes_ = comp_bytes[1:] + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1651
1784
  self.assertEqual(
1652
1785
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1653
1786
  None,
1654
1787
  )
1655
1788
 
1656
1789
  # zip64
1657
- bytes_ = b'dummy' + struct.pack('<2L2Q', SIG, 0x4ff4f23f, 5, 5)
1790
+ bytes_ = comp_bytes + struct.pack('<2L2Q', sig, raw_crc, comp_len, raw_len)
1658
1791
  self.assertEqual(
1659
1792
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), True),
1660
- (0x4ff4f23f, 5, 5, 24),
1793
+ (raw_crc, comp_len, raw_len, 24),
1661
1794
  )
1662
1795
 
1663
1796
  # offset
1664
- bytes_ = b'dummy' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5)
1797
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1665
1798
  self.assertEqual(
1666
1799
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 1, len(bytes_), False),
1667
1800
  None,
1668
1801
  )
1669
1802
 
1670
- bytes_ = b'123dummy' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5)
1803
+ bytes_ = b'123' + comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1671
1804
  self.assertEqual(
1672
1805
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1673
1806
  None,
1674
1807
  )
1675
1808
  self.assertEqual(
1676
1809
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 3, len(bytes_), False),
1677
- (0x4ff4f23f, 5, 5, 16),
1810
+ (raw_crc, comp_len, raw_len, 16),
1678
1811
  )
1679
1812
 
1680
1813
  # end_offset
1681
- bytes_ = b'dummy' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5)
1814
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len)
1682
1815
  self.assertEqual(
1683
1816
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_) - 1, False),
1684
1817
  None,
1685
1818
  )
1686
1819
 
1687
- bytes_ = b'dummy' + struct.pack('<4L', SIG, 0x4ff4f23f, 5, 5) + b'123'
1820
+ bytes_ = comp_bytes + struct.pack('<4L', sig, raw_crc, comp_len, raw_len) + b'123'
1688
1821
  self.assertEqual(
1689
1822
  repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_) - 3, False),
1690
- (0x4ff4f23f, 5, 5, 16),
1823
+ (raw_crc, comp_len, raw_len, 16),
1824
+ )
1825
+ self.assertEqual(
1826
+ repacker._scan_data_descriptor(io.BytesIO(bytes_), 0, len(bytes_), False),
1827
+ (raw_crc, comp_len, raw_len, 16),
1691
1828
  )
1692
1829
 
1693
1830
  def test_scan_data_descriptor_no_sig(self):
1694
1831
  repacker = zipfile._ZipRepacker()
1695
1832
 
1833
+ raw_bytes = comp_bytes = b'dummy'
1834
+ raw_len = comp_len = len(raw_bytes)
1835
+ raw_crc = zipfile.crc32(raw_bytes)
1836
+
1696
1837
  # basic
1697
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1838
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1698
1839
  self.assertEqual(
1699
1840
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1700
- (0x4ff4f23f, 5, 5, 12),
1841
+ (raw_crc, comp_len, raw_len, 12),
1701
1842
  )
1702
1843
 
1703
1844
  # return None if compressed size not match
1704
- bytes_ = b'dumm' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1845
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len + 1, raw_len)
1705
1846
  self.assertEqual(
1706
1847
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1707
1848
  None,
1708
1849
  )
1709
1850
 
1710
- # zip64
1711
- bytes_ = b'dummy' + struct.pack('<L2Q', 0x4ff4f23f, 5, 5)
1851
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len - 1, raw_len)
1712
1852
  self.assertEqual(
1713
- repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), True),
1714
- (0x4ff4f23f, 5, 5, 20),
1853
+ repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1854
+ None,
1715
1855
  )
1716
1856
 
1717
- # offset
1718
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1857
+ bytes_ = b'1' + comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1719
1858
  self.assertEqual(
1720
- repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 1, len(bytes_), False),
1859
+ repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1721
1860
  None,
1722
1861
  )
1723
1862
 
1724
- bytes_ = b'123dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1863
+ bytes_ = comp_bytes[1:] + struct.pack('<3L', raw_crc, comp_len, raw_len)
1725
1864
  self.assertEqual(
1726
1865
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1727
1866
  None,
1728
1867
  )
1868
+
1869
+ # zip64
1870
+ bytes_ = comp_bytes + struct.pack('<L2Q', raw_crc, comp_len, raw_len)
1871
+ self.assertEqual(
1872
+ repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), True),
1873
+ (raw_crc, comp_len, raw_len, 20),
1874
+ )
1875
+
1876
+ # offset
1877
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1878
+ self.assertEqual(
1879
+ repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 1, len(bytes_), False),
1880
+ None,
1881
+ )
1882
+
1883
+ bytes_ = b'123' + comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1729
1884
  self.assertEqual(
1730
1885
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 3, len(bytes_), False),
1731
- (0x4ff4f23f, 5, 5, 12),
1886
+ (raw_crc, comp_len, raw_len, 12),
1732
1887
  )
1733
1888
 
1734
1889
  # end_offset
1735
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1890
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1736
1891
  self.assertEqual(
1737
1892
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_) - 1, False),
1738
1893
  None,
1739
1894
  )
1740
1895
 
1741
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5) + b'123'
1896
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len) + b'123'
1742
1897
  self.assertEqual(
1743
1898
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_) - 3, False),
1744
- (0x4ff4f23f, 5, 5, 12),
1899
+ (raw_crc, comp_len, raw_len, 12),
1900
+ )
1901
+ self.assertEqual(
1902
+ repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False),
1903
+ (raw_crc, comp_len, raw_len, 12),
1745
1904
  )
1746
1905
 
1747
1906
  # chunk_size
1748
- bytes_ = b'dummy' + struct.pack('<3L', 0x4ff4f23f, 5, 5)
1907
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1749
1908
  self.assertEqual(
1750
1909
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False, 12),
1751
- (0x4ff4f23f, 5, 5, 12),
1910
+ (raw_crc, comp_len, raw_len, 12),
1752
1911
  )
1753
1912
  self.assertEqual(
1754
1913
  repacker._scan_data_descriptor_no_sig(io.BytesIO(bytes_), 0, len(bytes_), False, 1),
1755
- (0x4ff4f23f, 5, 5, 12),
1914
+ (raw_crc, comp_len, raw_len, 12),
1756
1915
  )
1757
1916
 
1758
1917
  def test_scan_data_descriptor_no_sig_by_decompression_stored(self):
@@ -1781,44 +1940,40 @@ class ZipRepackerTests(unittest.TestCase):
1781
1940
  def _test_scan_data_descriptor_no_sig_by_decompression(self, method):
1782
1941
  repacker = zipfile._ZipRepacker()
1783
1942
 
1784
- compressor = zipfile._get_compressor(method)
1785
-
1786
1943
  raw_bytes = b'dummy'
1787
1944
  raw_len = len(raw_bytes)
1945
+ raw_crc = zipfile.crc32(raw_bytes)
1946
+
1947
+ compressor = zipfile._get_compressor(method)
1788
1948
  comp_bytes = compressor.compress(raw_bytes)
1789
1949
  comp_bytes += compressor.flush()
1790
1950
  comp_len = len(comp_bytes)
1791
1951
 
1792
1952
  # basic
1793
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)
1953
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1794
1954
  self.assertEqual(
1795
1955
  repacker._scan_data_descriptor_no_sig_by_decompression(
1796
1956
  io.BytesIO(bytes_), 0, len(bytes_), False, method),
1797
- (0x4ff4f23f, comp_len, raw_len, 12),
1957
+ (raw_crc, comp_len, raw_len, 12),
1798
1958
  )
1799
1959
 
1800
- # return None if insufficient data length
1801
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)
1960
+ # return None if data length < DD signature
1961
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1802
1962
  self.assertEqual(
1803
1963
  repacker._scan_data_descriptor_no_sig_by_decompression(
1804
- io.BytesIO(bytes_), 0, len(bytes_) - 1, False, method),
1964
+ io.BytesIO(bytes_), 0, 11, False, method),
1805
1965
  None,
1806
1966
  )
1807
1967
 
1808
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)[:-1]
1968
+ # return None if compressed size not match
1969
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len + 1, raw_len)
1809
1970
  self.assertEqual(
1810
1971
  repacker._scan_data_descriptor_no_sig_by_decompression(
1811
1972
  io.BytesIO(bytes_), 0, len(bytes_), False, method),
1812
1973
  None,
1813
1974
  )
1814
- self.assertEqual(
1815
- repacker._scan_data_descriptor_no_sig_by_decompression(
1816
- io.BytesIO(bytes_), 0, len(bytes_) + 1, False, method),
1817
- None,
1818
- )
1819
1975
 
1820
- # return None if compressed size not match
1821
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len - 1, raw_len)
1976
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len - 1, raw_len)
1822
1977
  self.assertEqual(
1823
1978
  repacker._scan_data_descriptor_no_sig_by_decompression(
1824
1979
  io.BytesIO(bytes_), 0, len(bytes_), False, method),
@@ -1826,41 +1981,46 @@ class ZipRepackerTests(unittest.TestCase):
1826
1981
  )
1827
1982
 
1828
1983
  # zip64
1829
- bytes_ = comp_bytes + struct.pack('<L2Q', 0x4ff4f23f, comp_len, raw_len)
1984
+ bytes_ = comp_bytes + struct.pack('<L2Q', raw_crc, comp_len, raw_len)
1830
1985
  self.assertEqual(
1831
1986
  repacker._scan_data_descriptor_no_sig_by_decompression(
1832
1987
  io.BytesIO(bytes_), 0, len(bytes_), True, method),
1833
- (0x4ff4f23f, comp_len, raw_len, 20),
1988
+ (raw_crc, comp_len, raw_len, 20),
1834
1989
  )
1835
1990
 
1836
1991
  # offset
1837
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)
1992
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1838
1993
  self.assertEqual(
1839
1994
  repacker._scan_data_descriptor_no_sig_by_decompression(
1840
1995
  io.BytesIO(bytes_), 1, len(bytes_), False, method),
1841
1996
  None,
1842
1997
  )
1843
1998
 
1844
- bytes_ = b'123' + comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)
1999
+ bytes_ = b'123' + comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1845
2000
  self.assertEqual(
1846
2001
  repacker._scan_data_descriptor_no_sig_by_decompression(
1847
2002
  io.BytesIO(bytes_), 3, len(bytes_), False, method),
1848
- (0x4ff4f23f, comp_len, raw_len, 12),
2003
+ (raw_crc, comp_len, raw_len, 12),
1849
2004
  )
1850
2005
 
1851
2006
  # end_offset
1852
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len)
2007
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len)
1853
2008
  self.assertEqual(
1854
2009
  repacker._scan_data_descriptor_no_sig_by_decompression(
1855
- io.BytesIO(bytes_), 0, len(bytes_) - 2, False, method),
2010
+ io.BytesIO(bytes_), 0, len(bytes_) - 1, False, method),
1856
2011
  None,
1857
2012
  )
1858
2013
 
1859
- bytes_ = comp_bytes + struct.pack('<3L', 0x4ff4f23f, comp_len, raw_len) + b'123'
2014
+ bytes_ = comp_bytes + struct.pack('<3L', raw_crc, comp_len, raw_len) + b'123'
1860
2015
  self.assertEqual(
1861
2016
  repacker._scan_data_descriptor_no_sig_by_decompression(
1862
- io.BytesIO(bytes_), 0, len(bytes_) - 2, False, method),
1863
- (0x4ff4f23f, comp_len, raw_len, 12),
2017
+ io.BytesIO(bytes_), 0, len(bytes_) - 3, False, method),
2018
+ (raw_crc, comp_len, raw_len, 12),
2019
+ )
2020
+ self.assertEqual(
2021
+ repacker._scan_data_descriptor_no_sig_by_decompression(
2022
+ io.BytesIO(bytes_), 0, len(bytes_), False, method),
2023
+ (raw_crc, comp_len, raw_len, 12),
1864
2024
  )
1865
2025
 
1866
2026
  def _test_scan_data_descriptor_no_sig_by_decompression_invalid(self, method):
@@ -18,16 +18,19 @@ except ImportError:
18
18
  # polyfill for Python < 3.12
19
19
  from test.test_zipfile import Unseekable, requires_zlib
20
20
 
21
- ENABLED_RESOURCES = set(os.environ.get("TEST_RESOURCES", "").split(","))
22
-
23
- def requires(resource_name):
21
+ def requires_resource(res):
22
+ if not hasattr(requires_resource, '_resources'):
23
+ requires_resource._resources = set(os.environ.get("TEST_RESOURCES", "").split(","))
24
24
  return unittest.skipUnless(
25
- resource_name in ENABLED_RESOURCES,
26
- f"requires resource: {resource_name!r} (set envvar TEST_RESOURCES)"
25
+ res in requires_resource._resources,
26
+ f"requires resource {res!r} in envvar TEST_RESOURCES"
27
27
  )
28
28
 
29
+ @requires_resource('extralargefile')
30
+ def setUpModule():
31
+ pass
32
+
29
33
 
30
- @requires('extralargefile')
31
34
  class TestRepack(unittest.TestCase):
32
35
  def setUp(self):
33
36
  # Create test data.
File without changes
File without changes
File without changes