[go: nahoru, domu]

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
42namespace aapt {
43
44struct 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 */
61static 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
107struct CompileOptions {
108    std::string outputPath;
109    Maybe<std::string> resDir;
110    bool pseudolocalize = false;
111    bool legacyMode = false;
112    bool verbose = false;
113};
114
115static 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
129static 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 */
136static 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
184static 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
260static 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
294static 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
326static 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
372static 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
401static 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
424class CompileContext : public IAaptContext {
425public:
426    void setVerbose(bool val) {
427        mVerbose = val;
428    }
429
430    bool verbose() override {
431        return mVerbose;
432    }
433
434    IDiagnostics* getDiagnostics() override {
435       return &mDiagnostics;
436    }
437
438    NameMangler* getNameMangler() override {
439       abort();
440       return nullptr;
441    }
442
443    const std::u16string& getCompilationPackage() override {
444        static std::u16string empty;
445        return empty;
446    }
447
448    uint8_t getPackageId() override {
449       return 0x0;
450    }
451
452    SymbolTable* getExternalSymbols() override {
453       abort();
454       return nullptr;
455    }
456
457private:
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 */
466int 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