Resource Path Obfuscation
This CL allows aapt2 to obfuscate resource paths within the output apk
and move resources to shorter obfuscated paths. This reduces apk size
when there is a large number of resources since the path metadata exists
in 4 places in the apk.
This CL adds two arguments to aapt2, one to enable resource path
obfuscation and one to point to a path to output the path map to (for
later debugging).
Test: make aapt2_tests
Bug: b/75965637
Change-Id: I9cacafe1d17800d673566b2d61b0b88f3fb8d60c
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index ba498e1..a99cfe6 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -114,6 +114,7 @@
"optimize/MultiApkGenerator.cpp",
"optimize/ResourceDeduper.cpp",
"optimize/ResourceFilter.cpp",
+ "optimize/ResourcePathShortener.cpp",
"optimize/VersionCollapser.cpp",
"process/SymbolTable.cpp",
"split/TableSplitter.cpp",
diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp
index b353ff0..45719ef 100644
--- a/tools/aapt2/LoadedApk.cpp
+++ b/tools/aapt2/LoadedApk.cpp
@@ -223,8 +223,17 @@
io::IFile* file = iterator->Next();
std::string path = file->GetSource().path;
+ std::string output_path = path;
+ bool is_resource = path.find("res/") == 0;
+ if (is_resource) {
+ auto it = options.shortened_path_map.find(path);
+ if (it != options.shortened_path_map.end()) {
+ output_path = it->second;
+ }
+ }
+
// Skip resources that are not referenced if requested.
- if (path.find("res/") == 0 && referenced_resources.find(path) == referenced_resources.end()) {
+ if (is_resource && referenced_resources.find(output_path) == referenced_resources.end()) {
if (context->IsVerbose()) {
context->GetDiagnostics()->Note(DiagMessage()
<< "Removing resource '" << path << "' from APK.");
@@ -283,7 +292,8 @@
return false;
}
} else {
- if (!io::CopyFileToArchivePreserveCompression(context, file, path, writer)) {
+ if (!io::CopyFileToArchivePreserveCompression(
+ context, file, output_path, writer)) {
return false;
}
}
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 328b0be..2e6af18 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -41,6 +41,7 @@
#include "optimize/MultiApkGenerator.h"
#include "optimize/ResourceDeduper.h"
#include "optimize/ResourceFilter.h"
+#include "optimize/ResourcePathShortener.h"
#include "optimize/VersionCollapser.h"
#include "split/TableSplitter.h"
#include "util/Files.h"
@@ -52,6 +53,7 @@
using ::android::ResTable_config;
using ::android::StringPiece;
using ::android::base::ReadFileToString;
+using ::android::base::WriteStringToFile;
using ::android::base::StringAppendF;
using ::android::base::StringPrintf;
@@ -143,6 +145,21 @@
return 1;
}
+ if (options_.shorten_resource_paths) {
+ ResourcePathShortener shortener(options_.table_flattener_options.shortened_path_map);
+ if (!shortener.Consume(context_, apk->GetResourceTable())) {
+ context_->GetDiagnostics()->Error(DiagMessage() << "failed shortening resource paths");
+ return 1;
+ }
+ if (options_.shortened_paths_map_path
+ && !WriteShortenedPathsMap(options_.table_flattener_options.shortened_path_map,
+ options_.shortened_paths_map_path.value())) {
+ context_->GetDiagnostics()->Error(DiagMessage()
+ << "failed to write shortened resource paths to file");
+ return 1;
+ }
+ }
+
// Adjust the SplitConstraints so that their SDK version is stripped if it is less than or
// equal to the minSdk.
options_.split_constraints =
@@ -264,6 +281,15 @@
ArchiveEntry::kAlign, writer);
}
+ bool WriteShortenedPathsMap(const std::map<std::string, std::string> &path_map,
+ const std::string &file_path) {
+ std::stringstream ss;
+ for (auto it = path_map.cbegin(); it != path_map.cend(); ++it) {
+ ss << it->first << " -> " << it->second << "\n";
+ }
+ return WriteStringToFile(ss.str(), file_path);
+ }
+
OptimizeOptions options_;
OptimizeContext* context_;
};
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 43bc216..7df9cf7 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -55,6 +55,12 @@
// Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts
// are kept and will be written as output.
std::unordered_set<std::string> kept_artifacts;
+
+ // Whether or not to shorten resource paths in the APK.
+ bool shorten_resource_paths;
+
+ // Path to the output map of original resource paths to shortened paths.
+ Maybe<std::string> shortened_paths_map_path;
};
class OptimizeCommand : public Command {
@@ -100,6 +106,12 @@
AddOptionalSwitch("--enable-resource-obfuscation",
"Enables obfuscation of key string pool to single value",
&options_.table_flattener_options.collapse_key_stringpool);
+ AddOptionalSwitch("--enable-resource-path-shortening",
+ "Enables shortening of the path of the resources inside the APK.",
+ &options_.shorten_resource_paths);
+ AddOptionalFlag("--resource-path-shortening-map",
+ "Path to output the map of old resource paths to shortened paths.",
+ &options_.shortened_paths_map_path);
AddOptionalSwitch("-v", "Enables verbose logging", &verbose_);
}
@@ -108,6 +120,9 @@
private:
OptimizeOptions options_;
+ bool WriteObfuscatedPathsMap(const std::map<std::string, std::string> &path_map,
+ const std::string &file_path);
+
Maybe<std::string> config_path_;
Maybe<std::string> whitelist_path_;
Maybe<std::string> resources_config_path_;
@@ -121,4 +136,4 @@
}// namespace aapt
-#endif //AAPT2_OPTIMIZE_H
\ No newline at end of file
+#endif //AAPT2_OPTIMIZE_H
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 635cb21..71330e3 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -46,6 +46,9 @@
// When true, sort the entries in the values string pool by priority and configuration.
bool sort_stringpool_entries = true;
+
+ // Map from original resource paths to shortened resource paths.
+ std::map<std::string, std::string> shortened_path_map;
};
class TableFlattener : public IResourceTableConsumer {
diff --git a/tools/aapt2/optimize/ResourcePathShortener.cpp b/tools/aapt2/optimize/ResourcePathShortener.cpp
new file mode 100644
index 0000000..c5df3dd
--- /dev/null
+++ b/tools/aapt2/optimize/ResourcePathShortener.cpp
@@ -0,0 +1,112 @@
+/*
+ * 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/ResourcePathShortener.h"
+
+#include <math.h>
+#include <unordered_set>
+
+#include "androidfw/StringPiece.h"
+
+#include "ResourceTable.h"
+#include "ValueVisitor.h"
+
+
+static const std::string base64_chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789-_";
+
+namespace aapt {
+
+ResourcePathShortener::ResourcePathShortener(
+ std::map<std::string, std::string>& path_map_out)
+ : path_map_(path_map_out) {
+}
+
+std::string ShortenFileName(const android::StringPiece& file_path, int output_length) {
+ std::size_t hash_num = std::hash<android::StringPiece>{}(file_path);
+ std::string result = "";
+ // Convert to (modified) base64 so that it is a proper file path.
+ for (int i = 0; i < output_length; i++) {
+ uint8_t sextet = hash_num & 0x3f;
+ hash_num >>= 6;
+ result += base64_chars[sextet];
+ }
+ return result;
+}
+
+
+// Calculate the optimal hash length such that an average of 10% of resources
+// collide in their shortened path.
+// Reference: http://matt.might.net/articles/counting-hash-collisions/
+int OptimalShortenedLength(int num_resources) {
+ int num_chars = 2;
+ double N = 64*64; // hash space when hash is 2 chars long
+ double max_collisions = num_resources * 0.1;
+ while (num_resources - N + N * pow((N - 1) / N, num_resources) > max_collisions) {
+ N *= 64;
+ num_chars++;
+ }
+ return num_chars;
+}
+
+std::string GetShortenedPath(const android::StringPiece& shortened_filename,
+ const android::StringPiece& extension, int collision_count) {
+ std::string shortened_path = "res/" + shortened_filename.to_string();
+ if (collision_count > 0) {
+ shortened_path += std::to_string(collision_count);
+ }
+ shortened_path += extension;
+ return shortened_path;
+}
+
+bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) {
+ // used to detect collisions
+ std::unordered_set<std::string> shortened_paths;
+ std::unordered_set<FileReference*> file_refs;
+ for (auto& package : table->packages) {
+ for (auto& type : package->types) {
+ for (auto& entry : type->entries) {
+ for (auto& config_value : entry->values) {
+ FileReference* file_ref = ValueCast<FileReference>(config_value->value.get());
+ if (file_ref) {
+ file_refs.insert(file_ref);
+ }
+ }
+ }
+ }
+ }
+ int num_chars = OptimalShortenedLength(file_refs.size());
+ for (auto& file_ref : file_refs) {
+ android::StringPiece res_subdir, actual_filename, extension;
+ util::ExtractResFilePathParts(*file_ref->path, &res_subdir, &actual_filename, &extension);
+
+ std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars);
+ int collision_count = 0;
+ std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count);
+ while (shortened_paths.find(shortened_path) != shortened_paths.end()) {
+ collision_count++;
+ shortened_path = GetShortenedPath(shortened_filename, extension, collision_count);
+ }
+ shortened_paths.insert(shortened_path);
+ path_map_.insert({*file_ref->path, shortened_path});
+ file_ref->path = table->string_pool.MakeRef(shortened_path, file_ref->path.GetContext());
+ }
+ return true;
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/optimize/ResourcePathShortener.h b/tools/aapt2/optimize/ResourcePathShortener.h
new file mode 100644
index 0000000..f1074ef
--- /dev/null
+++ b/tools/aapt2/optimize/ResourcePathShortener.h
@@ -0,0 +1,44 @@
+/*
+ * 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_RESOURCEPATHSHORTENER_H
+#define AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H
+
+#include <map>
+
+#include "android-base/macros.h"
+
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+class ResourceTable;
+
+// Maps resources in the apk to shortened paths.
+class ResourcePathShortener : public IResourceTableConsumer {
+ public:
+ explicit ResourcePathShortener(std::map<std::string, std::string>& path_map_out);
+
+ bool Consume(IAaptContext* context, ResourceTable* table) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ResourcePathShortener);
+ std::map<std::string, std::string>& path_map_;
+};
+
+} // namespace aapt
+
+#endif // AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H
diff --git a/tools/aapt2/optimize/ResourcePathShortener_test.cpp b/tools/aapt2/optimize/ResourcePathShortener_test.cpp
new file mode 100644
index 0000000..88cadc7
--- /dev/null
+++ b/tools/aapt2/optimize/ResourcePathShortener_test.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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/ResourcePathShortener.h"
+
+#include "ResourceTable.h"
+#include "test/Test.h"
+
+using ::aapt::test::GetValue;
+using ::testing::Not;
+using ::testing::NotNull;
+using ::testing::Eq;
+
+namespace aapt {
+
+TEST(ResourcePathShortenerTest, FileRefPathsChangedInResourceTable) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .AddFileReference("android:drawable/xmlfile", "res/drawables/xmlfile.xml")
+ .AddFileReference("android:drawable/xmlfile2", "res/drawables/xmlfile2.xml")
+ .AddString("android:string/string", "res/should/still/be/the/same.png")
+ .Build();
+
+ std::map<std::string, std::string> path_map;
+ ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get()));
+
+ // Expect that the path map is populated
+ ASSERT_THAT(path_map.find("res/drawables/xmlfile.xml"), Not(Eq(path_map.end())));
+ ASSERT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
+
+ // The file paths were changed
+ EXPECT_THAT(path_map.at("res/drawables/xmlfile.xml"), Not(Eq("res/drawables/xmlfile.xml")));
+ EXPECT_THAT(path_map.at("res/drawables/xmlfile2.xml"), Not(Eq("res/drawables/xmlfile2.xml")));
+
+ // Different file paths should remain different
+ EXPECT_THAT(path_map["res/drawables/xmlfile.xml"],
+ Not(Eq(path_map["res/drawables/xmlfile2.xml"])));
+
+ FileReference* ref =
+ GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
+ ASSERT_THAT(ref, NotNull());
+ // The map correctly points to the new location of the file
+ EXPECT_THAT(path_map["res/drawables/xmlfile.xml"], Eq(*ref->path));
+
+ // Strings should not be affected, only file paths
+ EXPECT_THAT(
+ *GetValue<String>(table.get(), "android:string/string")->value,
+ Eq("res/should/still/be/the/same.png"));
+ EXPECT_THAT(path_map.find("res/should/still/be/the/same.png"), Eq(path_map.end()));
+}
+
+} // namespace aapt