# 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('