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 "Compile.h"
18 
19 #include "android-base/file.h"
20 #include "android-base/stringprintf.h"
21 #include "android-base/utf8.h"
22 #include "format/proto/ProtoDeserialize.h"
23 #include "io/StringStream.h"
24 #include "io/ZipArchive.h"
25 #include "java/AnnotationProcessor.h"
26 #include "test/Test.h"
27 
28 namespace aapt {
29 
30 using CompilerTest = CommandTestFixture;
31 
BuildPath(std::vector<std::string> args)32 std::string BuildPath(std::vector<std::string> args) {
33   std::string out;
34   if (args.empty()) {
35     return out;
36   }
37   out = args[0];
38   for (int i = 1; i < args.size(); i++) {
39     file::AppendPath(&out, args[i]);
40   }
41   return out;
42 }
43 
TestCompile(const std::string & path,const std::string & outDir,bool legacy,StdErrDiagnostics & diag)44 int TestCompile(const std::string& path, const std::string& outDir, bool legacy,
45                 StdErrDiagnostics& diag) {
46   std::vector<android::StringPiece> args;
47   args.push_back(path);
48   args.push_back("-o");
49   args.push_back(outDir);
50   if (legacy) {
51     args.push_back("--legacy");
52   }
53   return CompileCommand(&diag).Execute(args, &std::cerr);
54 }
55 
TEST_F(CompilerTest,MultiplePeriods)56 TEST_F(CompilerTest, MultiplePeriods) {
57   StdErrDiagnostics diag;
58   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
59   const std::string kResDir = BuildPath({android::base::Dirname(android::base::GetExecutablePath()),
60                                          "integration-tests", "CompileTest", "res"});
61   const std::string kOutDir = testing::TempDir();
62 
63   // Resource files without periods in the file name should not throw errors
64   const std::string path0 = BuildPath({kResDir, "values", "values.xml"});
65   const std::string path0_out = BuildPath({kOutDir, "values_values.arsc.flat"});
66   ::android::base::utf8::unlink(path0_out.c_str());
67   ASSERT_EQ(TestCompile(path0, kOutDir, /** legacy */ false, diag), 0);
68   ASSERT_EQ(::android::base::utf8::unlink(path0_out.c_str()), 0);
69   ASSERT_EQ(TestCompile(path0, kOutDir, /** legacy */ true, diag), 0);
70   ASSERT_EQ(::android::base::utf8::unlink(path0_out.c_str()), 0);
71 
72   const std::string path1 = BuildPath({kResDir, "drawable", "image.png"});
73   const std::string path1_out = BuildPath({kOutDir, "drawable_image.png.flat"});
74   ::android::base::utf8::unlink(path1_out.c_str());
75   ASSERT_EQ(TestCompile(path1, kOutDir, /** legacy */ false, diag), 0);
76   ASSERT_EQ(::android::base::utf8::unlink(path1_out.c_str()), 0);
77   ASSERT_EQ(TestCompile(path1, kOutDir, /** legacy */ true, diag), 0);
78   ASSERT_EQ(::android::base::utf8::unlink(path1_out.c_str()), 0);
79 
80   const std::string path2 = BuildPath({kResDir, "drawable", "image.9.png"});
81   const std::string path2_out = BuildPath({kOutDir, "drawable_image.9.png.flat"});
82   ::android::base::utf8::unlink(path2_out.c_str());
83   ASSERT_EQ(TestCompile(path2, kOutDir, /** legacy */ false, diag), 0);
84   ASSERT_EQ(::android::base::utf8::unlink(path2_out.c_str()), 0);
85   ASSERT_EQ(TestCompile(path2, kOutDir, /** legacy */ true, diag), 0);
86   ASSERT_EQ(::android::base::utf8::unlink(path2_out.c_str()), 0);
87 
88   // Resource files with periods in the file name should fail on non-legacy compilations
89   const std::string path3 = BuildPath({kResDir, "values", "values.all.xml"});
90   const std::string path3_out = BuildPath({kOutDir, "values_values.all.arsc.flat"});
91   ::android::base::utf8::unlink(path3_out.c_str());
92   ASSERT_NE(TestCompile(path3, kOutDir, /** legacy */ false, diag), 0);
93   ASSERT_NE(::android::base::utf8::unlink(path3_out.c_str()), 0);
94   ASSERT_EQ(TestCompile(path3, kOutDir, /** legacy */ true, diag), 0);
95   ASSERT_EQ(::android::base::utf8::unlink(path3_out.c_str()), 0);
96 
97   const std::string path4 = BuildPath({kResDir, "drawable", "image.small.png"});
98   const std::string path4_out = BuildPath({kOutDir, "drawable_image.small.png.flat"});
99   ::android::base::utf8::unlink(path4_out.c_str());
100   ASSERT_NE(TestCompile(path4, kOutDir, /** legacy */ false, diag), 0);
101   ASSERT_NE(::android::base::utf8::unlink(path4_out.c_str()), 0);
102   ASSERT_EQ(TestCompile(path4, kOutDir, /** legacy */ true, diag), 0);
103   ASSERT_EQ(::android::base::utf8::unlink(path4_out.c_str()), 0);
104 
105   const std::string path5 = BuildPath({kResDir, "drawable", "image.small.9.png"});
106   const std::string path5_out = BuildPath({kOutDir, "drawable_image.small.9.png.flat"});
107   ::android::base::utf8::unlink(path5_out.c_str());
108   ASSERT_NE(TestCompile(path5, kOutDir, /** legacy */ false, diag), 0);
109   ASSERT_NE(::android::base::utf8::unlink(path5_out.c_str()), 0);
110   ASSERT_EQ(TestCompile(path5, kOutDir, /** legacy */ true, diag), 0);
111   ASSERT_EQ(::android::base::utf8::unlink(path5_out.c_str()), 0);
112 }
113 
TEST_F(CompilerTest,DirInput)114 TEST_F(CompilerTest, DirInput) {
115   StdErrDiagnostics diag;
116   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
117   const std::string kResDir = BuildPath({android::base::Dirname(android::base::GetExecutablePath()),
118                                          "integration-tests", "CompileTest", "DirInput", "res"});
119   const std::string kOutputFlata = BuildPath({testing::TempDir(), "compiled.flata"});
120   ::android::base::utf8::unlink(kOutputFlata.c_str());
121 
122   std::vector<android::StringPiece> args;
123   args.push_back("--dir");
124   args.push_back(kResDir);
125   args.push_back("-o");
126   args.push_back(kOutputFlata);
127   args.push_back("-v");
128   ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0);
129 
130   {
131     // Check for the presence of the compiled files
132     std::string err;
133     std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err);
134     ASSERT_NE(zip, nullptr) << err;
135     ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr);
136     ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr);
137     ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr);
138   }
139   ASSERT_EQ(::android::base::utf8::unlink(kOutputFlata.c_str()), 0);
140 }
141 
TEST_F(CompilerTest,ZipInput)142 TEST_F(CompilerTest, ZipInput) {
143   StdErrDiagnostics diag;
144   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
145   const std::string kResZip =
146       BuildPath({android::base::Dirname(android::base::GetExecutablePath()), "integration-tests",
147                  "CompileTest", "ZipInput", "res.zip"});
148   const std::string kOutputFlata = BuildPath({testing::TempDir(), "compiled.flata"});
149 
150   ::android::base::utf8::unlink(kOutputFlata.c_str());
151 
152   std::vector<android::StringPiece> args;
153   args.push_back("--zip");
154   args.push_back(kResZip);
155   args.push_back("-o");
156   args.push_back(kOutputFlata);
157   ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0);
158 
159   {
160     // Check for the presence of the compiled files
161     std::string err;
162     std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err);
163     ASSERT_NE(zip, nullptr) << err;
164     ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr);
165     ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr);
166     ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr);
167   }
168   ASSERT_EQ(::android::base::utf8::unlink(kOutputFlata.c_str()), 0);
169 }
170 
171 /*
172  * This tests the "protection" from pseudo-translation of
173  * non-translatable files (starting with 'donotranslate')
174  * and strings (with the translatable="false" attribute)
175  *
176  * We check 4 string files, 2 translatable, and 2 not (based on file name)
177  * Each file contains 2 strings, one translatable, one not (attribute based)
178  * Each of these files are compiled and linked into one .apk, then we load the
179  * strings from the apk and check if there are pseudo-translated strings.
180  */
181 
182 // Using 000 and 111 because they are not changed by pseudo-translation,
183 // making our life easier.
184 constexpr static const char sTranslatableXmlContent[] =
185     "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
186     "<resources>"
187     "  <string name=\"normal\">000</string>"
188     "  <string name=\"non_translatable\" translatable=\"false\">111</string>"
189     "</resources>";
190 
AssertTranslations(CommandTestFixture * ctf,std::string file_name,std::vector<std::string> expected)191 static void AssertTranslations(CommandTestFixture *ctf, std::string file_name,
192     std::vector<std::string> expected) {
193 
194   StdErrDiagnostics diag;
195 
196   const std::string source_file = ctf->GetTestPath("/res/values/" + file_name + ".xml");
197   const std::string compiled_files_dir = ctf->GetTestPath("/compiled_" + file_name);
198   const std::string out_apk = ctf->GetTestPath("/" + file_name + ".apk");
199 
200   ctf->WriteFile(source_file, sTranslatableXmlContent);
201   CHECK(file::mkdirs(compiled_files_dir.data()));
202 
203   ASSERT_EQ(CompileCommand(&diag).Execute({
204       source_file,
205       "-o", compiled_files_dir,
206       "-v",
207       "--pseudo-localize"
208   }, &std::cerr), 0);
209 
210   ASSERT_TRUE(ctf->Link({
211       "--manifest", ctf->GetDefaultManifest(),
212       "-o", out_apk
213   }, compiled_files_dir, &diag));
214 
215   std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag);
216   ASSERT_NE(apk, nullptr);
217 
218   ResourceTable* table = apk->GetResourceTable();
219   ASSERT_NE(table, nullptr);
220   table->string_pool.Sort();
221 
222   const std::vector<std::unique_ptr<android::StringPool::Entry>>& pool_strings =
223       table->string_pool.strings();
224 
225   // The actual / expected vectors have the same size
226   const size_t pool_size = pool_strings.size();
227   ASSERT_EQ(pool_size, expected.size());
228 
229   for (size_t i = 0; i < pool_size; i++) {
230     std::string actual = pool_strings[i]->value;
231     ASSERT_EQ(actual, expected[i]);
232   }
233 }
234 
TEST_F(CompilerTest,DoNotTranslateTest)235 TEST_F(CompilerTest, DoNotTranslateTest) {
236   // The first string (000) is translatable, the second is not
237   // ar-XB uses "\u200F\u202E...\u202C\u200F"
238   std::vector<std::string> expected_translatable = {
239       "(F)[000 one]",  // en-XA-feminine
240       "(F)\xE2\x80\x8F\xE2\x80\xAE"
241       "000"
242       "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-feminine
243       "(M)[000 one]",              // en-XA-masculine
244       "(M)\xE2\x80\x8F\xE2\x80\xAE"
245       "000"
246       "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-masculine
247       "(N)[000 one]",              // en-XA-neuter
248       "(N)\xE2\x80\x8F\xE2\x80\xAE"
249       "000"
250       "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB-neuter
251       "000",                       // default locale
252       "111",                       // default locale
253       "[000 one]",                 // en-XA
254       "\xE2\x80\x8F\xE2\x80\xAE"
255       "000"
256       "\xE2\x80\xAC\xE2\x80\x8F",  // ar-XB
257   };
258   AssertTranslations(this, "foo", expected_translatable);
259   AssertTranslations(this, "foo_donottranslate", expected_translatable);
260 
261   // No translatable strings because these are non-translatable files
262   std::vector<std::string> expected_not_translatable = {
263       "000", "111", // default locale
264   };
265   AssertTranslations(this, "donottranslate", expected_not_translatable);
266   AssertTranslations(this, "donottranslate_foo", expected_not_translatable);
267 }
268 
TEST_F(CompilerTest,RelativePathTest)269 TEST_F(CompilerTest, RelativePathTest) {
270   StdErrDiagnostics diag;
271   const std::string res_path =
272       BuildPath({android::base::Dirname(android::base::GetExecutablePath()), "integration-tests",
273                  "CompileTest", "res"});
274 
275   const std::string path_values_colors = GetTestPath("values/colors.xml");
276   WriteFile(path_values_colors, "<resources>"
277                    "<color name=\"color_one\">#008577</color>"
278                    "</resources>");
279 
280   const std::string path_layout_layout_one = GetTestPath("layout/layout_one.xml");
281   WriteFile(path_layout_layout_one, "<LinearLayout "
282                    "xmlns:android=\"http://schemas.android.com/apk/res/android\">"
283                    "<TextBox android:id=\"@+id/text_one\" android:background=\"@color/color_one\"/>"
284                    "</LinearLayout>");
285 
286   const std::string compiled_files_dir =
287       BuildPath({testing::TempDir(), "integration-tests", "CompileTest", "compiled"});
288   CHECK(file::mkdirs(compiled_files_dir.data()));
289 
290   const std::string path_values_colors_out =
291       BuildPath({compiled_files_dir,"values_colors.arsc.flat"});
292   const std::string path_layout_layout_one_out =
293       BuildPath({compiled_files_dir, "layout_layout_one.flat"});
294   ::android::base::utf8::unlink(path_values_colors_out.c_str());
295   ::android::base::utf8::unlink(path_layout_layout_one_out.c_str());
296   const std::string apk_path =
297       BuildPath({testing::TempDir(), "integration-tests", "CompileTest", "out.apk"});
298 
299   const std::string source_set_res = BuildPath({"main", "res"});
300   const std::string relative_path_values_colors =
301       BuildPath({source_set_res, "values", "colors.xml"});
302   const std::string relative_path_layout_layout_one =
303       BuildPath({source_set_res, "layout", "layout_one.xml"});
304 
305   CompileCommand(&diag).Execute({
306     path_values_colors,
307     "-o",
308     compiled_files_dir,
309     "--source-path",
310     relative_path_values_colors},
311         &std::cerr);
312 
313   CompileCommand(&diag).Execute({
314     path_layout_layout_one,
315     "-o",
316     compiled_files_dir,
317     "--source-path",
318     relative_path_layout_layout_one},
319         &std::cerr);
320 
321   std::ifstream ifs_values(path_values_colors_out);
322   std::string content_values((std::istreambuf_iterator<char>(ifs_values)),
323                              (std::istreambuf_iterator<char>()));
324   ASSERT_NE(content_values.find(relative_path_values_colors), -1);
325   ASSERT_EQ(content_values.find(path_values_colors), -1);
326 
327   ASSERT_TRUE(Link({"-o", apk_path,
328                     "--manifest", GetDefaultManifest(),
329                     "--proto-format"},
330                     compiled_files_dir, &diag));
331 
332   std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(apk_path, &diag);
333   ResourceTable* resource_table = apk.get()->GetResourceTable();
334   const std::vector<std::unique_ptr<android::StringPool::Entry>>& pool_strings =
335       resource_table->string_pool.strings();
336 
337   ASSERT_EQ(pool_strings.size(), 2);
338   ASSERT_EQ(pool_strings[0]->value, "res/layout/layout_one.xml");
339   ASSERT_EQ(pool_strings[1]->value, "res/layout-v1/layout_one.xml");
340 
341   // Check resources.pb contains relative sources.
342   io::IFile* proto_file =
343       apk.get()->GetFileCollection()->FindFile("resources.pb");
344   std::unique_ptr<android::InputStream> proto_stream = proto_file->OpenInputStream();
345   io::ProtoInputStreamReader proto_reader(proto_stream.get());
346   pb::ResourceTable pb_table;
347   proto_reader.ReadMessage(&pb_table);
348 
349   const std::string pool_strings_proto = pb_table.source_pool().data();
350 
351   ASSERT_NE(pool_strings_proto.find(relative_path_values_colors), -1);
352   ASSERT_NE(pool_strings_proto.find(relative_path_layout_layout_one), -1);
353 }
354 
355 }  // namespace aapt
356