1 /*
2  * Copyright (C) 2015 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 "ConfigDescription.h"
18 #include "Diagnostics.h"
19 #include "Flags.h"
20 #include "ResourceParser.h"
21 #include "ResourceTable.h"
22 #include "compile/IdAssigner.h"
23 #include "compile/Png.h"
24 #include "compile/PseudolocaleGenerator.h"
25 #include "compile/XmlIdCollector.h"
26 #include "flatten/Archive.h"
27 #include "flatten/XmlFlattener.h"
28 #include "proto/ProtoSerialize.h"
29 #include "util/Files.h"
30 #include "util/Maybe.h"
31 #include "util/Util.h"
32 #include "xml/XmlDom.h"
33 #include "xml/XmlPullParser.h"
34 
35 #include <google/protobuf/io/zero_copy_stream_impl_lite.h>
36 #include <google/protobuf/io/coded_stream.h>
37 
38 #include <dirent.h>
39 #include <fstream>
40 #include <string>
41 
42 namespace aapt {
43 
44 struct ResourcePathData {
45     Source source;
46     std::u16string resourceDir;
47     std::u16string name;
48     std::string extension;
49 
50     // Original config str. We keep this because when we parse the config, we may add on
51     // version qualifiers. We want to preserve the original input so the output is easily
52     // computed before hand.
53     std::string configStr;
54     ConfigDescription config;
55 };
56 
57 /**
58  * Resource file paths are expected to look like:
59  * [--/res/]type[-config]/name
60  */
extractResourcePathData(const std::string & path,std::string * outError)61 static Maybe<ResourcePathData> extractResourcePathData(const std::string& path,
62                                                        std::string* outError) {
63     std::vector<std::string> parts = util::split(path, file::sDirSep);
64     if (parts.size() < 2) {
65         if (outError) *outError = "bad resource path";
66         return {};
67     }
68 
69     std::string& dir = parts[parts.size() - 2];
70     StringPiece dirStr = dir;
71 
72     StringPiece configStr;
73     ConfigDescription config;
74     size_t dashPos = dir.find('-');
75     if (dashPos != std::string::npos) {
76         configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1));
77         if (!ConfigDescription::parse(configStr, &config)) {
78             if (outError) {
79                 std::stringstream errStr;
80                 errStr << "invalid configuration '" << configStr << "'";
81                 *outError = errStr.str();
82             }
83             return {};
84         }
85         dirStr = dirStr.substr(0, dashPos);
86     }
87 
88     std::string& filename = parts[parts.size() - 1];
89     StringPiece name = filename;
90     StringPiece extension;
91     size_t dotPos = filename.find('.');
92     if (dotPos != std::string::npos) {
93         extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1));
94         name = name.substr(0, dotPos);
95     }
96 
97     return ResourcePathData{
98             Source(path),
99             util::utf8ToUtf16(dirStr),
100             util::utf8ToUtf16(name),
101             extension.toString(),
102             configStr.toString(),
103             config
104     };
105 }
106 
107 struct CompileOptions {
108     std::string outputPath;
109     Maybe<std::string> resDir;
110     bool pseudolocalize = false;
111     bool legacyMode = false;
112     bool verbose = false;
113 };
114 
buildIntermediateFilename(const ResourcePathData & data)115 static std::string buildIntermediateFilename(const ResourcePathData& data) {
116     std::stringstream name;
117     name << data.resourceDir;
118     if (!data.configStr.empty()) {
119         name << "-" << data.configStr;
120     }
121     name << "_" << data.name;
122     if (!data.extension.empty()) {
123         name << "." << data.extension;
124     }
125     name << ".flat";
126     return name.str();
127 }
128 
isHidden(const StringPiece & filename)129 static bool isHidden(const StringPiece& filename) {
130     return util::stringStartsWith<char>(filename, ".");
131 }
132 
133 /**
134  * Walks the res directory structure, looking for resource files.
135  */
loadInputFilesFromDir(IAaptContext * context,const CompileOptions & options,std::vector<ResourcePathData> * outPathData)136 static bool loadInputFilesFromDir(IAaptContext* context, const CompileOptions& options,
137                                   std::vector<ResourcePathData>* outPathData) {
138     const std::string& rootDir = options.resDir.value();
139     std::unique_ptr<DIR, decltype(closedir)*> d(opendir(rootDir.data()), closedir);
140     if (!d) {
141         context->getDiagnostics()->error(DiagMessage() << strerror(errno));
142         return false;
143     }
144 
145     while (struct dirent* entry = readdir(d.get())) {
146         if (isHidden(entry->d_name)) {
147             continue;
148         }
149 
150         std::string prefixPath = rootDir;
151         file::appendPath(&prefixPath, entry->d_name);
152 
153         if (file::getFileType(prefixPath) != file::FileType::kDirectory) {
154             continue;
155         }
156 
157         std::unique_ptr<DIR, decltype(closedir)*> subDir(opendir(prefixPath.data()), closedir);
158         if (!subDir) {
159             context->getDiagnostics()->error(DiagMessage() << strerror(errno));
160             return false;
161         }
162 
163         while (struct dirent* leafEntry = readdir(subDir.get())) {
164             if (isHidden(leafEntry->d_name)) {
165                 continue;
166             }
167 
168             std::string fullPath = prefixPath;
169             file::appendPath(&fullPath, leafEntry->d_name);
170 
171             std::string errStr;
172             Maybe<ResourcePathData> pathData = extractResourcePathData(fullPath, &errStr);
173             if (!pathData) {
174                 context->getDiagnostics()->error(DiagMessage() << errStr);
175                 return false;
176             }
177 
178             outPathData->push_back(std::move(pathData.value()));
179         }
180     }
181     return true;
182 }
183 
compileTable(IAaptContext * context,const CompileOptions & options,const ResourcePathData & pathData,IArchiveWriter * writer,const std::string & outputPath)184 static bool compileTable(IAaptContext* context, const CompileOptions& options,
185                          const ResourcePathData& pathData, IArchiveWriter* writer,
186                          const std::string& outputPath) {
187     ResourceTable table;
188     {
189         std::ifstream fin(pathData.source.path, std::ifstream::binary);
190         if (!fin) {
191             context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
192             return false;
193         }
194 
195 
196         // Parse the values file from XML.
197         xml::XmlPullParser xmlParser(fin);
198 
199         ResourceParserOptions parserOptions;
200         parserOptions.errorOnPositionalArguments = !options.legacyMode;
201 
202         // If the filename includes donottranslate, then the default translatable is false.
203         parserOptions.translatable = pathData.name.find(u"donottranslate") == std::string::npos;
204 
205         ResourceParser resParser(context->getDiagnostics(), &table, pathData.source,
206                                  pathData.config, parserOptions);
207         if (!resParser.parse(&xmlParser)) {
208             return false;
209         }
210 
211         fin.close();
212     }
213 
214     if (options.pseudolocalize) {
215         // Generate pseudo-localized strings (en-XA and ar-XB).
216         // These are created as weak symbols, and are only generated from default configuration
217         // strings and plurals.
218         PseudolocaleGenerator pseudolocaleGenerator;
219         if (!pseudolocaleGenerator.consume(context, &table)) {
220             return false;
221         }
222     }
223 
224     // Ensure we have the compilation package at least.
225     table.createPackage(context->getCompilationPackage());
226 
227     // Assign an ID to any package that has resources.
228     for (auto& pkg : table.packages) {
229         if (!pkg->id) {
230             // If no package ID was set while parsing (public identifiers), auto assign an ID.
231             pkg->id = context->getPackageId();
232         }
233     }
234 
235     // Create the file/zip entry.
236     if (!writer->startEntry(outputPath, 0)) {
237         context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open");
238         return false;
239     }
240 
241     std::unique_ptr<pb::ResourceTable> pbTable = serializeTableToPb(&table);
242 
243     // Wrap our IArchiveWriter with an adaptor that implements the ZeroCopyOutputStream interface.
244     {
245         google::protobuf::io::CopyingOutputStreamAdaptor adaptor(writer);
246 
247         if (!pbTable->SerializeToZeroCopyStream(&adaptor)) {
248             context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write");
249             return false;
250         }
251     }
252 
253     if (!writer->finishEntry()) {
254         context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to finish entry");
255         return false;
256     }
257     return true;
258 }
259 
writeHeaderAndBufferToWriter(const StringPiece & outputPath,const ResourceFile & file,const BigBuffer & buffer,IArchiveWriter * writer,IDiagnostics * diag)260 static bool writeHeaderAndBufferToWriter(const StringPiece& outputPath, const ResourceFile& file,
261                                          const BigBuffer& buffer, IArchiveWriter* writer,
262                                          IDiagnostics* diag) {
263     // Start the entry so we can write the header.
264     if (!writer->startEntry(outputPath, 0)) {
265         diag->error(DiagMessage(outputPath) << "failed to open file");
266         return false;
267     }
268 
269     // Create the header.
270     std::unique_ptr<pb::CompiledFile> pbCompiledFile = serializeCompiledFileToPb(file);
271 
272     {
273         // The stream must be destroyed before we finish the entry, or else
274         // some data won't be flushed.
275         // Wrap our IArchiveWriter with an adaptor that implements the ZeroCopyOutputStream
276         // interface.
277         google::protobuf::io::CopyingOutputStreamAdaptor adaptor(writer);
278         CompiledFileOutputStream outputStream(&adaptor, pbCompiledFile.get());
279         for (const BigBuffer::Block& block : buffer) {
280             if (!outputStream.Write(block.buffer.get(), block.size)) {
281                 diag->error(DiagMessage(outputPath) << "failed to write data");
282                 return false;
283             }
284         }
285     }
286 
287     if (!writer->finishEntry()) {
288         diag->error(DiagMessage(outputPath) << "failed to finish writing data");
289         return false;
290     }
291     return true;
292 }
293 
writeHeaderAndMmapToWriter(const StringPiece & outputPath,const ResourceFile & file,const android::FileMap & map,IArchiveWriter * writer,IDiagnostics * diag)294 static bool writeHeaderAndMmapToWriter(const StringPiece& outputPath, const ResourceFile& file,
295                                        const android::FileMap& map, IArchiveWriter* writer,
296                                        IDiagnostics* diag) {
297     // Start the entry so we can write the header.
298     if (!writer->startEntry(outputPath, 0)) {
299         diag->error(DiagMessage(outputPath) << "failed to open file");
300         return false;
301     }
302 
303     // Create the header.
304     std::unique_ptr<pb::CompiledFile> pbCompiledFile = serializeCompiledFileToPb(file);
305 
306     {
307         // The stream must be destroyed before we finish the entry, or else
308         // some data won't be flushed.
309         // Wrap our IArchiveWriter with an adaptor that implements the ZeroCopyOutputStream
310         // interface.
311         google::protobuf::io::CopyingOutputStreamAdaptor adaptor(writer);
312         CompiledFileOutputStream outputStream(&adaptor, pbCompiledFile.get());
313         if (!outputStream.Write(map.getDataPtr(), map.getDataLength())) {
314             diag->error(DiagMessage(outputPath) << "failed to write data");
315             return false;
316         }
317     }
318 
319     if (!writer->finishEntry()) {
320         diag->error(DiagMessage(outputPath) << "failed to finish writing data");
321         return false;
322     }
323     return true;
324 }
325 
compileXml(IAaptContext * context,const CompileOptions & options,const ResourcePathData & pathData,IArchiveWriter * writer,const std::string & outputPath)326 static bool compileXml(IAaptContext* context, const CompileOptions& options,
327                        const ResourcePathData& pathData, IArchiveWriter* writer,
328                        const std::string& outputPath) {
329 
330     std::unique_ptr<xml::XmlResource> xmlRes;
331     {
332         std::ifstream fin(pathData.source.path, std::ifstream::binary);
333         if (!fin) {
334             context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
335             return false;
336         }
337 
338         xmlRes = xml::inflate(&fin, context->getDiagnostics(), pathData.source);
339 
340         fin.close();
341     }
342 
343     if (!xmlRes) {
344         return false;
345     }
346 
347     // Collect IDs that are defined here.
348     XmlIdCollector collector;
349     if (!collector.consume(context, xmlRes.get())) {
350         return false;
351     }
352 
353     xmlRes->file.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
354     xmlRes->file.config = pathData.config;
355     xmlRes->file.source = pathData.source;
356 
357     BigBuffer buffer(1024);
358     XmlFlattenerOptions xmlFlattenerOptions;
359     xmlFlattenerOptions.keepRawValues = true;
360     XmlFlattener flattener(&buffer, xmlFlattenerOptions);
361     if (!flattener.consume(context, xmlRes.get())) {
362         return false;
363     }
364 
365     if (!writeHeaderAndBufferToWriter(outputPath, xmlRes->file, buffer, writer,
366                                       context->getDiagnostics())) {
367         return false;
368     }
369     return true;
370 }
371 
compilePng(IAaptContext * context,const CompileOptions & options,const ResourcePathData & pathData,IArchiveWriter * writer,const std::string & outputPath)372 static bool compilePng(IAaptContext* context, const CompileOptions& options,
373                        const ResourcePathData& pathData, IArchiveWriter* writer,
374                        const std::string& outputPath) {
375     BigBuffer buffer(4096);
376     ResourceFile resFile;
377     resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
378     resFile.config = pathData.config;
379     resFile.source = pathData.source;
380 
381     {
382         std::ifstream fin(pathData.source.path, std::ifstream::binary);
383         if (!fin) {
384             context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
385             return false;
386         }
387 
388         Png png(context->getDiagnostics());
389         if (!png.process(pathData.source, &fin, &buffer, {})) {
390             return false;
391         }
392     }
393 
394     if (!writeHeaderAndBufferToWriter(outputPath, resFile, buffer, writer,
395                                       context->getDiagnostics())) {
396         return false;
397     }
398     return true;
399 }
400 
compileFile(IAaptContext * context,const CompileOptions & options,const ResourcePathData & pathData,IArchiveWriter * writer,const std::string & outputPath)401 static bool compileFile(IAaptContext* context, const CompileOptions& options,
402                         const ResourcePathData& pathData, IArchiveWriter* writer,
403                         const std::string& outputPath) {
404     BigBuffer buffer(256);
405     ResourceFile resFile;
406     resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
407     resFile.config = pathData.config;
408     resFile.source = pathData.source;
409 
410     std::string errorStr;
411     Maybe<android::FileMap> f = file::mmapPath(pathData.source.path, &errorStr);
412     if (!f) {
413         context->getDiagnostics()->error(DiagMessage(pathData.source) << errorStr);
414         return false;
415     }
416 
417     if (!writeHeaderAndMmapToWriter(outputPath, resFile, f.value(), writer,
418                                     context->getDiagnostics())) {
419         return false;
420     }
421     return true;
422 }
423 
424 class CompileContext : public IAaptContext {
425 public:
setVerbose(bool val)426     void setVerbose(bool val) {
427         mVerbose = val;
428     }
429 
verbose()430     bool verbose() override {
431         return mVerbose;
432     }
433 
getDiagnostics()434     IDiagnostics* getDiagnostics() override {
435        return &mDiagnostics;
436     }
437 
getNameMangler()438     NameMangler* getNameMangler() override {
439        abort();
440        return nullptr;
441     }
442 
getCompilationPackage()443     const std::u16string& getCompilationPackage() override {
444         static std::u16string empty;
445         return empty;
446     }
447 
getPackageId()448     uint8_t getPackageId() override {
449        return 0x0;
450     }
451 
getExternalSymbols()452     SymbolTable* getExternalSymbols() override {
453        abort();
454        return nullptr;
455     }
456 
457 private:
458     StdErrDiagnostics mDiagnostics;
459     bool mVerbose = false;
460 
461 };
462 
463 /**
464  * Entry point for compilation phase. Parses arguments and dispatches to the correct steps.
465  */
compile(const std::vector<StringPiece> & args)466 int compile(const std::vector<StringPiece>& args) {
467     CompileContext context;
468     CompileOptions options;
469 
470     bool verbose = false;
471     Flags flags = Flags()
472             .requiredFlag("-o", "Output path", &options.outputPath)
473             .optionalFlag("--dir", "Directory to scan for resources", &options.resDir)
474             .optionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales "
475                             "(en-XA and ar-XB)", &options.pseudolocalize)
476             .optionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings",
477                             &options.legacyMode)
478             .optionalSwitch("-v", "Enables verbose logging", &verbose);
479     if (!flags.parse("aapt2 compile", args, &std::cerr)) {
480         return 1;
481     }
482 
483     context.setVerbose(verbose);
484 
485     std::unique_ptr<IArchiveWriter> archiveWriter;
486 
487     std::vector<ResourcePathData> inputData;
488     if (options.resDir) {
489         if (!flags.getArgs().empty()) {
490             // Can't have both files and a resource directory.
491             context.getDiagnostics()->error(DiagMessage() << "files given but --dir specified");
492             flags.usage("aapt2 compile", &std::cerr);
493             return 1;
494         }
495 
496         if (!loadInputFilesFromDir(&context, options, &inputData)) {
497             return 1;
498         }
499 
500         archiveWriter = createZipFileArchiveWriter(context.getDiagnostics(), options.outputPath);
501 
502     } else {
503         inputData.reserve(flags.getArgs().size());
504 
505         // Collect data from the path for each input file.
506         for (const std::string& arg : flags.getArgs()) {
507             std::string errorStr;
508             if (Maybe<ResourcePathData> pathData = extractResourcePathData(arg, &errorStr)) {
509                 inputData.push_back(std::move(pathData.value()));
510             } else {
511                 context.getDiagnostics()->error(DiagMessage() << errorStr << " (" << arg << ")");
512                 return 1;
513             }
514         }
515 
516         archiveWriter = createDirectoryArchiveWriter(context.getDiagnostics(), options.outputPath);
517     }
518 
519     if (!archiveWriter) {
520         return false;
521     }
522 
523     bool error = false;
524     for (ResourcePathData& pathData : inputData) {
525         if (options.verbose) {
526             context.getDiagnostics()->note(DiagMessage(pathData.source) << "processing");
527         }
528 
529         if (pathData.resourceDir == u"values") {
530             // Overwrite the extension.
531             pathData.extension = "arsc";
532 
533             const std::string outputFilename = buildIntermediateFilename(pathData);
534             if (!compileTable(&context, options, pathData, archiveWriter.get(), outputFilename)) {
535                 error = true;
536             }
537 
538         } else {
539             const std::string outputFilename = buildIntermediateFilename(pathData);
540             if (const ResourceType* type = parseResourceType(pathData.resourceDir)) {
541                 if (*type != ResourceType::kRaw) {
542                     if (pathData.extension == "xml") {
543                         if (!compileXml(&context, options, pathData, archiveWriter.get(),
544                                         outputFilename)) {
545                             error = true;
546                         }
547                     } else if (pathData.extension == "png" || pathData.extension == "9.png") {
548                         if (!compilePng(&context, options, pathData, archiveWriter.get(),
549                                         outputFilename)) {
550                             error = true;
551                         }
552                     } else {
553                         if (!compileFile(&context, options, pathData, archiveWriter.get(),
554                                          outputFilename)) {
555                             error = true;
556                         }
557                     }
558                 } else {
559                     if (!compileFile(&context, options, pathData, archiveWriter.get(),
560                                      outputFilename)) {
561                         error = true;
562                     }
563                 }
564             } else {
565                 context.getDiagnostics()->error(
566                         DiagMessage() << "invalid file path '" << pathData.source << "'");
567                 error = true;
568             }
569         }
570     }
571 
572     if (error) {
573         return 1;
574     }
575     return 0;
576 }
577 
578 } // namespace aapt
579