libredex/BundleResources.cpp (1,256 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// TODO (T91001948): Integrate protobuf dependency in supported platforms for
// open source
#include <memory>
#ifdef HAS_PROTOBUF
#include "BundleResources.h"
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include <boost/range/iterator_range.hpp>
#include <fstream>
#include <iomanip>
#include <map>
#include <queue>
#include <stdexcept>
#include <string>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/message.h>
#include "Debug.h"
#include "DexUtil.h"
#include "ReadMaybeMapped.h"
#include "RedexMappedFile.h"
#include "RedexResources.h"
#include "Trace.h"
namespace {
#define MAKE_RES_ID(package, type, entry) \
((PACKAGE_MASK_BIT & ((package) << PACKAGE_INDEX_BIT_SHIFT)) | \
(TYPE_MASK_BIT & ((type) << TYPE_INDEX_BIT_SHIFT)) | \
(ENTRY_MASK_BIT & (entry)))
void read_protobuf_file_contents(
const std::string& file,
const std::function<void(google::protobuf::io::CodedInputStream&, size_t)>&
fn) {
redex::read_file_with_contents(file, [&](const char* data, size_t size) {
if (size == 0) {
fprintf(stderr, "Unable to read protobuf file: %s\n", file.c_str());
return;
}
google::protobuf::io::CodedInputStream input(
(const google::protobuf::uint8*)data, size);
fn(input, size);
});
}
bool has_attribute(const aapt::pb::XmlElement& element,
const std::string& name) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (pb_attr.name() == name) {
return true;
}
}
return false;
}
bool has_primitive_attribute(const aapt::pb::XmlElement& element,
const std::string& name,
const aapt::pb::Primitive::OneofValueCase type) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (pb_attr.name() == name) {
if (pb_attr.has_compiled_item()) {
const auto& pb_item = pb_attr.compiled_item();
if (pb_item.has_prim() && pb_item.prim().oneof_value_case() == type) {
return true;
}
}
return false;
}
}
return false;
}
int get_int_attribute_value(const aapt::pb::XmlElement& element,
const std::string& name) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (pb_attr.name() == name) {
if (pb_attr.has_compiled_item()) {
const auto& pb_item = pb_attr.compiled_item();
if (pb_item.has_prim() && pb_item.prim().oneof_value_case() ==
aapt::pb::Primitive::kIntDecimalValue) {
return pb_item.prim().int_decimal_value();
}
}
}
}
throw std::runtime_error("Expected element " + element.name() +
" to have an int attribute " + name);
}
bool get_bool_attribute_value(const aapt::pb::XmlElement& element,
const std::string& name,
bool default_value) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (pb_attr.name() == name) {
if (pb_attr.has_compiled_item()) {
const auto& pb_item = pb_attr.compiled_item();
if (pb_item.has_prim() && pb_item.prim().oneof_value_case() ==
aapt::pb::Primitive::kBooleanValue) {
return pb_item.prim().boolean_value();
}
}
return default_value;
}
}
return default_value;
}
std::string get_string_attribute_value(const aapt::pb::XmlElement& element,
const std::string& name) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (pb_attr.name() == name) {
always_assert_log(!pb_attr.has_compiled_item(),
"Attribute %s expected to be a string!",
name.c_str());
return pb_attr.value();
}
}
return std::string("");
}
// Apply callback to element and its descendants, stopping if/when callback
// returns false
void traverse_element_and_children(
const aapt::pb::XmlElement& start,
const std::function<bool(const aapt::pb::XmlElement&)>& callback) {
std::queue<aapt::pb::XmlElement> q;
q.push(start);
while (!q.empty()) {
const auto& front = q.front();
if (!callback(front)) {
return;
}
for (const aapt::pb::XmlNode& pb_child : front.child()) {
if (pb_child.node_case() == aapt::pb::XmlNode::NodeCase::kElement) {
q.push(pb_child.element());
}
}
q.pop();
}
}
// Look for <search_tag> within the descendants of the given XML Node
bool find_nested_tag(const std::string& search_tag,
const aapt::pb::XmlElement& start) {
bool find_result = false;
traverse_element_and_children(
start, [&](const aapt::pb::XmlElement& element) {
bool keep_going = true;
if (&start != &element && element.name() == search_tag) {
find_result = true;
keep_going = false;
}
return keep_going;
});
return find_result;
}
inline std::string fully_qualified_external(const std::string& package_name,
const std::string& value) {
if (value.empty()) {
return value;
}
if (value.at(0) == '.') {
return java_names::external_to_internal(package_name + value);
}
return java_names::external_to_internal(value);
}
// Traverse a compound value message, and return a list of Item defined in
// this message.
std::vector<aapt::pb::Item> get_items_from_CV(
const aapt::pb::CompoundValue& comp_value) {
std::vector<aapt::pb::Item> ret;
if (comp_value.has_style()) {
// Style style -> Entry entry -> Item item.
const auto& entries = comp_value.style().entry();
for (int n = 0; n < entries.size(); ++n) {
if (entries[n].has_item()) {
ret.push_back(entries[n].item());
}
}
} else if (comp_value.has_array()) {
// Array array -> Element element -> Item item.
const auto& elements = comp_value.array().element();
for (int n = 0; n < elements.size(); ++n) {
if (elements[n].has_item()) {
ret.push_back(elements[n].item());
}
}
} else if (comp_value.has_plural()) {
// Plural plural -> Entry entry -> Item item.
const auto& entries = comp_value.plural().entry();
for (int n = 0; n < entries.size(); ++n) {
if (entries[n].has_item()) {
ret.push_back(entries[n].item());
}
}
}
return ret;
}
// Traverse a compound value message, and return a list of Reference messages
// used in this message.
std::vector<aapt::pb::Reference> get_references(
const aapt::pb::CompoundValue& comp_value) {
std::vector<aapt::pb::Reference> ret;
// Find refs from Item message.
const auto& items = get_items_from_CV(comp_value);
for (size_t i = 0; i < items.size(); i++) {
if (items[i].has_ref()) {
ret.push_back(items[i].ref());
}
}
// Find refs from other types of messages.
if (comp_value.has_attr()) {
// Attribute attr -> Symbol symbol -> Reference name.
const auto& symbols = comp_value.attr().symbol();
for (int i = 0; i < symbols.size(); i++) {
if (symbols[i].has_name()) {
ret.push_back(symbols[i].name());
}
}
} else if (comp_value.has_style()) {
// Style style -> Entry entry -> Reference key.
const auto& entries = comp_value.style().entry();
for (int i = 0; i < entries.size(); i++) {
if (entries[i].has_key()) {
ret.push_back(entries[i].key());
}
}
// Style style -> Reference parent.
if (comp_value.style().has_parent()) {
ret.push_back(comp_value.style().parent());
}
} else if (comp_value.has_styleable()) {
// Styleable styleable -> Entry entry -> Reference attr.
const auto& entries = comp_value.styleable().entry();
for (int i = 0; i < entries.size(); i++) {
if (entries[i].has_attr()) {
ret.push_back(entries[i].attr());
}
}
}
return ret;
}
void read_single_manifest(const std::string& manifest,
ManifestClassInfo* manifest_classes) {
TRACE(RES, 1, "Reading proto manifest at %s", manifest.c_str());
read_protobuf_file_contents(
manifest,
[&](google::protobuf::io::CodedInputStream& input, size_t size) {
std::unordered_map<std::string, ComponentTag> string_to_tag{
{"activity", ComponentTag::Activity},
{"activity-alias", ComponentTag::ActivityAlias},
{"provider", ComponentTag::Provider},
{"receiver", ComponentTag::Receiver},
{"service", ComponentTag::Service},
};
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
manifest.c_str());
if (pb_node.has_element() && pb_node.element().name() == "manifest") {
const auto& manifest_element = pb_node.element();
auto package_name =
get_string_attribute_value(manifest_element, "package");
traverse_element_and_children(
manifest_element, [&](const aapt::pb::XmlElement& element) {
const auto& tag = element.name();
if (tag == "application") {
auto classname = get_string_attribute_value(element, "name");
if (!classname.empty()) {
manifest_classes->application_classes.emplace(
fully_qualified_external(package_name, classname));
}
auto app_factory_cls = get_string_attribute_value(
element, "appComponentFactory");
if (!app_factory_cls.empty()) {
manifest_classes->application_classes.emplace(
fully_qualified_external(package_name,
app_factory_cls));
}
} else if (tag == "instrumentation") {
auto classname = get_string_attribute_value(element, "name");
always_assert(classname.size());
manifest_classes->instrumentation_classes.emplace(
fully_qualified_external(package_name, classname));
} else if (string_to_tag.count(tag)) {
std::string classname = get_string_attribute_value(
element,
tag != "activity-alias" ? "name" : "targetActivity");
always_assert(classname.size());
bool has_exported_attribute = has_primitive_attribute(
element, "exported", aapt::pb::Primitive::kBooleanValue);
bool has_permission_attribute =
has_attribute(element, "permission");
bool has_protection_level_attribute =
has_attribute(element, "protectionLevel");
bool is_exported =
get_bool_attribute_value(element, "exported",
/* default_value */ false);
BooleanXMLAttribute export_attribute;
if (has_exported_attribute) {
if (is_exported) {
export_attribute = BooleanXMLAttribute::True;
} else {
export_attribute = BooleanXMLAttribute::False;
}
} else {
export_attribute = BooleanXMLAttribute::Undefined;
}
// NOTE: This logic is analogous to the APK manifest reading
// code, which is wrong. This should be a bitmask, not a
// string. Returning the same messed up values here to at
// least be consistent for now.
std::string permission_attribute;
std::string protection_level_attribute;
if (has_permission_attribute) {
permission_attribute =
get_string_attribute_value(element, "permission");
}
if (has_protection_level_attribute) {
protection_level_attribute =
get_string_attribute_value(element, "protectionLevel");
}
ComponentTagInfo tag_info(
string_to_tag.at(tag),
fully_qualified_external(package_name, classname),
export_attribute,
permission_attribute,
protection_level_attribute);
if (tag == "provider") {
auto text =
get_string_attribute_value(element, "authorities");
parse_authorities(text, &tag_info.authority_classes);
} else {
tag_info.has_intent_filters =
find_nested_tag("intent-filter", element);
}
manifest_classes->component_tags.emplace_back(tag_info);
}
return true;
});
}
});
}
} // namespace
boost::optional<int32_t> BundleResources::get_min_sdk() {
std::string base_manifest = (boost::filesystem::path(m_directory) /
"base/manifest/AndroidManifest.xml")
.string();
boost::optional<int32_t> result = boost::none;
if (!boost::filesystem::exists(base_manifest)) {
return result;
}
TRACE(RES, 1, "Reading proto xml at %s", base_manifest.c_str());
read_protobuf_file_contents(
base_manifest,
[&](google::protobuf::io::CodedInputStream& input, size_t size) {
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
base_manifest.c_str());
if (pb_node.has_element()) {
const auto& manifest_element = pb_node.element();
for (const aapt::pb::XmlNode& pb_child : manifest_element.child()) {
if (pb_child.node_case() == aapt::pb::XmlNode::NodeCase::kElement) {
const auto& pb_element = pb_child.element();
if (pb_element.name() == "uses-sdk") {
if (has_primitive_attribute(
pb_element,
"minSdkVersion",
aapt::pb::Primitive::kIntDecimalValue)) {
result = boost::optional<int32_t>(
get_int_attribute_value(pb_element, "minSdkVersion"));
return;
}
}
}
}
}
});
return result;
}
ManifestClassInfo BundleResources::get_manifest_class_info() {
ManifestClassInfo manifest_classes;
boost::filesystem::path dir(m_directory);
for (auto& entry : boost::make_iterator_range(
boost::filesystem::directory_iterator(dir), {})) {
auto manifest = entry.path() / "manifest/AndroidManifest.xml";
if (boost::filesystem::exists(manifest)) {
read_single_manifest(manifest.string(), &manifest_classes);
}
}
return manifest_classes;
}
namespace {
void apply_rename_map(const std::map<std::string, std::string>& rename_map,
aapt::pb::XmlNode* node,
size_t* out_num_renamed) {
// NOTE: The implementation that follows is not at all similar to
// ApkResources though this is likely sufficient. ApkResources, when
// renaming will simply iterate through a string pool, picking up anything
// wherever it might be in the document. This is simply checking tag
// names, attribute values and text.
if (node->has_element()) {
auto element = node->mutable_element();
{
auto search = rename_map.find(element->name());
if (search != rename_map.end()) {
element->set_name(search->second);
(*out_num_renamed)++;
}
}
auto attr_size = element->attribute_size();
for (int i = 0; i < attr_size; i++) {
auto pb_attr = element->mutable_attribute(i);
auto search = rename_map.find(pb_attr->value());
if (search != rename_map.end()) {
pb_attr->set_value(search->second);
(*out_num_renamed)++;
}
}
auto child_size = element->child_size();
for (int i = 0; i < child_size; i++) {
auto child = element->mutable_child(i);
apply_rename_map(rename_map, child, out_num_renamed);
}
} else {
auto search = rename_map.find(node->text());
if (search != rename_map.end()) {
node->set_text(search->second);
(*out_num_renamed)++;
}
}
}
} // namespace
bool BundleResources::rename_classes_in_layout(
const std::string& file_path,
const std::map<std::string, std::string>& rename_map,
size_t* out_num_renamed) {
bool write_failed = false;
read_protobuf_file_contents(
file_path,
[&](google::protobuf::io::CodedInputStream& input, size_t size) {
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
file_path.c_str());
size_t num_renamed = 0;
apply_rename_map(rename_map, &pb_node, &num_renamed);
if (num_renamed > 0) {
std::ofstream out(file_path, std::ofstream::binary);
if (pb_node.SerializeToOstream(&out)) {
*out_num_renamed = num_renamed;
} else {
write_failed = true;
}
}
});
return !write_failed;
}
namespace {
std::vector<std::string> find_subdir_in_modules(
const std::string& extracted_dir, const std::string& subdir) {
std::vector<std::string> dirs;
boost::filesystem::path dir(extracted_dir);
for (auto& entry : boost::make_iterator_range(
boost::filesystem::directory_iterator(dir), {})) {
auto maybe = entry.path() / subdir;
if (boost::filesystem::exists(maybe)) {
dirs.emplace_back(maybe.string());
}
}
return dirs;
}
} // namespace
std::vector<std::string> BundleResources::find_res_directories() {
return find_subdir_in_modules(m_directory, "res");
}
std::vector<std::string> BundleResources::find_lib_directories() {
return find_subdir_in_modules(m_directory, "lib");
}
std::string BundleResources::get_base_assets_dir() {
return m_directory + "/base/assets";
}
namespace {
const std::unordered_set<std::string> NON_CLASS_ELEMENTS = {
"fragment", "view", "dialog", "activity", "intent",
};
const std::vector<std::string> CLASS_XML_ATTRIBUTES = {
"class",
"name",
"targetClass",
};
// Collect all resource ids referred in an given xml element.
// attr->compiled_item->ref->id
void collect_rids_for_element(const aapt::pb::XmlElement& element,
std::unordered_set<uint32_t>& result) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
if (!pb_attr.has_compiled_item()) {
continue;
}
const auto& pb_item = pb_attr.compiled_item();
if (pb_item.has_ref()) {
auto rid = pb_item.ref().id();
if (rid > PACKAGE_RESID_START) {
result.emplace(rid);
}
}
}
}
void collect_layout_classes_and_attributes_for_element(
const aapt::pb::XmlElement& element,
const std::unordered_map<std::string, std::string>& ns_uri_to_prefix,
const std::unordered_set<std::string>& attributes_to_read,
std::unordered_set<std::string>* out_classes,
std::unordered_multimap<std::string, std::string>* out_attributes) {
const auto& element_name = element.name();
if (NON_CLASS_ELEMENTS.count(element_name) > 0) {
for (const auto& attr : CLASS_XML_ATTRIBUTES) {
auto classname = get_string_attribute_value(element, attr);
if (!classname.empty() && classname.find('.') != std::string::npos) {
auto internal = java_names::external_to_internal(classname);
TRACE(RES, 9,
"Considering %s as possible class in XML "
"resource from element %s",
internal.c_str(), element_name.c_str());
out_classes->emplace(internal);
break;
}
}
} else if (element_name.find('.') != std::string::npos) {
// Consider the element name itself as a possible class in the
// application
auto internal = java_names::external_to_internal(element_name);
TRACE(RES, 9, "Considering %s as possible class in XML resource",
internal.c_str());
out_classes->emplace(internal);
}
if (!attributes_to_read.empty()) {
for (const aapt::pb::XmlAttribute& pb_attr : element.attribute()) {
const auto& attr_name = pb_attr.name();
const auto& uri = pb_attr.namespace_uri();
std::string fully_qualified =
ns_uri_to_prefix.count(uri) == 0
? attr_name
: (ns_uri_to_prefix.at(uri) + ":" + attr_name);
if (attributes_to_read.count(fully_qualified) > 0) {
always_assert_log(!pb_attr.has_compiled_item(),
"Only supporting string values for attributes. "
"Given attribute: %s",
fully_qualified.c_str());
auto value = pb_attr.value();
out_attributes->emplace(fully_qualified, value);
}
}
}
}
void change_resource_id_in_pb_reference(
const std::map<uint32_t, uint32_t>& old_to_new, aapt::pb::Reference* ref) {
auto ref_id = ref->id();
if (old_to_new.count(ref_id)) {
auto new_id = old_to_new.at(ref_id);
ref->set_id(new_id);
}
}
void change_resource_id_in_value_reference(
const std::map<uint32_t, uint32_t>& old_to_new, aapt::pb::Value* value) {
if (value->has_item()) {
auto pb_item = value->mutable_item();
if (pb_item->has_ref()) {
change_resource_id_in_pb_reference(old_to_new, pb_item->mutable_ref());
}
} else if (value->has_compound_value()) {
auto pb_compound_value = value->mutable_compound_value();
if (pb_compound_value->has_attr()) {
auto pb_attr = pb_compound_value->mutable_attr();
auto symbol_size = pb_attr->symbol_size();
for (int i = 0; i < symbol_size; ++i) {
auto symbol = pb_attr->mutable_symbol(i);
if (symbol->has_name()) {
change_resource_id_in_pb_reference(old_to_new,
symbol->mutable_name());
}
}
} else if (pb_compound_value->has_style()) {
auto pb_style = pb_compound_value->mutable_style();
if (pb_style->has_parent()) {
change_resource_id_in_pb_reference(old_to_new,
pb_style->mutable_parent());
}
auto entry_size = pb_style->entry_size();
for (int i = 0; i < entry_size; ++i) {
auto entry = pb_style->mutable_entry(i);
if (entry->has_key()) {
change_resource_id_in_pb_reference(old_to_new, entry->mutable_key());
}
if (entry->has_item()) {
auto pb_item = entry->mutable_item();
if (pb_item->has_ref()) {
change_resource_id_in_pb_reference(old_to_new,
pb_item->mutable_ref());
}
}
}
} else if (pb_compound_value->has_styleable()) {
auto pb_styleable = pb_compound_value->mutable_styleable();
auto entry_size = pb_styleable->entry_size();
for (int i = 0; i < entry_size; ++i) {
auto entry = pb_styleable->mutable_entry(i);
if (entry->has_attr()) {
change_resource_id_in_pb_reference(old_to_new, entry->mutable_attr());
}
}
} else if (pb_compound_value->has_array()) {
auto pb_array = pb_compound_value->mutable_array();
auto entry_size = pb_array->element_size();
for (int i = 0; i < entry_size; ++i) {
auto element = pb_array->mutable_element(i);
if (element->has_item()) {
auto pb_item = element->mutable_item();
if (pb_item->has_ref()) {
change_resource_id_in_pb_reference(old_to_new,
pb_item->mutable_ref());
}
}
}
} else if (pb_compound_value->has_plural()) {
auto pb_plural = pb_compound_value->mutable_plural();
auto entry_size = pb_plural->entry_size();
for (int i = 0; i < entry_size; ++i) {
auto entry = pb_plural->mutable_entry(i);
if (entry->has_item()) {
auto pb_item = entry->mutable_item();
if (pb_item->has_ref()) {
change_resource_id_in_pb_reference(old_to_new,
pb_item->mutable_ref());
}
}
}
}
}
}
void remove_or_change_resource_ids(
const std::unordered_set<uint32_t>& ids_to_remove,
const std::map<uint32_t, uint32_t>& old_to_new,
uint32_t package_id,
aapt::pb::Type* type) {
google::protobuf::RepeatedPtrField<aapt::pb::Entry> new_entries;
for (const auto& entry : type->entry()) {
uint32_t res_id =
MAKE_RES_ID(package_id, type->type_id().id(), entry.entry_id().id());
if (ids_to_remove.count(res_id)) {
continue;
}
auto copy_entry = new aapt::pb::Entry(entry);
if (old_to_new.count(res_id)) {
uint32_t new_res_id = old_to_new.at(res_id);
uint32_t new_entry_id = ENTRY_MASK_BIT & new_res_id;
always_assert_log(copy_entry->has_entry_id(),
"Entry don't have id %s",
copy_entry->DebugString().c_str());
auto entry_id = copy_entry->mutable_entry_id();
entry_id->set_id(new_entry_id);
auto config_value_size = copy_entry->config_value_size();
for (int i = 0; i < config_value_size; ++i) {
auto config_value = copy_entry->mutable_config_value(i);
always_assert_log(config_value->has_value(),
"ConfigValue don't have value %s\nEntry:\n%s",
config_value->DebugString().c_str(),
copy_entry->DebugString().c_str());
auto value = config_value->mutable_value();
change_resource_id_in_value_reference(old_to_new, value);
}
}
new_entries.AddAllocated(copy_entry);
}
type->clear_entry();
type->mutable_entry()->Swap(&new_entries);
}
void change_resource_id_in_xml_references(
const std::map<uint32_t, uint32_t>& kept_to_remapped_ids,
aapt::pb::XmlNode* node,
size_t* num_resource_id_changed) {
if (!node->has_element()) {
return;
}
auto element = node->mutable_element();
auto attr_size = element->attribute_size();
for (int i = 0; i < attr_size; i++) {
auto pb_attr = element->mutable_attribute(i);
auto attr_id = pb_attr->resource_id();
if (attr_id > 0 && kept_to_remapped_ids.count(attr_id)) {
auto new_id = kept_to_remapped_ids.at(attr_id);
if (new_id != attr_id) {
(*num_resource_id_changed)++;
pb_attr->set_resource_id(new_id);
}
}
if (pb_attr->has_compiled_item()) {
auto pb_item = pb_attr->mutable_compiled_item();
if (pb_item->has_ref()) {
auto ref = pb_item->mutable_ref();
auto ref_id = ref->id();
if (kept_to_remapped_ids.count(ref_id)) {
auto new_id = kept_to_remapped_ids.at(ref_id);
(*num_resource_id_changed)++;
ref->set_id(new_id);
}
}
}
}
auto child_size = element->child_size();
for (int i = 0; i < child_size; i++) {
auto child = element->mutable_child(i);
change_resource_id_in_xml_references(kept_to_remapped_ids, child,
num_resource_id_changed);
}
}
} // namespace
void BundleResources::collect_layout_classes_and_attributes_for_file(
const std::string& file_path,
const std::unordered_set<std::string>& attributes_to_read,
std::unordered_set<std::string>* out_classes,
std::unordered_multimap<std::string, std::string>* out_attributes) {
if (is_raw_resource(file_path)) {
return;
}
TRACE(RES,
9,
"BundleResources collecting classes and attributes for file: %s",
file_path.c_str());
read_protobuf_file_contents(
file_path,
[&](google::protobuf::io::CodedInputStream& input, size_t size) {
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
file_path.c_str());
if (pb_node.has_element()) {
const auto& root = pb_node.element();
std::unordered_map<std::string, std::string> ns_uri_to_prefix;
for (const auto& ns_decl : root.namespace_declaration()) {
if (!ns_decl.uri().empty() && !ns_decl.prefix().empty()) {
ns_uri_to_prefix.emplace(ns_decl.uri(), ns_decl.prefix());
}
}
traverse_element_and_children(
root, [&](const aapt::pb::XmlElement& element) {
collect_layout_classes_and_attributes_for_element(
element, ns_uri_to_prefix, attributes_to_read, out_classes,
out_attributes);
return true;
});
}
});
}
size_t BundleResources::remap_xml_reference_attributes(
const std::string& filename,
const std::map<uint32_t, uint32_t>& kept_to_remapped_ids) {
if (is_raw_resource(filename)) {
return 0;
}
TRACE(RES,
9,
"BundleResources changing resource id for xml file: %s",
filename.c_str());
size_t num_changed = 0;
read_protobuf_file_contents(
filename,
[&](google::protobuf::io::CodedInputStream& input, size_t /* unused */) {
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
filename.c_str());
change_resource_id_in_xml_references(kept_to_remapped_ids, &pb_node,
&num_changed);
if (num_changed > 0) {
std::ofstream out(filename, std::ofstream::binary);
always_assert(pb_node.SerializeToOstream(&out));
}
});
return num_changed;
}
std::vector<std::string> BundleResources::find_resources_files() {
std::vector<std::string> paths;
boost::filesystem::path dir(m_directory);
for (auto& entry : boost::make_iterator_range(
boost::filesystem::directory_iterator(dir), {})) {
auto resources_file = entry.path() / "resources.pb";
if (boost::filesystem::exists(resources_file)) {
paths.emplace_back(resources_file.string());
}
}
return paths;
}
std::unordered_set<std::string> BundleResources::find_all_xml_files() {
std::unordered_set<std::string> all_xml_files;
boost::filesystem::path dir(m_directory);
for (auto& entry : boost::make_iterator_range(
boost::filesystem::directory_iterator(dir), {})) {
auto manifest = entry.path() / "manifest/AndroidManifest.xml";
if (boost::filesystem::exists(manifest)) {
all_xml_files.emplace(manifest.string());
}
auto res_path = entry.path() / "/res";
for (const std::string& path : get_xml_files(res_path.string())) {
all_xml_files.emplace(path);
}
}
return all_xml_files;
}
std::unordered_set<uint32_t> BundleResources::get_xml_reference_attributes(
const std::string& filename) {
std::unordered_set<uint32_t> result;
if (is_raw_resource(filename)) {
return result;
}
read_protobuf_file_contents(
filename,
[&](google::protobuf::io::CodedInputStream& input, size_t size) {
aapt::pb::XmlNode pb_node;
bool read_finish = pb_node.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResource failed to read %s",
filename.c_str());
if (pb_node.has_element()) {
const auto& start = pb_node.element();
traverse_element_and_children(
start, [&](const aapt::pb::XmlElement& element) {
collect_rids_for_element(element, result);
return true;
});
}
});
return result;
}
void ResourcesPbFile::remap_res_ids_and_serialize(
const std::vector<std::string>& resource_files,
const std::map<uint32_t, uint32_t>& old_to_new) {
for (const auto& resources_pb_path : resource_files) {
TRACE(RES,
9,
"BundleResources changing resource data for file: %s",
resources_pb_path.c_str());
read_protobuf_file_contents(
resources_pb_path,
[&](google::protobuf::io::CodedInputStream& input,
size_t /* unused */) {
aapt::pb::ResourceTable pb_restable;
bool read_finish = pb_restable.ParseFromCodedStream(&input);
always_assert_log(read_finish,
"BundleResoource failed to read %s",
resources_pb_path.c_str());
int package_size = pb_restable.package_size();
for (int i = 0; i < package_size; i++) {
auto package = pb_restable.mutable_package(i);
auto current_package_id = package->package_id().id();
int type_size = package->type_size();
for (int j = 0; j < type_size; j++) {
auto type = package->mutable_type(j);
remove_or_change_resource_ids(m_ids_to_remove, old_to_new,
current_package_id, type);
}
}
std::ofstream out(resources_pb_path, std::ofstream::binary);
always_assert(pb_restable.SerializeToOstream(&out));
});
}
}
namespace {
void remap_entry_file_paths(
const std::unordered_map<std::string, std::string>& old_to_new,
uint32_t package_id,
uint32_t type_id,
aapt::pb::Entry* entry) {
uint32_t res_id = MAKE_RES_ID(package_id, type_id, entry->entry_id().id());
auto config_size = entry->config_value_size();
for (int i = 0; i < config_size; i++) {
auto value = entry->mutable_config_value(i)->mutable_value();
if (value->has_item()) {
auto item = value->mutable_item();
if (item->has_file()) {
auto file = item->mutable_file();
auto search = old_to_new.find(file->path());
if (search != old_to_new.end()) {
TRACE(RES, 8, "Writing file path %s to ID 0x%x",
search->second.c_str(), res_id);
file->set_path(search->second);
}
}
}
}
}
} // namespace
void ResourcesPbFile::remap_file_paths_and_serialize(
const std::vector<std::string>& resource_files,
const std::unordered_map<std::string, std::string>& old_to_new) {
for (const auto& resources_pb_path : resource_files) {
TRACE(RES,
9,
"BundleResources changing file paths for file: %s",
resources_pb_path.c_str());
read_protobuf_file_contents(
resources_pb_path,
[&](google::protobuf::io::CodedInputStream& input,
size_t /* unused */) {
aapt::pb::ResourceTable pb_restable;
bool read_finish = pb_restable.ParseFromCodedStream(&input);
always_assert_log(read_finish,
"BundleResoource failed to read %s",
resources_pb_path.c_str());
int package_size = pb_restable.package_size();
for (int i = 0; i < package_size; i++) {
auto package = pb_restable.mutable_package(i);
auto current_package_id = package->package_id().id();
int type_size = package->type_size();
for (int j = 0; j < type_size; j++) {
auto type = package->mutable_type(j);
auto current_type_id = type->type_id().id();
int entry_size = type->entry_size();
for (int k = 0; k < entry_size; k++) {
remap_entry_file_paths(old_to_new, current_package_id,
current_type_id, type->mutable_entry(k));
}
}
}
std::ofstream out(resources_pb_path, std::ofstream::binary);
always_assert(pb_restable.SerializeToOstream(&out));
});
}
}
namespace {
std::string module_name_from_pb_path(const std::string& resources_pb_path) {
auto p = boost::filesystem::path(resources_pb_path);
return p.parent_path().filename().string();
}
} // namespace
std::string ResourcesPbFile::resolve_module_name_for_resource_id(
uint32_t res_id) {
auto package_id = res_id >> 24;
always_assert_log(m_package_id_to_module_name.count(package_id) > 0,
"Unknown package for resource id %X", res_id);
return m_package_id_to_module_name.at(package_id);
}
namespace {
void reset_pb_source(google::protobuf::Message* message) {
if (message == nullptr) {
return;
}
const google::protobuf::Descriptor* desc = message->GetDescriptor();
const google::protobuf::Reflection* refl = message->GetReflection();
for (int i = 0; i < desc->field_count(); i++) {
const google::protobuf::FieldDescriptor* field_desc = desc->field(i);
auto repeated = field_desc->is_repeated();
auto cpp_type = field_desc->cpp_type();
if (cpp_type == google::protobuf::FieldDescriptor::CPPTYPE_UINT32 &&
refl->HasField(*message, field_desc) && !repeated) {
auto name = field_desc->name();
if (name == "path_idx" || name == "line_number" ||
name == "column_number") {
TRACE(RES, 9, "resetting uint32 field: %s", name.c_str());
refl->SetUInt32(message, field_desc, 0);
}
} else if (cpp_type == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) {
if (repeated) {
// Note: HasField not relevant for repeated fields.
auto size = refl->FieldSize(*message, field_desc);
for (int j = 0; j < size; j++) {
auto sub_message =
refl->MutableRepeatedMessage(message, field_desc, j);
reset_pb_source(sub_message);
}
} else if (refl->HasField(*message, field_desc)) {
auto sub_message = refl->MutableMessage(message, field_desc);
reset_pb_source(sub_message);
}
}
}
}
bool compare_reference(const aapt::pb::Reference& a,
const aapt::pb::Reference& b) {
if (a.type() != b.type()) {
return a.type() < b.type();
}
if (a.id() != b.id()) {
return a.id() < b.id();
}
int name_compare = a.name().compare(b.name());
if (name_compare != 0) {
return name_compare < 0;
}
if (a.private_() != b.private_()) {
return a.private_();
}
// Just make some kind of consistent ordering of unknown/false/true that is
// meaningless outside this function.
auto dynamic_to_int = [](const aapt::pb::Reference& r) {
if (!r.has_is_dynamic()) {
return -1;
}
if (!r.is_dynamic().value()) {
return 0;
}
return 1;
};
auto da = dynamic_to_int(a);
auto db = dynamic_to_int(b);
if (da != db) {
return da < db;
}
return false;
}
void reorder_style(aapt::pb::Style* style) {
std::stable_sort(
style->mutable_entry()->begin(),
style->mutable_entry()->end(),
[style](const aapt::pb::Style_Entry& a, const aapt::pb::Style_Entry& b) {
always_assert_log(a.has_key() && b.has_key(),
"Unexpected styleable missing reference: %s",
style->DebugString().c_str());
return compare_reference(a.key(), b.key());
});
}
void reorder_config_value_repeated_field(aapt::pb::ResourceTable* pb_restable) {
for (int package_idx = 0; package_idx < pb_restable->package_size();
package_idx++) {
auto package = pb_restable->mutable_package(package_idx);
for (int type_idx = 0; type_idx < package->type_size(); type_idx++) {
auto type = package->mutable_type(type_idx);
for (int entry_idx = 0; entry_idx < type->entry_size(); entry_idx++) {
auto entry = type->mutable_entry(entry_idx);
for (int cv_idx = 0; cv_idx < entry->config_value_size(); cv_idx++) {
auto config_value = entry->mutable_config_value(cv_idx);
if (config_value->has_value()) {
auto value = config_value->mutable_value();
if (value->has_compound_value()) {
auto compound_value = value->mutable_compound_value();
if (compound_value->has_style()) {
reorder_style(compound_value->mutable_style());
}
}
}
}
}
}
}
}
} // namespace
void ResourcesPbFile::collect_resource_data_for_file(
const std::string& resources_pb_path) {
TRACE(RES,
9,
"BundleResources collecting resource data for file: %s",
resources_pb_path.c_str());
read_protobuf_file_contents(
resources_pb_path,
[&](google::protobuf::io::CodedInputStream& input, size_t /* unused */) {
aapt::pb::ResourceTable pb_restable;
bool read_finish = pb_restable.ParseFromCodedStream(&input);
always_assert_log(read_finish, "BundleResoource failed to read %s",
resources_pb_path.c_str());
if (pb_restable.has_source_pool()) {
// Source positions refer to ResStringPool entries which are file
// paths from the perspective of the build machine. Not relevant for
// further operations, set them to a predictable value.
// NOTE: Not all input .aab files will have this data; release style
// bundles should omit this data.
reset_pb_source(&pb_restable);
}
// Repeated fields might not be comming in ordered, to make following
// config_value comparison work with different order, reorder repeated
// fields in config_value's value
reorder_config_value_repeated_field(&pb_restable);
for (const aapt::pb::Package& pb_package : pb_restable.package()) {
auto current_package_id = pb_package.package_id().id();
TRACE(RES, 9, "Package: %s %X", pb_package.package_name().c_str(),
current_package_id);
m_package_id_to_module_name.emplace(
current_package_id, module_name_from_pb_path(resources_pb_path));
for (const aapt::pb::Type& pb_type : pb_package.type()) {
auto current_type_id = pb_type.type_id().id();
const auto& current_type_name = pb_type.name();
TRACE(RES, 9, " Type: %s %X", current_type_name.c_str(),
current_type_id);
always_assert(m_type_id_to_names.count(current_type_id) == 0 ||
m_type_id_to_names.at(current_type_id) ==
current_type_name);
m_type_id_to_names[current_type_id] = current_type_name;
for (const aapt::pb::Entry& pb_entry : pb_type.entry()) {
if (m_package_id == 0xFFFFFFFF) {
m_package_id = current_package_id;
}
always_assert_log(
m_package_id == current_package_id,
"Broken assumption for only one package for resources.");
std::string name_string = pb_entry.name();
auto current_entry_id = pb_entry.entry_id().id();
auto current_resource_id = MAKE_RES_ID(
current_package_id, current_type_id, current_entry_id);
TRACE(RES, 9, " Entry: %s %X %X", pb_entry.name().c_str(),
current_entry_id, current_resource_id);
sorted_res_ids.add(current_resource_id);
always_assert(m_existed_res_ids.count(current_resource_id) == 0);
m_existed_res_ids.emplace(current_resource_id);
id_to_name.emplace(current_resource_id, name_string);
name_to_ids[name_string].push_back(current_resource_id);
m_res_id_to_configvalue.emplace(current_resource_id,
pb_entry.config_value());
}
}
}
});
}
std::unordered_set<uint32_t> ResourcesPbFile::get_types_by_name(
const std::unordered_set<std::string>& type_names) {
always_assert(m_type_id_to_names.size() > 0);
std::unordered_set<uint32_t> type_ids;
for (const auto& pair : m_type_id_to_names) {
if (type_names.count(pair.second) == 1) {
type_ids.emplace((pair.first) << TYPE_INDEX_BIT_SHIFT);
}
}
return type_ids;
}
void ResourcesPbFile::delete_resource(uint32_t res_id) {
// Keep track of res_id and delete later in remap_res_ids_and_serialize.
m_ids_to_remove.emplace(res_id);
}
namespace {
const std::string KNOWN_RES_DIR = std::string(RES_DIRECTORY) + "/";
bool is_resource_file(const std::string& str) {
return boost::algorithm::starts_with(str, KNOWN_RES_DIR);
}
} // namespace
std::vector<std::string> ResourcesPbFile::get_files_by_rid(
uint32_t res_id, ResourcePathType path_type) {
std::vector<std::string> ret;
if (m_res_id_to_configvalue.count(res_id) == 0) {
return ret;
}
const std::string& module_name = resolve_module_name_for_resource_id(res_id);
auto handle_path = [&](const std::string& file_path) {
if (is_resource_file(file_path)) {
if (path_type == ResourcePathType::ZipPath) {
ret.emplace_back(module_name + "/" + file_path);
} else {
ret.emplace_back(file_path);
}
}
};
const auto& out_values = m_res_id_to_configvalue.at(res_id);
for (auto i = 0; i < out_values.size(); i++) {
const auto& value = out_values[i].value();
if (value.has_item() && value.item().has_file()) {
// Item
const auto& file_path = value.item().file().path();
handle_path(file_path);
} else if (value.has_compound_value()) {
// For coumpound value, we flatten it and check all its item messages.
const auto& items = get_items_from_CV(value.compound_value());
for (size_t n = 0; n < items.size(); n++) {
if (items[n].has_file()) {
const auto& file_path = items[n].file().path();
handle_path(file_path);
}
}
}
}
return ret;
}
void ResourcesPbFile::walk_references_for_resource(
uint32_t resID,
ResourcePathType path_type,
std::unordered_set<uint32_t>* nodes_visited,
std::unordered_set<std::string>* potential_file_paths) {
if (nodes_visited->find(resID) != nodes_visited->end()) {
// Return directly if a node is visited.
return;
}
nodes_visited->emplace(resID);
auto module_name = resolve_module_name_for_resource_id(resID);
auto& initial_values = m_res_id_to_configvalue.at(resID);
std::stack<const aapt::pb::ConfigValue*> nodes_to_explore;
auto push_to_stack = [&nodes_to_explore](const aapt::pb::ConfigValue& cv) {
nodes_to_explore.push(&cv);
};
std::for_each(initial_values.begin(), initial_values.end(), push_to_stack);
while (!nodes_to_explore.empty()) {
const auto& r = nodes_to_explore.top();
const auto& value = r->value();
nodes_to_explore.pop();
std::vector<aapt::pb::Item> items;
std::vector<aapt::pb::Reference> refs;
if (value.has_compound_value()) {
items = get_items_from_CV(value.compound_value());
refs = get_references(value.compound_value());
} else {
items.push_back(value.item());
if (value.item().has_ref()) {
refs.push_back(value.item().ref());
}
}
// For each Item, store the path of FileReference into string values.
for (size_t i = 0; i < items.size(); i++) {
const auto& item = items[i];
if (item.has_file()) {
if (path_type == ResourcePathType::ZipPath) {
// NOTE: We are mapping original given resource ID to a module name,
// when in reality resource ID for current item from the stack could
// be several references away. This should work for all our expected
// inputs but is shaky nonetheless.
auto item_path = module_name + "/" + item.file().path();
potential_file_paths->insert(item_path);
} else {
potential_file_paths->insert(item.file().path());
}
continue;
}
}
// For each Reference, follow its id to traverse the resources.
for (size_t i = 0; i < refs.size(); i++) {
std::vector<uint32_t> ref_ids;
if (refs[i].id() != 0) {
ref_ids.push_back(refs[i].id());
} else if (!refs[i].name().empty()) {
// Since id of a Reference message is optional, once ref_id =0, it is
// possible that the resource is refered by name. If we can make sure it
// won't happen, this branch can be removed.
ref_ids = get_res_ids_by_name(refs[i].name());
}
for (size_t n = 0; n < ref_ids.size(); n++) {
// Skip if the node has been visited.
const auto ref_id = ref_ids[n];
if (ref_id <= PACKAGE_RESID_START ||
nodes_visited->find(ref_id) != nodes_visited->end()) {
continue;
}
nodes_visited->insert(ref_id);
const auto& inner_values = m_res_id_to_configvalue.at(ref_id);
std::for_each(inner_values.begin(), inner_values.end(), push_to_stack);
}
}
}
}
std::unique_ptr<ResourceTableFile> BundleResources::load_res_table() {
const auto& res_pb_file_paths = find_resources_files();
auto to_return = std::make_unique<ResourcesPbFile>(ResourcesPbFile());
for (const auto& res_pb_file_path : res_pb_file_paths) {
to_return->collect_resource_data_for_file(res_pb_file_path);
}
return to_return;
}
BundleResources::~BundleResources() {}
size_t ResourcesPbFile::get_hash_from_values(
const ConfigValues& config_values) {
size_t hash = 0;
for (int i = 0; i < config_values.size(); ++i) {
const auto& value = config_values[i].value();
std::string value_str;
if (value.has_item()) {
value.item().SerializeToString(&value_str);
} else {
value.compound_value().SerializeToString(&value_str);
}
boost::hash_combine(hash, value_str);
}
return hash;
}
void ResourcesPbFile::collect_resid_values_and_hashes(
const std::vector<uint32_t>& ids,
std::map<size_t, std::vector<uint32_t>>* res_by_hash) {
for (uint32_t id : ids) {
const auto& config_values = m_res_id_to_configvalue.at(id);
(*res_by_hash)[get_hash_from_values(config_values)].push_back(id);
}
}
bool ResourcesPbFile::resource_value_identical(uint32_t a_id, uint32_t b_id) {
if ((a_id & PACKAGE_MASK_BIT) != (b_id & PACKAGE_MASK_BIT) ||
(a_id & TYPE_MASK_BIT) != (b_id & TYPE_MASK_BIT)) {
return false;
}
const auto& config_values_a = m_res_id_to_configvalue.at(a_id);
const auto& config_values_b = m_res_id_to_configvalue.at(b_id);
if (config_values_a.size() != config_values_b.size()) {
return false;
}
// For ResTable in arsc there seems to be assumption that configuration
// will be in same order for list of configvalues.
// https://fburl.com/code/optgs5k3 Not sure if this will hold for protobuf
// representation as well.
for (int i = 0; i < config_values_a.size(); ++i) {
const auto& config_value_a = config_values_a[i];
const auto& config_value_b = config_values_b[i];
const auto& config_a = config_value_a.config();
std::string config_a_str;
config_a.SerializeToString(&config_a_str);
const auto& config_b = config_value_b.config();
std::string config_b_str;
config_b.SerializeToString(&config_b_str);
if (config_a_str != config_b_str) {
return false;
}
const auto& value_a = config_value_a.value();
const auto& value_b = config_value_b.value();
// Not sure if this should be compared
if (value_a.weak() != value_b.weak()) {
return false;
}
if (value_a.has_item() != value_b.has_item()) {
return false;
}
std::string value_a_str;
std::string value_b_str;
if (value_a.has_item()) {
value_a.item().SerializeToString(&value_a_str);
value_b.item().SerializeToString(&value_b_str);
} else {
value_a.compound_value().SerializeToString(&value_a_str);
value_b.compound_value().SerializeToString(&value_b_str);
}
if (value_a_str != value_b_str) {
return false;
}
}
return true;
}
ResourcesPbFile::~ResourcesPbFile() {}
#endif // HAS_PROTOBUF