# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities to process compresssed files."""
import contextlib
import logging
import os
import pathlib
import re
import shutil
import struct
import tempfile
import zipfile
class _ApkFileManager:
def __init__(self, temp_dir):
self._temp_dir = pathlib.Path(temp_dir)
self._subdir_by_apks_path = {}
self._infolist_by_path = {}
def _MapPath(self, path):
# Use numbered subdirectories for uniqueness.
# Suffix with basename(path) for readability.
default = '-'.join(
[str(len(self._subdir_by_apks_path)),
os.path.basename(path)])
return self._temp_dir / self._subdir_by_apks_path.setdefault(path, default)
def InfoList(self, path):
"""Returns zipfile.ZipFile(path).infolist()."""
ret = self._infolist_by_path.get(path)
if ret is None:
with zipfile.ZipFile(path) as z:
ret = z.infolist()
self._infolist_by_path[path] = ret
return ret
def SplitPath(self, minimal_apks_path, split_name):
"""Returns the path to the apk split extracted by ExtractSplits.
Args:
minimal_apks_path: The .apks file that was passed to ExtractSplits().
split_name: Then name of the split.
Returns:
Path to the extracted .apk file.
"""
subdir = self._subdir_by_apks_path[minimal_apks_path]
if '-' in split_name:
name = f'{split_name}.apk'
else:
name = f'{split_name}-master.apk'
return self._temp_dir / subdir / 'splits' / name
def ExtractSplits(self, minimal_apks_path):
"""Extracts the master splits in the given .apks file.
Returns:
List of split names, with "base" always appearing first.
"""
dest = self._MapPath(minimal_apks_path)
split_names = []
logging.debug('Extracting %s', minimal_apks_path)
with zipfile.ZipFile(minimal_apks_path) as z:
for filename in z.namelist():
# E.g.:
# splits/base-master.apk
# splits/base-hi.apk
# splits/vr-master.apk
# splits/vr-en.apk
m = re.match(r'splits/(.*)-master\.apk', filename)
if m:
split_names.append(m.group(1))
z.extract(filename, dest)
# Also analyze -hi locale splits, since resource_sizes.py does this.
m = re.match(r'splits/(.*-hi)\.apk', filename)
if m:
split_names.append(m.group(1))
z.extract(filename, dest)
logging.debug('Extracting %s (done)', minimal_apks_path)
# Make "base" comes first since that's the main chunk of work.
# Also so that --abi-filter detection looks at it first.
return sorted(split_names, key=lambda x: (not x.startswith('base'), x))
@contextlib.contextmanager
def ApkFileManager():
"""Context manager that extracts apk splits to a temp dir."""
# Cannot use tempfile.TemporaryDirectory() here because our use of
# multiprocessing results in __del__ methods being called in forked processes.
temp_dir = tempfile.mkdtemp(suffix='-supersize')
zip_files = _ApkFileManager(temp_dir)
yield zip_files
shutil.rmtree(temp_dir)
@contextlib.contextmanager
def UnzipToTemp(zip_path, inner_path):
"""Extract a |inner_path| from a |zip_path| file to an auto-deleted temp file.
Args:
zip_path: Path to the zip file.
inner_path: Path to the file within |zip_path| to extract.
Yields:
The path of the temp created (and auto-deleted when context exits).
"""
try:
logging.debug('Extracting %s', inner_path)
_, suffix = os.path.splitext(inner_path)
# Can't use NamedTemporaryFile() because it deletes via __del__, which will
# trigger in both this and the fork()'ed processes.
fd, temp_file = tempfile.mkstemp(suffix=suffix)
with zipfile.ZipFile(zip_path) as z:
os.write(fd, z.read(inner_path))
os.close(fd)
logging.debug('Extracting %s (done)', inner_path)
yield temp_file
finally:
os.unlink(temp_file)
def ReadZipInfoExtraFieldLength(zip_file, zip_info):
"""Reads the value of |extraLength| from |zip_info|'s local file header.
|zip_info| has an |extra| field, but it's read from the central directory.
Android's zipalign tool sets the extra field only in local file headers.
"""
# Refer to https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
zip_file.fp.seek(zip_info.header_offset + 28)
return struct.unpack('<H', zip_file.fp.read(2))[0]
def MeasureApkSignatureBlock(zip_file):
"""Measures the size of the v2 / v3 signing block.
Refer to: https://source.android.com/security/apksigning/v2
"""
# Seek to "end of central directory" struct.
eocd_offset_from_end = -22 - len(zip_file.comment)
zip_file.fp.seek(eocd_offset_from_end, os.SEEK_END)
assert zip_file.fp.read(4) == b'PK\005\006', (
'failed to find end-of-central-directory')
# Read out the "start of central directory" offset.
zip_file.fp.seek(eocd_offset_from_end + 16, os.SEEK_END)
start_of_central_directory = struct.unpack('<I', zip_file.fp.read(4))[0]
# Compute the offset after the last zip entry.
last_info = max(zip_file.infolist(), key=lambda i: i.header_offset)
last_header_size = (30 + len(last_info.filename) +
ReadZipInfoExtraFieldLength(zip_file, last_info))
end_of_last_file = (last_info.header_offset + last_header_size +
last_info.compress_size)
return start_of_central_directory - end_of_last_file