1 /*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "optimize/Obfuscator.h"
18
19 #include <map>
20 #include <memory>
21 #include <string>
22 #include <utility>
23
24 #include "ResourceTable.h"
25 #include "android-base/file.h"
26 #include "test/Test.h"
27
28 using ::aapt::test::GetValue;
29 using ::testing::AnyOf;
30 using ::testing::Contains;
31 using ::testing::Eq;
32 using ::testing::HasSubstr;
33 using ::testing::IsFalse;
34 using ::testing::IsTrue;
35 using ::testing::Not;
36 using ::testing::NotNull;
37
38 namespace aapt {
39
40 namespace {
41
GetExtension(android::StringPiece path)42 android::StringPiece GetExtension(android::StringPiece path) {
43 auto iter = std::find(path.begin(), path.end(), '.');
44 return android::StringPiece(iter, path.end() - iter);
45 }
46
FillTable(aapt::test::ResourceTableBuilder & builder,int start,int end)47 void FillTable(aapt::test::ResourceTableBuilder& builder, int start, int end) {
48 for (int i = start; i < end; i++) {
49 builder.AddFileReference("android:drawable/xmlfile" + std::to_string(i),
50 "res/drawable/xmlfile" + std::to_string(i) + ".xml");
51 }
52 }
53
54 class FakeObfuscator : public Obfuscator {
55 public:
FakeObfuscator(OptimizeOptions & optimize_options,const std::unordered_map<std::string,std::string> & shortened_name_map)56 explicit FakeObfuscator(OptimizeOptions& optimize_options,
57 const std::unordered_map<std::string, std::string>& shortened_name_map)
58 : Obfuscator(optimize_options), shortened_name_map_(shortened_name_map) {
59 }
60
61 protected:
ShortenFileName(android::StringPiece file_path,int output_length)62 std::string ShortenFileName(android::StringPiece file_path, int output_length) override {
63 return shortened_name_map_[std::string(file_path)];
64 }
65
66 private:
67 std::unordered_map<std::string, std::string> shortened_name_map_;
68 DISALLOW_COPY_AND_ASSIGN(FakeObfuscator);
69 };
70
TEST(ObfuscatorTest,FileRefPathsChangedInResourceTable)71 TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) {
72 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
73
74 std::unique_ptr<ResourceTable> table =
75 test::ResourceTableBuilder()
76 .AddFileReference("android:drawable/xmlfile", "res/drawables/xmlfile.xml")
77 .AddFileReference("android:drawable/xmlfile2", "res/drawables/xmlfile2.xml")
78 .AddString("android:string/string", "res/should/still/be/the/same.png")
79 .Build();
80
81 OptimizeOptions options{.shorten_resource_paths = true};
82 std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
83 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
84
85 // Expect that the path map is populated
86 ASSERT_THAT(path_map.find("res/drawables/xmlfile.xml"), Not(Eq(path_map.end())));
87 ASSERT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
88
89 // The file paths were changed
90 EXPECT_THAT(path_map.at("res/drawables/xmlfile.xml"), Not(Eq("res/drawables/xmlfile.xml")));
91 EXPECT_THAT(path_map.at("res/drawables/xmlfile2.xml"), Not(Eq("res/drawables/xmlfile2.xml")));
92
93 // Different file paths should remain different
94 EXPECT_THAT(path_map["res/drawables/xmlfile.xml"],
95 Not(Eq(path_map["res/drawables/xmlfile2.xml"])));
96
97 FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
98 ASSERT_THAT(ref, NotNull());
99 // The map correctly points to the new location of the file
100 EXPECT_THAT(path_map["res/drawables/xmlfile.xml"], Eq(*ref->path));
101
102 // Strings should not be affected, only file paths
103 EXPECT_THAT(*GetValue<String>(table.get(), "android:string/string")->value,
104 Eq("res/should/still/be/the/same.png"));
105 EXPECT_THAT(path_map.find("res/should/still/be/the/same.png"), Eq(path_map.end()));
106 }
107
TEST(ObfuscatorTest,SkipColorFileRefPaths)108 TEST(ObfuscatorTest, SkipColorFileRefPaths) {
109 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
110
111 std::unique_ptr<ResourceTable> table =
112 test::ResourceTableBuilder()
113 .AddFileReference("android:color/colorlist", "res/color/colorlist.xml")
114 .AddFileReference("android:color/colorlist", "res/color-mdp-v21/colorlist.xml",
115 test::ParseConfigOrDie("mdp-v21"))
116 .Build();
117
118 OptimizeOptions options{.shorten_resource_paths = true};
119 std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
120 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
121
122 // Expect that the path map to not contain the ColorStateList
123 ASSERT_THAT(path_map.find("res/color/colorlist.xml"), Eq(path_map.end()));
124 ASSERT_THAT(path_map.find("res/color-mdp-v21/colorlist.xml"), Eq(path_map.end()));
125 }
126
TEST(ObfuscatorTest,SkipPathShortenExemptions)127 TEST(ObfuscatorTest, SkipPathShortenExemptions) {
128 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
129
130 std::unique_ptr<ResourceTable> table =
131 test::ResourceTableBuilder()
132 .AddFileReference("android:drawable/xmlfile", "res/drawables/xmlfile.xml")
133 .AddFileReference("android:drawable/xmlfile2", "res/drawables/xmlfile2.xml")
134 .AddString("android:string/string", "res/should/still/be/the/same.png")
135 .Build();
136
137 OptimizeOptions options{.shorten_resource_paths = true};
138 TableFlattenerOptions& flattenerOptions = options.table_flattener_options;
139 flattenerOptions.path_shorten_exemptions.insert(
140 ResourceName({}, ResourceType::kDrawable, "xmlfile"));
141 std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
142 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
143
144 // Expect that the path map to not contain the first drawable which is in exemption set
145 EXPECT_THAT(path_map.find("res/drawables/xmlfile.xml"), Eq(path_map.end()));
146
147 // Expect that the path map to contain the second drawable which is not in exemption set
148 EXPECT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
149
150 FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
151 ASSERT_THAT(ref, NotNull());
152 ASSERT_THAT(HasFailure(), IsFalse());
153 // The path of first drawable in exemption was not changed
154 EXPECT_THAT("res/drawables/xmlfile.xml", Eq(*ref->path));
155
156 // The file path of second drawable not in exemption set was changed
157 EXPECT_THAT(path_map.at("res/drawables/xmlfile2.xml"), Not(Eq("res/drawables/xmlfile2.xml")));
158
159 FileReference* ref2 = GetValue<FileReference>(table.get(), "android:drawable/xmlfile2");
160 ASSERT_THAT(ref, NotNull());
161 // The map of second drawable not in exemption correctly points to the new location of the file
162 EXPECT_THAT(path_map["res/drawables/xmlfile2.xml"], Eq(*ref2->path));
163 }
164
TEST(ObfuscatorTest,KeepExtensions)165 TEST(ObfuscatorTest, KeepExtensions) {
166 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
167
168 std::string original_xml_path = "res/drawable/xmlfile.xml";
169 std::string original_png_path = "res/drawable/pngfile.png";
170
171 std::unique_ptr<ResourceTable> table =
172 test::ResourceTableBuilder()
173 .AddFileReference("android:color/xmlfile", original_xml_path)
174 .AddFileReference("android:color/pngfile", original_png_path)
175 .Build();
176
177 OptimizeOptions options{.shorten_resource_paths = true};
178 std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
179 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
180
181 // Expect that the path map is populated
182 ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end())));
183 ASSERT_THAT(path_map.find("res/drawable/pngfile.png"), Not(Eq(path_map.end())));
184
185 EXPECT_THAT(GetExtension(path_map[original_xml_path]), Eq(android::StringPiece(".xml")));
186 EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png")));
187 }
188
TEST(ObfuscatorTest,ShortenedToReservedWindowsNames)189 TEST(ObfuscatorTest, ShortenedToReservedWindowsNames) {
190 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
191
192 std::string original_path_1 = "res/drawable/pngfile_1.png";
193 std::string original_path_2 = "res/drawable/pngfile_2.png";
194 std::string original_path_3 = "res/drawable/pngfile_3.png";
195 std::string original_path_4 = "res/drawable/pngfile_4.png";
196 std::string original_path_5 = "res/drawable/pngfile_5.png";
197 std::string original_path_6 = "res/drawable/pngfile_6.png";
198 std::string original_path_7 = "res/drawable/pngfile_7.png";
199 std::string original_path_8 = "res/drawable/pngfile_8.png";
200 std::string original_path_9 = "res/drawable/pngfile_9.png";
201
202 std::unique_ptr<ResourceTable> table =
203 test::ResourceTableBuilder()
204 .AddFileReference("android:drawable/pngfile_1", original_path_1)
205 .AddFileReference("android:drawable/pngfile_2", original_path_2)
206 .AddFileReference("android:drawable/pngfile_3", original_path_3)
207 .AddFileReference("android:drawable/pngfile_4", original_path_4)
208 .AddFileReference("android:drawable/pngfile_5", original_path_5)
209 .AddFileReference("android:drawable/pngfile_6", original_path_6)
210 .AddFileReference("android:drawable/pngfile_7", original_path_7)
211 .AddFileReference("android:drawable/pngfile_8", original_path_8)
212 .AddFileReference("android:drawable/pngfile_9", original_path_9)
213 .Build();
214
215 OptimizeOptions options{.shorten_resource_paths = true};
216 std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
217 auto obfuscator = FakeObfuscator(
218 options,
219 {
220 {original_path_1, "CON"},
221 {original_path_2, "Prn"},
222 {original_path_3, "AuX"},
223 {original_path_4, "nul"},
224 {original_path_5, "cOM"},
225 {original_path_6, "lPt"},
226 {original_path_7, "lPt"},
227 {original_path_8, "lPt"}, // 6, 7, and 8 will be appended with a number to disambiguate
228 {original_path_9, "F0o"}, // This one is not reserved
229 });
230 ASSERT_TRUE(obfuscator.Consume(context.get(), table.get()));
231
232 // Expect that the path map is populated
233 ASSERT_THAT(path_map.find(original_path_1), Not(Eq(path_map.end())));
234 ASSERT_THAT(path_map.find(original_path_2), Not(Eq(path_map.end())));
235 ASSERT_THAT(path_map.find(original_path_3), Not(Eq(path_map.end())));
236 ASSERT_THAT(path_map.find(original_path_4), Not(Eq(path_map.end())));
237 ASSERT_THAT(path_map.find(original_path_5), Not(Eq(path_map.end())));
238 ASSERT_THAT(path_map.find(original_path_6), Not(Eq(path_map.end())));
239 ASSERT_THAT(path_map.find(original_path_7), Not(Eq(path_map.end())));
240 ASSERT_THAT(path_map.find(original_path_8), Not(Eq(path_map.end())));
241 ASSERT_THAT(path_map.find(original_path_9), Not(Eq(path_map.end())));
242
243 EXPECT_THAT(path_map[original_path_1], Eq("res/_CON.png"));
244 EXPECT_THAT(path_map[original_path_2], Eq("res/_Prn.png"));
245 EXPECT_THAT(path_map[original_path_3], Eq("res/_AuX.png"));
246 EXPECT_THAT(path_map[original_path_4], Eq("res/_nul.png"));
247 EXPECT_THAT(path_map[original_path_5], Eq("res/_cOM.png"));
248 EXPECT_THAT(path_map[original_path_9], Eq("res/F0o.png"));
249
250 std::set<std::string> lpt_shortened_names{path_map[original_path_6], path_map[original_path_7],
251 path_map[original_path_8]};
252 EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt.png"));
253 EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt1.png"));
254 EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt2.png"));
255 }
256
TEST(ObfuscatorTest,DeterministicallyHandleCollisions)257 TEST(ObfuscatorTest, DeterministicallyHandleCollisions) {
258 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
259
260 // 4000 resources is the limit at which the hash space is expanded to 3
261 // letters to reduce collisions, we want as many collisions as possible thus
262 // N-1.
263 const auto kNumResources = 3999;
264 const auto kNumTries = 5;
265
266 test::ResourceTableBuilder builder1;
267 FillTable(builder1, 0, kNumResources);
268 std::unique_ptr<ResourceTable> table1 = builder1.Build();
269 OptimizeOptions options{.shorten_resource_paths = true};
270 std::map<std::string, std::string>& expected_mapping =
271 options.table_flattener_options.shortened_path_map;
272 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table1.get()));
273
274 // We are trying to ensure lack of non-determinism, it is not simple to prove
275 // a negative, thus we must try the test a few times so that the test itself
276 // is non-flaky. Basically create the pathmap 5 times from the same set of
277 // resources but a different order of addition and then ensure they are always
278 // mapped to the same short path.
279 for (int i = 0; i < kNumTries; i++) {
280 test::ResourceTableBuilder builder2;
281 // This loop adds resources to the resource table in the range of
282 // [0:kNumResources). Adding the file references in different order makes
283 // non-determinism more likely to surface. Thus we add resources
284 // [start_index:kNumResources) first then [0:start_index). We also use a
285 // different start_index each run.
286 int start_index = (kNumResources / kNumTries) * i;
287 FillTable(builder2, start_index, kNumResources);
288 FillTable(builder2, 0, start_index);
289 std::unique_ptr<ResourceTable> table2 = builder2.Build();
290
291 OptimizeOptions actualOptimizerOptions{.shorten_resource_paths = true};
292 TableFlattenerOptions& actual_options = actualOptimizerOptions.table_flattener_options;
293 std::map<std::string, std::string>& actual_mapping = actual_options.shortened_path_map;
294 ASSERT_TRUE(Obfuscator(actualOptimizerOptions).Consume(context.get(), table2.get()));
295
296 for (auto& item : actual_mapping) {
297 ASSERT_THAT(expected_mapping[item.first], Eq(item.second));
298 }
299 }
300 }
301
TEST(ObfuscatorTest,DumpIdResourceMap)302 TEST(ObfuscatorTest, DumpIdResourceMap) {
303 std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
304
305 OverlayableItem overlayable_item(std::make_shared<Overlayable>("TestName", "overlay://theme"));
306 overlayable_item.policies |= PolicyFlags::PRODUCT_PARTITION;
307 overlayable_item.policies |= PolicyFlags::SYSTEM_PARTITION;
308 overlayable_item.policies |= PolicyFlags::VENDOR_PARTITION;
309
310 std::string original_xml_path = "res/drawable/xmlfile.xml";
311 std::string original_png_path = "res/drawable/pngfile.png";
312
313 std::string name = "com.app.test:string/overlayable";
314 std::unique_ptr<ResourceTable> table =
315 test::ResourceTableBuilder()
316 .AddFileReference("android:color/xmlfile", original_xml_path)
317 .AddFileReference("android:color/pngfile", original_png_path)
318 .AddValue("com.app.test:color/mycolor", aapt::ResourceId(0x7f020000),
319 aapt::util::make_unique<aapt::BinaryPrimitive>(
320 uint8_t(android::Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
321 .AddString("com.app.test:string/mystring", ResourceId(0x7f030000), "hi")
322 .AddString("com.app.test:string/in_exemption", ResourceId(0x7f030001), "Hi")
323 .AddString(name, ResourceId(0x7f030002), "HI")
324 .SetOverlayable(name, overlayable_item)
325 .Build();
326
327 OptimizeOptions options{.shorten_resource_paths = true};
328 TableFlattenerOptions& flattenerOptions = options.table_flattener_options;
329 flattenerOptions.collapse_key_stringpool = true;
330 flattenerOptions.name_collapse_exemptions.insert(
331 ResourceName({}, ResourceType::kString, "in_exemption"));
332 auto& id_resource_map = flattenerOptions.id_resource_map;
333 ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
334
335 // Expect that the id resource name map is populated
336 ASSERT_THAT(id_resource_map.find(0x7f020000), Not(Eq(id_resource_map.end())));
337 EXPECT_THAT(id_resource_map.at(0x7f020000), Eq("mycolor"));
338 ASSERT_THAT(id_resource_map.find(0x7f030000), Not(Eq(id_resource_map.end())));
339 EXPECT_THAT(id_resource_map.at(0x7f030000), Eq("mystring"));
340 EXPECT_THAT(id_resource_map.find(0x7f030001), Eq(id_resource_map.end()));
341 EXPECT_THAT(id_resource_map.find(0x7f030002), Eq(id_resource_map.end()));
342 }
343
TEST(ObfuscatorTest,IsEnabledWithDefaultOption)344 TEST(ObfuscatorTest, IsEnabledWithDefaultOption) {
345 OptimizeOptions options;
346 Obfuscator obfuscatorWithDefaultOption(options);
347 ASSERT_THAT(obfuscatorWithDefaultOption.IsEnabled(), Eq(false));
348 }
349
TEST(ObfuscatorTest,IsEnabledWithShortenPathOption)350 TEST(ObfuscatorTest, IsEnabledWithShortenPathOption) {
351 OptimizeOptions options{.shorten_resource_paths = true};
352 Obfuscator obfuscatorWithShortenPathOption(options);
353 ASSERT_THAT(obfuscatorWithShortenPathOption.IsEnabled(), Eq(true));
354 }
355
TEST(ObfuscatorTest,IsEnabledWithCollapseStringPoolOption)356 TEST(ObfuscatorTest, IsEnabledWithCollapseStringPoolOption) {
357 OptimizeOptions options;
358 options.table_flattener_options.collapse_key_stringpool = true;
359 Obfuscator obfuscatorWithCollapseStringPoolOption(options);
360 ASSERT_THAT(obfuscatorWithCollapseStringPoolOption.IsEnabled(), Eq(true));
361 }
362
TEST(ObfuscatorTest,IsEnabledWithShortenPathAndCollapseStringPoolOption)363 TEST(ObfuscatorTest, IsEnabledWithShortenPathAndCollapseStringPoolOption) {
364 OptimizeOptions options{.shorten_resource_paths = true};
365 options.table_flattener_options.collapse_key_stringpool = true;
366 Obfuscator obfuscatorWithCollapseStringPoolOption(options);
367 ASSERT_THAT(obfuscatorWithCollapseStringPoolOption.IsEnabled(), Eq(true));
368 }
369
getProtocolBufferTableUnderTest()370 static std::unique_ptr<ResourceTable> getProtocolBufferTableUnderTest() {
371 std::string original_xml_path = "res/drawable/xmlfile.xml";
372 std::string original_png_path = "res/drawable/pngfile.png";
373
374 return test::ResourceTableBuilder()
375 .AddFileReference("com.app.test:drawable/xmlfile", original_xml_path)
376 .AddFileReference("com.app.test:drawable/pngfile", original_png_path)
377 .AddValue("com.app.test:color/mycolor", aapt::ResourceId(0x7f020000),
378 aapt::util::make_unique<aapt::BinaryPrimitive>(
379 uint8_t(android::Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
380 .AddString("com.app.test:string/mystring", ResourceId(0x7f030000), "hello world")
381 .Build();
382 }
383
TEST(ObfuscatorTest,WriteObfuscationMapInProtocolBufferFormat)384 TEST(ObfuscatorTest, WriteObfuscationMapInProtocolBufferFormat) {
385 OptimizeOptions options{.shorten_resource_paths = true};
386 options.table_flattener_options.collapse_key_stringpool = true;
387 Obfuscator obfuscator(options);
388 ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(),
389 getProtocolBufferTableUnderTest().get()));
390
391 const auto map_path = testing::TempDir() + "/obfuscated_map.pb";
392 ASSERT_TRUE(obfuscator.WriteObfuscationMap(map_path));
393
394 std::string pbOut;
395 ASSERT_TRUE(android::base::ReadFileToString(map_path, &pbOut, false /* follow_symlinks */));
396 EXPECT_THAT(pbOut, HasSubstr("drawable/xmlfile.xml"));
397 EXPECT_THAT(pbOut, HasSubstr("drawable/pngfile.png"));
398 EXPECT_THAT(pbOut, HasSubstr("mycolor"));
399 EXPECT_THAT(pbOut, HasSubstr("mystring"));
400 pb::ResourceMappings resourceMappings;
401 ASSERT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue());
402 ASSERT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2));
403 auto& resource_names = resourceMappings.collapsed_names().resource_names();
404 EXPECT_THAT(resource_names.at(0).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
405 EXPECT_THAT(resource_names.at(1).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
406 auto& shortened_paths = resourceMappings.shortened_paths();
407 EXPECT_THAT(shortened_paths.resource_paths_size(), Eq(2));
408 EXPECT_THAT(shortened_paths.resource_paths(0).original_path(),
409 AnyOf(Eq("res/drawable/pngfile.png"), Eq("res/drawable/xmlfile.xml")));
410 EXPECT_THAT(shortened_paths.resource_paths(1).original_path(),
411 AnyOf(Eq("res/drawable/pngfile.png"), Eq("res/drawable/xmlfile.xml")));
412 }
413
TEST(ObfuscatorTest,WriteObfuscatingMapWithNonEnabledOption)414 TEST(ObfuscatorTest, WriteObfuscatingMapWithNonEnabledOption) {
415 OptimizeOptions options;
416 Obfuscator obfuscator(options);
417 ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(),
418 getProtocolBufferTableUnderTest().get()));
419
420 const auto map_path = testing::TempDir() + "/obfuscated_map.pb";
421 ASSERT_TRUE(obfuscator.WriteObfuscationMap(map_path));
422
423 std::string pbOut;
424 ASSERT_TRUE(android::base::ReadFileToString(map_path, &pbOut, false /* follow_symlinks */));
425 ASSERT_THAT(pbOut, Eq(""));
426 }
427
428 } // namespace
429
430 } // namespace aapt
431