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