blob: b3b0f65e54da94896b980485652fa5f8603e7a81 [file] [log] [blame]
Adam Lesinski1ab598f2015-08-14 14:26:04 -07001/*
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"
Adam Lesinski1ab598f2015-08-14 14:26:04 -070022#include "compile/IdAssigner.h"
23#include "compile/Png.h"
Adam Lesinski393b5f02015-12-17 13:03:11 -080024#include "compile/PseudolocaleGenerator.h"
Adam Lesinski1ab598f2015-08-14 14:26:04 -070025#include "compile/XmlIdCollector.h"
Adam Lesinskia40e9722015-11-24 19:11:46 -080026#include "flatten/Archive.h"
Adam Lesinski1ab598f2015-08-14 14:26:04 -070027#include "flatten/FileExportWriter.h"
28#include "flatten/TableFlattener.h"
29#include "flatten/XmlFlattener.h"
30#include "util/Files.h"
31#include "util/Maybe.h"
32#include "util/Util.h"
Adam Lesinski467f1712015-11-16 17:35:44 -080033#include "xml/XmlDom.h"
34#include "xml/XmlPullParser.h"
Adam Lesinski1ab598f2015-08-14 14:26:04 -070035
Adam Lesinskia40e9722015-11-24 19:11:46 -080036#include <dirent.h>
Adam Lesinski1ab598f2015-08-14 14:26:04 -070037#include <fstream>
38#include <string>
39
40namespace aapt {
41
42struct ResourcePathData {
43 Source source;
44 std::u16string resourceDir;
45 std::u16string name;
46 std::string extension;
47
48 // Original config str. We keep this because when we parse the config, we may add on
49 // version qualifiers. We want to preserve the original input so the output is easily
50 // computed before hand.
51 std::string configStr;
52 ConfigDescription config;
53};
54
55/**
56 * Resource file paths are expected to look like:
57 * [--/res/]type[-config]/name
58 */
59static Maybe<ResourcePathData> extractResourcePathData(const std::string& path,
60 std::string* outError) {
61 std::vector<std::string> parts = util::split(path, file::sDirSep);
62 if (parts.size() < 2) {
63 if (outError) *outError = "bad resource path";
64 return {};
65 }
66
67 std::string& dir = parts[parts.size() - 2];
68 StringPiece dirStr = dir;
69
70 StringPiece configStr;
71 ConfigDescription config;
72 size_t dashPos = dir.find('-');
73 if (dashPos != std::string::npos) {
74 configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1));
75 if (!ConfigDescription::parse(configStr, &config)) {
76 if (outError) {
77 std::stringstream errStr;
78 errStr << "invalid configuration '" << configStr << "'";
79 *outError = errStr.str();
80 }
81 return {};
82 }
83 dirStr = dirStr.substr(0, dashPos);
84 }
85
86 std::string& filename = parts[parts.size() - 1];
87 StringPiece name = filename;
88 StringPiece extension;
89 size_t dotPos = filename.find('.');
90 if (dotPos != std::string::npos) {
91 extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1));
92 name = name.substr(0, dotPos);
93 }
94
95 return ResourcePathData{
Adam Lesinskia40e9722015-11-24 19:11:46 -080096 Source(path),
Adam Lesinski1ab598f2015-08-14 14:26:04 -070097 util::utf8ToUtf16(dirStr),
98 util::utf8ToUtf16(name),
99 extension.toString(),
100 configStr.toString(),
101 config
102 };
103}
104
105struct CompileOptions {
106 std::string outputPath;
Adam Lesinskia40e9722015-11-24 19:11:46 -0800107 Maybe<std::string> resDir;
Adam Lesinski7751afc2016-01-06 15:45:28 -0800108 std::vector<std::u16string> products;
Adam Lesinski393b5f02015-12-17 13:03:11 -0800109 bool pseudolocalize = false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700110 bool verbose = false;
111};
112
Adam Lesinskia40e9722015-11-24 19:11:46 -0800113static std::string buildIntermediateFilename(const ResourcePathData& data) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700114 std::stringstream name;
115 name << data.resourceDir;
116 if (!data.configStr.empty()) {
117 name << "-" << data.configStr;
118 }
119 name << "_" << data.name << "." << data.extension << ".flat";
Adam Lesinskia40e9722015-11-24 19:11:46 -0800120 return name.str();
121}
122
123static bool isHidden(const StringPiece& filename) {
124 return util::stringStartsWith<char>(filename, ".");
125}
126
127/**
128 * Walks the res directory structure, looking for resource files.
129 */
130static bool loadInputFilesFromDir(IAaptContext* context, const CompileOptions& options,
131 std::vector<ResourcePathData>* outPathData) {
132 const std::string& rootDir = options.resDir.value();
133 std::unique_ptr<DIR, decltype(closedir)*> d(opendir(rootDir.data()), closedir);
134 if (!d) {
135 context->getDiagnostics()->error(DiagMessage() << strerror(errno));
136 return false;
137 }
138
139 while (struct dirent* entry = readdir(d.get())) {
140 if (isHidden(entry->d_name)) {
141 continue;
142 }
143
144 std::string prefixPath = rootDir;
145 file::appendPath(&prefixPath, entry->d_name);
146
147 if (file::getFileType(prefixPath) != file::FileType::kDirectory) {
148 continue;
149 }
150
151 std::unique_ptr<DIR, decltype(closedir)*> subDir(opendir(prefixPath.data()), closedir);
152 if (!subDir) {
153 context->getDiagnostics()->error(DiagMessage() << strerror(errno));
154 return false;
155 }
156
157 while (struct dirent* leafEntry = readdir(subDir.get())) {
158 if (isHidden(leafEntry->d_name)) {
159 continue;
160 }
161
162 std::string fullPath = prefixPath;
163 file::appendPath(&fullPath, leafEntry->d_name);
164
165 std::string errStr;
166 Maybe<ResourcePathData> pathData = extractResourcePathData(fullPath, &errStr);
167 if (!pathData) {
168 context->getDiagnostics()->error(DiagMessage() << errStr);
169 return false;
170 }
171
172 outPathData->push_back(std::move(pathData.value()));
173 }
174 }
175 return true;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700176}
177
178static bool compileTable(IAaptContext* context, const CompileOptions& options,
Adam Lesinskia40e9722015-11-24 19:11:46 -0800179 const ResourcePathData& pathData, IArchiveWriter* writer,
180 const std::string& outputPath) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700181 ResourceTable table;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700182 {
183 std::ifstream fin(pathData.source.path, std::ifstream::binary);
184 if (!fin) {
185 context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
186 return false;
187 }
188
189
190 // Parse the values file from XML.
Adam Lesinski467f1712015-11-16 17:35:44 -0800191 xml::XmlPullParser xmlParser(fin);
Adam Lesinski9f222042015-11-04 13:51:45 -0800192
193 ResourceParserOptions parserOptions;
Adam Lesinski7751afc2016-01-06 15:45:28 -0800194 parserOptions.products = options.products;
Adam Lesinski9f222042015-11-04 13:51:45 -0800195
196 // If the filename includes donottranslate, then the default translatable is false.
197 parserOptions.translatable = pathData.name.find(u"donottranslate") == std::string::npos;
198
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700199 ResourceParser resParser(context->getDiagnostics(), &table, pathData.source,
Adam Lesinski9f222042015-11-04 13:51:45 -0800200 pathData.config, parserOptions);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700201 if (!resParser.parse(&xmlParser)) {
202 return false;
203 }
204
205 fin.close();
206 }
207
Adam Lesinski393b5f02015-12-17 13:03:11 -0800208 if (options.pseudolocalize) {
209 // Generate pseudo-localized strings (en-XA and ar-XB).
210 // These are created as weak symbols, and are only generated from default configuration
211 // strings and plurals.
212 PseudolocaleGenerator pseudolocaleGenerator;
213 if (!pseudolocaleGenerator.consume(context, &table)) {
214 return false;
215 }
216 }
217
Adam Lesinski83f22552015-11-07 11:51:23 -0800218 // Ensure we have the compilation package at least.
219 table.createPackage(context->getCompilationPackage());
220
Adam Lesinskia40e9722015-11-24 19:11:46 -0800221 // Assign an ID to any package that has resources.
Adam Lesinski83f22552015-11-07 11:51:23 -0800222 for (auto& pkg : table.packages) {
223 if (!pkg->id) {
224 // If no package ID was set while parsing (public identifiers), auto assign an ID.
225 pkg->id = context->getPackageId();
226 }
Adam Lesinski9ba47d82015-10-13 11:37:10 -0700227 }
228
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700229 // Assign IDs to prepare the table for flattening.
230 IdAssigner idAssigner;
231 if (!idAssigner.consume(context, &table)) {
232 return false;
233 }
234
235 // Flatten the table.
236 BigBuffer buffer(1024);
237 TableFlattenerOptions tableFlattenerOptions;
238 tableFlattenerOptions.useExtendedChunks = true;
239 TableFlattener flattener(&buffer, tableFlattenerOptions);
240 if (!flattener.consume(context, &table)) {
241 return false;
242 }
243
Adam Lesinskia40e9722015-11-24 19:11:46 -0800244 if (!writer->startEntry(outputPath, 0)) {
245 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open");
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700246 return false;
247 }
248
Adam Lesinskia40e9722015-11-24 19:11:46 -0800249 if (writer->writeEntry(buffer)) {
250 if (writer->finishEntry()) {
251 return true;
252 }
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700253 }
Adam Lesinskia40e9722015-11-24 19:11:46 -0800254
255 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write");
256 return false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700257}
258
259static bool compileXml(IAaptContext* context, const CompileOptions& options,
Adam Lesinskia40e9722015-11-24 19:11:46 -0800260 const ResourcePathData& pathData, IArchiveWriter* writer,
261 const std::string& outputPath) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700262
Adam Lesinski467f1712015-11-16 17:35:44 -0800263 std::unique_ptr<xml::XmlResource> xmlRes;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700264
265 {
266 std::ifstream fin(pathData.source.path, std::ifstream::binary);
267 if (!fin) {
268 context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
269 return false;
270 }
271
272 xmlRes = xml::inflate(&fin, context->getDiagnostics(), pathData.source);
273
274 fin.close();
275 }
276
277 if (!xmlRes) {
278 return false;
279 }
280
281 // Collect IDs that are defined here.
282 XmlIdCollector collector;
283 if (!collector.consume(context, xmlRes.get())) {
284 return false;
285 }
286
Adam Lesinskia40e9722015-11-24 19:11:46 -0800287 xmlRes->file.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700288 xmlRes->file.config = pathData.config;
289 xmlRes->file.source = pathData.source;
290
291 BigBuffer buffer(1024);
292 ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &xmlRes->file);
293
294 XmlFlattenerOptions xmlFlattenerOptions;
295 xmlFlattenerOptions.keepRawValues = true;
296 XmlFlattener flattener(fileExportWriter.getBuffer(), xmlFlattenerOptions);
297 if (!flattener.consume(context, xmlRes.get())) {
298 return false;
299 }
300
301 fileExportWriter.finish();
302
Adam Lesinskia40e9722015-11-24 19:11:46 -0800303 if (!writer->startEntry(outputPath, 0)) {
304 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open");
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700305 return false;
306 }
307
Adam Lesinskia40e9722015-11-24 19:11:46 -0800308 if (writer->writeEntry(buffer)) {
309 if (writer->finishEntry()) {
310 return true;
311 }
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700312 }
Adam Lesinskia40e9722015-11-24 19:11:46 -0800313
314 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write");
315 return false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700316}
317
318static bool compilePng(IAaptContext* context, const CompileOptions& options,
Adam Lesinskia40e9722015-11-24 19:11:46 -0800319 const ResourcePathData& pathData, IArchiveWriter* writer,
320 const std::string& outputPath) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700321 BigBuffer buffer(4096);
322 ResourceFile resFile;
Adam Lesinskia40e9722015-11-24 19:11:46 -0800323 resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700324 resFile.config = pathData.config;
325 resFile.source = pathData.source;
326
327 ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &resFile);
328
329 {
330 std::ifstream fin(pathData.source.path, std::ifstream::binary);
331 if (!fin) {
332 context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno));
333 return false;
334 }
335
336 Png png(context->getDiagnostics());
337 if (!png.process(pathData.source, &fin, fileExportWriter.getBuffer(), {})) {
338 return false;
339 }
340 }
341
342 fileExportWriter.finish();
343
Adam Lesinskia40e9722015-11-24 19:11:46 -0800344 if (!writer->startEntry(outputPath, 0)) {
345 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open");
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700346 return false;
347 }
348
Adam Lesinskia40e9722015-11-24 19:11:46 -0800349 if (writer->writeEntry(buffer)) {
350 if (writer->finishEntry()) {
351 return true;
352 }
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700353 }
Adam Lesinskia40e9722015-11-24 19:11:46 -0800354
355 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write");
356 return false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700357}
358
359static bool compileFile(IAaptContext* context, const CompileOptions& options,
Adam Lesinskia40e9722015-11-24 19:11:46 -0800360 const ResourcePathData& pathData, IArchiveWriter* writer,
361 const std::string& outputPath) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700362 BigBuffer buffer(256);
363 ResourceFile resFile;
Adam Lesinskia40e9722015-11-24 19:11:46 -0800364 resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700365 resFile.config = pathData.config;
366 resFile.source = pathData.source;
367
368 ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &resFile);
369
370 std::string errorStr;
371 Maybe<android::FileMap> f = file::mmapPath(pathData.source.path, &errorStr);
372 if (!f) {
373 context->getDiagnostics()->error(DiagMessage(pathData.source) << errorStr);
374 return false;
375 }
376
Adam Lesinskia40e9722015-11-24 19:11:46 -0800377 if (!writer->startEntry(outputPath, 0)) {
378 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open");
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700379 return false;
380 }
381
382 // Manually set the size and don't call finish(). This is because we are not copying from
383 // the buffer the entire file.
384 fileExportWriter.getChunkHeader()->size =
385 util::hostToDevice32(buffer.size() + f.value().getDataLength());
Adam Lesinskia40e9722015-11-24 19:11:46 -0800386
387 if (writer->writeEntry(buffer)) {
388 if (writer->writeEntry(f.value().getDataPtr(), f.value().getDataLength())) {
389 if (writer->finishEntry()) {
390 return true;
391 }
392 }
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700393 }
394
Adam Lesinskia40e9722015-11-24 19:11:46 -0800395 context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write");
396 return false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700397}
398
399class CompileContext : public IAaptContext {
400private:
401 StdErrDiagnostics mDiagnostics;
402
403public:
404 IDiagnostics* getDiagnostics() override {
405 return &mDiagnostics;
406 }
407
408 NameMangler* getNameMangler() override {
409 abort();
410 return nullptr;
411 }
412
413 StringPiece16 getCompilationPackage() override {
414 return {};
415 }
416
417 uint8_t getPackageId() override {
Adam Lesinski9ba47d82015-10-13 11:37:10 -0700418 return 0x0;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700419 }
420
421 ISymbolTable* getExternalSymbols() override {
422 abort();
423 return nullptr;
424 }
425};
426
427/**
428 * Entry point for compilation phase. Parses arguments and dispatches to the correct steps.
429 */
430int compile(const std::vector<StringPiece>& args) {
431 CompileOptions options;
432
Adam Lesinski7751afc2016-01-06 15:45:28 -0800433 Maybe<std::string> productList;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700434 Flags flags = Flags()
435 .requiredFlag("-o", "Output path", &options.outputPath)
Adam Lesinski7751afc2016-01-06 15:45:28 -0800436 .optionalFlag("--product", "Comma separated list of product types to compile",
437 &productList)
Adam Lesinskia40e9722015-11-24 19:11:46 -0800438 .optionalFlag("--dir", "Directory to scan for resources", &options.resDir)
Adam Lesinski393b5f02015-12-17 13:03:11 -0800439 .optionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales "
440 "(en-XA and ar-XB)", &options.pseudolocalize)
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700441 .optionalSwitch("-v", "Enables verbose logging", &options.verbose);
442 if (!flags.parse("aapt2 compile", args, &std::cerr)) {
443 return 1;
444 }
445
Adam Lesinski7751afc2016-01-06 15:45:28 -0800446 if (productList) {
447 for (StringPiece part : util::tokenize<char>(productList.value(), ',')) {
448 options.products.push_back(util::utf8ToUtf16(part));
449 }
Adam Lesinski9ba47d82015-10-13 11:37:10 -0700450 }
451
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700452 CompileContext context;
Adam Lesinskia40e9722015-11-24 19:11:46 -0800453 std::unique_ptr<IArchiveWriter> archiveWriter;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700454
455 std::vector<ResourcePathData> inputData;
Adam Lesinskia40e9722015-11-24 19:11:46 -0800456 if (options.resDir) {
457 if (!flags.getArgs().empty()) {
458 // Can't have both files and a resource directory.
459 context.getDiagnostics()->error(DiagMessage() << "files given but --dir specified");
460 flags.usage("aapt2 compile", &std::cerr);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700461 return 1;
462 }
Adam Lesinskia40e9722015-11-24 19:11:46 -0800463
464 if (!loadInputFilesFromDir(&context, options, &inputData)) {
465 return 1;
466 }
467
468 archiveWriter = createZipFileArchiveWriter(context.getDiagnostics(), options.outputPath);
469
470 } else {
471 inputData.reserve(flags.getArgs().size());
472
473 // Collect data from the path for each input file.
474 for (const std::string& arg : flags.getArgs()) {
475 std::string errorStr;
476 if (Maybe<ResourcePathData> pathData = extractResourcePathData(arg, &errorStr)) {
477 inputData.push_back(std::move(pathData.value()));
478 } else {
479 context.getDiagnostics()->error(DiagMessage() << errorStr << " (" << arg << ")");
480 return 1;
481 }
482 }
483
484 archiveWriter = createDirectoryArchiveWriter(context.getDiagnostics(), options.outputPath);
485 }
486
487 if (!archiveWriter) {
488 return false;
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700489 }
490
491 bool error = false;
492 for (ResourcePathData& pathData : inputData) {
493 if (options.verbose) {
494 context.getDiagnostics()->note(DiagMessage(pathData.source) << "processing");
495 }
496
497 if (pathData.resourceDir == u"values") {
498 // Overwrite the extension.
499 pathData.extension = "arsc";
500
Adam Lesinskia40e9722015-11-24 19:11:46 -0800501 const std::string outputFilename = buildIntermediateFilename(pathData);
502 if (!compileTable(&context, options, pathData, archiveWriter.get(), outputFilename)) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700503 error = true;
504 }
505
506 } else {
Adam Lesinskia40e9722015-11-24 19:11:46 -0800507 const std::string outputFilename = buildIntermediateFilename(pathData);
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700508 if (const ResourceType* type = parseResourceType(pathData.resourceDir)) {
509 if (*type != ResourceType::kRaw) {
510 if (pathData.extension == "xml") {
Adam Lesinskia40e9722015-11-24 19:11:46 -0800511 if (!compileXml(&context, options, pathData, archiveWriter.get(),
512 outputFilename)) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700513 error = true;
514 }
515 } else if (pathData.extension == "png" || pathData.extension == "9.png") {
Adam Lesinskia40e9722015-11-24 19:11:46 -0800516 if (!compilePng(&context, options, pathData, archiveWriter.get(),
517 outputFilename)) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700518 error = true;
519 }
520 } else {
Adam Lesinskia40e9722015-11-24 19:11:46 -0800521 if (!compileFile(&context, options, pathData, archiveWriter.get(),
522 outputFilename)) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700523 error = true;
524 }
525 }
526 } else {
Adam Lesinskia40e9722015-11-24 19:11:46 -0800527 if (!compileFile(&context, options, pathData, archiveWriter.get(),
528 outputFilename)) {
Adam Lesinski1ab598f2015-08-14 14:26:04 -0700529 error = true;
530 }
531 }
532 } else {
533 context.getDiagnostics()->error(
534 DiagMessage() << "invalid file path '" << pathData.source << "'");
535 error = true;
536 }
537 }
538 }
539
540 if (error) {
541 return 1;
542 }
543 return 0;
544}
545
546} // namespace aapt