From 326e35ffaf0ee1e3d07c977217f4e600088fd9d5 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 12 Apr 2021 07:50:42 -0700 Subject: [PATCH] Add tag to aapt2 AAPT2 Macros are compile-time resources definitions that are expanded when referenced during the link phase. A macro must be defined in the res/values.xml directory. A macro definition for a macro named "foo" looks like the following: contents When "@macro/foo" is used in the res/values directory or in a compiled XML file, the contents of the macro replace the macro reference and then the substituted contents are compiled and linked. If the macro contents reference xml namespaces from its original definition, the namespaces of the original macro definition will be used to determine which package is being referenced. Macros can be used anywhere resources can be referenced using the @package:type/entry syntax. Macros are not included in the final resource table or the R.java since they are not actual resources. Bug: 175616308 Test: aapt2_tests Change-Id: I48b29ab6564357b32b4b4e32bff7ef06036382bc --- tools/aapt2/Resource.cpp | 3 + tools/aapt2/Resource.h | 1 + tools/aapt2/ResourceParser.cpp | 114 ++++-- tools/aapt2/ResourceParser.h | 20 +- tools/aapt2/ResourceParser_test.cpp | 84 ++++ tools/aapt2/ResourceUtils.cpp | 9 +- tools/aapt2/ResourceUtils.h | 4 +- tools/aapt2/ResourceValues.cpp | 35 +- tools/aapt2/ResourceValues.h | 27 ++ tools/aapt2/Resources.proto | 37 ++ tools/aapt2/StringPool.h | 4 + tools/aapt2/ValueTransformer.cpp | 1 + tools/aapt2/ValueTransformer.h | 2 + tools/aapt2/ValueVisitor.h | 6 + tools/aapt2/cmd/Link.cpp | 6 +- tools/aapt2/cmd/Link_test.cpp | 106 ++++++ tools/aapt2/format/binary/TableFlattener.cpp | 7 +- .../aapt2/format/binary/XmlFlattener_test.cpp | 2 +- tools/aapt2/format/proto/ProtoDeserialize.cpp | 41 ++ tools/aapt2/format/proto/ProtoSerialize.cpp | 35 ++ .../format/proto/ProtoSerialize_test.cpp | 34 ++ tools/aapt2/java/JavaClassGenerator.cpp | 5 +- tools/aapt2/java/JavaClassGenerator_test.cpp | 21 + tools/aapt2/java/ProguardRules_test.cpp | 2 +- tools/aapt2/link/Linkers.h | 4 +- tools/aapt2/link/ReferenceLinker.cpp | 360 +++++++++++------- tools/aapt2/link/ReferenceLinker.h | 60 ++- tools/aapt2/link/ReferenceLinker_test.cpp | 18 + tools/aapt2/link/XmlCompatVersioner_test.cpp | 8 +- tools/aapt2/link/XmlReferenceLinker.cpp | 46 +-- tools/aapt2/link/XmlReferenceLinker_test.cpp | 20 +- tools/aapt2/xml/XmlPullParser.cpp | 4 + tools/aapt2/xml/XmlPullParser.h | 12 +- 33 files changed, 879 insertions(+), 259 deletions(-) diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp index b78f48ce7f176..6364ccdd09e5c 100644 --- a/tools/aapt2/Resource.cpp +++ b/tools/aapt2/Resource.cpp @@ -78,6 +78,8 @@ StringPiece to_string(ResourceType type) { return "interpolator"; case ResourceType::kLayout: return "layout"; + case ResourceType::kMacro: + return "macro"; case ResourceType::kMenu: return "menu"; case ResourceType::kMipmap: @@ -119,6 +121,7 @@ static const std::map sResourceTypeMap{ {"integer", ResourceType::kInteger}, {"interpolator", ResourceType::kInterpolator}, {"layout", ResourceType::kLayout}, + {"macro", ResourceType::kMacro}, {"menu", ResourceType::kMenu}, {"mipmap", ResourceType::kMipmap}, {"navigation", ResourceType::kNavigation}, diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index cf938703e1e99..307c21d9dc965 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -57,6 +57,7 @@ enum class ResourceType { kInteger, kInterpolator, kLayout, + kMacro, kMenu, kMipmap, kNavigation, diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 24c60b740bc35..1efabbb46fd59 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -627,6 +627,16 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, } return true; + } else if (resource_type == "macro") { + if (!maybe_name) { + diag_->Error(DiagMessage(out_resource->source) + << "<" << parser->element_name() << "> missing 'name' attribute"); + return false; + } + + out_resource->name.type = ResourceType::kMacro; + out_resource->name.entry = maybe_name.value().to_string(); + return ParseMacro(parser, out_resource); } if (can_be_item) { @@ -726,16 +736,8 @@ bool ResourceParser::ParseItem(xml::XmlPullParser* parser, return true; } -/** - * Reads the entire XML subtree and attempts to parse it as some Item, - * with typeMask denoting which items it can be. If allowRawValue is - * true, a RawString is returned if the XML couldn't be parsed as - * an Item. If allowRawValue is false, nullptr is returned in this - * case. - */ -std::unique_ptr ResourceParser::ParseXml(xml::XmlPullParser* parser, - const uint32_t type_mask, - const bool allow_raw_value) { +std::optional ResourceParser::CreateFlattenSubTree( + xml::XmlPullParser* parser) { const size_t begin_xml_line = parser->line_number(); std::string raw_value; @@ -745,30 +747,60 @@ std::unique_ptr ResourceParser::ParseXml(xml::XmlPullParser* parser, return {}; } - if (!style_string.spans.empty()) { + return FlattenedXmlSubTree{.raw_value = raw_value, + .style_string = style_string, + .untranslatable_sections = untranslatable_sections, + .namespace_resolver = parser, + .source = source_.WithLine(begin_xml_line)}; +} + +/** + * Reads the entire XML subtree and attempts to parse it as some Item, + * with typeMask denoting which items it can be. If allowRawValue is + * true, a RawString is returned if the XML couldn't be parsed as + * an Item. If allowRawValue is false, nullptr is returned in this + * case. + */ +std::unique_ptr ResourceParser::ParseXml(xml::XmlPullParser* parser, const uint32_t type_mask, + const bool allow_raw_value) { + auto sub_tree = CreateFlattenSubTree(parser); + if (!sub_tree.has_value()) { + return {}; + } + return ParseXml(sub_tree.value(), type_mask, allow_raw_value, *table_, config_, *diag_); +} + +std::unique_ptr ResourceParser::ParseXml(const FlattenedXmlSubTree& xmlsub_tree, + const uint32_t type_mask, const bool allow_raw_value, + ResourceTable& table, + const android::ConfigDescription& config, + IDiagnostics& diag) { + if (!xmlsub_tree.style_string.spans.empty()) { // This can only be a StyledString. std::unique_ptr styled_string = - util::make_unique(table_->string_pool.MakeRef( - style_string, StringPool::Context(StringPool::Context::kNormalPriority, config_))); - styled_string->untranslatable_sections = std::move(untranslatable_sections); + util::make_unique(table.string_pool.MakeRef( + xmlsub_tree.style_string, + StringPool::Context(StringPool::Context::kNormalPriority, config))); + styled_string->untranslatable_sections = xmlsub_tree.untranslatable_sections; return std::move(styled_string); } auto on_create_reference = [&](const ResourceName& name) { // name.package can be empty here, as it will assume the package name of the // table. - std::unique_ptr id = util::make_unique(); - id->SetSource(source_.WithLine(begin_xml_line)); - table_->AddResource(NewResourceBuilder(name).SetValue(std::move(id)).Build(), diag_); + auto id = util::make_unique(); + id->SetSource(xmlsub_tree.source); + return table.AddResource(NewResourceBuilder(name).SetValue(std::move(id)).Build(), &diag); }; // Process the raw value. - std::unique_ptr processed_item = - ResourceUtils::TryParseItemForAttribute(raw_value, type_mask, on_create_reference); + std::unique_ptr processed_item = ResourceUtils::TryParseItemForAttribute( + xmlsub_tree.raw_value, type_mask, on_create_reference); if (processed_item) { // Fix up the reference. - if (Reference* ref = ValueCast(processed_item.get())) { - ResolvePackage(parser, ref); + if (auto ref = ValueCast(processed_item.get())) { + ref->allow_raw = allow_raw_value; + ResolvePackage(xmlsub_tree.namespace_resolver, ref); } return processed_item; } @@ -777,17 +809,16 @@ std::unique_ptr ResourceParser::ParseXml(xml::XmlPullParser* parser, if (type_mask & android::ResTable_map::TYPE_STRING) { // Use the trimmed, escaped string. std::unique_ptr string = util::make_unique( - table_->string_pool.MakeRef(style_string.str, StringPool::Context(config_))); - string->untranslatable_sections = std::move(untranslatable_sections); + table.string_pool.MakeRef(xmlsub_tree.style_string.str, StringPool::Context(config))); + string->untranslatable_sections = xmlsub_tree.untranslatable_sections; return std::move(string); } if (allow_raw_value) { // We can't parse this so return a RawString if we are allowed. - return util::make_unique( - table_->string_pool.MakeRef(util::TrimWhitespace(raw_value), - StringPool::Context(config_))); - } else if (util::TrimWhitespace(raw_value).empty()) { + return util::make_unique(table.string_pool.MakeRef( + util::TrimWhitespace(xmlsub_tree.raw_value), StringPool::Context(config))); + } else if (util::TrimWhitespace(xmlsub_tree.raw_value).empty()) { // If the text is empty, and the value is not allowed to be a string, encode it as a @null. return ResourceUtils::MakeNull(); } @@ -850,6 +881,35 @@ bool ResourceParser::ParseString(xml::XmlPullParser* parser, return true; } +bool ResourceParser::ParseMacro(xml::XmlPullParser* parser, ParsedResource* out_resource) { + auto sub_tree = CreateFlattenSubTree(parser); + if (!sub_tree) { + return false; + } + + if (out_resource->config != ConfigDescription::DefaultConfig()) { + diag_->Error(DiagMessage(out_resource->source) + << " tags cannot be declared in configurations other than the default " + "configuration'"); + return false; + } + + auto macro = std::make_unique(); + macro->raw_value = std::move(sub_tree->raw_value); + macro->style_string = std::move(sub_tree->style_string); + macro->untranslatable_sections = std::move(sub_tree->untranslatable_sections); + + for (const auto& decl : parser->package_decls()) { + macro->alias_namespaces.emplace_back( + Macro::Namespace{.alias = decl.prefix, + .package_name = decl.package.package, + .is_private = decl.package.private_namespace}); + } + + out_resource->value = std::move(macro); + return true; +} + bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource) { if (options_.visibility) { diag_->Error(DiagMessage(out_resource->source) diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index af0db8c0ba2ca..5c92def50616f 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -57,6 +57,14 @@ struct ResourceParserOptions { Maybe visibility; }; +struct FlattenedXmlSubTree { + std::string raw_value; + StyleString style_string; + std::vector untranslatable_sections; + xml::IPackageDeclStack* namespace_resolver; + Source source; +}; + /* * Parses an XML file for resources and adds them to a ResourceTable. */ @@ -67,9 +75,16 @@ class ResourceParser { const ResourceParserOptions& options = {}); bool Parse(xml::XmlPullParser* parser); + static std::unique_ptr ParseXml(const FlattenedXmlSubTree& xmlsub_tree, uint32_t type_mask, + bool allow_raw_value, ResourceTable& table, + const android::ConfigDescription& config, + IDiagnostics& diag); + private: DISALLOW_COPY_AND_ASSIGN(ResourceParser); + std::optional CreateFlattenSubTree(xml::XmlPullParser* parser); + // Parses the XML subtree as a StyleString (flattened XML representation for strings with // formatting). If parsing fails, false is returned and the out parameters are left in an // unspecified state. Otherwise, @@ -96,7 +111,7 @@ class ResourceParser { bool ParseItem(xml::XmlPullParser* parser, ParsedResource* out_resource, uint32_t format); bool ParseString(xml::XmlPullParser* parser, ParsedResource* out_resource); - + bool ParseMacro(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseStagingPublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource); @@ -108,8 +123,7 @@ class ResourceParser { bool ParseAttrImpl(xml::XmlPullParser* parser, ParsedResource* out_resource, bool weak); Maybe ParseEnumOrFlagItem(xml::XmlPullParser* parser, const android::StringPiece& tag); - bool ParseStyle(const ResourceType type, xml::XmlPullParser* parser, - ParsedResource* out_resource); + bool ParseStyle(ResourceType type, xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseStyleItem(xml::XmlPullParser* parser, Style* style); bool ParseDeclareStyleable(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseArray(xml::XmlPullParser* parser, ParsedResource* out_resource); diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 4a509be567765..279ebcba2f71e 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -336,6 +336,90 @@ TEST_F(ResourceParserTest, ParseAttr) { EXPECT_THAT(attr->type_mask, Eq(ResTable_map::TYPE_ANY)); } +TEST_F(ResourceParserTest, ParseMacro) { + std::string input = R"(12345)"; + ASSERT_TRUE(TestParse(input)); + + Macro* macro = test::GetValue(&table_, "macro/foo"); + ASSERT_THAT(macro, NotNull()); + EXPECT_THAT(macro->raw_value, Eq("12345")); + EXPECT_THAT(macro->style_string.str, Eq("12345")); + EXPECT_THAT(macro->style_string.spans, IsEmpty()); + EXPECT_THAT(macro->untranslatable_sections, IsEmpty()); + EXPECT_THAT(macro->alias_namespaces, IsEmpty()); +} + +TEST_F(ResourceParserTest, ParseMacroUntranslatableSection) { + std::string input = R"( +This being human is a guest house.)"; + ASSERT_TRUE(TestParse(input)); + + Macro* macro = test::GetValue(&table_, "macro/foo"); + ASSERT_THAT(macro, NotNull()); + EXPECT_THAT(macro->raw_value, Eq("\nThis being human is a guest house.")); + EXPECT_THAT(macro->style_string.str, Eq(" This being human is a guest house.")); + EXPECT_THAT(macro->style_string.spans.size(), Eq(1)); + EXPECT_THAT(macro->style_string.spans[0].name, Eq("b")); + EXPECT_THAT(macro->style_string.spans[0].first_char, Eq(12)); + EXPECT_THAT(macro->style_string.spans[0].last_char, Eq(16)); + ASSERT_THAT(macro->untranslatable_sections.size(), Eq(1)); + EXPECT_THAT(macro->untranslatable_sections[0].start, Eq(12)); + EXPECT_THAT(macro->untranslatable_sections[0].end, Eq(17)); + EXPECT_THAT(macro->alias_namespaces, IsEmpty()); +} + +TEST_F(ResourceParserTest, ParseMacroNamespaces) { + std::string input = R"( +@app:string/foo)"; + ASSERT_TRUE(TestParse(input)); + + Macro* macro = test::GetValue(&table_, "macro/foo"); + ASSERT_THAT(macro, NotNull()); + EXPECT_THAT(macro->raw_value, Eq("\n@app:string/foo")); + EXPECT_THAT(macro->style_string.str, Eq("@app:string/foo")); + EXPECT_THAT(macro->style_string.spans, IsEmpty()); + EXPECT_THAT(macro->untranslatable_sections, IsEmpty()); + EXPECT_THAT(macro->alias_namespaces.size(), Eq(1)); + EXPECT_THAT(macro->alias_namespaces[0].alias, Eq("app")); + EXPECT_THAT(macro->alias_namespaces[0].package_name, Eq("android")); + EXPECT_THAT(macro->alias_namespaces[0].is_private, Eq(false)); +} + +TEST_F(ResourceParserTest, ParseMacroReference) { + std::string input = R"(@macro/foo)"; + ASSERT_TRUE(TestParse(input)); + + Reference* macro = test::GetValue(&table_, "string/res_string"); + ASSERT_THAT(macro, NotNull()); + EXPECT_THAT(macro->type_flags, Eq(ResTable_map::TYPE_STRING)); + EXPECT_THAT(macro->allow_raw, Eq(false)); + + input = R"()"; + + ASSERT_TRUE(TestParse(input)); + Style* style = test::GetValue + )"; + + const std::string xml_values = + R"( + )"; + + // Build a library with a public attribute + const std::string lib_res = GetTestPath("test-res"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), values, lib_res, &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/layout/layout.xml"), xml_values, lib_res, &diag)); + + const std::string lib_apk = GetTestPath("test.apk"); + // clang-format off + auto lib_link_args = LinkCommandBuilder(this) + .SetManifestFile(ManifestBuilder(this).SetPackageName("com.test").Build()) + .AddCompiledResDir(lib_res, &diag) + .AddFlag("--no-auto-version") + .Build(lib_apk); + // clang-format on + ASSERT_TRUE(Link(lib_link_args, &diag)); + + auto apk = LoadedApk::LoadApkFromPath(lib_apk, &diag); + ASSERT_THAT(apk, NotNull()); + + // Test that the type flags determines the value type + auto actual_bool = + test::GetValue(apk->GetResourceTable(), "com.test:bool/is_enabled_bool"); + ASSERT_THAT(actual_bool, NotNull()); + EXPECT_EQ(android::Res_value::TYPE_INT_BOOLEAN, actual_bool->value.dataType); + EXPECT_EQ(0xffffffffu, actual_bool->value.data); + + auto actual_str = + test::GetValue(apk->GetResourceTable(), "com.test:string/is_enabled_str"); + ASSERT_THAT(actual_str, NotNull()); + EXPECT_EQ(*actual_str->value, "true"); + + // Test nested data structures + auto actual_array = test::GetValue(apk->GetResourceTable(), "com.test:array/my_array"); + ASSERT_THAT(actual_array, NotNull()); + EXPECT_THAT(actual_array->elements.size(), Eq(1)); + + auto array_el_ref = ValueCast(actual_array->elements[0].get()); + ASSERT_THAT(array_el_ref, NotNull()); + EXPECT_THAT(array_el_ref->value.dataType, Eq(android::Res_value::TYPE_INT_BOOLEAN)); + EXPECT_THAT(array_el_ref->value.data, Eq(0xffffffffu)); + + auto actual_style = test::GetValue