blob: 34cf26d2ebaff63bada6c37449bc459266760080 [file] [log] [blame]
# 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.
"""Functions that modify resources in protobuf format.
Format reference:
https://cs.android.com/search?q=f:aapt2.*Resources.proto
"""
import logging
import os
import struct
import sys
import zipfile
from util import build_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'
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 _ProcessProtoXmlNode(xml_node):
if not xml_node.HasField('element'):
return
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
class _ResourceStripper:
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 e in entries:
full_name = '{}/{}'.format(type_and_name, e.key.name)
if not self.keep_predicate(full_name):
logging.debug('Stripped %s/%s', self.partial_path, full_name)
else:
new_entries.append(e)
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(filename, data):
# https://cs.android.com/search?q=f:aapt2.*Container.cpp
size_idx = len(_FLAT_ARSC_HEADER)
proto_idx = size_idx + 8
if data[:size_idx] != _FLAT_ARSC_HEADER:
raise Exception(f'Wrong header bytes in {filename}')
# 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(filename, data)
if stripper.StripTable(table):
data = _FlatBytesFromTable(table)
return data
_ProcessZip(partial_path, process_file)