diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index eb3a99ae63317..dc181ea5c4c29 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -110,6 +110,7 @@ cc_library_host_static { "link/XmlReferenceLinker.cpp", "optimize/MultiApkGenerator.cpp", "optimize/ResourceDeduper.cpp", + "optimize/ResourceFilter.cpp", "optimize/VersionCollapser.cpp", "process/SymbolTable.cpp", "split/TableSplitter.cpp", diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 12ab883454111..b264524043ce5 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 9c76119f9504a..63ebc60ccc3a3 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -38,6 +38,7 @@ #include "io/Util.h" #include "optimize/MultiApkGenerator.h" #include "optimize/ResourceDeduper.h" +#include "optimize/ResourceFilter.h" #include "optimize/VersionCollapser.h" #include "split/TableSplitter.h" #include "util/Files.h" @@ -62,6 +63,9 @@ struct OptimizeOptions { // Details of the app extracted from the AndroidManifest.xml AppInfo app_info; + // Blacklist of unused resources that should be removed from the apk. + std::unordered_set resources_blacklist; + // Split APK options. TableSplitterOptions table_splitter_options; @@ -147,6 +151,13 @@ class OptimizeCommand { if (context_->IsVerbose()) { context_->GetDiagnostics()->Note(DiagMessage() << "Optimizing APK..."); } + if (!options_.resources_blacklist.empty()) { + ResourceFilter filter(options_.resources_blacklist); + if (!filter.Consume(context_, apk->GetResourceTable())) { + context_->GetDiagnostics()->Error(DiagMessage() << "failed filtering resources"); + return 1; + } + } VersionCollapser collapser; if (!collapser.Consume(context_, apk->GetResourceTable())) { @@ -284,16 +295,62 @@ class OptimizeCommand { OptimizeContext* context_; }; -bool ExtractWhitelistFromConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { +bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { std::string contents; if (!ReadFileToString(path, &contents, true)) { context->GetDiagnostics()->Error(DiagMessage() << "failed to parse whitelist from config file: " << path); return false; } - for (const StringPiece& resource_name : util::Tokenize(contents, ',')) { - options->table_flattener_options.whitelisted_resources.insert(resource_name.to_string()); + for (StringPiece resource_name : util::Tokenize(contents, ',')) { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.to_string()); + } + return true; +} + +bool ExtractConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { + std::string content; + if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { + context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist"); + return false; + } + + size_t line_no = 0; + for (StringPiece line : util::Tokenize(content, '\n')) { + line_no++; + line = util::TrimWhitespace(line); + if (line.empty()) { + continue; + } + + auto split_line = util::Split(line, '#'); + if (split_line.size() < 2) { + context->GetDiagnostics()->Error(DiagMessage(line) << "No # found in line"); + return false; + } + StringPiece resource_string = split_line[0]; + StringPiece directives = split_line[1]; + ResourceNameRef resource_name; + if (!ResourceUtils::ParseResourceName(resource_string, &resource_name)) { + context->GetDiagnostics()->Error(DiagMessage(line) << "Malformed resource name"); + return false; + } + if (!resource_name.package.empty()) { + context->GetDiagnostics()->Error(DiagMessage(line) + << "Package set for resource. Only use type/name"); + return false; + } + for (StringPiece directive : util::Tokenize(directives, ',')) { + if (directive == "remove") { + options->resources_blacklist.insert(resource_name.ToResourceName()); + } else if (directive == "no_obfuscate") { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.entry.to_string()); + } + } } return true; } @@ -322,6 +379,7 @@ int Optimize(const std::vector& args) { OptimizeOptions options; Maybe config_path; Maybe whitelist_path; + Maybe resources_config_path; Maybe target_densities; std::vector configs; std::vector split_args; @@ -340,10 +398,15 @@ int Optimize(const std::vector& args) { "All the resources that would be unused on devices of the given densities will be \n" "removed from the APK.", &target_densities) - .OptionalFlag("--whitelist-config-path", + .OptionalFlag("--whitelist-path", "Path to the whitelist.cfg file containing whitelisted resources \n" "whose names should not be altered in final resource tables.", &whitelist_path) + .OptionalFlag("--resources-config-path", + "Path to the resources.cfg file containing the list of resources and \n" + "directives to each resource. \n" + "Format: type/resource_name#[directive][,directive]", + &resources_config_path) .OptionalFlagList("-c", "Comma separated list of configurations to include. The default\n" "is all configurations.", @@ -460,12 +523,19 @@ int Optimize(const std::vector& args) { if (options.table_flattener_options.collapse_key_stringpool) { if (whitelist_path) { std::string& path = whitelist_path.value(); - if (!ExtractWhitelistFromConfig(path, &context, &options)) { + if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options)) { return 1; } } } + if (resources_config_path) { + std::string& path = resources_config_path.value(); + if (!ExtractConfig(path, &context, &options)) { + return 1; + } + } + if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) { return 1; } diff --git a/tools/aapt2/optimize/ResourceFilter.cpp b/tools/aapt2/optimize/ResourceFilter.cpp new file mode 100644 index 0000000000000..250b65197a7d1 --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourceFilter.h" + +#include "ResourceTable.h" + +namespace aapt { + +ResourceFilter::ResourceFilter(const std::unordered_set& blacklist) + : blacklist_(blacklist) { +} + +bool ResourceFilter::Consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto it = type->entries.begin(); it != type->entries.end(); ) { + ResourceName resource = ResourceName({}, type->type, (*it)->name); + if (blacklist_.find(resource) != blacklist_.end()) { + it = type->entries.erase(it); + } else { + ++it; + } + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/optimize/ResourceFilter.h b/tools/aapt2/optimize/ResourceFilter.h new file mode 100644 index 0000000000000..d4baf654b0ff7 --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_OPTIMIZE_RESOURCEFILTER_H +#define AAPT_OPTIMIZE_RESOURCEFILTER_H + +#include "android-base/macros.h" + +#include "process/IResourceTableConsumer.h" + +#include + +namespace aapt { + +// Removes non-whitelisted entries from resource table. +class ResourceFilter : public IResourceTableConsumer { + public: + explicit ResourceFilter(const std::unordered_set& blacklist); + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(ResourceFilter); + std::unordered_set blacklist_; +}; + +} // namespace aapt + +#endif // AAPT_OPTIMIZE_RESOURCEFILTER_H diff --git a/tools/aapt2/optimize/ResourceFilter_test.cpp b/tools/aapt2/optimize/ResourceFilter_test.cpp new file mode 100644 index 0000000000000..800b2bfd0403f --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter_test.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourceFilter.h" + +#include "ResourceTable.h" +#include "test/Test.h" + +using ::aapt::test::HasValue; +using ::testing::Not; + +namespace aapt { + +TEST(ResourceFilterTest, SomeValuesAreFilteredOut) { + std::unique_ptr context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/notblacklisted2", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted2", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + ResourceName({}, ResourceType::kString, "blacklisted2"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:string/notblacklisted2", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted2", default_config))); +} + +TEST(ResourceFilterTest, TypeIsCheckedBeforeFiltering) { + std::unique_ptr context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/blacklisted", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/blacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/notblacklisted", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); +} + +} // namespace aapt