| # Copyright 2020 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Functions that modify resources in protobuf format. |
| |
| Format reference: |
| https://cs.android.com/android/platform/superproject/+/master:frameworks/base/tools/aapt2/Resources.proto |
| """ |
| |
| import logging |
| import os |
| import struct |
| import sys |
| import zipfile |
| |
| from util import build_utils |
| from util import resource_utils |
| |
| sys.path[1:1] = [ |
| # `Resources_pb2` module imports `descriptor`, which imports `six`. |
| os.path.join(build_utils.DIR_SOURCE_ROOT, 'third_party', 'six', 'src'), |
| # Make sure the pb2 files are able to import google.protobuf |
| os.path.join(build_utils.DIR_SOURCE_ROOT, 'third_party', 'protobuf', |
| 'python'), |
| ] |
| |
| from proto import Resources_pb2 |
| |
| # First bytes in an .flat.arsc file. |
| # uint32: Magic ("ARSC"), version (1), num_entries (1), type (0) |
| _FLAT_ARSC_HEADER = b'AAPT\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00' |
| |
| # The package ID hardcoded for shared libraries. See |
| # _HardcodeSharedLibraryDynamicAttributes() for more details. If this value |
| # changes make sure to change REQUIRED_PACKAGE_IDENTIFIER in WebLayerImpl.java. |
| SHARED_LIBRARY_HARDCODED_ID = 36 |
| |
| |
| def _ProcessZip(zip_path, process_func): |
| """Filters a .zip file via: new_bytes = process_func(filename, data).""" |
| has_changes = False |
| zip_entries = [] |
| with zipfile.ZipFile(zip_path) as src_zip: |
| for info in src_zip.infolist(): |
| data = src_zip.read(info) |
| new_data = process_func(info.filename, data) |
| if new_data is not data: |
| has_changes = True |
| data = new_data |
| zip_entries.append((info, data)) |
| |
| # Overwrite the original zip file. |
| if has_changes: |
| with zipfile.ZipFile(zip_path, 'w') as f: |
| for info, data in zip_entries: |
| f.writestr(info, data) |
| |
| |
| def _ProcessProtoItem(item): |
| if not item.HasField('ref'): |
| return |
| |
| # If this is a dynamic attribute (type ATTRIBUTE, package ID 0), hardcode |
| # the package to SHARED_LIBRARY_HARDCODED_ID. |
| if item.ref.type == Resources_pb2.Reference.ATTRIBUTE and not (item.ref.id |
| & 0xff000000): |
| item.ref.id |= (0x01000000 * SHARED_LIBRARY_HARDCODED_ID) |
| item.ref.ClearField('is_dynamic') |
| |
| |
| def _ProcessProtoValue(value): |
| if value.HasField('item'): |
| _ProcessProtoItem(value.item) |
| return |
| |
| compound_value = value.compound_value |
| if compound_value.HasField('style'): |
| for entry in compound_value.style.entry: |
| _ProcessProtoItem(entry.item) |
| elif compound_value.HasField('array'): |
| for element in compound_value.array.element: |
| _ProcessProtoItem(element.item) |
| elif compound_value.HasField('plural'): |
| for entry in compound_value.plural.entry: |
| _ProcessProtoItem(entry.item) |
| |
| |
| def _ProcessProtoXmlNode(xml_node): |
| if not xml_node.HasField('element'): |
| return |
| |
| for attribute in xml_node.element.attribute: |
| _ProcessProtoItem(attribute.compiled_item) |
| |
| for child in xml_node.element.child: |
| _ProcessProtoXmlNode(child) |
| |
| |
| def _SplitLocaleResourceType(_type, allowed_resource_names): |
| """Splits locale specific resources out of |_type| and returns them. |
| |
| Any locale specific resources will be removed from |_type|, and a new |
| Resources_pb2.Type value will be returned which contains those resources. |
| |
| Args: |
| _type: A Resources_pb2.Type value |
| allowed_resource_names: Names of locale resources that should be kept in the |
| main type. |
| """ |
| locale_entries = [] |
| for entry in _type.entry: |
| if entry.name in allowed_resource_names: |
| continue |
| |
| # First collect all resources values with a locale set. |
| config_values_with_locale = [] |
| for config_value in entry.config_value: |
| if config_value.config.locale: |
| config_values_with_locale.append(config_value) |
| |
| if config_values_with_locale: |
| # Remove the locale resources from the original entry |
| for value in config_values_with_locale: |
| entry.config_value.remove(value) |
| |
| # Add locale resources to a new Entry, and save for later. |
| locale_entry = Resources_pb2.Entry() |
| locale_entry.CopyFrom(entry) |
| del locale_entry.config_value[:] |
| locale_entry.config_value.extend(config_values_with_locale) |
| locale_entries.append(locale_entry) |
| |
| if not locale_entries: |
| return None |
| |
| # Copy the original type and replace the entries with |locale_entries|. |
| locale_type = Resources_pb2.Type() |
| locale_type.CopyFrom(_type) |
| del locale_type.entry[:] |
| locale_type.entry.extend(locale_entries) |
| return locale_type |
| |
| |
| def _HardcodeInTable(table, is_bundle_module, shared_resources_allowlist): |
| translations_package = None |
| if is_bundle_module: |
| # A separate top level package will be added to the resources, which |
| # contains only locale specific resources. The package ID of the locale |
| # resources is hardcoded to SHARED_LIBRARY_HARDCODED_ID. This causes |
| # resources in locale splits to all get assigned |
| # SHARED_LIBRARY_HARDCODED_ID as their package ID, which prevents a bug |
| # in shared library bundles where each split APK gets a separate dynamic |
| # ID, and cannot be accessed by the main APK. |
| translations_package = Resources_pb2.Package() |
| translations_package.package_id.id = SHARED_LIBRARY_HARDCODED_ID |
| translations_package.package_name = (table.package[0].package_name + |
| '_translations') |
| |
| # These resources are allowed in the base resources, since they are needed |
| # by WebView. |
| allowed_resource_names = set() |
| if shared_resources_allowlist: |
| allowed_resource_names = set( |
| resource_utils.GetRTxtStringResourceNames(shared_resources_allowlist)) |
| |
| for package in table.package: |
| for _type in package.type: |
| for entry in _type.entry: |
| for config_value in entry.config_value: |
| _ProcessProtoValue(config_value.value) |
| |
| if translations_package is not None: |
| locale_type = _SplitLocaleResourceType(_type, allowed_resource_names) |
| if locale_type: |
| translations_package.type.add().CopyFrom(locale_type) |
| |
| if translations_package is not None: |
| table.package.add().CopyFrom(translations_package) |
| |
| |
| def HardcodeSharedLibraryDynamicAttributes(zip_path, |
| is_bundle_module, |
| shared_resources_allowlist=None): |
| """Hardcodes the package IDs of dynamic attributes and locale resources. |
| |
| Hardcoding dynamic attribute package IDs is a workaround for b/147674078, |
| which affects Android versions pre-N. Hardcoding locale resource package IDs |
| is a workaround for b/155437035, which affects resources built with |
| --shared-lib on all Android versions |
| |
| Args: |
| zip_path: Path to proto APK file. |
| is_bundle_module: True for bundle modules. |
| shared_resources_allowlist: Set of resource names to not extract out of the |
| main package. |
| """ |
| |
| def process_func(filename, data): |
| if filename == 'resources.pb': |
| table = Resources_pb2.ResourceTable() |
| table.ParseFromString(data) |
| _HardcodeInTable(table, is_bundle_module, shared_resources_allowlist) |
| data = table.SerializeToString() |
| elif filename.endswith('.xml') and not filename.startswith('res/raw'): |
| xml_node = Resources_pb2.XmlNode() |
| xml_node.ParseFromString(data) |
| _ProcessProtoXmlNode(xml_node) |
| data = xml_node.SerializeToString() |
| return data |
| |
| _ProcessZip(zip_path, process_func) |
| |
| |
| class _ResourceStripper(object): |
| def __init__(self, partial_path, keep_predicate): |
| self.partial_path = partial_path |
| self.keep_predicate = keep_predicate |
| self._has_changes = False |
| |
| @staticmethod |
| def _IterStyles(entry): |
| for config_value in entry.config_value: |
| value = config_value.value |
| if value.HasField('compound_value'): |
| compound_value = value.compound_value |
| if compound_value.HasField('style'): |
| yield compound_value.style |
| |
| def _StripStyles(self, entry, type_and_name): |
| # Strip style entries that refer to attributes that have been stripped. |
| for style in self._IterStyles(entry): |
| entries = style.entry |
| new_entries = [] |
| for entry in entries: |
| full_name = '{}/{}'.format(type_and_name, entry.key.name) |
| if not self.keep_predicate(full_name): |
| logging.debug('Stripped %s/%s', self.partial_path, full_name) |
| else: |
| new_entries.append(entry) |
| |
| if len(new_entries) != len(entries): |
| self._has_changes = True |
| del entries[:] |
| entries.extend(new_entries) |
| |
| def _StripEntries(self, entries, type_name): |
| new_entries = [] |
| for entry in entries: |
| type_and_name = '{}/{}'.format(type_name, entry.name) |
| if not self.keep_predicate(type_and_name): |
| logging.debug('Stripped %s/%s', self.partial_path, type_and_name) |
| else: |
| new_entries.append(entry) |
| self._StripStyles(entry, type_and_name) |
| |
| if len(new_entries) != len(entries): |
| self._has_changes = True |
| del entries[:] |
| entries.extend(new_entries) |
| |
| def StripTable(self, table): |
| self._has_changes = False |
| for package in table.package: |
| for _type in package.type: |
| self._StripEntries(_type.entry, _type.name) |
| return self._has_changes |
| |
| |
| def _TableFromFlatBytes(data): |
| # https://cs.android.com/android/platform/superproject/+/master:frameworks/base/tools/aapt2/format/Container.cpp |
| size_idx = len(_FLAT_ARSC_HEADER) |
| proto_idx = size_idx + 8 |
| if data[:size_idx] != _FLAT_ARSC_HEADER: |
| raise Exception('Error parsing {} in {}'.format(info.filename, zip_path)) |
| # Size is stored as uint64. |
| size = struct.unpack('<Q', data[size_idx:proto_idx])[0] |
| table = Resources_pb2.ResourceTable() |
| proto_bytes = data[proto_idx:proto_idx + size] |
| table.ParseFromString(proto_bytes) |
| return table |
| |
| |
| def _FlatBytesFromTable(table): |
| proto_bytes = table.SerializeToString() |
| size = struct.pack('<Q', len(proto_bytes)) |
| overage = len(proto_bytes) % 4 |
| padding = b'\0' * (4 - overage) if overage else b'' |
| return b''.join((_FLAT_ARSC_HEADER, size, proto_bytes, padding)) |
| |
| |
| def StripUnwantedResources(partial_path, keep_predicate): |
| """Removes resources from .arsc.flat files inside of a .zip. |
| |
| Args: |
| partial_path: Path to a .zip containing .arsc.flat entries |
| keep_predicate: Given "$partial_path/$res_type/$res_name", returns |
| whether to keep the resource. |
| """ |
| stripper = _ResourceStripper(partial_path, keep_predicate) |
| |
| def process_file(filename, data): |
| if filename.endswith('.arsc.flat'): |
| table = _TableFromFlatBytes(data) |
| if stripper.StripTable(table): |
| data = _FlatBytesFromTable(table) |
| return data |
| |
| _ProcessZip(partial_path, process_file) |