1*6777b538SAndroid Build Coastguard Worker# Copyright 2023 The Chromium Authors 2*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 3*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file. 4*6777b538SAndroid Build Coastguard Worker"""Helper functions for dealing with .zip files.""" 5*6777b538SAndroid Build Coastguard Worker 6*6777b538SAndroid Build Coastguard Workerimport os 7*6777b538SAndroid Build Coastguard Workerimport pathlib 8*6777b538SAndroid Build Coastguard Workerimport posixpath 9*6777b538SAndroid Build Coastguard Workerimport stat 10*6777b538SAndroid Build Coastguard Workerimport time 11*6777b538SAndroid Build Coastguard Workerimport zipfile 12*6777b538SAndroid Build Coastguard Worker 13*6777b538SAndroid Build Coastguard Worker_FIXED_ZIP_HEADER_LEN = 30 14*6777b538SAndroid Build Coastguard Worker 15*6777b538SAndroid Build Coastguard Worker 16*6777b538SAndroid Build Coastguard Workerdef _set_alignment(zip_obj, zip_info, alignment): 17*6777b538SAndroid Build Coastguard Worker """Sets a ZipInfo's extra field such that the file will be aligned. 18*6777b538SAndroid Build Coastguard Worker 19*6777b538SAndroid Build Coastguard Worker Args: 20*6777b538SAndroid Build Coastguard Worker zip_obj: The ZipFile object that is being written. 21*6777b538SAndroid Build Coastguard Worker zip_info: The ZipInfo object about to be written. 22*6777b538SAndroid Build Coastguard Worker alignment: The amount of alignment (e.g. 4, or 4*1024). 23*6777b538SAndroid Build Coastguard Worker """ 24*6777b538SAndroid Build Coastguard Worker header_size = _FIXED_ZIP_HEADER_LEN + len(zip_info.filename) 25*6777b538SAndroid Build Coastguard Worker pos = zip_obj.fp.tell() + header_size 26*6777b538SAndroid Build Coastguard Worker padding_needed = (alignment - (pos % alignment)) % alignment 27*6777b538SAndroid Build Coastguard Worker 28*6777b538SAndroid Build Coastguard Worker # Python writes |extra| to both the local file header and the central 29*6777b538SAndroid Build Coastguard Worker # directory's file header. Android's zipalign tool writes only to the 30*6777b538SAndroid Build Coastguard Worker # local file header, so there is more overhead in using Python to align. 31*6777b538SAndroid Build Coastguard Worker zip_info.extra = b'\0' * padding_needed 32*6777b538SAndroid Build Coastguard Worker 33*6777b538SAndroid Build Coastguard Worker 34*6777b538SAndroid Build Coastguard Workerdef _hermetic_date_time(timestamp=None): 35*6777b538SAndroid Build Coastguard Worker if not timestamp: 36*6777b538SAndroid Build Coastguard Worker return (2001, 1, 1, 0, 0, 0) 37*6777b538SAndroid Build Coastguard Worker utc_time = time.gmtime(timestamp) 38*6777b538SAndroid Build Coastguard Worker return (utc_time.tm_year, utc_time.tm_mon, utc_time.tm_mday, utc_time.tm_hour, 39*6777b538SAndroid Build Coastguard Worker utc_time.tm_min, utc_time.tm_sec) 40*6777b538SAndroid Build Coastguard Worker 41*6777b538SAndroid Build Coastguard Worker 42*6777b538SAndroid Build Coastguard Workerdef add_to_zip_hermetic(zip_file, 43*6777b538SAndroid Build Coastguard Worker zip_path, 44*6777b538SAndroid Build Coastguard Worker *, 45*6777b538SAndroid Build Coastguard Worker src_path=None, 46*6777b538SAndroid Build Coastguard Worker data=None, 47*6777b538SAndroid Build Coastguard Worker compress=None, 48*6777b538SAndroid Build Coastguard Worker alignment=None, 49*6777b538SAndroid Build Coastguard Worker timestamp=None): 50*6777b538SAndroid Build Coastguard Worker """Adds a file to the given ZipFile with a hard-coded modified time. 51*6777b538SAndroid Build Coastguard Worker 52*6777b538SAndroid Build Coastguard Worker Args: 53*6777b538SAndroid Build Coastguard Worker zip_file: ZipFile instance to add the file to. 54*6777b538SAndroid Build Coastguard Worker zip_path: Destination path within the zip file (or ZipInfo instance). 55*6777b538SAndroid Build Coastguard Worker src_path: Path of the source file. Mutually exclusive with |data|. 56*6777b538SAndroid Build Coastguard Worker data: File data as a string. 57*6777b538SAndroid Build Coastguard Worker compress: Whether to enable compression. Default is taken from ZipFile 58*6777b538SAndroid Build Coastguard Worker constructor. 59*6777b538SAndroid Build Coastguard Worker alignment: If set, align the data of the entry to this many bytes. 60*6777b538SAndroid Build Coastguard Worker timestamp: The last modification date and time for the archive member. 61*6777b538SAndroid Build Coastguard Worker """ 62*6777b538SAndroid Build Coastguard Worker assert (src_path is None) != (data is None), ( 63*6777b538SAndroid Build Coastguard Worker '|src_path| and |data| are mutually exclusive.') 64*6777b538SAndroid Build Coastguard Worker if isinstance(zip_path, zipfile.ZipInfo): 65*6777b538SAndroid Build Coastguard Worker zipinfo = zip_path 66*6777b538SAndroid Build Coastguard Worker zip_path = zipinfo.filename 67*6777b538SAndroid Build Coastguard Worker else: 68*6777b538SAndroid Build Coastguard Worker zipinfo = zipfile.ZipInfo(filename=zip_path) 69*6777b538SAndroid Build Coastguard Worker zipinfo.external_attr = 0o644 << 16 70*6777b538SAndroid Build Coastguard Worker 71*6777b538SAndroid Build Coastguard Worker zipinfo.date_time = _hermetic_date_time(timestamp) 72*6777b538SAndroid Build Coastguard Worker 73*6777b538SAndroid Build Coastguard Worker if alignment: 74*6777b538SAndroid Build Coastguard Worker _set_alignment(zip_file, zipinfo, alignment) 75*6777b538SAndroid Build Coastguard Worker 76*6777b538SAndroid Build Coastguard Worker # Filenames can contain backslashes, but it is more likely that we've 77*6777b538SAndroid Build Coastguard Worker # forgotten to use forward slashes as a directory separator. 78*6777b538SAndroid Build Coastguard Worker assert '\\' not in zip_path, 'zip_path should not contain \\: ' + zip_path 79*6777b538SAndroid Build Coastguard Worker assert not posixpath.isabs(zip_path), 'Absolute zip path: ' + zip_path 80*6777b538SAndroid Build Coastguard Worker assert not zip_path.startswith('..'), 'Should not start with ..: ' + zip_path 81*6777b538SAndroid Build Coastguard Worker assert posixpath.normpath(zip_path) == zip_path, ( 82*6777b538SAndroid Build Coastguard Worker f'Non-canonical zip_path: {zip_path} vs: {posixpath.normpath(zip_path)}') 83*6777b538SAndroid Build Coastguard Worker assert zip_path not in zip_file.namelist(), ( 84*6777b538SAndroid Build Coastguard Worker 'Tried to add a duplicate zip entry: ' + zip_path) 85*6777b538SAndroid Build Coastguard Worker 86*6777b538SAndroid Build Coastguard Worker if src_path and os.path.islink(src_path): 87*6777b538SAndroid Build Coastguard Worker zipinfo.external_attr |= stat.S_IFLNK << 16 # mark as a symlink 88*6777b538SAndroid Build Coastguard Worker zip_file.writestr(zipinfo, os.readlink(src_path)) 89*6777b538SAndroid Build Coastguard Worker return 90*6777b538SAndroid Build Coastguard Worker 91*6777b538SAndroid Build Coastguard Worker # Maintain the executable bit. 92*6777b538SAndroid Build Coastguard Worker if src_path: 93*6777b538SAndroid Build Coastguard Worker st = os.stat(src_path) 94*6777b538SAndroid Build Coastguard Worker for mode in (stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH): 95*6777b538SAndroid Build Coastguard Worker if st.st_mode & mode: 96*6777b538SAndroid Build Coastguard Worker zipinfo.external_attr |= mode << 16 97*6777b538SAndroid Build Coastguard Worker 98*6777b538SAndroid Build Coastguard Worker if src_path: 99*6777b538SAndroid Build Coastguard Worker with open(src_path, 'rb') as f: 100*6777b538SAndroid Build Coastguard Worker data = f.read() 101*6777b538SAndroid Build Coastguard Worker 102*6777b538SAndroid Build Coastguard Worker # zipfile will deflate even when it makes the file bigger. To avoid 103*6777b538SAndroid Build Coastguard Worker # growing files, disable compression at an arbitrary cut off point. 104*6777b538SAndroid Build Coastguard Worker if len(data) < 16: 105*6777b538SAndroid Build Coastguard Worker compress = False 106*6777b538SAndroid Build Coastguard Worker 107*6777b538SAndroid Build Coastguard Worker # None converts to ZIP_STORED, when passed explicitly rather than the 108*6777b538SAndroid Build Coastguard Worker # default passed to the ZipFile constructor. 109*6777b538SAndroid Build Coastguard Worker compress_type = zip_file.compression 110*6777b538SAndroid Build Coastguard Worker if compress is not None: 111*6777b538SAndroid Build Coastguard Worker compress_type = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED 112*6777b538SAndroid Build Coastguard Worker zip_file.writestr(zipinfo, data, compress_type) 113*6777b538SAndroid Build Coastguard Worker 114*6777b538SAndroid Build Coastguard Worker 115*6777b538SAndroid Build Coastguard Workerdef add_files_to_zip(inputs, 116*6777b538SAndroid Build Coastguard Worker output, 117*6777b538SAndroid Build Coastguard Worker *, 118*6777b538SAndroid Build Coastguard Worker base_dir=None, 119*6777b538SAndroid Build Coastguard Worker compress=None, 120*6777b538SAndroid Build Coastguard Worker zip_prefix_path=None, 121*6777b538SAndroid Build Coastguard Worker timestamp=None): 122*6777b538SAndroid Build Coastguard Worker """Creates a zip file from a list of files. 123*6777b538SAndroid Build Coastguard Worker 124*6777b538SAndroid Build Coastguard Worker Args: 125*6777b538SAndroid Build Coastguard Worker inputs: A list of paths to zip, or a list of (zip_path, fs_path) tuples. 126*6777b538SAndroid Build Coastguard Worker output: Path, fileobj, or ZipFile instance to add files to. 127*6777b538SAndroid Build Coastguard Worker base_dir: Prefix to strip from inputs. 128*6777b538SAndroid Build Coastguard Worker compress: Whether to compress 129*6777b538SAndroid Build Coastguard Worker zip_prefix_path: Path prepended to file path in zip file. 130*6777b538SAndroid Build Coastguard Worker timestamp: Unix timestamp to use for files in the archive. 131*6777b538SAndroid Build Coastguard Worker """ 132*6777b538SAndroid Build Coastguard Worker if base_dir is None: 133*6777b538SAndroid Build Coastguard Worker base_dir = '.' 134*6777b538SAndroid Build Coastguard Worker input_tuples = [] 135*6777b538SAndroid Build Coastguard Worker for tup in inputs: 136*6777b538SAndroid Build Coastguard Worker if isinstance(tup, str): 137*6777b538SAndroid Build Coastguard Worker src_path = tup 138*6777b538SAndroid Build Coastguard Worker zip_path = os.path.relpath(src_path, base_dir) 139*6777b538SAndroid Build Coastguard Worker # Zip files always use / as path separator. 140*6777b538SAndroid Build Coastguard Worker if os.path.sep != posixpath.sep: 141*6777b538SAndroid Build Coastguard Worker zip_path = str(pathlib.Path(zip_path).as_posix()) 142*6777b538SAndroid Build Coastguard Worker tup = (zip_path, src_path) 143*6777b538SAndroid Build Coastguard Worker input_tuples.append(tup) 144*6777b538SAndroid Build Coastguard Worker 145*6777b538SAndroid Build Coastguard Worker # Sort by zip path to ensure stable zip ordering. 146*6777b538SAndroid Build Coastguard Worker input_tuples.sort(key=lambda tup: tup[0]) 147*6777b538SAndroid Build Coastguard Worker 148*6777b538SAndroid Build Coastguard Worker out_zip = output 149*6777b538SAndroid Build Coastguard Worker if not isinstance(output, zipfile.ZipFile): 150*6777b538SAndroid Build Coastguard Worker out_zip = zipfile.ZipFile(output, 'w') 151*6777b538SAndroid Build Coastguard Worker 152*6777b538SAndroid Build Coastguard Worker try: 153*6777b538SAndroid Build Coastguard Worker for zip_path, fs_path in input_tuples: 154*6777b538SAndroid Build Coastguard Worker if zip_prefix_path: 155*6777b538SAndroid Build Coastguard Worker zip_path = posixpath.join(zip_prefix_path, zip_path) 156*6777b538SAndroid Build Coastguard Worker add_to_zip_hermetic(out_zip, 157*6777b538SAndroid Build Coastguard Worker zip_path, 158*6777b538SAndroid Build Coastguard Worker src_path=fs_path, 159*6777b538SAndroid Build Coastguard Worker compress=compress, 160*6777b538SAndroid Build Coastguard Worker timestamp=timestamp) 161*6777b538SAndroid Build Coastguard Worker finally: 162*6777b538SAndroid Build Coastguard Worker if output is not out_zip: 163*6777b538SAndroid Build Coastguard Worker out_zip.close() 164*6777b538SAndroid Build Coastguard Worker 165*6777b538SAndroid Build Coastguard Worker 166*6777b538SAndroid Build Coastguard Workerdef zip_directory(output, base_dir, **kwargs): 167*6777b538SAndroid Build Coastguard Worker """Zips all files in the given directory.""" 168*6777b538SAndroid Build Coastguard Worker inputs = [] 169*6777b538SAndroid Build Coastguard Worker for root, _, files in os.walk(base_dir): 170*6777b538SAndroid Build Coastguard Worker for f in files: 171*6777b538SAndroid Build Coastguard Worker inputs.append(os.path.join(root, f)) 172*6777b538SAndroid Build Coastguard Worker 173*6777b538SAndroid Build Coastguard Worker add_files_to_zip(inputs, output, base_dir=base_dir, **kwargs) 174*6777b538SAndroid Build Coastguard Worker 175*6777b538SAndroid Build Coastguard Worker 176*6777b538SAndroid Build Coastguard Workerdef merge_zips(output, input_zips, path_transform=None, compress=None): 177*6777b538SAndroid Build Coastguard Worker """Combines all files from |input_zips| into |output|. 178*6777b538SAndroid Build Coastguard Worker 179*6777b538SAndroid Build Coastguard Worker Args: 180*6777b538SAndroid Build Coastguard Worker output: Path, fileobj, or ZipFile instance to add files to. 181*6777b538SAndroid Build Coastguard Worker input_zips: Iterable of paths to zip files to merge. 182*6777b538SAndroid Build Coastguard Worker path_transform: Called for each entry path. Returns a new path, or None to 183*6777b538SAndroid Build Coastguard Worker skip the file. 184*6777b538SAndroid Build Coastguard Worker compress: Overrides compression setting from origin zip entries. 185*6777b538SAndroid Build Coastguard Worker """ 186*6777b538SAndroid Build Coastguard Worker assert not isinstance(input_zips, str) # Easy mistake to make. 187*6777b538SAndroid Build Coastguard Worker if isinstance(output, zipfile.ZipFile): 188*6777b538SAndroid Build Coastguard Worker out_zip = output 189*6777b538SAndroid Build Coastguard Worker out_filename = output.filename 190*6777b538SAndroid Build Coastguard Worker else: 191*6777b538SAndroid Build Coastguard Worker assert isinstance(output, str), 'Was: ' + repr(output) 192*6777b538SAndroid Build Coastguard Worker out_zip = zipfile.ZipFile(output, 'w') 193*6777b538SAndroid Build Coastguard Worker out_filename = output 194*6777b538SAndroid Build Coastguard Worker 195*6777b538SAndroid Build Coastguard Worker # Include paths in the existing zip here to avoid adding duplicate files. 196*6777b538SAndroid Build Coastguard Worker crc_by_name = {i.filename: (out_filename, i.CRC) for i in out_zip.infolist()} 197*6777b538SAndroid Build Coastguard Worker 198*6777b538SAndroid Build Coastguard Worker try: 199*6777b538SAndroid Build Coastguard Worker for in_file in input_zips: 200*6777b538SAndroid Build Coastguard Worker with zipfile.ZipFile(in_file, 'r') as in_zip: 201*6777b538SAndroid Build Coastguard Worker for info in in_zip.infolist(): 202*6777b538SAndroid Build Coastguard Worker # Ignore directories. 203*6777b538SAndroid Build Coastguard Worker if info.filename[-1] == '/': 204*6777b538SAndroid Build Coastguard Worker continue 205*6777b538SAndroid Build Coastguard Worker if path_transform: 206*6777b538SAndroid Build Coastguard Worker dst_name = path_transform(info.filename) 207*6777b538SAndroid Build Coastguard Worker if dst_name is None: 208*6777b538SAndroid Build Coastguard Worker continue 209*6777b538SAndroid Build Coastguard Worker else: 210*6777b538SAndroid Build Coastguard Worker dst_name = info.filename 211*6777b538SAndroid Build Coastguard Worker 212*6777b538SAndroid Build Coastguard Worker data = in_zip.read(info) 213*6777b538SAndroid Build Coastguard Worker 214*6777b538SAndroid Build Coastguard Worker # If there's a duplicate file, ensure contents is the same and skip 215*6777b538SAndroid Build Coastguard Worker # adding it multiple times. 216*6777b538SAndroid Build Coastguard Worker if dst_name in crc_by_name: 217*6777b538SAndroid Build Coastguard Worker orig_filename, orig_crc = crc_by_name[dst_name] 218*6777b538SAndroid Build Coastguard Worker new_crc = zipfile.crc32(data) 219*6777b538SAndroid Build Coastguard Worker if new_crc == orig_crc: 220*6777b538SAndroid Build Coastguard Worker continue 221*6777b538SAndroid Build Coastguard Worker msg = f"""File appeared in multiple inputs with differing contents. 222*6777b538SAndroid Build Coastguard WorkerFile: {dst_name} 223*6777b538SAndroid Build Coastguard WorkerInput1: {orig_filename} 224*6777b538SAndroid Build Coastguard WorkerInput2: {in_file}""" 225*6777b538SAndroid Build Coastguard Worker raise Exception(msg) 226*6777b538SAndroid Build Coastguard Worker 227*6777b538SAndroid Build Coastguard Worker if compress is not None: 228*6777b538SAndroid Build Coastguard Worker compress_entry = compress 229*6777b538SAndroid Build Coastguard Worker else: 230*6777b538SAndroid Build Coastguard Worker compress_entry = info.compress_type != zipfile.ZIP_STORED 231*6777b538SAndroid Build Coastguard Worker add_to_zip_hermetic(out_zip, 232*6777b538SAndroid Build Coastguard Worker dst_name, 233*6777b538SAndroid Build Coastguard Worker data=data, 234*6777b538SAndroid Build Coastguard Worker compress=compress_entry) 235*6777b538SAndroid Build Coastguard Worker crc_by_name[dst_name] = (in_file, out_zip.getinfo(dst_name).CRC) 236*6777b538SAndroid Build Coastguard Worker finally: 237*6777b538SAndroid Build Coastguard Worker if output is not out_zip: 238*6777b538SAndroid Build Coastguard Worker out_zip.close() 239