Skip to content

Commit 8e65d2f

Browse files
authored
Explicitly store implicit parent directories in zip files (#640)
* Explicitly store implicit parent directories in zip files Tooling in the Java world (and likely elsewhere) has come to depend on all directories implicilty present as parent directories of files to be listed explicitly as a member of a ZIP file. * Address review comments
1 parent cde1177 commit 8e65d2f

File tree

2 files changed

+53
-18
lines changed

2 files changed

+53
-18
lines changed

pkg/private/zip/build_zip.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import zipfile
2121

2222
from pkg.private import build_info
23-
from pkg.private import helpers
2423
from pkg.private import manifest
2524

2625
ZIP_EPOCH = 315532800
@@ -98,7 +97,7 @@ def close(self):
9897
self.zip_file.close()
9998
self.zip_file = None
10099

101-
def make_zipinfo(self, path: str, mode: int):
100+
def make_zipinfo(self, path: str, mode: str):
102101
"""Create a Zipinfo.
103102
104103
Args:
@@ -121,19 +120,17 @@ def make_zipinfo(self, path: str, mode: int):
121120
entry_info.external_attr = f_mode << 16
122121
return entry_info
123122

124-
def add_manifest_entry(self, options, entry):
123+
def add_manifest_entry(self, entry):
125124
"""Add an entry to the zip file.
126125
127126
Args:
128-
options: parsed options
129127
zip_file: ZipFile to write to
130128
entry: manifest entry
131129
"""
132130
entry_type, dest, src, mode, user, group = entry
133131

134132
# Use the pkg_tar mode/owner remaping as a fallback
135-
non_abs_path = dest.strip('/')
136-
dst_path = _combine_paths(options.directory, non_abs_path)
133+
dst_path = dest.strip('/')
137134
if entry_type == manifest.ENTRY_IS_DIR and not dst_path.endswith('/'):
138135
dst_path += '/'
139136
entry_info = self.make_zipinfo(path=dst_path, mode=mode)
@@ -189,24 +186,47 @@ def add_tree(self, tree_top: str, destpath: str, mode: int):
189186
dest_dir = dest + rel_path_from_top + '/'
190187
else:
191188
dest_dir = dest
189+
to_write[dest_dir] = None
192190
for file in files:
193191
to_write[dest_dir + file] = root + '/' + file
194192

195193
for path in sorted(to_write.keys()):
196194
content_path = to_write[path]
197-
# If mode is unspecified, derive the mode from the file's mode.
198-
if mode is None:
199-
f_mode = 0o755 if os.access(content_path, os.X_OK) else 0o644
200-
else:
201-
f_mode = mode
202-
entry_info = self.make_zipinfo(path=path, mode=f_mode)
203-
entry_info.compress_type = zipfile.ZIP_DEFLATED
204-
if not content_path:
205-
self.zip_file.writestr(entry_info, '')
206-
else:
195+
if content_path:
196+
# If mode is unspecified, derive the mode from the file's mode.
197+
if mode is None:
198+
f_mode = "0o755" if os.access(content_path, os.X_OK) else self.default_mode
199+
else:
200+
f_mode = mode
201+
entry_info = self.make_zipinfo(path=path, mode=f_mode)
202+
entry_info.compress_type = zipfile.ZIP_DEFLATED
207203
with open(content_path, 'rb') as src:
208204
self.zip_file.writestr(entry_info, src.read())
205+
else:
206+
# Implicitly created directory
207+
dir_path = path
208+
if not dir_path.endswith('/'):
209+
dir_path += '/'
210+
entry_info = self.make_zipinfo(path=dir_path, mode="0o755")
211+
entry_info.compress_type = zipfile.ZIP_STORED
212+
# Set directory bits
213+
entry_info.external_attr |= (UNIX_DIR_BIT << 16) | MSDOS_DIR_BIT
214+
self.zip_file.writestr(entry_info, '')
209215

216+
def _load_manifest(prefix, manifest_fp):
217+
manifest_map = {}
218+
for entry in json.load(manifest_fp):
219+
entry[1] = _combine_paths(prefix, entry[1])
220+
manifest_map[entry[1]] = entry
221+
manifest_keys = list(manifest_map.keys())
222+
# Add all parent directories of entries that have not been added explicitly.
223+
for dest in manifest_keys:
224+
parent = dest
225+
for _ in range(dest.count("/")):
226+
parent, _, _ = parent.rpartition("/")
227+
if parent and parent not in manifest_map:
228+
manifest_map[parent] = [manifest.ENTRY_IS_DIR, parent, None, "0o755", None, None]
229+
return sorted(manifest_map.values(), key = lambda x: x[1])
210230

211231
def main(args):
212232
unix_ts = max(ZIP_EPOCH, args.timestamp)
@@ -218,11 +238,11 @@ def main(args):
218238
default_mode = int(args.mode, 8)
219239

220240
with open(args.manifest, 'r') as manifest_fp:
221-
manifest = json.load(manifest_fp)
241+
manifest = _load_manifest(args.directory, manifest_fp)
222242
with ZipWriter(
223243
args.output, time_stamp=ts, default_mode=default_mode) as zip_out:
224244
for entry in manifest:
225-
zip_out.add_manifest_entry(args, entry)
245+
zip_out.add_manifest_entry(entry)
226246

227247

228248
if __name__ == '__main__':

tests/zip/zip_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def test_basic(self):
3838
{"filename": "foodir/", "isdir": True, "attr": 0o711},
3939
{"filename": "hello.txt", "crc": HELLO_CRC},
4040
{"filename": "loremipsum.txt", "crc": LOREM_CRC},
41+
{"filename": "usr/", "isdir": True, "attr": 0o755},
42+
{"filename": "usr/bin/", "isdir": True, "attr": 0o755},
4143
{"filename": "usr/bin/foo", "isexe": True, "data": "/usr/local/foo/foo.real"},
4244
]
4345
else:
@@ -46,6 +48,8 @@ def test_basic(self):
4648
{"filename": "foodir/", "isdir": True, "attr": 0o711},
4749
{"filename": "hello.txt", "crc": HELLO_CRC},
4850
{"filename": "loremipsum.txt", "crc": LOREM_CRC},
51+
{"filename": "usr/", "isdir": True, "attr": 0o755},
52+
{"filename": "usr/bin/", "isdir": True, "attr": 0o755},
4953
{"filename": "usr/bin/foo", "isexe": True, "data": "/usr/local/foo/foo.real"},
5054
]
5155

@@ -68,13 +72,18 @@ def test_permissions(self):
6872

6973
def test_package_dir(self):
7074
self.assertZipFileContent("test_zip_package_dir0.zip", [
75+
{"filename": "abc/", "isdir": True, "attr": 0o755},
76+
{"filename": "abc/def/", "isdir": True, "attr": 0o755},
7177
{"filename": "abc/def/hello.txt", "crc": HELLO_CRC},
7278
{"filename": "abc/def/loremipsum.txt", "crc": LOREM_CRC},
7379
{"filename": "abc/def/mylink", "attr": 0o777},
7480
])
7581

7682
def test_package_dir_substitution(self):
7783
self.assertZipFileContent("test_zip_package_dir_substitution.zip", [
84+
{"filename": "level1/", "isdir": True, "attr": 0o755},
85+
{"filename": "level1/some_value/", "isdir": True, "attr": 0o755},
86+
{"filename": "level1/some_value/level3/", "isdir": True, "attr": 0o755},
7887
{"filename": "level1/some_value/level3/hello.txt", "crc": HELLO_CRC},
7988
{"filename": "level1/some_value/level3/loremipsum.txt", "crc": LOREM_CRC},
8089
])
@@ -96,13 +105,19 @@ def test_zip_strip_prefix_zipcontent(self):
96105

97106
def test_zip_strip_prefix_dot(self):
98107
self.assertZipFileContent("test-zip-strip_prefix-dot.zip", [
108+
{"filename": "zipcontent/", "isdir": True, "attr": 0o755},
99109
{"filename": "zipcontent/loremipsum.txt", "crc": LOREM_CRC},
100110
])
101111

102112
def test_zip_tree(self):
103113
self.assertZipFileContent("test_zip_tree.zip", [
114+
{"filename": "generate_tree/", "isdir": True, "attr": 0o755},
115+
{"filename": "generate_tree/a/", "isdir": True, "attr": 0o755},
104116
{"filename": "generate_tree/a/a"},
117+
{"filename": "generate_tree/a/b/", "isdir": True, "attr": 0o755},
105118
{"filename": "generate_tree/a/b/c"},
119+
{"filename": "generate_tree/b/", "isdir": True, "attr": 0o755},
120+
{"filename": "generate_tree/b/c/", "isdir": True, "attr": 0o755},
106121
{"filename": "generate_tree/b/c/d"},
107122
{"filename": "generate_tree/b/d"},
108123
{"filename": "generate_tree/b/e"},

0 commit comments

Comments
 (0)