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