Home | History | Annotate | Download | only in compile
      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  */
     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 
    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 
    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  */
    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 
    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 
    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 
    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 
    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 
    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 
    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:
    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 
    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  */
    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