diff options
Diffstat (limited to 'tools')
86 files changed, 3662 insertions, 1479 deletions
diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp index 861efd5077fe..69392d66e21f 100644 --- a/tools/aapt/XMLNode.cpp +++ b/tools/aapt/XMLNode.cpp @@ -474,9 +474,9 @@ void printXMLBlock(ResXMLTree* block) if (value.dataType == Res_value::TYPE_NULL) { printf("=(null)"); } else if (value.dataType == Res_value::TYPE_REFERENCE) { - printf("=@0x%x", (int)value.data); + printf("=@0x%08x", (int)value.data); } else if (value.dataType == Res_value::TYPE_ATTRIBUTE) { - printf("=?0x%x", (int)value.data); + printf("=?0x%08x", (int)value.data); } else if (value.dataType == Res_value::TYPE_STRING) { printf("=\"%s\"", ResTable::normalizeForOutput(String8(block->getAttributeStringValue(i, diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 48cfc4453d8f..2ecf25b751b6 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -15,6 +15,7 @@ // toolSources = [ + "cmd/Command.cpp", "cmd/Compile.cpp", "cmd/Convert.cpp", "cmd/Diff.cpp", @@ -111,6 +112,7 @@ cc_library_host_static { "link/XmlReferenceLinker.cpp", "optimize/MultiApkGenerator.cpp", "optimize/ResourceDeduper.cpp", + "optimize/ResourceFilter.cpp", "optimize/VersionCollapser.cpp", "process/SymbolTable.cpp", "split/TableSplitter.cpp", @@ -123,7 +125,6 @@ cc_library_host_static { "ConfigDescription.cpp", "Debug.cpp", "DominatorTree.cpp", - "Flags.cpp", "java/AnnotationProcessor.cpp", "java/ClassDefinition.cpp", "java/JavaClassGenerator.cpp", diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h index d6f599520d71..75123537116f 100644 --- a/tools/aapt2/AppInfo.h +++ b/tools/aapt2/AppInfo.h @@ -31,9 +31,12 @@ struct AppInfo { // The app's minimum SDK version, if it is defined. Maybe<int> min_sdk_version; - // The app's version code, if it is defined. + // The app's version code (the lower 32 bits of the long version code), if it is defined. Maybe<uint32_t> version_code; + // The app's version code major (the upper 32 bits of the long version code), if it is defined. + Maybe<uint32_t> version_code_major; + // The app's revision code, if it is defined. Maybe<uint32_t> revision_code; diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index f064cb14248f..0a517ab8a2de 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -408,6 +408,41 @@ void Debug::DumpHex(const void* data, size_t len) { } } +void Debug::DumpResStringPool(const android::ResStringPool* pool, text::Printer* printer) { + using namespace android; + + if (pool->getError() == NO_INIT) { + printer->Print("String pool is unitialized.\n"); + return; + } else if (pool->getError() != NO_ERROR) { + printer->Print("String pool is corrupt/invalid.\n"); + return; + } + + SortedVector<const void*> uniqueStrings; + const size_t N = pool->size(); + for (size_t i=0; i<N; i++) { + size_t len; + if (pool->isUTF8()) { + uniqueStrings.add(pool->string8At(i, &len)); + } else { + uniqueStrings.add(pool->stringAt(i, &len)); + } + } + + printer->Print(StringPrintf("String pool of %zd unique %s %s strings, %zd entries and %zd styles " + "using %zd bytes:\n", uniqueStrings.size(), + pool->isUTF8() ? "UTF-8" : "UTF-16", + pool->isSorted() ? "sorted" : "non-sorted", N, pool->styleCount(), + pool->bytes())); + + const size_t NS = pool->size(); + for (size_t s=0; s<NS; s++) { + String8 str = pool->string8ObjectAt(s); + printer->Print(StringPrintf("String #%zd : %s\n", s, str.string())); + } +} + namespace { class XmlPrinter : public xml::ConstVisitor { diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index 382707e1d4cd..a43197cacf7b 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -38,6 +38,7 @@ struct Debug { static void PrintStyleGraph(ResourceTable* table, const ResourceName& target_style); static void DumpHex(const void* data, size_t len); static void DumpXml(const xml::XmlResource& doc, text::Printer* printer); + static void DumpResStringPool(const android::ResStringPool* pool, text::Printer* printer); }; } // namespace aapt diff --git a/tools/aapt2/Flags.h b/tools/aapt2/Flags.h deleted file mode 100644 index 3b3ae710dc7b..000000000000 --- a/tools/aapt2/Flags.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_FLAGS_H -#define AAPT_FLAGS_H - -#include <functional> -#include <ostream> -#include <string> -#include <unordered_set> -#include <vector> - -#include "androidfw/StringPiece.h" - -#include "util/Maybe.h" - -namespace aapt { - -class Flags { - public: - Flags& RequiredFlag(const android::StringPiece& name, const android::StringPiece& description, - std::string* value); - Flags& RequiredFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::vector<std::string>* value); - Flags& OptionalFlag(const android::StringPiece& name, const android::StringPiece& description, - Maybe<std::string>* value); - Flags& OptionalFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::vector<std::string>* value); - Flags& OptionalFlagList(const android::StringPiece& name, const android::StringPiece& description, - std::unordered_set<std::string>* value); - Flags& OptionalSwitch(const android::StringPiece& name, const android::StringPiece& description, - bool* value); - - void Usage(const android::StringPiece& command, std::ostream* out); - - bool Parse(const android::StringPiece& command, const std::vector<android::StringPiece>& args, - std::ostream* outError); - - const std::vector<std::string>& GetArgs(); - - private: - struct Flag { - std::string name; - std::string description; - std::function<bool(const android::StringPiece& value)> action; - bool required; - size_t num_args; - - bool parsed; - }; - - std::vector<Flag> flags_; - std::vector<std::string> args_; -}; - -} // namespace aapt - -#endif // AAPT_FLAGS_H diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp index 1dd46ba813db..a73d56c8f951 100644 --- a/tools/aapt2/LoadedApk.cpp +++ b/tools/aapt2/LoadedApk.cpp @@ -76,7 +76,7 @@ std::unique_ptr<LoadedApk> LoadedApk::LoadProtoApkFromFileCollection( } std::string error; - table = util::make_unique<ResourceTable>(); + table = util::make_unique<ResourceTable>(/** validate_resources **/ false); if (!DeserializeTableFromPb(pb_table, collection.get(), table.get(), &error)) { diag->Error(DiagMessage(source) << "failed to deserialize " << kProtoResourceTablePath << ": " << error); @@ -120,7 +120,7 @@ std::unique_ptr<LoadedApk> LoadedApk::LoadBinaryApkFromFileCollection( io::IFile* table_file = collection->FindFile(kApkResourceTablePath); if (table_file != nullptr) { - table = util::make_unique<ResourceTable>(); + table = util::make_unique<ResourceTable>(/** validate_resources **/ false); std::unique_ptr<io::IData> data = table_file->OpenAsData(); if (data == nullptr) { diag->Error(DiagMessage(source) << "failed to open " << kApkResourceTablePath); @@ -184,10 +184,7 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table std::unique_ptr<io::IFileCollectionIterator> iterator = apk_->Iterator(); while (iterator->HasNext()) { io::IFile* file = iterator->Next(); - std::string path = file->GetSource().path; - // The name of the path has the format "<zip-file-name>@<path-to-file>". - path = path.substr(path.find('@') + 1); // Skip resources that are not referenced if requested. if (path.find("res/") == 0 && referenced_resources.find(path) == referenced_resources.end()) { @@ -257,6 +254,53 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table return true; } +std::unique_ptr<xml::XmlResource> LoadedApk::LoadXml(const std::string& file_path, + IDiagnostics* diag) { + io::IFile* file = apk_->FindFile(file_path); + if (file == nullptr) { + diag->Error(DiagMessage() << "failed to find file"); + return nullptr; + } + + std::unique_ptr<xml::XmlResource> doc; + if (format_ == ApkFormat::kProto) { + std::unique_ptr<io::InputStream> in = file->OpenInputStream(); + if (!in) { + diag->Error(DiagMessage() << "failed to open file"); + return nullptr; + } + + io::ZeroCopyInputAdaptor adaptor(in.get()); + pb::XmlNode pb_node; + if (!pb_node.ParseFromZeroCopyStream(&adaptor)) { + diag->Error(DiagMessage() << "failed to parse file as proto XML"); + return nullptr; + } + + std::string err; + doc = DeserializeXmlResourceFromPb(pb_node, &err); + if (!doc) { + diag->Error(DiagMessage() << "failed to deserialize proto XML: " << err); + return nullptr; + } + } else if (format_ == ApkFormat::kBinary) { + std::unique_ptr<io::IData> data = file->OpenAsData(); + if (!data) { + diag->Error(DiagMessage() << "failed to open file"); + return nullptr; + } + + std::string err; + doc = xml::Inflate(data->data(), data->size(), &err); + if (!doc) { + diag->Error(DiagMessage() << "failed to parse file as binary XML: " << err); + return nullptr; + } + } + + return doc; +} + ApkFormat LoadedApk::DetermineApkFormat(io::IFileCollection* apk) { if (apk->FindFile(kApkResourceTablePath) != nullptr) { return ApkFormat::kBinary; diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h index 41f879d0cdc3..dcb085aa2b0b 100644 --- a/tools/aapt2/LoadedApk.h +++ b/tools/aapt2/LoadedApk.h @@ -70,6 +70,10 @@ class LoadedApk { return apk_.get(); } + ApkFormat GetApkFormat() { + return format_; + } + const ResourceTable* GetResourceTable() const { return table_.get(); } @@ -106,6 +110,8 @@ class LoadedApk { const TableFlattenerOptions& options, FilterChain* filters, IArchiveWriter* writer, xml::XmlResource* manifest = nullptr); + /** Loads the file as an xml document. */ + std::unique_ptr<xml::XmlResource> LoadXml(const std::string& file_path, IDiagnostics* diag); private: DISALLOW_COPY_AND_ASSIGN(LoadedApk); diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 808b29cfd844..37013c07ffef 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -29,6 +29,13 @@ #include "androidfw/StringPiece.h" #include "Diagnostics.h" +#include "cmd/Command.h" +#include "cmd/Compile.h" +#include "cmd/Convert.h" +#include "cmd/Diff.h" +#include "cmd/Dump.h" +#include "cmd/Link.h" +#include "cmd/Optimize.h" #include "util/Files.h" #include "util/Util.h" @@ -43,114 +50,121 @@ static const char* sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. static const char* sMinorVersion = "19"; -static void PrintVersion() { - std::cerr << StringPrintf("Android Asset Packaging Tool (aapt) %s:%s", sMajorVersion, - sMinorVersion) - << std::endl; -} - -static void PrintUsage() { - std::cerr << "\nusage: aapt2 [compile|link|dump|diff|optimize|convert|version] ..." << std::endl; -} +/** Prints the version information of AAPT2. */ +class VersionCommand : public Command { + public: + explicit VersionCommand() : Command("version") { + SetDescription("Prints the version of aapt."); + } -extern int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics); -extern int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics); -extern int Dump(const std::vector<StringPiece>& args); -extern int Diff(const std::vector<StringPiece>& args); -extern int Optimize(const std::vector<StringPiece>& args); -extern int Convert(const std::vector<StringPiece>& args); - -static int ExecuteCommand(const StringPiece& command, const std::vector<StringPiece>& args, - IDiagnostics* diagnostics) { - if (command == "compile" || command == "c") { - return Compile(args, diagnostics); - } else if (command == "link" || command == "l") { - return Link(args, diagnostics); - } else if (command == "dump" || command == "d") { - return Dump(args); - } else if (command == "diff") { - return Diff(args); - } else if (command == "optimize") { - return Optimize(args); - } else if (command == "convert") { - return Convert(args); - } else if (command == "version") { - PrintVersion(); + int Action(const std::vector<std::string>& /* args */) override { + std::cerr << StringPrintf("Android Asset Packaging Tool (aapt) %s:%s", sMajorVersion, + sMinorVersion) + << std::endl; return 0; } - diagnostics->Error(DiagMessage() << "unknown command '" << command << "'"); - return -1; -} +}; + +/** The main entry point of AAPT. */ +class MainCommand : public Command { + public: + explicit MainCommand(IDiagnostics* diagnostics) : Command("aapt2"), diagnostics_(diagnostics) { + AddOptionalSubcommand(util::make_unique<CompileCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<LinkCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<DumpCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<DiffCommand>()); + AddOptionalSubcommand(util::make_unique<OptimizeCommand>()); + AddOptionalSubcommand(util::make_unique<ConvertCommand>()); + AddOptionalSubcommand(util::make_unique<VersionCommand>()); + } -static void RunDaemon(IDiagnostics* diagnostics) { - std::cout << "Ready" << std::endl; - - // Run in daemon mode. The first line of input is the command. This can be 'quit' which ends - // the daemon mode. Each subsequent line is a single parameter to the command. The end of a - // invocation is signaled by providing an empty line. At any point, an EOF signal or the - // command 'quit' will end the daemon mode. - while (true) { - std::vector<std::string> raw_args; - for (std::string line; std::getline(std::cin, line) && !line.empty();) { - raw_args.push_back(line); + int Action(const std::vector<std::string>& args) override { + if (args.size() == 0) { + diagnostics_->Error(DiagMessage() << "no subcommand specified"); + } else { + diagnostics_->Error(DiagMessage() << "unknown subcommand '" << args[0] << "'"); } - if (!std::cin) { - break; - } + Usage(&std::cerr); + return -1; + } - // An empty command does nothing. - if (raw_args.empty()) { - continue; - } + private: + IDiagnostics* diagnostics_; +}; - if (raw_args[0] == "quit") { - break; - } +/* + * Run in daemon mode. The first line of input is the command. This can be 'quit' which ends + * the daemon mode. Each subsequent line is a single parameter to the command. The end of a + * invocation is signaled by providing an empty line. At any point, an EOF signal or the + * command 'quit' will end the daemon mode. + */ +class DaemonCommand : public Command { + public: + explicit DaemonCommand(IDiagnostics* diagnostics) : Command("daemon", "m"), + diagnostics_(diagnostics) { + SetDescription("Runs aapt in daemon mode. Each subsequent line is a single parameter to the\n" + "command. The end of an invocation is signaled by providing an empty line."); + } - std::vector<StringPiece> args; - args.insert(args.end(), ++raw_args.begin(), raw_args.end()); - int ret = ExecuteCommand(raw_args[0], args, diagnostics); - if (ret != 0) { - std::cerr << "Error" << std::endl; + int Action(const std::vector<std::string>& /* args */) override { + std::cout << "Ready" << std::endl; + + while (true) { + std::vector<std::string> raw_args; + for (std::string line; std::getline(std::cin, line) && !line.empty();) { + raw_args.push_back(line); + } + + if (!std::cin) { + break; + } + + // An empty command does nothing. + if (raw_args.empty()) { + continue; + } + + // End the dameon + if (raw_args[0] == "quit") { + break; + } + + std::vector<StringPiece> args; + args.insert(args.end(), raw_args.begin(), raw_args.end()); + if (MainCommand(diagnostics_).Execute(args, &std::cerr) != 0) { + std::cerr << "Error" << std::endl; + } + std::cerr << "Done" << std::endl; } - std::cerr << "Done" << std::endl; + std::cout << "Exiting daemon" << std::endl; + + return 0; } - std::cout << "Exiting daemon" << std::endl; -} + + private: + IDiagnostics* diagnostics_; +}; } // namespace aapt int MainImpl(int argc, char** argv) { - if (argc < 2) { - std::cerr << "no command specified\n"; - aapt::PrintUsage(); + if (argc < 1) { return -1; } - argv += 1; - argc -= 1; - - aapt::StdErrDiagnostics diagnostics; - // Collect the arguments starting after the program name and command name. std::vector<StringPiece> args; for (int i = 1; i < argc; i++) { args.push_back(argv[i]); } - const StringPiece command(argv[0]); - if (command != "daemon" && command != "m") { - // Single execution. - const int result = aapt::ExecuteCommand(command, args, &diagnostics); - if (result < 0) { - aapt::PrintUsage(); - } - return result; - } + // Add the daemon subcommand here so it cannot be called while executing the daemon + aapt::StdErrDiagnostics diagnostics; + auto main_command = new aapt::MainCommand(&diagnostics); + main_command->AddOptionalSubcommand(aapt::util::make_unique<aapt::DaemonCommand>(&diagnostics)); - aapt::RunDaemon(&diagnostics); - return 0; + return main_command->Execute(args, &std::cerr); } int main(int argc, char** argv) { diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp index b78f48ce7f17..ae01170a6894 100644 --- a/tools/aapt2/Resource.cpp +++ b/tools/aapt2/Resource.cpp @@ -96,6 +96,8 @@ StringPiece to_string(ResourceType type) { return "styleable"; case ResourceType::kTransition: return "transition"; + case ResourceType::kUnknown: + return "unknown"; case ResourceType::kXml: return "xml"; } diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 6fcf0f6d19cd..879d0bd8d94e 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -66,6 +66,11 @@ enum class ResourceType { kStyle, kStyleable, kTransition, + + // Not a parsed type. It is only used when loading resource tables that may have modified type + // names + kUnknown, + kXml, }; diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 7f48544c0ae4..8719a233d774 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -208,6 +208,15 @@ class SegmentNode : public Node { } }; +// A chunk of text in the XML string within a CDATA tags. +class CdataSegmentNode : public SegmentNode { + public: + + void Build(StringBuilder* builder) const override { + builder->AppendText(data, /* preserve_spaces */ true); + } +}; + // A tag that will be encoded into the final flattened string. Tags like <b> or <i>. class SpanNode : public Node { public: @@ -244,6 +253,7 @@ bool ResourceParser::FlattenXmlSubtree( std::vector<Node*> node_stack; node_stack.push_back(&root); + bool cdata_block = false; bool saw_span_node = false; SegmentNode* first_segment = nullptr; SegmentNode* last_segment = nullptr; @@ -253,11 +263,15 @@ bool ResourceParser::FlattenXmlSubtree( const xml::XmlPullParser::Event event = parser->event(); // First take care of any SegmentNodes that should be created. - if (event == xml::XmlPullParser::Event::kStartElement || - event == xml::XmlPullParser::Event::kEndElement) { + if (event == xml::XmlPullParser::Event::kStartElement + || event == xml::XmlPullParser::Event::kEndElement + || event == xml::XmlPullParser::Event::kCdataStart + || event == xml::XmlPullParser::Event::kCdataEnd) { if (!current_text.empty()) { - std::unique_ptr<SegmentNode> segment_node = util::make_unique<SegmentNode>(); + std::unique_ptr<SegmentNode> segment_node = (cdata_block) + ? util::make_unique<CdataSegmentNode>() : util::make_unique<SegmentNode>(); segment_node->data = std::move(current_text); + last_segment = node_stack.back()->AddChild(std::move(segment_node)); if (first_segment == nullptr) { first_segment = last_segment; @@ -333,6 +347,16 @@ bool ResourceParser::FlattenXmlSubtree( } } break; + case xml::XmlPullParser::Event::kCdataStart: { + cdata_block = true; + break; + } + + case xml::XmlPullParser::Event::kCdataEnd: { + cdata_block = false; + break; + } + default: // ignore. break; @@ -447,6 +471,9 @@ bool ResourceParser::ParseResources(xml::XmlPullParser* parser) { parsed_resource.config = config_; parsed_resource.source = source_.WithLine(parser->line_number()); parsed_resource.comment = std::move(comment); + if (options_.visibility) { + parsed_resource.visibility_level = options_.visibility.value(); + } // Extract the product name if it exists. if (Maybe<StringPiece> maybe_product = xml::FindNonEmptyAttribute(parser, "product")) { @@ -774,7 +801,8 @@ std::unique_ptr<Item> ResourceParser::ParseXml(xml::XmlPullParser* parser, if (allow_raw_value) { // We can't parse this so return a RawString if we are allowed. return util::make_unique<RawString>( - table_->string_pool.MakeRef(raw_value, StringPool::Context(config_))); + table_->string_pool.MakeRef(util::TrimWhitespace(raw_value), + StringPool::Context(config_))); } return {}; } @@ -836,6 +864,12 @@ bool ResourceParser::ParseString(xml::XmlPullParser* parser, } bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<public> tag not allowed with --visibility flag"); + return false; + } + if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config << "' for <public> tag"); @@ -878,6 +912,12 @@ bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out } bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<public-group> tag not allowed with --visibility flag"); + return false; + } + if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config @@ -999,6 +1039,11 @@ bool ResourceParser::ParseSymbolImpl(xml::XmlPullParser* parser, } bool ResourceParser::ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out_resource) { + if (options_.visibility) { + diag_->Error(DiagMessage(out_resource->source) + << "<java-symbol> and <symbol> tags not allowed with --visibility flag"); + return false; + } if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config << "' for <" @@ -1070,6 +1115,9 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource child_resource.name.entry = maybe_name.value().to_string(); child_resource.source = item_source; child_resource.overlayable = true; + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } out_resource->child_resources.push_back(std::move(child_resource)); xml::XmlPullParser::SkipCurrentElement(parser); @@ -1212,6 +1260,9 @@ bool ResourceParser::ParseAttrImpl(xml::XmlPullParser* parser, child_resource.name = symbol.symbol.name.value(); child_resource.source = item_source; child_resource.value = util::make_unique<Id>(); + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } out_resource->child_resources.push_back(std::move(child_resource)); symbol.symbol.SetComment(std::move(comment)); @@ -1589,6 +1640,9 @@ bool ResourceParser::ParseDeclareStyleable(xml::XmlPullParser* parser, child_resource.name = child_ref.name.value(); child_resource.source = item_source; child_resource.comment = std::move(comment); + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } if (!ParseAttrImpl(parser, &child_resource, true)) { error = true; diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index fb9dbd0cd0fd..68130c2512d3 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -45,6 +45,10 @@ struct ResourceParserOptions { * warnings. */ bool error_on_positional_arguments = true; + + // If visibility was forced, we need to use it when creating a new resource and also error if we + // try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. + Maybe<Visibility::Level> visibility; }; /* diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 41b4041efb7a..ee496d501113 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -497,6 +497,24 @@ TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedItems) { EXPECT_THAT(style->entries[0].key.name, Eq(make_value(test::ParseNameOrDie("android:attr/bar")))); } +TEST_F(ResourceParserTest, ParseStyleWithRawStringItem) { + std::string input = R"( + <style name="foo"> + <item name="bar"> + com.helloworld.AppClass + </item> + </style>)"; + ASSERT_TRUE(TestParse(input)); + + Style* style = test::GetValue<Style>(&table_, "style/foo"); + ASSERT_THAT(style, NotNull()); + EXPECT_THAT(style->entries[0].value, NotNull()); + RawString* value = ValueCast<RawString>(style->entries[0].value.get()); + EXPECT_THAT(value, NotNull()); + EXPECT_THAT(*value->value, StrEq(R"(com.helloworld.AppClass)")); +} + + TEST_F(ResourceParserTest, ParseStyleWithInferredParent) { ASSERT_TRUE(TestParse(R"(<style name="foo.bar"/>)")); @@ -971,4 +989,40 @@ TEST_F(ResourceParserTest, ParseIdItem) { ASSERT_FALSE(TestParse(input)); } +TEST_F(ResourceParserTest, ParseCData) { + std::string input = R"( + <string name="foo"><![CDATA[some text and ' apostrophe]]></string>)"; + + ASSERT_TRUE(TestParse(input)); + String* output = test::GetValue<String>(&table_, "string/foo"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq("some text and ' apostrophe")); + + // Double quotes should not change the state of whitespace processing + input = R"(<string name="foo2">Hello<![CDATA[ "</string>' ]]> World</string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo2"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string("Hello \"</string>' World").data())); + + // Cdata blocks should not have their whitespace trimmed + input = R"(<string name="foo3"> <![CDATA[ text ]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo3"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string(" text ").data())); + + input = R"(<string name="foo4"> <![CDATA[]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo4"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string("").data())); + + input = R"(<string name="foo5"> <![CDATA[ ]]> </string>)"; + ASSERT_TRUE(TestParse(input)); + output = test::GetValue<String>(&table_, "string/foo5"); + ASSERT_THAT(output, NotNull()); + EXPECT_THAT(*output, StrValueEq(std::string(" ").data())); +} + } // namespace aapt diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index d0faac30425a..23322ab277bf 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -26,6 +26,7 @@ #include "androidfw/ResourceTypes.h" #include "ConfigDescription.h" +#include "Debug.h" #include "NameMangler.h" #include "ResourceValues.h" #include "ValueVisitor.h" @@ -38,8 +39,9 @@ using ::android::base::StringPrintf; namespace aapt { -static bool less_than_type(const std::unique_ptr<ResourceTableType>& lhs, ResourceType rhs) { - return lhs->type < rhs; +static bool less_than_type_and_id(const std::unique_ptr<ResourceTableType>& lhs, + const std::pair<ResourceType, Maybe<uint8_t>>& rhs) { + return lhs->type < rhs.first || (lhs->type == rhs.first && rhs.second && lhs->id < rhs.second); } template <typename T> @@ -49,9 +51,9 @@ static bool less_than_struct_with_name(const std::unique_ptr<T>& lhs, const Stri template <typename T> static bool less_than_struct_with_name_and_id(const std::unique_ptr<T>& lhs, - const std::pair<StringPiece, Maybe<uint8_t>>& rhs) { + const std::pair<StringPiece, Maybe<uint16_t>>& rhs) { int name_cmp = lhs->name.compare(0, lhs->name.size(), rhs.first.data(), rhs.first.size()); - return name_cmp < 0 || (name_cmp == 0 && lhs->id < rhs.second); + return name_cmp < 0 || (name_cmp == 0 && rhs.second && lhs->id < rhs.second); } ResourceTablePackage* ResourceTable::FindPackage(const StringPiece& name) const { @@ -115,42 +117,52 @@ ResourceTablePackage* ResourceTable::FindOrCreatePackage(const StringPiece& name return packages.emplace(iter, std::move(new_package))->get(); } -ResourceTableType* ResourceTablePackage::FindType(ResourceType type) { +ResourceTableType* ResourceTablePackage::FindType(ResourceType type, const Maybe<uint8_t> id) { const auto last = types.end(); - auto iter = std::lower_bound(types.begin(), last, type, less_than_type); - if (iter != last && (*iter)->type == type) { + auto iter = std::lower_bound(types.begin(), last, std::make_pair(type, id), + less_than_type_and_id); + if (iter != last && (*iter)->type == type && (!id || id == (*iter)->id)) { return iter->get(); } return nullptr; } -ResourceTableType* ResourceTablePackage::FindOrCreateType(ResourceType type) { +ResourceTableType* ResourceTablePackage::FindOrCreateType(ResourceType type, + const Maybe<uint8_t> id) { const auto last = types.end(); - auto iter = std::lower_bound(types.begin(), last, type, less_than_type); - if (iter != last && (*iter)->type == type) { + auto iter = std::lower_bound(types.begin(), last, std::make_pair(type, id), + less_than_type_and_id); + if (iter != last && (*iter)->type == type && (!id || id == (*iter)->id)) { return iter->get(); } - return types.emplace(iter, new ResourceTableType(type))->get(); + + auto new_type = new ResourceTableType(type); + new_type->id = id; + return types.emplace(iter, std::move(new_type))->get(); } -ResourceEntry* ResourceTableType::FindEntry(const StringPiece& name) { +ResourceEntry* ResourceTableType::FindEntry(const StringPiece& name, const Maybe<uint16_t> id) { const auto last = entries.end(); - auto iter = - std::lower_bound(entries.begin(), last, name, less_than_struct_with_name<ResourceEntry>); - if (iter != last && name == (*iter)->name) { + auto iter = std::lower_bound(entries.begin(), last, std::make_pair(name, id), + less_than_struct_with_name_and_id<ResourceEntry>); + if (iter != last && name == (*iter)->name && (!id || id == (*iter)->id)) { return iter->get(); } return nullptr; } -ResourceEntry* ResourceTableType::FindOrCreateEntry(const StringPiece& name) { +ResourceEntry* ResourceTableType::FindOrCreateEntry(const StringPiece& name, + const Maybe<uint16_t > id) { auto last = entries.end(); - auto iter = - std::lower_bound(entries.begin(), last, name, less_than_struct_with_name<ResourceEntry>); - if (iter != last && name == (*iter)->name) { + auto iter = std::lower_bound(entries.begin(), last, std::make_pair(name, id), + less_than_struct_with_name_and_id<ResourceEntry>); + if (iter != last && name == (*iter)->name && (!id || id == (*iter)->id)) { return iter->get(); } - return entries.emplace(iter, new ResourceEntry(name))->get(); + + auto new_entry = new ResourceEntry(name); + new_entry->id = id; + return entries.emplace(iter, std::move(new_entry))->get(); } ResourceConfigValue* ResourceEntry::FindValue(const ConfigDescription& config) { @@ -302,9 +314,15 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist // Keep the existing attribute. return CollisionResult::kKeepOriginal; } + return CollisionResult::kConflict; } +ResourceTable::CollisionResult ResourceTable::IgnoreCollision(Value* /** existing **/, + Value* /** incoming **/) { + return CollisionResult::kKeepBoth; +} + static StringPiece ResourceNameValidator(const StringPiece& name) { if (!IsValidResourceEntryName(name)) { return name; @@ -321,15 +339,17 @@ bool ResourceTable::AddResource(const ResourceNameRef& name, const StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag) { - return AddResourceImpl(name, {}, config, product, std::move(value), ResourceNameValidator, - ResolveValueCollision, diag); + return AddResourceImpl(name, ResourceId{}, config, product, std::move(value), + (validate_resources_ ? ResourceNameValidator : SkipNameValidator), + (validate_resources_ ? ResolveValueCollision : IgnoreCollision), diag); } bool ResourceTable::AddResourceWithId(const ResourceNameRef& name, const ResourceId& res_id, const ConfigDescription& config, const StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag) { - return AddResourceImpl(name, res_id, config, product, std::move(value), ResourceNameValidator, - ResolveValueCollision, diag); + return AddResourceImpl(name, res_id, config, product, std::move(value), + (validate_resources_ ? ResourceNameValidator : SkipNameValidator), + (validate_resources_ ? ResolveValueCollision : IgnoreCollision), diag); } bool ResourceTable::AddFileReference(const ResourceNameRef& name, @@ -337,14 +357,18 @@ bool ResourceTable::AddFileReference(const ResourceNameRef& name, const Source& source, const StringPiece& path, IDiagnostics* diag) { - return AddFileReferenceImpl(name, config, source, path, nullptr, ResourceNameValidator, diag); + return AddFileReferenceImpl(name, config, source, path, nullptr, + (validate_resources_ ? ResourceNameValidator : SkipNameValidator), + diag); } bool ResourceTable::AddFileReferenceMangled(const ResourceNameRef& name, const ConfigDescription& config, const Source& source, const StringPiece& path, io::IFile* file, IDiagnostics* diag) { - return AddFileReferenceImpl(name, config, source, path, file, SkipNameValidator, diag); + return AddFileReferenceImpl(name, config, source, path, file, + (validate_resources_ ? ResourceNameValidator : SkipNameValidator), + diag); } bool ResourceTable::AddFileReferenceImpl(const ResourceNameRef& name, @@ -363,7 +387,7 @@ bool ResourceTable::AddResourceMangled(const ResourceNameRef& name, const Config const StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag) { return AddResourceImpl(name, ResourceId{}, config, product, std::move(value), SkipNameValidator, - ResolveValueCollision, diag); + (validate_resources_ ? ResolveValueCollision : IgnoreCollision), diag); } bool ResourceTable::AddResourceWithIdMangled(const ResourceNameRef& name, const ResourceId& id, @@ -371,7 +395,7 @@ bool ResourceTable::AddResourceWithIdMangled(const ResourceNameRef& name, const const StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag) { return AddResourceImpl(name, id, config, product, std::move(value), SkipNameValidator, - ResolveValueCollision, diag); + (validate_resources_ ? ResolveValueCollision : IgnoreCollision), diag); } bool ResourceTable::ValidateName(NameValidator name_validator, const ResourceNameRef& name, @@ -398,37 +422,57 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI return false; } + // Check for package names appearing twice with two different package ids ResourceTablePackage* package = FindOrCreatePackage(name.package); if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { - diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id - << " but package '" << package->name << "' already has ID " - << StringPrintf("%02x", package->id.value())); + diag->Error(DiagMessage(source) + << "trying to add resource '" << name << "' with ID " << res_id + << " but package '" << package->name << "' already has ID " + << StringPrintf("%02x", package->id.value())); return false; } - ResourceTableType* type = package->FindOrCreateType(name.type); - if (res_id.is_valid_dynamic() && type->id && type->id.value() != res_id.type_id()) { + // Whether or not to error on duplicate resources + bool check_id = validate_resources_ && res_id.is_valid_dynamic(); + // Whether or not to create a duplicate resource if the id does not match + bool use_id = !validate_resources_ && res_id.is_valid_dynamic(); + + ResourceTableType* type = package->FindOrCreateType(name.type, use_id ? res_id.type_id() + : Maybe<uint8_t>()); + + // Check for types appearing twice with two different type ids + if (check_id && type->id && type->id.value() != res_id.type_id()) { diag->Error(DiagMessage(source) - << "trying to add resource '" << name << "' with ID " << res_id << " but type '" - << type->type << "' already has ID " << StringPrintf("%02x", type->id.value())); + << "trying to add resource '" << name << "' with ID " << res_id + << " but type '" << type->type << "' already has ID " + << StringPrintf("%02x", type->id.value())); return false; } - ResourceEntry* entry = type->FindOrCreateEntry(name.entry); - if (res_id.is_valid_dynamic() && entry->id && entry->id.value() != res_id.entry_id()) { + ResourceEntry* entry = type->FindOrCreateEntry(name.entry, use_id ? res_id.entry_id() + : Maybe<uint16_t>()); + + // Check for entries appearing twice with two different entry ids + if (check_id && entry->id && entry->id.value() != res_id.entry_id()) { diag->Error(DiagMessage(source) - << "trying to add resource '" << name << "' with ID " << res_id - << " but resource already has ID " - << ResourceId(package->id.value(), type->id.value(), entry->id.value())); + << "trying to add resource '" << name << "' with ID " << res_id + << " but resource already has ID " + << ResourceId(package->id.value(), type->id.value(), entry->id.value())); return false; } ResourceConfigValue* config_value = entry->FindOrCreateValue(config, product); - if (config_value->value == nullptr) { + if (!config_value->value) { // Resource does not exist, add it now. config_value->value = std::move(value); } else { switch (conflict_resolver(config_value->value.get(), value.get())) { + case CollisionResult::kKeepBoth: + // Insert the value ignoring for duplicate configurations + entry->values.push_back(util::make_unique<ResourceConfigValue>(config, product)); + entry->values.back()->value = std::move(value); + break; + case CollisionResult::kTakeNew: // Take the incoming value. config_value->value = std::move(value); @@ -450,17 +494,22 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI type->id = res_id.type_id(); entry->id = res_id.entry_id(); } + return true; } +bool ResourceTable::GetValidateResources() { + return validate_resources_; +} + bool ResourceTable::SetVisibility(const ResourceNameRef& name, const Visibility& visibility, IDiagnostics* diag) { - return SetVisibilityImpl(name, visibility, ResourceId{}, ResourceNameValidator, diag); + return SetVisibilityImpl(name, visibility, {}, ResourceNameValidator, diag); } bool ResourceTable::SetVisibilityMangled(const ResourceNameRef& name, const Visibility& visibility, IDiagnostics* diag) { - return SetVisibilityImpl(name, visibility, ResourceId{}, SkipNameValidator, diag); + return SetVisibilityImpl(name, visibility, {}, SkipNameValidator, diag); } bool ResourceTable::SetVisibilityWithId(const ResourceNameRef& name, const Visibility& visibility, @@ -484,28 +533,42 @@ bool ResourceTable::SetVisibilityImpl(const ResourceNameRef& name, const Visibil return false; } + // Check for package names appearing twice with two different package ids ResourceTablePackage* package = FindOrCreatePackage(name.package); if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { - diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id - << " but package '" << package->name << "' already has ID " - << StringPrintf("%02x", package->id.value())); + diag->Error(DiagMessage(source) + << "trying to add resource '" << name << "' with ID " << res_id + << " but package '" << package->name << "' already has ID " + << StringPrintf("%02x", package->id.value())); return false; } - ResourceTableType* type = package->FindOrCreateType(name.type); - if (res_id.is_valid_dynamic() && type->id && type->id.value() != res_id.type_id()) { + // Whether or not to error on duplicate resources + bool check_id = validate_resources_ && res_id.is_valid_dynamic(); + // Whether or not to create a duplicate resource if the id does not match + bool use_id = !validate_resources_ && res_id.is_valid_dynamic(); + + ResourceTableType* type = package->FindOrCreateType(name.type, use_id ? res_id.type_id() + : Maybe<uint8_t>()); + + // Check for types appearing twice with two different type ids + if (check_id && type->id && type->id.value() != res_id.type_id()) { diag->Error(DiagMessage(source) - << "trying to add resource '" << name << "' with ID " << res_id << " but type '" - << type->type << "' already has ID " << StringPrintf("%02x", type->id.value())); + << "trying to add resource '" << name << "' with ID " << res_id + << " but type '" << type->type << "' already has ID " + << StringPrintf("%02x", type->id.value())); return false; } - ResourceEntry* entry = type->FindOrCreateEntry(name.entry); - if (res_id.is_valid_dynamic() && entry->id && entry->id.value() != res_id.entry_id()) { + ResourceEntry* entry = type->FindOrCreateEntry(name.entry, use_id ? res_id.entry_id() + : Maybe<uint16_t>()); + + // Check for entries appearing twice with two different entry ids + if (check_id && entry->id && entry->id.value() != res_id.entry_id()) { diag->Error(DiagMessage(source) - << "trying to add resource '" << name << "' with ID " << res_id - << " but resource already has ID " - << ResourceId(package->id.value(), type->id.value(), entry->id.value())); + << "trying to add resource '" << name << "' with ID " << res_id + << " but resource already has ID " + << ResourceId(package->id.value(), type->id.value(), entry->id.value())); return false; } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index 8534eaaf0366..5a43a2d86cfe 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -146,8 +146,10 @@ class ResourceTableType { explicit ResourceTableType(const ResourceType type) : type(type) {} - ResourceEntry* FindEntry(const android::StringPiece& name); - ResourceEntry* FindOrCreateEntry(const android::StringPiece& name); + ResourceEntry* FindEntry(const android::StringPiece& name, + Maybe<uint16_t> id = Maybe<uint16_t>()); + ResourceEntry* FindOrCreateEntry(const android::StringPiece& name, + Maybe<uint16_t> id = Maybe<uint16_t>()); private: DISALLOW_COPY_AND_ASSIGN(ResourceTableType); @@ -163,8 +165,9 @@ class ResourceTablePackage { std::vector<std::unique_ptr<ResourceTableType>> types; ResourceTablePackage() = default; - ResourceTableType* FindType(ResourceType type); - ResourceTableType* FindOrCreateType(const ResourceType type); + ResourceTableType* FindType(ResourceType type, Maybe<uint8_t> id = Maybe<uint8_t>()); + ResourceTableType* FindOrCreateType(const ResourceType type, + Maybe<uint8_t> id = Maybe<uint8_t>()); private: DISALLOW_COPY_AND_ASSIGN(ResourceTablePackage); @@ -174,14 +177,18 @@ class ResourceTablePackage { class ResourceTable { public: ResourceTable() = default; + explicit ResourceTable(bool validate_resources) : validate_resources_(validate_resources) {} - enum class CollisionResult { kKeepOriginal, kConflict, kTakeNew }; + enum class CollisionResult { kKeepBoth, kKeepOriginal, kConflict, kTakeNew }; using CollisionResolverFunc = std::function<CollisionResult(Value*, Value*)>; // When a collision of resources occurs, this method decides which value to keep. static CollisionResult ResolveValueCollision(Value* existing, Value* incoming); + // When a collision of resources occurs, this method keeps both values + static CollisionResult IgnoreCollision(Value* existing, Value* incoming); + bool AddResource(const ResourceNameRef& name, const ConfigDescription& config, const android::StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag); @@ -208,6 +215,8 @@ class ResourceTable { const android::StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag); + bool GetValidateResources(); + bool SetVisibility(const ResourceNameRef& name, const Visibility& visibility, IDiagnostics* diag); bool SetVisibilityMangled(const ResourceNameRef& name, const Visibility& visibility, IDiagnostics* diag); @@ -299,6 +308,9 @@ class ResourceTable { const Visibility& symbol, NameValidator name_validator, IDiagnostics* diag); + // Controls whether the table validates resource names and prevents duplicate resource names + bool validate_resources_ = true; + DISALLOW_COPY_AND_ASSIGN(ResourceTable); }; diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp index 7fa8ea2f7f94..1aa97511dd37 100644 --- a/tools/aapt2/ResourceTable_test.cpp +++ b/tools/aapt2/ResourceTable_test.cpp @@ -258,4 +258,44 @@ TEST(ResourceTableTest, SetOverlayable) { ASSERT_FALSE(table.SetOverlayable(name, overlayable, test::GetDiagnostics())); } +TEST(ResourceTableTest, AllowDuplictaeResourcesNames) { + ResourceTable table(/* validate_resources */ false); + + const ResourceName foo_name = test::ParseNameOrDie("android:bool/foo"); + ASSERT_TRUE(table.AddResourceWithId(foo_name, ResourceId(0x7f0100ff), ConfigDescription{} , "", + test::BuildPrimitive(android::Res_value::TYPE_INT_BOOLEAN, 0), + test::GetDiagnostics())); + ASSERT_TRUE(table.AddResourceWithId(foo_name, ResourceId(0x7f010100), ConfigDescription{} , "", + test::BuildPrimitive(android::Res_value::TYPE_INT_BOOLEAN, 1), + test::GetDiagnostics())); + + ASSERT_TRUE(table.SetVisibilityWithId(foo_name, Visibility{Visibility::Level::kPublic}, + ResourceId(0x7f0100ff), test::GetDiagnostics())); + ASSERT_TRUE(table.SetVisibilityWithId(foo_name, Visibility{Visibility::Level::kPrivate}, + ResourceId(0x7f010100), test::GetDiagnostics())); + + auto package = table.FindPackageById(0x7f); + ASSERT_THAT(package, NotNull()); + auto type = package->FindType(ResourceType::kBool); + ASSERT_THAT(type, NotNull()); + + auto entry1 = type->FindEntry("foo", 0x00ff); + ASSERT_THAT(entry1, NotNull()); + ASSERT_THAT(entry1->id, Eq(0x00ff)); + ASSERT_THAT(entry1->values[0], NotNull()); + ASSERT_THAT(entry1->values[0]->value, NotNull()); + ASSERT_THAT(ValueCast<BinaryPrimitive>(entry1->values[0]->value.get()), NotNull()); + ASSERT_THAT(ValueCast<BinaryPrimitive>(entry1->values[0]->value.get())->value.data, Eq(0u)); + ASSERT_THAT(entry1->visibility.level, Visibility::Level::kPublic); + + auto entry2 = type->FindEntry("foo", 0x0100); + ASSERT_THAT(entry2, NotNull()); + ASSERT_THAT(entry2->id, Eq(0x0100)); + ASSERT_THAT(entry2->values[0], NotNull()); + ASSERT_THAT(entry1->values[0]->value, NotNull()); + ASSERT_THAT(ValueCast<BinaryPrimitive>(entry2->values[0]->value.get()), NotNull()); + ASSERT_THAT(ValueCast<BinaryPrimitive>(entry2->values[0]->value.get())->value.data, Eq(1u)); + ASSERT_THAT(entry2->visibility.level, Visibility::Level::kPrivate); +} + } // namespace aapt diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index 560077cc322c..c48765b7b947 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -797,16 +797,20 @@ StringBuilder::StringBuilder(bool preserve_spaces) : preserve_spaces_(preserve_spaces), quote_(preserve_spaces) { } -StringBuilder& StringBuilder::AppendText(const std::string& text) { +StringBuilder& StringBuilder::AppendText(const std::string& text, bool preserve_spaces) { if (!error_.empty()) { return *this; } + // Enable preserving spaces if it is enabled for this append or the StringBuilder was constructed + // to preserve spaces + preserve_spaces = (preserve_spaces) ? preserve_spaces : preserve_spaces_; + const size_t previous_len = xml_string_.text.size(); Utf8Iterator iter(text); while (iter.HasNext()) { char32_t codepoint = iter.Next(); - if (!quote_ && iswspace(codepoint)) { + if (!preserve_spaces && !quote_ && iswspace(codepoint)) { if (!last_codepoint_was_space_) { // Emit a space if it's the first. xml_string_.text += ' '; @@ -827,7 +831,6 @@ StringBuilder& StringBuilder::AppendText(const std::string& text) { case U't': xml_string_.text += '\t'; break; - case U'n': xml_string_.text += '\n'; break; @@ -855,12 +858,12 @@ StringBuilder& StringBuilder::AppendText(const std::string& text) { break; } } - } else if (!preserve_spaces_ && codepoint == U'"') { + } else if (!preserve_spaces && codepoint == U'"') { // Only toggle the quote state when we are not preserving spaces. quote_ = !quote_; - } else if (!quote_ && codepoint == U'\'') { - // This should be escaped. + } else if (!preserve_spaces && !quote_ && codepoint == U'\'') { + // This should be escaped when we are not preserving spaces error_ = StringPrintf("unescaped apostrophe in string\n\"%s\"", text.c_str()); return *this; diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h index 7af2fe06b908..410ef28ce78a 100644 --- a/tools/aapt2/ResourceUtils.h +++ b/tools/aapt2/ResourceUtils.h @@ -267,8 +267,10 @@ class StringBuilder { // single quotations can be used without escaping them. explicit StringBuilder(bool preserve_spaces = false); - // Appends a chunk of text. - StringBuilder& AppendText(const std::string& text); + // Appends a chunk of text. If preserve_spaces is true, whitespace removal is not performed, and + // single quotations can be used without escaping them for this append. Otherwise, the + // StringBuilder will behave as it was constructed. + StringBuilder& AppendText(const std::string& text, bool preserve_spaces = false); // Starts a Span (tag) with the given name. The name is expected to be of the form: // "tag_name;attr1=value;attr2=value;" diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp index 11f3fa3bc6cd..5ce464074335 100644 --- a/tools/aapt2/ResourceUtils_test.cpp +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -254,6 +254,29 @@ TEST(ResourceUtilsTest, StringBuilderUnicodeCodes) { TEST(ResourceUtilsTest, StringBuilderPreserveSpaces) { EXPECT_THAT(ResourceUtils::StringBuilder(true /*preserve_spaces*/).AppendText("\"").to_string(), Eq("\"")); + + // Single quotes should be able to be used without escaping them when preserving spaces and the + // spaces should not be trimmed + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys ") + .AppendText(" 'this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow ") + .to_string(), + Eq(" hey guys 'this is so cool' wow ")); + + // Reading a double quote while preserving spaces should not change the quote state + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys ") + .AppendText(" \"this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow ") + .to_string(), + Eq(" hey guys \"this is so cool' wow ")); + EXPECT_THAT(ResourceUtils::StringBuilder() + .AppendText(" hey guys\" ") + .AppendText(" \"this is so cool' ", /* preserve_spaces */ true) + .AppendText(" wow \" ") + .to_string(), + Eq(" hey guys \"this is so cool' wow ")); } } // namespace aapt diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index 8ebde752bc4b..f4b0124abcda 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -25,8 +25,8 @@ using android::StringPiece; namespace aapt { -static const char* sDevelopmentSdkCodeName = "P"; -static ApiVersion sDevelopmentSdkLevel = 28; +static const char* sDevelopmentSdkCodeName = "Q"; +static ApiVersion sDevelopmentSdkLevel = 10000; static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x021c, 1}, @@ -54,6 +54,7 @@ static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x0530, SDK_NOUGAT_MR1}, {0x0568, SDK_O}, {0x056d, SDK_O_MR1}, + {0x0586, SDK_P}, }; static bool less_entry_id(const std::pair<uint16_t, ApiVersion>& p, uint16_t entryId) { diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h index 0f312d6998f1..92934c343960 100644 --- a/tools/aapt2/Source.h +++ b/tools/aapt2/Source.h @@ -31,12 +31,16 @@ namespace aapt { struct Source { std::string path; Maybe<size_t> line; + Maybe<std::string> archive; Source() = default; inline Source(const android::StringPiece& path) : path(path.to_string()) { // NOLINT(implicit) } + inline Source(const android::StringPiece& path, const android::StringPiece& archive) + : path(path.to_string()), archive(archive.to_string()) {} + inline Source(const android::StringPiece& path, size_t line) : path(path.to_string()), line(line) {} @@ -45,10 +49,14 @@ struct Source { } std::string to_string() const { + std::string s = path; + if (archive) { + s = ::android::base::StringPrintf("%s@%s", archive.value().c_str(), s.c_str()); + } if (line) { - return ::android::base::StringPrintf("%s:%zd", path.c_str(), line.value()); + s = ::android::base::StringPrintf("%s:%zd", s.c_str(), line.value()); } - return path; + return s; } }; diff --git a/tools/aapt2/StringPool.cpp b/tools/aapt2/StringPool.cpp index b37e1fbd9693..8eabd3225d87 100644 --- a/tools/aapt2/StringPool.cpp +++ b/tools/aapt2/StringPool.cpp @@ -367,7 +367,7 @@ const std::string kStringTooLarge = "STRING_TOO_LARGE"; static bool EncodeString(const std::string& str, const bool utf8, BigBuffer* out, IDiagnostics* diag) { if (utf8) { - const std::string& encoded = str; + const std::string& encoded = util::Utf8ToModifiedUtf8(str); const ssize_t utf16_length = utf8_to_utf16_length( reinterpret_cast<const uint8_t*>(encoded.data()), encoded.size()); CHECK(utf16_length >= 0); diff --git a/tools/aapt2/StringPool_test.cpp b/tools/aapt2/StringPool_test.cpp index 4b3afe2bb962..0778564ee079 100644 --- a/tools/aapt2/StringPool_test.cpp +++ b/tools/aapt2/StringPool_test.cpp @@ -303,6 +303,25 @@ TEST(StringPoolTest, Flatten) { } } +TEST(StringPoolTest, FlattenModifiedUTF8) { + using namespace android; // For NO_ERROR on Windows. + StdErrDiagnostics diag; + StringPool pool; + StringPool::Ref ref_a = pool.MakeRef("\xF0\x90\x90\x80"); // 𐐀 (U+10400) + StringPool::Ref ref_b = pool.MakeRef("foo \xF0\x90\x90\xB7 bar"); // 𐐷 (U+10437) + StringPool::Ref ref_c = pool.MakeRef("\xF0\x90\x90\x80\xF0\x90\x90\xB7"); + + BigBuffer buffer(1024); + StringPool::FlattenUtf8(&buffer, pool, &diag); + std::unique_ptr<uint8_t[]> data = util::Copy(buffer); + + // Check that the 4 byte utf-8 codepoint is encoded using 2 3 byte surrogate pairs + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + EXPECT_THAT(util::GetString(test, 0), Eq("\xED\xA0\x81\xED\xB0\x80")); + EXPECT_THAT(util::GetString(test, 1), Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar")); + EXPECT_THAT(util::GetString(test, 2), Eq("\xED\xA0\x81\xED\xB0\x80\xED\xA0\x81\xED\xB0\xB7")); +} TEST(StringPoolTest, MaxEncodingLength) { StdErrDiagnostics diag; diff --git a/tools/aapt2/Flags.cpp b/tools/aapt2/cmd/Command.cpp index 84977ab424cc..09411b95bcfe 100644 --- a/tools/aapt2/Flags.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "Flags.h" +#include "Command.h" #include <iomanip> #include <iostream> @@ -29,87 +29,113 @@ using android::StringPiece; namespace aapt { -Flags& Flags::RequiredFlag(const StringPiece& name, - const StringPiece& description, std::string* value) { +void Command::AddRequiredFlag(const StringPiece& name, + const StringPiece& description, std::string* value) { auto func = [value](const StringPiece& arg) -> bool { *value = arg.to_string(); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, true, 1, false}); - return *this; } -Flags& Flags::RequiredFlagList(const StringPiece& name, - const StringPiece& description, - std::vector<std::string>* value) { +void Command::AddRequiredFlagList(const StringPiece& name, + const StringPiece& description, + std::vector<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->push_back(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, true, 1, false}); - return *this; } -Flags& Flags::OptionalFlag(const StringPiece& name, - const StringPiece& description, - Maybe<std::string>* value) { +void Command::AddOptionalFlag(const StringPiece& name, + const StringPiece& description, + Maybe<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { *value = arg.to_string(); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalFlagList(const StringPiece& name, - const StringPiece& description, - std::vector<std::string>* value) { +void Command::AddOptionalFlagList(const StringPiece& name, + const StringPiece& description, + std::vector<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->push_back(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalFlagList(const StringPiece& name, - const StringPiece& description, - std::unordered_set<std::string>* value) { +void Command::AddOptionalFlagList(const StringPiece& name, + const StringPiece& description, + std::unordered_set<std::string>* value) { auto func = [value](const StringPiece& arg) -> bool { value->insert(arg.to_string()); return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 1, false}); - return *this; } -Flags& Flags::OptionalSwitch(const StringPiece& name, - const StringPiece& description, bool* value) { +void Command::AddOptionalSwitch(const StringPiece& name, + const StringPiece& description, bool* value) { auto func = [value](const StringPiece& arg) -> bool { *value = true; return true; }; flags_.push_back(Flag{name.to_string(), description.to_string(), func, false, 0, false}); - return *this; } -void Flags::Usage(const StringPiece& command, std::ostream* out) { +void Command::AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand) { + subcommand->fullname_ = name_ + " " + subcommand->name_; + subcommands_.push_back(std::move(subcommand)); +} + +void Command::SetDescription(const android::StringPiece& description) { + description_ = description.to_string(); +} + +void Command::Usage(std::ostream* out) { constexpr size_t kWidth = 50; - *out << command << " [options]"; + *out << fullname_; + + if (!subcommands_.empty()) { + *out << " [subcommand]"; + } + + *out << " [options]"; for (const Flag& flag : flags_) { if (flag.required) { *out << " " << flag.name << " arg"; } } - *out << " files...\n\nOptions:\n"; + *out << " files...\n"; + + if (!subcommands_.empty()) { + *out << "\nSubcommands:\n"; + for (auto& subcommand : subcommands_) { + std::string argline = subcommand->name_; + + // Split the description by newlines and write out the argument (which is + // empty after the first line) followed by the description line. This will make sure + // that multiline descriptions are still right justified and aligned. + for (StringPiece line : util::Tokenize(subcommand->description_, '\n')) { + *out << " " << std::setw(kWidth) << std::left << argline << line << "\n"; + argline = " "; + } + } + } + + *out << "\nOptions:\n"; for (const Flag& flag : flags_) { std::string argline = flag.name; @@ -118,10 +144,8 @@ void Flags::Usage(const StringPiece& command, std::ostream* out) { } // Split the description by newlines and write out the argument (which is - // empty after - // the first line) followed by the description line. This will make sure - // that multiline - // descriptions are still right justified and aligned. + // empty after the first line) followed by the description line. This will make sure + // that multiline descriptions are still right justified and aligned. for (StringPiece line : util::Tokenize(flag.description, '\n')) { *out << " " << std::setw(kWidth) << std::left << argline << line << "\n"; argline = " "; @@ -132,19 +156,29 @@ void Flags::Usage(const StringPiece& command, std::ostream* out) { out->flush(); } -bool Flags::Parse(const StringPiece& command, - const std::vector<StringPiece>& args, - std::ostream* out_error) { +int Command::Execute(const std::vector<android::StringPiece>& args, std::ostream* out_error) { + std::vector<std::string> file_args; + for (size_t i = 0; i < args.size(); i++) { StringPiece arg = args[i]; if (*(arg.data()) != '-') { - args_.push_back(arg.to_string()); + // Continue parsing as the sub command if the first argument matches one of the subcommands + if (i == 0) { + for (auto& subcommand : subcommands_) { + if (arg == subcommand->name_ || arg==subcommand->short_name_) { + return subcommand->Execute( + std::vector<android::StringPiece>(args.begin() + 1, args.end()), out_error); + } + } + } + + file_args.push_back(arg.to_string()); continue; } if (arg == "-h" || arg == "--help") { - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } bool match = false; @@ -154,7 +188,7 @@ bool Flags::Parse(const StringPiece& command, i++; if (i >= args.size()) { *out_error << flag.name << " missing argument.\n\n"; - Usage(command, out_error); + Usage(out_error); return false; } flag.action(args[i]); @@ -169,21 +203,20 @@ bool Flags::Parse(const StringPiece& command, if (!match) { *out_error << "unknown option '" << arg << "'.\n\n"; - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } } for (const Flag& flag : flags_) { if (flag.required && !flag.parsed) { *out_error << "missing required flag " << flag.name << "\n\n"; - Usage(command, out_error); - return false; + Usage(out_error); + return 1; } } - return true; -} -const std::vector<std::string>& Flags::GetArgs() { return args_; } + return Action(file_args); +} } // namespace aapt diff --git a/tools/aapt2/cmd/Command.h b/tools/aapt2/cmd/Command.h new file mode 100644 index 000000000000..71dc6fe48da1 --- /dev/null +++ b/tools/aapt2/cmd/Command.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_COMMAND_H +#define AAPT_COMMAND_H + +#include <functional> +#include <ostream> +#include <string> +#include <unordered_set> +#include <vector> + +#include "androidfw/StringPiece.h" + +#include "util/Maybe.h" + +namespace aapt { + +class Command { + public: + explicit Command(const android::StringPiece& name) : name_(name.to_string()), + fullname_(name.to_string()) { } + explicit Command(const android::StringPiece& name, const android::StringPiece& short_name) + : name_(name.to_string()), short_name_(short_name.to_string()), fullname_(name.to_string()) {} + virtual ~Command() = default; + + void AddRequiredFlag(const android::StringPiece& name, const android::StringPiece& description, + std::string* value); + void AddRequiredFlagList(const android::StringPiece& name, const android::StringPiece& + description, std::vector<std::string>* value); + void AddOptionalFlag(const android::StringPiece& name, const android::StringPiece& description, + Maybe<std::string>* value); + void AddOptionalFlagList(const android::StringPiece& name, + const android::StringPiece& description, std::vector<std::string>* value); + void AddOptionalFlagList(const android::StringPiece& name, + const android::StringPiece& description, std::unordered_set<std::string>* value); + void AddOptionalSwitch(const android::StringPiece& name, const android::StringPiece& description, + bool* value); + void AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand); + + void SetDescription(const android::StringPiece& name); + + /** Prints the help menu of the command. */ + void Usage(std::ostream* out); + + /** + * Parses the command line arguments, sets the flag variable values, and runs the action of + * the command. If the arguments fail to parse to the command and its subcommands, then the action + * will not be run and the usage will be printed instead. + **/ + int Execute(const std::vector<android::StringPiece>& args, std::ostream* outError); + + /** The action to preform when the command is executed. */ + virtual int Action(const std::vector<std::string>& args) = 0; + + private: + struct Flag { + std::string name; + std::string description; + std::function<bool(const android::StringPiece& value)> action; + bool required; + size_t num_args; + + bool parsed; + }; + + std::string description_; + std::string name_; + std::string short_name_; + std::string fullname_; + std::vector<Flag> flags_; + std::vector<std::unique_ptr<Command>> subcommands_; +}; + +} // namespace aapt + +#endif // AAPT_COMMAND_H diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index ab8a4b77a89d..62c19fbfcdd3 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -14,8 +14,9 @@ * limitations under the License. */ -#include <dirent.h> +#include "Compile.h" +#include <dirent.h> #include <string> #include "android-base/errors.h" @@ -27,9 +28,9 @@ #include "ConfigDescription.h" #include "Diagnostics.h" -#include "Flags.h" #include "ResourceParser.h" #include "ResourceTable.h" +#include "cmd/Util.h" #include "compile/IdAssigner.h" #include "compile/InlineXmlFormatParser.h" #include "compile/Png.h" @@ -40,8 +41,10 @@ #include "format/proto/ProtoSerialize.h" #include "io/BigBufferStream.h" #include "io/FileStream.h" +#include "io/FileSystem.h" #include "io/StringStream.h" #include "io/Util.h" +#include "io/ZipArchive.h" #include "util/Files.h" #include "util/Maybe.h" #include "util/Util.h" @@ -120,16 +123,6 @@ static Maybe<ResourcePathData> ExtractResourcePathData(const std::string& path, extension.to_string(), config_str.to_string(), config}; } -struct CompileOptions { - std::string output_path; - Maybe<std::string> res_dir; - Maybe<std::string> generate_text_symbols_path; - bool pseudolocalize = false; - bool no_png_crunch = false; - bool legacy_mode = false; - bool verbose = false; -}; - static std::string BuildIntermediateContainerFilename(const ResourcePathData& data) { std::stringstream name; name << data.resource_dir; @@ -144,81 +137,20 @@ static std::string BuildIntermediateContainerFilename(const ResourcePathData& da return name.str(); } -static bool IsHidden(const StringPiece& filename) { - return util::StartsWith(filename, "."); -} - -// Walks the res directory structure, looking for resource files. -static bool LoadInputFilesFromDir(IAaptContext* context, const CompileOptions& options, - std::vector<ResourcePathData>* out_path_data) { - const std::string& root_dir = options.res_dir.value(); - std::unique_ptr<DIR, decltype(closedir)*> d(opendir(root_dir.data()), closedir); - if (!d) { - context->GetDiagnostics()->Error(DiagMessage(root_dir) << "failed to open directory: " - << SystemErrorCodeToString(errno)); - return false; - } - - while (struct dirent* entry = readdir(d.get())) { - if (IsHidden(entry->d_name)) { - continue; - } - - std::string prefix_path = root_dir; - file::AppendPath(&prefix_path, entry->d_name); - - if (file::GetFileType(prefix_path) != file::FileType::kDirectory) { - continue; - } - - std::unique_ptr<DIR, decltype(closedir)*> subdir(opendir(prefix_path.data()), closedir); - if (!subdir) { - context->GetDiagnostics()->Error(DiagMessage(prefix_path) << "failed to open directory: " - << SystemErrorCodeToString(errno)); - return false; - } - - while (struct dirent* leaf_entry = readdir(subdir.get())) { - if (IsHidden(leaf_entry->d_name)) { - continue; - } - - std::string full_path = prefix_path; - file::AppendPath(&full_path, leaf_entry->d_name); - - std::string err_str; - Maybe<ResourcePathData> path_data = ExtractResourcePathData(full_path, &err_str); - if (!path_data) { - context->GetDiagnostics()->Error(DiagMessage(full_path) << err_str); - return false; - } - - out_path_data->push_back(std::move(path_data.value())); - } - } - - // File-system directory enumeration order is platform-dependent. Sort the result to remove any - // inconsistencies between platforms. - std::sort( - out_path_data->begin(), out_path_data->end(), - [](const ResourcePathData& a, const ResourcePathData& b) { return a.source < b.source; }); - return true; -} - static bool CompileTable(IAaptContext* context, const CompileOptions& options, - const ResourcePathData& path_data, IArchiveWriter* writer, + const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { ResourceTable table; { - FileInputStream fin(path_data.source.path); - if (fin.HadError()) { + auto fin = file->OpenInputStream(); + if (fin->HadError()) { context->GetDiagnostics()->Error(DiagMessage(path_data.source) - << "failed to open file: " << fin.GetError()); + << "failed to open file: " << fin->GetError()); return false; } // Parse the values file from XML. - xml::XmlPullParser xml_parser(&fin); + xml::XmlPullParser xml_parser(fin.get()); ResourceParserOptions parser_options; parser_options.error_on_positional_arguments = !options.legacy_mode; @@ -226,8 +158,12 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // If the filename includes donottranslate, then the default translatable is false. parser_options.translatable = path_data.name.find("donottranslate") == std::string::npos; + // If visibility was forced, we need to use it when creating a new resource and also error if + // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. + parser_options.visibility = options.visibility; + ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config, - parser_options); + parser_options); if (!res_parser.Parse(&xml_parser)) { return false; } @@ -293,36 +229,48 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, Printer r_txt_printer(&fout_text); for (const auto& package : table.packages) { - for (const auto& type : package->types) { - for (const auto& entry : type->entries) { - // Check access modifiers. - switch(entry->visibility.level) { - case Visibility::Level::kUndefined : - r_txt_printer.Print("default "); - break; - case Visibility::Level::kPublic : - r_txt_printer.Print("public "); - break; - case Visibility::Level::kPrivate : - r_txt_printer.Print("private "); - } + // Only print resources defined locally, e.g. don't write android attributes. + if (package->name.empty()) { + for (const auto& type : package->types) { + for (const auto& entry : type->entries) { + // Check access modifiers. + switch (entry->visibility.level) { + case Visibility::Level::kUndefined : + r_txt_printer.Print("default "); + break; + case Visibility::Level::kPublic : + r_txt_printer.Print("public "); + break; + case Visibility::Level::kPrivate : + r_txt_printer.Print("private "); + } - if (type->type != ResourceType::kStyleable) { - r_txt_printer.Print("int "); - r_txt_printer.Print(to_string(type->type)); - r_txt_printer.Print(" "); - r_txt_printer.Println(entry->name); - } else { - r_txt_printer.Print("int[] styleable "); - r_txt_printer.Println(entry->name); - - if (!entry->values.empty()) { - auto styleable = static_cast<const Styleable*>(entry->values.front()->value.get()); - for (const auto& attr : styleable->entries) { - r_txt_printer.Print("default int styleable "); - r_txt_printer.Print(entry->name); - r_txt_printer.Print("_"); - r_txt_printer.Println(attr.name.value().entry); + if (type->type != ResourceType::kStyleable) { + r_txt_printer.Print("int "); + r_txt_printer.Print(to_string(type->type)); + r_txt_printer.Print(" "); + r_txt_printer.Println(entry->name); + } else { + r_txt_printer.Print("int[] styleable "); + r_txt_printer.Println(entry->name); + + if (!entry->values.empty()) { + auto styleable = + static_cast<const Styleable*>(entry->values.front()->value.get()); + for (const auto& attr : styleable->entries) { + // The visibility of the children under the styleable does not matter as they are + // nested under their parent and use its visibility. + r_txt_printer.Print("default int styleable "); + r_txt_printer.Print(entry->name); + // If the package name is present, also include it in the mangled name (e.g. + // "android") + if (!attr.name.value().package.empty()) { + r_txt_printer.Print("_"); + r_txt_printer.Print(MakePackageSafeName(attr.name.value().package)); + } + r_txt_printer.Print("_"); + r_txt_printer.Println(attr.name.value().entry); + } } } } @@ -401,7 +349,7 @@ static bool IsValidFile(IAaptContext* context, const std::string& input_path) { } static bool CompileXml(IAaptContext* context, const CompileOptions& options, - const ResourcePathData& path_data, IArchiveWriter* writer, + const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { if (context->IsVerbose()) { context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling XML"); @@ -409,18 +357,17 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, std::unique_ptr<xml::XmlResource> xmlres; { - FileInputStream fin(path_data.source.path); - if (fin.HadError()) { + auto fin = file->OpenInputStream(); + if (fin->HadError()) { context->GetDiagnostics()->Error(DiagMessage(path_data.source) - << "failed to open file: " << fin.GetError()); + << "failed to open file: " << fin->GetError()); return false; } - xmlres = xml::Inflate(&fin, context->GetDiagnostics(), path_data.source); - } - - if (!xmlres) { - return false; + xmlres = xml::Inflate(fin.get(), context->GetDiagnostics(), path_data.source); + if (!xmlres) { + return false; + } } xmlres->file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name); @@ -501,7 +448,7 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, } static bool CompilePng(IAaptContext* context, const CompileOptions& options, - const ResourcePathData& path_data, IArchiveWriter* writer, + const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { if (context->IsVerbose()) { context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling PNG"); @@ -515,15 +462,17 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, res_file.type = ResourceFile::Type::kPng; { - std::string content; - if (!android::base::ReadFileToString(path_data.source.path, &content, - true /*follow_symlinks*/)) { - context->GetDiagnostics()->Error(DiagMessage(path_data.source) - << "failed to open file: " - << SystemErrorCodeToString(errno)); + auto data = file->OpenAsData(); + if (!data) { + context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file "); return false; } + // Read the file as a string + char buffer_2[data->size()]; + memcpy(&buffer_2, data->data(), data->size()); + StringPiece content(buffer_2, data->size()); + BigBuffer crunched_png_buffer(4096); io::BigBufferOutputStream crunched_png_buffer_out(&crunched_png_buffer); @@ -591,7 +540,7 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, if (context->IsVerbose()) { // For debugging only, use the legacy PNG cruncher and compare the resulting file sizes. // This will help catch exotic cases where the new code may generate larger PNGs. - std::stringstream legacy_stream(content); + std::stringstream legacy_stream(content.to_string()); BigBuffer legacy_buffer(4096); Png png(context->GetDiagnostics()); if (!png.process(path_data.source, &legacy_stream, &legacy_buffer, {})) { @@ -605,41 +554,31 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, } io::BigBufferInputStream buffer_in(&buffer); - if (!WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer, - context->GetDiagnostics())) { - return false; - } - return true; + return WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer, + context->GetDiagnostics()); } static bool CompileFile(IAaptContext* context, const CompileOptions& options, - const ResourcePathData& path_data, IArchiveWriter* writer, + const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { if (context->IsVerbose()) { context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling file"); } - BigBuffer buffer(256); ResourceFile res_file; res_file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name); res_file.config = path_data.config; res_file.source = path_data.source; res_file.type = ResourceFile::Type::kUnknown; - std::string error_str; - Maybe<android::FileMap> f = file::MmapPath(path_data.source.path, &error_str); - if (!f) { - context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to mmap file: " - << error_str); + auto data = file->OpenAsData(); + if (!data) { + context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file "); return false; } - io::MmappedData mmapped_in(std::move(f.value())); - if (!WriteHeaderAndDataToWriter(output_path, res_file, &mmapped_in, writer, - context->GetDiagnostics())) { - return false; - } - return true; + return WriteHeaderAndDataToWriter(output_path, res_file, data.get(), writer, + context->GetDiagnostics()); } class CompileContext : public IAaptContext { @@ -694,79 +633,33 @@ class CompileContext : public IAaptContext { bool verbose_ = false; }; -// Entry point for compilation phase. Parses arguments and dispatches to the correct steps. -int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { - CompileContext context(diagnostics); - CompileOptions options; - - bool verbose = false; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path", &options.output_path) - .OptionalFlag("--dir", "Directory to scan for resources", &options.res_dir) - .OptionalFlag("--output-text-symbols", - "Generates a text file containing the resource symbols in the\n" - "specified file", - &options.generate_text_symbols_path) - .OptionalSwitch("--pseudo-localize", - "Generate resources for pseudo-locales " - "(en-XA and ar-XB)", - &options.pseudolocalize) - .OptionalSwitch("--no-crunch", "Disables PNG processing", &options.no_png_crunch) - .OptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", - &options.legacy_mode) - .OptionalSwitch("-v", "Enables verbose logging", &verbose); - if (!flags.Parse("aapt2 compile", args, &std::cerr)) { - return 1; - } - - context.SetVerbose(verbose); - - std::unique_ptr<IArchiveWriter> archive_writer; - - std::vector<ResourcePathData> input_data; - if (options.res_dir) { - if (!flags.GetArgs().empty()) { - // Can't have both files and a resource directory. - context.GetDiagnostics()->Error(DiagMessage() << "files given but --dir specified"); - flags.Usage("aapt2 compile", &std::cerr); - return 1; - } - - if (!LoadInputFilesFromDir(&context, options, &input_data)) { - return 1; - } - - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options.output_path); +int Compile(IAaptContext* context, io::IFileCollection* inputs, IArchiveWriter* output_writer, + CompileOptions& options) { + bool error = false; - } else { - input_data.reserve(flags.GetArgs().size()); + // Iterate over the input files in a stable, platform-independent manner + auto file_iterator = inputs->Iterator(); + while (file_iterator->HasNext()) { + auto file = file_iterator->Next(); + std::string path = file->GetSource().path; - // Collect data from the path for each input file. - for (const std::string& arg : flags.GetArgs()) { - std::string error_str; - if (Maybe<ResourcePathData> path_data = ExtractResourcePathData(arg, &error_str)) { - input_data.push_back(std::move(path_data.value())); - } else { - context.GetDiagnostics()->Error(DiagMessage() << error_str << " (" << arg << ")"); - return 1; - } + // Skip hidden input files + if (file::IsHidden(path)) { + continue; } - archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options.output_path); - } - - if (!archive_writer) { - return 1; - } - - bool error = false; - for (ResourcePathData& path_data : input_data) { - if (options.verbose) { - context.GetDiagnostics()->Note(DiagMessage(path_data.source) << "processing"); + if (!options.res_zip && !IsValidFile(context, path)) { + error = true; + continue; } - if (!IsValidFile(&context, path_data.source.path)) { + // Extract resource type information from the full path + std::string err_str; + ResourcePathData path_data; + if (auto maybe_path_data = ExtractResourcePathData(path, &err_str)) { + path_data = maybe_path_data.value(); + } else { + context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << err_str); error = true; continue; } @@ -784,13 +677,13 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { if (path_data.extension == "xml") { compile_func = &CompileXml; } else if ((!options.no_png_crunch && path_data.extension == "png") - || path_data.extension == "9.png") { + || path_data.extension == "9.png") { compile_func = &CompilePng; } } } else { - context.GetDiagnostics()->Error(DiagMessage() - << "invalid file path '" << path_data.source << "'"); + context->GetDiagnostics()->Error(DiagMessage() + << "invalid file path '" << path_data.source << "'"); error = true; continue; } @@ -800,17 +693,95 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { if (compile_func != &CompileFile && !options.legacy_mode && std::count(path_data.name.begin(), path_data.name.end(), '.') != 0) { error = true; - context.GetDiagnostics()->Error(DiagMessage() << "resource file '" << path_data.source.path - << "' name cannot contain '.' other than for" - << "specifying the extension"); + context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) + << "file name cannot contain '.' other than for" + << " specifying the extension"); continue; } - // Compile the file. const std::string out_path = BuildIntermediateContainerFilename(path_data); - error |= !compile_func(&context, options, path_data, archive_writer.get(), out_path); + error |= !compile_func(context, options, path_data, file, output_writer, out_path); } + return error ? 1 : 0; } +int CompileCommand::Action(const std::vector<std::string>& args) { + CompileContext context(diagnostic_); + context.SetVerbose(options_.verbose); + + if (visibility_) { + if (visibility_.value() == "public") { + options_.visibility = Visibility::Level::kPublic; + } else if (visibility_.value() == "private") { + options_.visibility = Visibility::Level::kPrivate; + } else if (visibility_.value() == "default") { + options_.visibility = Visibility::Level::kUndefined; + } else { + context.GetDiagnostics()->Error( + DiagMessage() << "Unrecognized visibility level passes to --visibility: '" + << visibility_.value() << "'. Accepted levels: public, private, default"); + return 1; + } + } + + std::unique_ptr<io::IFileCollection> file_collection; + std::unique_ptr<IArchiveWriter> archive_writer; + + // Collect the resources files to compile + if (options_.res_dir && options_.res_zip) { + context.GetDiagnostics()->Error(DiagMessage() + << "only one of --dir and --zip can be specified"); + return 1; + } else if (options_.res_dir) { + if (!args.empty()) { + context.GetDiagnostics()->Error(DiagMessage() << "files given but --dir specified"); + Usage(&std::cerr); + return 1; + } + + // Load the files from the res directory + std::string err; + file_collection = io::FileCollection::Create(options_.res_dir.value(), &err); + if (!file_collection) { + context.GetDiagnostics()->Error(DiagMessage(options_.res_dir.value()) << err); + return 1; + } + + archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); + } else if (options_.res_zip) { + if (!args.empty()) { + context.GetDiagnostics()->Error(DiagMessage() << "files given but --zip specified"); + Usage(&std::cerr); + return 1; + } + + // Load a zip file containing a res directory + std::string err; + file_collection = io::ZipFileCollection::Create(options_.res_zip.value(), &err); + if (!file_collection) { + context.GetDiagnostics()->Error(DiagMessage(options_.res_zip.value()) << err); + return 1; + } + + archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); + } else { + auto collection = util::make_unique<io::FileCollection>(); + + // Collect data from the path for each input file. + for (const std::string& arg : args) { + collection->InsertFile(arg); + } + + file_collection = std::move(collection); + archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path); + } + + if (!archive_writer) { + return 1; + } + + return Compile(&context, file_collection.get(), archive_writer.get(), options_); +} + } // namespace aapt diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index d95cf1c22732..c429d5f5d4b2 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -1,14 +1,77 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #ifndef AAPT2_COMPILE_H #define AAPT2_COMPILE_H #include "androidfw/StringPiece.h" - +#include "format/Archive.h" +#include "process/IResourceTableConsumer.h" +#include "Command.h" #include "Diagnostics.h" +#include "ResourceTable.h" namespace aapt { - int Compile(const std::vector<android::StringPiece>& args, IDiagnostics* diagnostics); +struct CompileOptions { + std::string output_path; + Maybe<std::string> res_dir; + Maybe<std::string> res_zip; + Maybe<std::string> generate_text_symbols_path; + Maybe<Visibility::Level> visibility; + bool pseudolocalize = false; + bool no_png_crunch = false; + bool legacy_mode = false; + bool verbose = false; +}; + +/** Parses flags and compiles resources to be used in linking. */ +class CompileCommand : public Command { + public: + explicit CompileCommand(IDiagnostics* diagnostic) : Command("compile", "c"), + diagnostic_(diagnostic) { + SetDescription("Compiles resources to be linked into an apk."); + AddRequiredFlag("-o", "Output path", &options_.output_path); + AddOptionalFlag("--dir", "Directory to scan for resources", &options_.res_dir); + AddOptionalFlag("--zip", "Zip file containing the res directory to scan for resources", + &options_.res_zip); + AddOptionalFlag("--output-text-symbols", + "Generates a text file containing the resource symbols in the\n" + "specified file", &options_.generate_text_symbols_path); + AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " + "(en-XA and ar-XB)", &options_.pseudolocalize); + AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch); + AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", + &options_.legacy_mode); + AddOptionalFlag("--visibility", + "Sets the visibility of the compiled resources to the specified\n" + "level. Accepted levels: public, private, default", &visibility_); + AddOptionalSwitch("-v", "Enables verbose logging", &options_.verbose); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diagnostic_; + CompileOptions options_; + Maybe<std::string> visibility_; +}; +int Compile(IAaptContext* context, io::IFileCollection* inputs, + IArchiveWriter* output_writer, CompileOptions& options); }// namespace aapt #endif //AAPT2_COMPILE_H diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp index 212f2cf26e0d..dd5198ce86da 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -18,21 +18,22 @@ #include "android-base/file.h" #include "io/StringStream.h" +#include "io/ZipArchive.h" #include "java/AnnotationProcessor.h" #include "test/Test.h" namespace aapt { -int TestCompile(std::string path, std::string outDir, bool legacy, StdErrDiagnostics& diag) { +int TestCompile(const std::string& path, const std::string& outDir, bool legacy, + StdErrDiagnostics& diag) { std::vector<android::StringPiece> args; args.push_back(path); args.push_back("-o"); args.push_back(outDir); - args.push_back("-v"); if (legacy) { args.push_back("--legacy"); } - return aapt::Compile(args, &diag); + return CompileCommand(&diag).Execute(args, &std::cerr); } TEST(CompilerTest, MultiplePeriods) { @@ -93,4 +94,56 @@ TEST(CompilerTest, MultiplePeriods) { ASSERT_EQ(remove(path5_out.c_str()), 0); } -}
\ No newline at end of file +TEST(CompilerTest, DirInput) { + StdErrDiagnostics diag; + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const std::string kResDir = android::base::Dirname(android::base::GetExecutablePath()) + + "/integration-tests/CompileTest/DirInput/res"; + const std::string kOutputFlata = android::base::Dirname(android::base::GetExecutablePath()) + + "/integration-tests/CompileTest/DirInput/compiled.flata"; + remove(kOutputFlata.c_str()); + + std::vector<android::StringPiece> args; + args.push_back("--dir"); + args.push_back(kResDir); + args.push_back("-o"); + args.push_back(kOutputFlata); + ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0); + + // Check for the presence of the compiled files + std::string err; + std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err); + ASSERT_NE(zip, nullptr) << err; + ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr); + ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr); + ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr); + ASSERT_EQ(remove(kOutputFlata.c_str()), 0); +} + +TEST(CompilerTest, ZipInput) { + StdErrDiagnostics diag; + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const std::string kResZip = android::base::Dirname(android::base::GetExecutablePath()) + + "/integration-tests/CompileTest/ZipInput/res.zip"; + const std::string kOutputFlata = android::base::Dirname(android::base::GetExecutablePath()) + + "/integration-tests/CompileTest/ZipInput/compiled.flata"; + remove(kOutputFlata.c_str()); + + std::vector<android::StringPiece> args; + args.push_back("--zip"); + args.push_back(kResZip); + args.push_back("-o"); + args.push_back(kOutputFlata); + ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0); + + // Check for the presence of the compiled files + std::string err; + std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err); + ASSERT_NE(zip, nullptr) << err; + ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr); + ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr); + ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr); + ASSERT_EQ(remove(kOutputFlata.c_str()), 0); +} + +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index eb307fb1ddce..86b1f4c54deb 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -14,12 +14,13 @@ * limitations under the License. */ +#include "Convert.h" + #include <vector> #include "android-base/macros.h" #include "androidfw/StringPiece.h" -#include "Flags.h" #include "LoadedApk.h" #include "ValueVisitor.h" #include "cmd/Util.h" @@ -104,10 +105,7 @@ bool ConvertApk(IAaptContext* context, unique_ptr<LoadedApk> apk, IApkSerializer std::unique_ptr<io::IFileCollectionIterator> iterator = apk->GetFileCollection()->Iterator(); while (iterator->HasNext()) { io::IFile* file = iterator->Next(); - std::string path = file->GetSource().path; - // The name of the path has the format "<zip-file-name>@<path-to-file>". - path = path.substr(path.find('@') + 1); // Manifest, resource table and resources have already been taken care of. if (path == kAndroidManifestPath || @@ -321,37 +319,18 @@ class Context : public IAaptContext { StdErrDiagnostics diag_; }; -int Convert(const vector<StringPiece>& args) { - - static const char* kOutputFormatProto = "proto"; - static const char* kOutputFormatBinary = "binary"; - - Context context; - std::string output_path; - Maybe<std::string> output_format; - TableFlattenerOptions options; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path", &output_path) - .OptionalFlag("--output-format", StringPrintf("Format of the output. Accepted values are " - "'%s' and '%s'. When not set, defaults to '%s'.", kOutputFormatProto, - kOutputFormatBinary, kOutputFormatBinary), &output_format) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.use_sparse_entries) - .OptionalSwitch("-v", "Enables verbose logging", &context.verbose_); - if (!flags.Parse("aapt2 convert", args, &std::cerr)) { - return 1; - } +const char* ConvertCommand::kOutputFormatProto = "proto"; +const char* ConvertCommand::kOutputFormatBinary = "binary"; - if (flags.GetArgs().size() != 1) { +int ConvertCommand::Action(const std::vector<std::string>& args) { + if (args.size() != 1) { std::cerr << "must supply a single proto APK\n"; - flags.Usage("aapt2 convert", &std::cerr); + Usage(&std::cerr); return 1; } - const StringPiece& path = flags.GetArgs()[0]; + Context context; + const StringPiece& path = args[0]; unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics()); if (apk == nullptr) { context.GetDiagnostics()->Error(DiagMessage(path) << "failed to load APK"); @@ -367,24 +346,24 @@ int Convert(const vector<StringPiece>& args) { context.package_ = app_info.value().package; unique_ptr<IArchiveWriter> writer = - CreateZipFileArchiveWriter(context.GetDiagnostics(), output_path); + CreateZipFileArchiveWriter(context.GetDiagnostics(), output_path_); if (writer == nullptr) { return 1; } unique_ptr<IApkSerializer> serializer; - if (!output_format || output_format.value() == kOutputFormatBinary) { - serializer.reset(new BinaryApkSerializer(&context, apk->GetSource(), options)); - } else if (output_format.value() == kOutputFormatProto) { + if (!output_format_ || output_format_.value() == ConvertCommand::kOutputFormatBinary) { + + serializer.reset(new BinaryApkSerializer(&context, apk->GetSource(), options_)); + } else if (output_format_.value() == ConvertCommand::kOutputFormatProto) { serializer.reset(new ProtoApkSerializer(&context, apk->GetSource())); } else { context.GetDiagnostics()->Error(DiagMessage(path) - << "Invalid value for flag --output-format: " - << output_format.value()); + << "Invalid value for flag --output-format: " + << output_format_.value()); return 1; } - return ConvertApk(&context, std::move(apk), serializer.get(), writer.get()) ? 0 : 1; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h new file mode 100644 index 000000000000..fcec23d16f78 --- /dev/null +++ b/tools/aapt2/cmd/Convert.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_CONVERT_H +#define AAPT2_CONVERT_H + +#include "Command.h" +#include "format/binary/TableFlattener.h" + +namespace aapt { + +class ConvertCommand : public Command { + public: + explicit ConvertCommand() : Command("convert") { + SetDescription("Converts an apk between binary and proto formats."); + AddRequiredFlag("-o", "Output path", &output_path_); + AddOptionalFlag("--output-format", android::base::StringPrintf("Format of the output. " + "Accepted values are '%s' and '%s'. When not set, defaults to '%s'.", + kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); + AddOptionalSwitch("--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.", + &options_.use_sparse_entries); + AddOptionalSwitch("-v", "Enables verbose logging", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + const static char* kOutputFormatProto; + const static char* kOutputFormatBinary; + + TableFlattenerOptions options_; + std::string output_path_; + Maybe<std::string> output_format_; + bool verbose_ = false; +}; + +}// namespace aapt + +#endif //AAPT2_CONVERT_H diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index 12113ed8a48a..262f4fc4e394 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -14,9 +14,10 @@ * limitations under the License. */ +#include "Diff.h" + #include "android-base/macros.h" -#include "Flags.h" #include "LoadedApk.h" #include "ValueVisitor.h" #include "process/IResourceTableConsumer.h" @@ -344,23 +345,18 @@ static void ZeroOutAppReferences(ResourceTable* table) { VisitAllValuesInTable(table, &visitor); } -int Diff(const std::vector<StringPiece>& args) { +int DiffCommand::Action(const std::vector<std::string>& args) { DiffContext context; - Flags flags; - if (!flags.Parse("aapt2 diff", args, &std::cerr)) { - return 1; - } - - if (flags.GetArgs().size() != 2u) { + if (args.size() != 2u) { std::cerr << "must have two apks as arguments.\n\n"; - flags.Usage("aapt2 diff", &std::cerr); + Usage(&std::cerr); return 1; } IDiagnostics* diag = context.GetDiagnostics(); - std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(flags.GetArgs()[0], diag); - std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(flags.GetArgs()[1], diag); + std::unique_ptr<LoadedApk> apk_a = LoadedApk::LoadApkFromPath(args[0], diag); + std::unique_ptr<LoadedApk> apk_b = LoadedApk::LoadApkFromPath(args[1], diag); if (!apk_a || !apk_b) { return 1; } diff --git a/tools/aapt2/cmd/Diff.h b/tools/aapt2/cmd/Diff.h new file mode 100644 index 000000000000..c38888863be9 --- /dev/null +++ b/tools/aapt2/cmd/Diff.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_DIFF_H +#define AAPT2_DIFF_H + +#include "Command.h" + +namespace aapt { + +class DiffCommand : public Command { + public: + explicit DiffCommand() : Command("diff") { + SetDescription("Prints the differences in resources of two apks."); + } + + int Action(const std::vector<std::string>& args) override; +}; + +}// namespace aapt + +#endif //AAPT2_DIFF_H diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index 6bb762aafb3b..5cb30b6f5db2 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "Dump.h" + #include <cinttypes> #include <vector> @@ -22,10 +24,11 @@ #include "Debug.h" #include "Diagnostics.h" -#include "Flags.h" #include "LoadedApk.h" +#include "Util.h" #include "format/Container.h" #include "format/binary/BinaryResourceParser.h" +#include "format/binary/XmlFlattener.h" #include "format/proto/ProtoDeserialize.h" #include "io/FileStream.h" #include "io/ZipArchive.h" @@ -39,13 +42,6 @@ using ::android::base::StringPrintf; namespace aapt { -struct DumpOptions { - DebugPrintTableOptions print_options; - - // The path to a file within an APK to dump. - Maybe<std::string> file_to_dump_path; -}; - static const char* ResourceFileTypeToString(const ResourceFile::Type& type) { switch (type) { case ResourceFile::Type::kPng: @@ -77,299 +73,371 @@ static void DumpCompiledFile(const ResourceFile& file, const Source& source, off printer->Println(StringPrintf("Data: offset=%" PRIi64 " length=%zd", offset, len)); } -static bool DumpXmlFile(IAaptContext* context, io::IFile* file, bool proto, - text::Printer* printer) { - std::unique_ptr<xml::XmlResource> doc; - if (proto) { - std::unique_ptr<io::InputStream> in = file->OpenInputStream(); - if (in == nullptr) { - context->GetDiagnostics()->Error(DiagMessage() << "failed to open file"); - return false; - } +namespace { - io::ZeroCopyInputAdaptor adaptor(in.get()); - pb::XmlNode pb_node; - if (!pb_node.ParseFromZeroCopyStream(&adaptor)) { - context->GetDiagnostics()->Error(DiagMessage() << "failed to parse file as proto XML"); - return false; - } +class DumpContext : public IAaptContext { + public: + PackageType GetPackageType() override { + // Doesn't matter. + return PackageType::kApp; + } - std::string err; - doc = DeserializeXmlResourceFromPb(pb_node, &err); - if (doc == nullptr) { - context->GetDiagnostics()->Error(DiagMessage() << "failed to deserialize proto XML"); - return false; - } - printer->Println("Proto XML"); - } else { - std::unique_ptr<io::IData> data = file->OpenAsData(); - if (data == nullptr) { - context->GetDiagnostics()->Error(DiagMessage() << "failed to open file"); - return false; - } + IDiagnostics* GetDiagnostics() override { + return &diagnostics_; + } - std::string err; - doc = xml::Inflate(data->data(), data->size(), &err); - if (doc == nullptr) { - context->GetDiagnostics()->Error(DiagMessage() << "failed to parse file as binary XML"); - return false; - } - printer->Println("Binary XML"); + NameMangler* GetNameMangler() override { + UNIMPLEMENTED(FATAL); + return nullptr; } - Debug::DumpXml(*doc, printer); - return true; -} + const std::string& GetCompilationPackage() override { + static std::string empty; + return empty; + } -static bool TryDumpFile(IAaptContext* context, const std::string& file_path, - const DumpOptions& options) { - // Use a smaller buffer so that there is less latency for dumping to stdout. - constexpr size_t kStdOutBufferSize = 1024u; - io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); - Printer printer(&fout); + uint8_t GetPackageId() override { + return 0; + } - std::string err; - std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(file_path, &err); - if (zip) { - ResourceTable table; - bool proto = false; - if (io::IFile* file = zip->FindFile("resources.pb")) { - proto = true; + SymbolTable* GetExternalSymbols() override { + UNIMPLEMENTED(FATAL); + return nullptr; + } - std::unique_ptr<io::IData> data = file->OpenAsData(); - if (data == nullptr) { - context->GetDiagnostics()->Error(DiagMessage(file_path) << "failed to open resources.pb"); - return false; - } + bool IsVerbose() override { + return verbose_; + } - pb::ResourceTable pb_table; - if (!pb_table.ParseFromArray(data->data(), data->size())) { - context->GetDiagnostics()->Error(DiagMessage(file_path) << "invalid resources.pb"); - return false; - } + void SetVerbose(bool val) { + verbose_ = val; + } - if (!DeserializeTableFromPb(pb_table, zip.get(), &table, &err)) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to parse table: " << err); - return false; - } - } else if (io::IFile* file = zip->FindFile("resources.arsc")) { - std::unique_ptr<io::IData> data = file->OpenAsData(); - if (!data) { - context->GetDiagnostics()->Error(DiagMessage(file_path) << "failed to open resources.arsc"); - return false; - } + int GetMinSdkVersion() override { + return 0; + } - BinaryResourceParser parser(context->GetDiagnostics(), &table, Source(file_path), - data->data(), data->size()); - if (!parser.Parse()) { - return false; - } - } + private: + StdErrDiagnostics diagnostics_; + bool verbose_ = false; +}; - if (!options.file_to_dump_path) { - if (proto) { - printer.Println("Proto APK"); - } else { - printer.Println("Binary APK"); - } - Debug::PrintTable(table, options.print_options, &printer); - return true; +} // namespace + +// Use a smaller buffer so that there is less latency for dumping to stdout. +constexpr size_t kStdOutBufferSize = 1024u; + +int DumpAPCCommand::Action(const std::vector<std::string>& args) { + DumpContext context; + DebugPrintTableOptions print_options; + print_options.show_sources = true; + print_options.show_values = !no_values_; + + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump container specified."); + return 1; + } + + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); + + for (auto container : args) { + io::FileInputStream input(container); + if (input.HadError()) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to open file: " << input.GetError()); + return false; } - io::IFile* file = zip->FindFile(options.file_to_dump_path.value()); - if (file == nullptr) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "file '" << options.file_to_dump_path.value() - << "' not found in APK"); + // Try as a compiled file. + ContainerReader reader(&input); + if (reader.HadError()) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to read container: " << reader.GetError()); return false; } - return DumpXmlFile(context, file, proto, &printer); + + printer.Println("AAPT2 Container (APC)"); + ContainerReaderEntry* entry; + std::string error; + while ((entry = reader.Next()) != nullptr) { + if (entry->Type() == ContainerEntryType::kResTable) { + printer.Println("kResTable"); + + pb::ResourceTable pb_table; + if (!entry->GetResTable(&pb_table)) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to parse proto table: " + << entry->GetError()); + continue; + } + + ResourceTable table; + error.clear(); + if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &error)) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to parse table: " << error); + continue; + } + + printer.Indent(); + Debug::PrintTable(table, print_options, &printer); + printer.Undent(); + } else if (entry->Type() == ContainerEntryType::kResFile) { + printer.Println("kResFile"); + pb::internal::CompiledFile pb_compiled_file; + off64_t offset; + size_t length; + if (!entry->GetResFileOffsets(&pb_compiled_file, &offset, &length)) { + context.GetDiagnostics()->Error( + DiagMessage(container) << "failed to parse compiled proto file: " + << entry->GetError()); + continue; + } + + ResourceFile file; + if (!DeserializeCompiledFileFromPb(pb_compiled_file, &file, &error)) { + context.GetDiagnostics()->Warn(DiagMessage(container) + << "failed to parse compiled file: " << error); + continue; + } + + printer.Indent(); + DumpCompiledFile(file, Source(container), offset, length, &printer); + printer.Undent(); + } + } } - err.clear(); + return 0; +} - io::FileInputStream input(file_path); - if (input.HadError()) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to open file: " << input.GetError()); - return false; +int DumpConfigsCommand::Action(const std::vector<std::string>& args) { + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; } - // Try as a compiled file. - ContainerReader reader(&input); - if (reader.HadError()) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to read container: " << reader.GetError()); - return false; + auto loaded_apk = LoadedApk::LoadApkFromPath(args[0], diag_); + if (!loaded_apk) { + return 1; } - printer.Println("AAPT2 Container (APC)"); - ContainerReaderEntry* entry; - while ((entry = reader.Next()) != nullptr) { - if (entry->Type() == ContainerEntryType::kResTable) { - printer.Println("kResTable"); - - pb::ResourceTable pb_table; - if (!entry->GetResTable(&pb_table)) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to parse proto table: " << entry->GetError()); - continue; - } + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); - ResourceTable table; - err.clear(); - if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &err)) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to parse table: " << err); - continue; + // Comparison function used to order configurations + auto compare = [](ConfigDescription c1, ConfigDescription c2) -> bool { + return c1.compare(c2) < 0; + }; + + // Insert the configurations into a set in order to keep every configuarion seen + std::set<ConfigDescription, decltype(compare)> configs(compare); + for (auto& package : loaded_apk->GetResourceTable()->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + for (auto& value : entry->values) { + configs.insert(value->config); + } } + } + } - printer.Indent(); - Debug::PrintTable(table, options.print_options, &printer); - printer.Undent(); - } else if (entry->Type() == ContainerEntryType::kResFile) { - printer.Println("kResFile"); - pb::internal::CompiledFile pb_compiled_file; - off64_t offset; - size_t length; - if (!entry->GetResFileOffsets(&pb_compiled_file, &offset, &length)) { - context->GetDiagnostics()->Error( - DiagMessage(file_path) << "failed to parse compiled proto file: " << entry->GetError()); - continue; - } + // Print the configurations in order + for (auto& config : configs) { + printer.Print(StringPrintf("%s\n", config.to_string().data())); + } - ResourceFile file; - std::string error; - if (!DeserializeCompiledFileFromPb(pb_compiled_file, &file, &error)) { - context->GetDiagnostics()->Warn(DiagMessage(file_path) - << "failed to parse compiled file: " << error); - continue; - } + return 0; +} - printer.Indent(); - DumpCompiledFile(file, Source(file_path), offset, length, &printer); - printer.Undent(); +int DumpStringsCommand::Action(const std::vector<std::string>& args) { + DumpContext context; + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; + } + + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); + + for (auto apk : args) { + auto loaded_apk = LoadedApk::LoadApkFromPath(apk, diag_); + if (!loaded_apk) { + return 1; } + + // Load the run-time xml string pool using the flattened data + BigBuffer buffer(4096); + StringPool::FlattenUtf8(&buffer, loaded_apk->GetResourceTable()->string_pool, + context.GetDiagnostics()); + auto data = buffer.to_string(); + android::ResStringPool pool(data.data(), data.size(), false); + Debug::DumpResStringPool(&pool, &printer); } - return true; + + return 0; } -static bool DumpPackageName(IAaptContext* context, const std::string& file_path) { - auto loaded_apk = LoadedApk::LoadApkFromPath(file_path, context->GetDiagnostics()); - if (!loaded_apk) { - return false; +int DumpTableCommand::Action(const std::vector<std::string>& args) { + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; } - constexpr size_t kStdOutBufferSize = 1024u; io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); Printer printer(&fout); - xml::Element* manifest_el = loaded_apk->GetManifest()->root.get(); - if (!manifest_el) { - context->GetDiagnostics()->Error(DiagMessage() << "No AndroidManifest."); - return false; - } + DebugPrintTableOptions print_options; + print_options.show_sources = true; + print_options.show_values = !no_values_; - xml::Attribute* attr = manifest_el->FindAttribute({}, "package"); - if (!attr) { - context->GetDiagnostics()->Error(DiagMessage() << "No package name."); - return false; + for (auto apk : args) { + auto loaded_apk = LoadedApk::LoadApkFromPath(apk, diag_); + if (!loaded_apk) { + return 1; + } + + if (loaded_apk->GetApkFormat() == ApkFormat::kProto) { + printer.Println("Proto APK"); + } else { + printer.Println("Binary APK"); + } + + Debug::PrintTable(*loaded_apk->GetResourceTable(), print_options, &printer); } - printer.Println(StringPrintf("%s", attr->value.c_str())); - return true; + return 0; } -namespace { - -class DumpContext : public IAaptContext { - public: - PackageType GetPackageType() override { - // Doesn't matter. - return PackageType::kApp; +int DumpXmlTreeCommand::Action(const std::vector<std::string>& args) { + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified"); + return 1; } - IDiagnostics* GetDiagnostics() override { - return &diagnostics_; + auto loaded_apk = LoadedApk::LoadApkFromPath(args[0], diag_); + if (!loaded_apk) { + return 1; } - NameMangler* GetNameMangler() override { - UNIMPLEMENTED(FATAL); - return nullptr; - } + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); - const std::string& GetCompilationPackage() override { - static std::string empty; - return empty; - } + // Dump the xml tree of every passed in file + for (auto file : files_) { + auto xml = loaded_apk->LoadXml(file, diag_); + if (!xml) { + return 1; + } - uint8_t GetPackageId() override { - return 0; + Debug::DumpXml(*xml, &printer); } - SymbolTable* GetExternalSymbols() override { - UNIMPLEMENTED(FATAL); - return nullptr; - } + return 0; +} - bool IsVerbose() override { - return verbose_; +int DumpXmlStringsCommand::Action(const std::vector<std::string>& args) { + DumpContext context; + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; } - void SetVerbose(bool val) { - verbose_ = val; + auto loaded_apk = LoadedApk::LoadApkFromPath(args[0], diag_); + if (!loaded_apk) { + return 1; } - int GetMinSdkVersion() override { - return 0; - } + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); - private: - StdErrDiagnostics diagnostics_; - bool verbose_ = false; -}; + // Dump the xml strings of every passed in file + for (auto xml_file : files_) { + android::ResXMLTree tree; -} // namespace + if (loaded_apk->GetApkFormat() == ApkFormat::kProto) { + auto xml = loaded_apk->LoadXml(xml_file, diag_); + if (!xml) { + return 1; + } -// Entry point for dump command. -int Dump(const std::vector<StringPiece>& args) { - bool verbose = false; - bool no_values = false; - DumpOptions options; - Flags flags = Flags() - .OptionalSwitch("--no-values", - "Suppresses output of values when displaying resource tables.", - &no_values) - .OptionalFlag("--file", "Dumps the specified file from the APK passed as arg.", - &options.file_to_dump_path) - .OptionalSwitch("-v", "increase verbosity of output", &verbose); - if (!flags.Parse("aapt2 dump", args, &std::cerr)) { - return 1; - } + // Flatten the xml document to get a binary representation of the proto xml file + BigBuffer buffer(4096); + XmlFlattenerOptions options = {}; + options.keep_raw_values = true; + XmlFlattener flattener(&buffer, options); + if (!flattener.Consume(&context, xml.get())) { + return 1; + } - DumpContext context; - context.SetVerbose(verbose); + // Load the run-time xml tree using the flattened data + std::string data = buffer.to_string(); + tree.setTo(data.data(), data.size(), /** copyData */ true); - auto parsedArgs = flags.GetArgs(); - if (parsedArgs.size() > 1 && parsedArgs[0] == "packagename") { - parsedArgs.erase(parsedArgs.begin()); - for (const std::string& arg : parsedArgs) { - if (!DumpPackageName(&context, arg)) { + } else if (loaded_apk->GetApkFormat() == ApkFormat::kBinary) { + io::IFile* file = loaded_apk->GetFileCollection()->FindFile(xml_file); + if (!file) { + diag_->Error(DiagMessage(xml_file) << "file '" << xml_file << "' not found in APK"); + return 1; + } + + std::unique_ptr<io::IData> data = file->OpenAsData(); + if (!data) { + diag_->Error(DiagMessage() << "failed to open file"); return 1; } + + // Load the run-time xml tree from the file data + tree.setTo(data->data(), data->size(), /** copyData */ true); } - return 0; + + Debug::DumpResStringPool(&tree.getStrings(), &printer); } - options.print_options.show_sources = true; - options.print_options.show_values = !no_values; - for (const std::string& arg : parsedArgs) { - if (!TryDumpFile(&context, arg, options)) { - return 1; - } + return 0; +} + +int DumpPackageNameCommand::Action(const std::vector<std::string>& args) { + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; + } + + auto loaded_apk = LoadedApk::LoadApkFromPath(args[0], diag_); + if (!loaded_apk) { + return 1; } + + io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + Printer printer(&fout); + + xml::Element* manifest_el = loaded_apk->GetManifest()->root.get(); + if (!manifest_el) { + diag_->Error(DiagMessage() << "No AndroidManifest."); + return 1; + } + + xml::Attribute* attr = manifest_el->FindAttribute({}, "package"); + if (!attr) { + diag_->Error(DiagMessage() << "No package name."); + return 1; + } + printer.Println(StringPrintf("%s", attr->value.c_str())); + return 0; } +/** Preform no action because a subcommand is required. */ +int DumpCommand::Action(const std::vector<std::string>& args) { + if (args.size() == 0) { + diag_->Error(DiagMessage() << "no subcommand specified"); + } else { + diag_->Error(DiagMessage() << "unknown subcommand '" << args[0] << "'"); + } + + Usage(&std::cerr); + return 1; +} + } // namespace aapt diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h new file mode 100644 index 000000000000..0724d6203e8f --- /dev/null +++ b/tools/aapt2/cmd/Dump.h @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_DUMP_H +#define AAPT2_DUMP_H + +#include "Command.h" +#include "Debug.h" + +namespace aapt { + +/** Command the contents of files generated from the compilation stage. */ +class DumpAPCCommand : public Command { + public: + explicit DumpAPCCommand(IDiagnostics* diag) : Command("apc"), diag_(diag) { + SetDescription("Print the contents of the AAPT2 Container (APC) generated fom compilation."); + AddOptionalSwitch("--no-values", "Suppresses output of values when displaying resource tables.", + &no_values_); + AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; + bool verbose_ = false; + bool no_values_ = false; +}; + +/** Prints every configuration used by a resource in an APK. */ +class DumpConfigsCommand : public Command { + public: + explicit DumpConfigsCommand(IDiagnostics* diag) : Command("configurations"), diag_(diag) { + SetDescription("Print every configuration used by a resource in the APK."); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; +}; + +/** Prints the contents of the resource table string pool in the APK. */ +class DumpStringsCommand : public Command { + public: + explicit DumpStringsCommand(IDiagnostics* diag) : Command("strings"), diag_(diag) { + SetDescription("Print the contents of the resource table string pool in the APK."); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; +}; + +/** Prints the contents of the resource table from the APK. */ +class DumpTableCommand : public Command { + public: + explicit DumpTableCommand(IDiagnostics* diag) : Command("resources"), diag_(diag) { + SetDescription("Print the contents of the resource table from the APK."); + AddOptionalSwitch("--no-values", "Suppresses output of values when displaying resource tables.", + &no_values_); + AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; + bool verbose_ = false; + bool no_values_ = false; +}; + +/** Prints the string pool of a compiled xml in an APK. */ +class DumpXmlStringsCommand : public Command { +public: + explicit DumpXmlStringsCommand(IDiagnostics* diag) : Command("xmlstrings"), diag_(diag) { + SetDescription("Print the string pool of a compiled xml in an APK."); + AddRequiredFlagList("--file", "A compiled xml file to print", &files_); + } + + int Action(const std::vector<std::string>& args) override; + +private: + IDiagnostics* diag_; + std::vector<std::string> files_; +}; + + +/** Prints the tree of a compiled xml in an APK. */ +class DumpXmlTreeCommand : public Command { + public: + explicit DumpXmlTreeCommand(IDiagnostics* diag) : Command("xmltree"), diag_(diag) { + SetDescription("Print the tree of a compiled xml in an APK."); + AddRequiredFlagList("--file", "A compiled xml file to print", &files_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; + std::vector<std::string> files_; +}; + +/** Prints the contents of the resource table from the APK. */ +class DumpPackageNameCommand : public Command { + public: + explicit DumpPackageNameCommand(IDiagnostics* diag) : Command("packagename"), diag_(diag) { + SetDescription("Print the package name of the APK."); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; +}; + +/** The default dump command. Performs no action because a subcommand is required. */ +class DumpCommand : public Command { + public: + explicit DumpCommand(IDiagnostics* diag) : Command("dump", "d"), diag_(diag) { + AddOptionalSubcommand(util::make_unique<DumpAPCCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpConfigsCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpPackageNameCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpStringsCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpTableCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpXmlStringsCommand>(diag_)); + AddOptionalSubcommand(util::make_unique<DumpXmlTreeCommand>(diag_)); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; +}; + +}// namespace aapt + +#endif //AAPT2_DUMP_H diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index db42e7cb3e02..119f56a5013c 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -14,9 +14,12 @@ * limitations under the License. */ +#include "Link.h" + #include <sys/stat.h> #include <cinttypes> +#include <algorithm> #include <queue> #include <unordered_map> #include <vector> @@ -28,7 +31,6 @@ #include "AppInfo.h" #include "Debug.h" -#include "Flags.h" #include "LoadedApk.h" #include "Locale.h" #include "NameMangler.h" @@ -73,70 +75,6 @@ using ::android::base::StringPrintf; namespace aapt { -enum class OutputFormat { - kApk, - kProto, -}; - -struct LinkOptions { - std::string output_path; - std::string manifest_path; - std::vector<std::string> include_paths; - std::vector<std::string> overlay_files; - std::vector<std::string> assets_dirs; - bool output_to_directory = false; - bool auto_add_overlay = false; - OutputFormat output_format = OutputFormat::kApk; - - // Java/Proguard options. - Maybe<std::string> generate_java_class_path; - Maybe<std::string> custom_java_package; - std::set<std::string> extra_java_packages; - Maybe<std::string> generate_text_symbols_path; - Maybe<std::string> generate_proguard_rules_path; - Maybe<std::string> generate_main_dex_proguard_rules_path; - bool generate_conditional_proguard_rules = false; - bool generate_non_final_ids = false; - std::vector<std::string> javadoc_annotations; - Maybe<std::string> private_symbols; - - // Optimizations/features. - bool no_auto_version = false; - bool no_version_vectors = false; - bool no_version_transitions = false; - bool no_resource_deduping = false; - bool no_xml_namespaces = false; - bool do_not_compress_anything = false; - std::unordered_set<std::string> extensions_to_not_compress; - - // Static lib options. - bool no_static_lib_packages = false; - - // AndroidManifest.xml massaging options. - ManifestFixerOptions manifest_fixer_options; - - // Products to use/filter on. - std::unordered_set<std::string> products; - - // Flattening options. - TableFlattenerOptions table_flattener_options; - - // Split APK options. - TableSplitterOptions table_splitter_options; - std::vector<SplitConstraints> split_constraints; - std::vector<std::string> split_paths; - - // Stable ID options. - std::unordered_map<ResourceName, ResourceId> stable_id_map; - Maybe<std::string> resource_id_map_path; - - // When 'true', allow reserved package IDs to be used for applications. Pre-O, the platform - // treats negative resource IDs [those with a package ID of 0x80 or higher] as invalid. - // In order to work around this limitation, we allow the use of traditionally reserved - // resource IDs [those between 0x02 and 0x7E]. - bool allow_reserved_package_id = false; -}; - class LinkContext : public IAaptContext { public: LinkContext(IDiagnostics* diagnostics) @@ -486,7 +424,7 @@ std::vector<std::unique_ptr<xml::XmlResource>> ResourceFileFlattener::LinkAndVer return {}; } - if (options_.update_proguard_spec && !proguard::CollectProguardRules(doc, keep_set_)) { + if (options_.update_proguard_spec && !proguard::CollectProguardRules(context_, doc, keep_set_)) { return {}; } @@ -532,6 +470,10 @@ ResourceFile::Type XmlFileTypeForOutputFormat(OutputFormat format) { return ResourceFile::Type::kUnknown; } +static auto kDrawableVersions = std::map<std::string, ApiVersion>{ + { "adaptive-icon" , SDK_O }, +}; + bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archive_writer) { bool error = false; std::map<std::pair<ConfigDescription, StringPiece>, FileOperation> config_sorted_files; @@ -629,6 +571,20 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv FileOperation& file_op = map_entry.second; if (file_op.xml_to_flatten) { + // Check minimum sdk versions supported for drawables + auto drawable_entry = kDrawableVersions.find(file_op.xml_to_flatten->root->name); + if (drawable_entry != kDrawableVersions.end()) { + if (drawable_entry->second > context_->GetMinSdkVersion() + && drawable_entry->second > config.sdkVersion) { + context_->GetDiagnostics()->Error(DiagMessage(file_op.xml_to_flatten->file.source) + << "<" << drawable_entry->first << "> elements " + << "require a sdk version of at least " + << (int16_t) drawable_entry->second); + error = true; + continue; + } + } + std::vector<std::unique_ptr<xml::XmlResource>> versioned_docs = LinkAndVersionXmlFile(table, &file_op); if (versioned_docs.empty()) { @@ -771,9 +727,9 @@ static int32_t FindFrameworkAssetManagerCookie(const android::AssetManager& asse return table.getTableCookie(idx); } -class LinkCommand { +class Linker { public: - LinkCommand(LinkContext* context, const LinkOptions& options) + Linker(LinkContext* context, const LinkOptions& options) : options_(options), context_(context), final_table_(), @@ -961,6 +917,18 @@ class LinkCommand { app_info.version_code = maybe_code.value(); } + if (xml::Attribute* version_code_major_attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor")) { + Maybe<uint32_t> maybe_code = ResourceUtils::ParseInt(version_code_major_attr->value); + if (!maybe_code) { + diag->Error(DiagMessage(xml_res->file.source.WithLine(manifest_el->line_number)) + << "invalid android:versionCodeMajor '" + << version_code_major_attr->value << "'"); + return {}; + } + app_info.version_code_major = maybe_code.value(); + } + if (xml::Attribute* revision_code_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "revisionCode")) { Maybe<uint32_t> maybe_code = ResourceUtils::ParseInt(revision_code_attr->value); @@ -1702,6 +1670,7 @@ class LinkCommand { TableMergerOptions table_merger_options; table_merger_options.auto_add_overlay = options_.auto_add_overlay; + table_merger_options.strict_visibility = options_.strict_visibility; table_merger_ = util::make_unique<TableMerger>(context_, &final_table_, table_merger_options); if (context_->IsVerbose()) { @@ -2025,188 +1994,12 @@ class LinkCommand { Maybe<std::string> included_feature_base_; }; -int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { - LinkContext context(diagnostics); - LinkOptions options; - std::vector<std::string> overlay_arg_list; - std::vector<std::string> extra_java_packages; - Maybe<std::string> package_id; - std::vector<std::string> configs; - Maybe<std::string> preferred_density; - Maybe<std::string> product_list; - bool legacy_x_flag = false; - bool require_localization = false; - bool verbose = false; - bool shared_lib = false; - bool static_lib = false; - bool proto_format = false; - Maybe<std::string> stable_id_file_path; - std::vector<std::string> split_args; - Flags flags = - Flags() - .RequiredFlag("-o", "Output path.", &options.output_path) - .RequiredFlag("--manifest", "Path to the Android manifest to build.", - &options.manifest_path) - .OptionalFlagList("-I", "Adds an Android APK to link against.", &options.include_paths) - .OptionalFlagList("-A", - "An assets directory to include in the APK. These are unprocessed.", - &options.assets_dirs) - .OptionalFlagList("-R", - "Compilation unit to link, using `overlay` semantics.\n" - "The last conflicting resource given takes precedence.", - &overlay_arg_list) - .OptionalFlag("--package-id", - "Specify the package ID to use for this app. Must be greater or equal to\n" - "0x7f and can't be used with --static-lib or --shared-lib.", - &package_id) - .OptionalFlag("--java", "Directory in which to generate R.java.", - &options.generate_java_class_path) - .OptionalFlag("--proguard", "Output file for generated Proguard rules.", - &options.generate_proguard_rules_path) - .OptionalFlag("--proguard-main-dex", - "Output file for generated Proguard rules for the main dex.", - &options.generate_main_dex_proguard_rules_path) - .OptionalSwitch("--proguard-conditional-keep-rules", - "Generate conditional Proguard keep rules.", - &options.generate_conditional_proguard_rules) - .OptionalSwitch("--no-auto-version", - "Disables automatic style and layout SDK versioning.", - &options.no_auto_version) - .OptionalSwitch("--no-version-vectors", - "Disables automatic versioning of vector drawables. Use this only\n" - "when building with vector drawable support library.", - &options.no_version_vectors) - .OptionalSwitch("--no-version-transitions", - "Disables automatic versioning of transition resources. Use this only\n" - "when building with transition support library.", - &options.no_version_transitions) - .OptionalSwitch("--no-resource-deduping", - "Disables automatic deduping of resources with\n" - "identical values across compatible configurations.", - &options.no_resource_deduping) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.table_flattener_options.use_sparse_entries) - .OptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.", - &legacy_x_flag) - .OptionalSwitch("-z", "Require localization of strings marked 'suggested'.", - &require_localization) - .OptionalFlagList("-c", - "Comma separated list of configurations to include. The default\n" - "is all configurations.", - &configs) - .OptionalFlag("--preferred-density", - "Selects the closest matching density and strips out all others.", - &preferred_density) - .OptionalFlag("--product", "Comma separated list of product names to keep", &product_list) - .OptionalSwitch("--output-to-dir", - "Outputs the APK contents to a directory specified by -o.", - &options.output_to_directory) - .OptionalSwitch("--no-xml-namespaces", - "Removes XML namespace prefix and URI information from\n" - "AndroidManifest.xml and XML binaries in res/*.", - &options.no_xml_namespaces) - .OptionalFlag("--min-sdk-version", - "Default minimum SDK version to use for AndroidManifest.xml.", - &options.manifest_fixer_options.min_sdk_version_default) - .OptionalFlag("--target-sdk-version", - "Default target SDK version to use for AndroidManifest.xml.", - &options.manifest_fixer_options.target_sdk_version_default) - .OptionalFlag("--version-code", - "Version code (integer) to inject into the AndroidManifest.xml if none is\n" - "present.", - &options.manifest_fixer_options.version_code_default) - .OptionalFlag("--version-name", - "Version name to inject into the AndroidManifest.xml if none is present.", - &options.manifest_fixer_options.version_name_default) - .OptionalSwitch("--replace-version", - "If --version-code and/or --version-name are specified, these\n" - "values will replace any value already in the manifest. By\n" - "default, nothing is changed if the manifest already defines\n" - "these attributes.", - &options.manifest_fixer_options.replace_version) - .OptionalFlag("--compile-sdk-version-code", - "Version code (integer) to inject into the AndroidManifest.xml if none is\n" - "present.", - &options.manifest_fixer_options.compile_sdk_version) - .OptionalFlag("--compile-sdk-version-name", - "Version name to inject into the AndroidManifest.xml if none is present.", - &options.manifest_fixer_options.compile_sdk_version_codename) - .OptionalSwitch("--shared-lib", "Generates a shared Android runtime library.", - &shared_lib) - .OptionalSwitch("--static-lib", "Generate a static Android library.", &static_lib) - .OptionalSwitch("--proto-format", - "Generates compiled resources in Protobuf format.\n" - "Suitable as input to the bundle tool for generating an App Bundle.", - &proto_format) - .OptionalSwitch("--no-static-lib-packages", - "Merge all library resources under the app's package.", - &options.no_static_lib_packages) - .OptionalSwitch("--non-final-ids", - "Generates R.java without the final modifier. This is implied when\n" - "--static-lib is specified.", - &options.generate_non_final_ids) - .OptionalFlag("--stable-ids", "File containing a list of name to ID mapping.", - &stable_id_file_path) - .OptionalFlag("--emit-ids", - "Emit a file at the given path with a list of name to ID mappings,\n" - "suitable for use with --stable-ids.", - &options.resource_id_map_path) - .OptionalFlag("--private-symbols", - "Package name to use when generating R.java for private symbols.\n" - "If not specified, public and private symbols will use the application's\n" - "package name.", - &options.private_symbols) - .OptionalFlag("--custom-package", "Custom Java package under which to generate R.java.", - &options.custom_java_package) - .OptionalFlagList("--extra-packages", - "Generate the same R.java but with different package names.", - &extra_java_packages) - .OptionalFlagList("--add-javadoc-annotation", - "Adds a JavaDoc annotation to all generated Java classes.", - &options.javadoc_annotations) - .OptionalFlag("--output-text-symbols", - "Generates a text file containing the resource symbols of the R class in\n" - "the specified folder.", - &options.generate_text_symbols_path) - .OptionalSwitch("--allow-reserved-package-id", - "Allows the use of a reserved package ID. This should on be used for\n" - "packages with a pre-O min-sdk\n", - &options.allow_reserved_package_id) - .OptionalSwitch("--auto-add-overlay", - "Allows the addition of new resources in overlays without\n" - "<add-resource> tags.", - &options.auto_add_overlay) - .OptionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml.", - &options.manifest_fixer_options.rename_manifest_package) - .OptionalFlag("--rename-instrumentation-target-package", - "Changes the name of the target package for instrumentation. Most useful\n" - "when used in conjunction with --rename-manifest-package.", - &options.manifest_fixer_options.rename_instrumentation_target_package) - .OptionalFlagList("-0", "File extensions not to compress.", - &options.extensions_to_not_compress) - .OptionalSwitch("--warn-manifest-validation", - "Treat manifest validation errors as warnings.", - &options.manifest_fixer_options.warn_validation) - .OptionalFlagList("--split", - "Split resources matching a set of configs out to a Split APK.\n" - "Syntax: path/to/output.apk:<config>[,<config>[...]].\n" - "On Windows, use a semicolon ';' separator instead.", - &split_args) - .OptionalSwitch("-v", "Enables verbose logging.", &verbose) - .OptionalSwitch("--debug-mode", - "Inserts android:debuggable=\"true\" in to the application node of the\n" - "manifest, making the application debuggable even on production devices.", - &options.manifest_fixer_options.debug_mode); - - if (!flags.Parse("aapt2 link", args, &std::cerr)) { - return 1; - } +int LinkCommand::Action(const std::vector<std::string>& args) { + LinkContext context(diag_); // Expand all argument-files passed into the command line. These start with '@'. std::vector<std::string> arg_list; - for (const std::string& arg : flags.GetArgs()) { + for (const std::string& arg : args) { if (util::StartsWith(arg, "@")) { const std::string path = arg.substr(1, arg.size() - 1); std::string error; @@ -2220,27 +2013,27 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { } // Expand all argument-files passed to -R. - for (const std::string& arg : overlay_arg_list) { + for (const std::string& arg : overlay_arg_list_) { if (util::StartsWith(arg, "@")) { const std::string path = arg.substr(1, arg.size() - 1); std::string error; - if (!file::AppendArgsFromFile(path, &options.overlay_files, &error)) { + if (!file::AppendArgsFromFile(path, &options_.overlay_files, &error)) { context.GetDiagnostics()->Error(DiagMessage(path) << error); return 1; } } else { - options.overlay_files.push_back(arg); + options_.overlay_files.push_back(arg); } } - if (verbose) { - context.SetVerbose(verbose); + if (verbose_) { + context.SetVerbose(verbose_); } - if (int{shared_lib} + int{static_lib} + int{proto_format} > 1) { + if (int{shared_lib_} + int{static_lib_} + int{proto_format_} > 1) { context.GetDiagnostics()->Error( DiagMessage() - << "only one of --shared-lib, --static-lib, or --proto_format can be defined"); + << "only one of --shared-lib, --static-lib, or --proto_format can be defined"); return 1; } @@ -2248,26 +2041,26 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { context.SetPackageType(PackageType::kApp); context.SetPackageId(kAppPackageId); - if (shared_lib) { + if (shared_lib_) { context.SetPackageType(PackageType::kSharedLib); context.SetPackageId(0x00); - } else if (static_lib) { + } else if (static_lib_) { context.SetPackageType(PackageType::kStaticLib); - options.output_format = OutputFormat::kProto; - } else if (proto_format) { - options.output_format = OutputFormat::kProto; + options_.output_format = OutputFormat::kProto; + } else if (proto_format_) { + options_.output_format = OutputFormat::kProto; } - if (package_id) { + if (package_id_) { if (context.GetPackageType() != PackageType::kApp) { context.GetDiagnostics()->Error( DiagMessage() << "can't specify --package-id when not building a regular app"); return 1; } - const Maybe<uint32_t> maybe_package_id_int = ResourceUtils::ParseInt(package_id.value()); + const Maybe<uint32_t> maybe_package_id_int = ResourceUtils::ParseInt(package_id_.value()); if (!maybe_package_id_int) { - context.GetDiagnostics()->Error(DiagMessage() << "package ID '" << package_id.value() + context.GetDiagnostics()->Error(DiagMessage() << "package ID '" << package_id_.value() << "' is not a valid integer"); return 1; } @@ -2275,7 +2068,7 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { const uint32_t package_id_int = maybe_package_id_int.value(); if (package_id_int > std::numeric_limits<uint8_t>::max() || package_id_int == kFrameworkPackageId - || (!options.allow_reserved_package_id && package_id_int < kAppPackageId)) { + || (!options_.allow_reserved_package_id && package_id_int < kAppPackageId)) { context.GetDiagnostics()->Error( DiagMessage() << StringPrintf( "invalid package ID 0x%02x. Must be in the range 0x7f-0xff.", package_id_int)); @@ -2285,71 +2078,71 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { } // Populate the set of extra packages for which to generate R.java. - for (std::string& extra_package : extra_java_packages) { + for (std::string& extra_package : extra_java_packages_) { // A given package can actually be a colon separated list of packages. for (StringPiece package : util::Split(extra_package, ':')) { - options.extra_java_packages.insert(package.to_string()); + options_.extra_java_packages.insert(package.to_string()); } } - if (product_list) { - for (StringPiece product : util::Tokenize(product_list.value(), ',')) { + if (product_list_) { + for (StringPiece product : util::Tokenize(product_list_.value(), ',')) { if (product != "" && product != "default") { - options.products.insert(product.to_string()); + options_.products.insert(product.to_string()); } } } std::unique_ptr<IConfigFilter> filter; - if (!configs.empty()) { - filter = ParseConfigFilterParameters(configs, context.GetDiagnostics()); + if (!configs_.empty()) { + filter = ParseConfigFilterParameters(configs_, context.GetDiagnostics()); if (filter == nullptr) { return 1; } - options.table_splitter_options.config_filter = filter.get(); + options_.table_splitter_options.config_filter = filter.get(); } - if (preferred_density) { + if (preferred_density_) { Maybe<uint16_t> density = - ParseTargetDensityParameter(preferred_density.value(), context.GetDiagnostics()); + ParseTargetDensityParameter(preferred_density_.value(), context.GetDiagnostics()); if (!density) { return 1; } - options.table_splitter_options.preferred_densities.push_back(density.value()); + options_.table_splitter_options.preferred_densities.push_back(density.value()); } // Parse the split parameters. - for (const std::string& split_arg : split_args) { - options.split_paths.push_back({}); - options.split_constraints.push_back({}); - if (!ParseSplitParameter(split_arg, context.GetDiagnostics(), &options.split_paths.back(), - &options.split_constraints.back())) { + for (const std::string& split_arg : split_args_) { + options_.split_paths.push_back({}); + options_.split_constraints.push_back({}); + if (!ParseSplitParameter(split_arg, context.GetDiagnostics(), &options_.split_paths.back(), + &options_.split_constraints.back())) { return 1; } } - if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path) { - if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path.value(), - &options.stable_id_map)) { + if (context.GetPackageType() != PackageType::kStaticLib && stable_id_file_path_) { + if (!LoadStableIdMap(context.GetDiagnostics(), stable_id_file_path_.value(), + &options_.stable_id_map)) { return 1; } } // Populate some default no-compress extensions that are already compressed. - options.extensions_to_not_compress.insert( + options_.extensions_to_not_compress.insert( {".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg", - ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", - ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", - ".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"}); + ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", + ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", + ".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"}); // Turn off auto versioning for static-libs. if (context.GetPackageType() == PackageType::kStaticLib) { - options.no_auto_version = true; - options.no_version_vectors = true; - options.no_version_transitions = true; + options_.no_auto_version = true; + options_.no_version_vectors = true; + options_.no_version_transitions = true; } - LinkCommand cmd(&context, options); + Linker cmd(&context, options_); return cmd.Run(arg_list); } diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h new file mode 100644 index 000000000000..e58a93ec416c --- /dev/null +++ b/tools/aapt2/cmd/Link.h @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_LINK_H +#define AAPT2_LINK_H + +#include "Command.h" +#include "Diagnostics.h" +#include "Resource.h" +#include "split/TableSplitter.h" +#include "format/binary/TableFlattener.h" +#include "link/ManifestFixer.h" + +namespace aapt { + +enum class OutputFormat { + kApk, + kProto, +}; + +struct LinkOptions { + std::string output_path; + std::string manifest_path; + std::vector<std::string> include_paths; + std::vector<std::string> overlay_files; + std::vector<std::string> assets_dirs; + bool output_to_directory = false; + bool auto_add_overlay = false; + OutputFormat output_format = OutputFormat::kApk; + + // Java/Proguard options. + Maybe<std::string> generate_java_class_path; + Maybe<std::string> custom_java_package; + std::set<std::string> extra_java_packages; + Maybe<std::string> generate_text_symbols_path; + Maybe<std::string> generate_proguard_rules_path; + Maybe<std::string> generate_main_dex_proguard_rules_path; + bool generate_conditional_proguard_rules = false; + bool generate_non_final_ids = false; + std::vector<std::string> javadoc_annotations; + Maybe<std::string> private_symbols; + + // Optimizations/features. + bool no_auto_version = false; + bool no_version_vectors = false; + bool no_version_transitions = false; + bool no_resource_deduping = false; + bool no_xml_namespaces = false; + bool do_not_compress_anything = false; + std::unordered_set<std::string> extensions_to_not_compress; + + // Static lib options. + bool no_static_lib_packages = false; + + // AndroidManifest.xml massaging options. + ManifestFixerOptions manifest_fixer_options; + + // Products to use/filter on. + std::unordered_set<std::string> products; + + // Flattening options. + TableFlattenerOptions table_flattener_options; + + // Split APK options. + TableSplitterOptions table_splitter_options; + std::vector<SplitConstraints> split_constraints; + std::vector<std::string> split_paths; + + // Stable ID options. + std::unordered_map<ResourceName, ResourceId> stable_id_map; + Maybe<std::string> resource_id_map_path; + + // When 'true', allow reserved package IDs to be used for applications. Pre-O, the platform + // treats negative resource IDs [those with a package ID of 0x80 or higher] as invalid. + // In order to work around this limitation, we allow the use of traditionally reserved + // resource IDs [those between 0x02 and 0x7E]. + bool allow_reserved_package_id = false; + + // Whether we should fail on definitions of a resource with conflicting visibility. + bool strict_visibility = false; +}; + +class LinkCommand : public Command { + public: + explicit LinkCommand(IDiagnostics* diag) : Command("link", "l"), + diag_(diag) { + SetDescription("Links resources into an apk."); + AddRequiredFlag("-o", "Output path.", &options_.output_path); + AddRequiredFlag("--manifest", "Path to the Android manifest to build.", + &options_.manifest_path); + AddOptionalFlagList("-I", "Adds an Android APK to link against.", &options_.include_paths); + AddOptionalFlagList("-A", "An assets directory to include in the APK. These are unprocessed.", + &options_.assets_dirs); + AddOptionalFlagList("-R", "Compilation unit to link, using `overlay` semantics.\n" + "The last conflicting resource given takes precedence.", &overlay_arg_list_); + AddOptionalFlag("--package-id", + "Specify the package ID to use for this app. Must be greater or equal to\n" + "0x7f and can't be used with --static-lib or --shared-lib.", &package_id_); + AddOptionalFlag("--java", "Directory in which to generate R.java.", + &options_.generate_java_class_path); + AddOptionalFlag("--proguard", "Output file for generated Proguard rules.", + &options_.generate_proguard_rules_path); + AddOptionalFlag("--proguard-main-dex", + "Output file for generated Proguard rules for the main dex.", + &options_.generate_main_dex_proguard_rules_path); + AddOptionalSwitch("--proguard-conditional-keep-rules", + "Generate conditional Proguard keep rules.", + &options_.generate_conditional_proguard_rules); + AddOptionalSwitch("--no-auto-version", "Disables automatic style and layout SDK versioning.", + &options_.no_auto_version); + AddOptionalSwitch("--no-version-vectors", + "Disables automatic versioning of vector drawables. Use this only\n" + "when building with vector drawable support library.", + &options_.no_version_vectors); + AddOptionalSwitch("--no-version-transitions", + "Disables automatic versioning of transition resources. Use this only\n" + "when building with transition support library.", + &options_.no_version_transitions); + AddOptionalSwitch("--no-resource-deduping", "Disables automatic deduping of resources with\n" + "identical values across compatible configurations.", + &options_.no_resource_deduping); + AddOptionalSwitch("--enable-sparse-encoding", + "This decreases APK size at the cost of resource retrieval performance.", + &options_.table_flattener_options.use_sparse_entries); + AddOptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.", + &legacy_x_flag_); + AddOptionalSwitch("-z", "Require localization of strings marked 'suggested'.", + &require_localization_); + AddOptionalFlagList("-c", + "Comma separated list of configurations to include. The default\n" + "is all configurations.", &configs_); + AddOptionalFlag("--preferred-density", + "Selects the closest matching density and strips out all others.", + &preferred_density_); + AddOptionalFlag("--product", "Comma separated list of product names to keep", &product_list_); + AddOptionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified by -o.", + &options_.output_to_directory); + AddOptionalSwitch("--no-xml-namespaces", "Removes XML namespace prefix and URI information\n" + "from AndroidManifest.xml and XML binaries in res/*.", + &options_.no_xml_namespaces); + AddOptionalFlag("--min-sdk-version", + "Default minimum SDK version to use for AndroidManifest.xml.", + &options_.manifest_fixer_options.min_sdk_version_default); + AddOptionalFlag("--target-sdk-version", + "Default target SDK version to use for AndroidManifest.xml.", + &options_.manifest_fixer_options.target_sdk_version_default); + AddOptionalFlag("--version-code", + "Version code (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", &options_.manifest_fixer_options.version_code_default); + AddOptionalFlag("--version-code-major", + "Version code major (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", &options_.manifest_fixer_options.version_code_major_default); + AddOptionalFlag("--version-name", + "Version name to inject into the AndroidManifest.xml if none is present.", + &options_.manifest_fixer_options.version_name_default); + AddOptionalSwitch("--replace-version", + "If --version-code and/or --version-name are specified, these\n" + "values will replace any value already in the manifest. By\n" + "default, nothing is changed if the manifest already defines\n" + "these attributes.", + &options_.manifest_fixer_options.replace_version); + AddOptionalFlag("--compile-sdk-version-code", + "Version code (integer) to inject into the AndroidManifest.xml if none is\n" + "present.", + &options_.manifest_fixer_options.compile_sdk_version); + AddOptionalFlag("--compile-sdk-version-name", + "Version name to inject into the AndroidManifest.xml if none is present.", + &options_.manifest_fixer_options.compile_sdk_version_codename); + AddOptionalSwitch("--shared-lib", "Generates a shared Android runtime library.", + &shared_lib_); + AddOptionalSwitch("--static-lib", "Generate a static Android library.", &static_lib_); + AddOptionalSwitch("--proto-format", + "Generates compiled resources in Protobuf format.\n" + "Suitable as input to the bundle tool for generating an App Bundle.", + &proto_format_); + AddOptionalSwitch("--no-static-lib-packages", + "Merge all library resources under the app's package.", + &options_.no_static_lib_packages); + AddOptionalSwitch("--non-final-ids", + "Generates R.java without the final modifier. This is implied when\n" + "--static-lib is specified.", + &options_.generate_non_final_ids); + AddOptionalFlag("--stable-ids", "File containing a list of name to ID mapping.", + &stable_id_file_path_); + AddOptionalFlag("--emit-ids", + "Emit a file at the given path with a list of name to ID mappings,\n" + "suitable for use with --stable-ids.", + &options_.resource_id_map_path); + AddOptionalFlag("--private-symbols", + "Package name to use when generating R.java for private symbols.\n" + "If not specified, public and private symbols will use the application's\n" + "package name.", + &options_.private_symbols); + AddOptionalFlag("--custom-package", "Custom Java package under which to generate R.java.", + &options_.custom_java_package); + AddOptionalFlagList("--extra-packages", + "Generate the same R.java but with different package names.", + &extra_java_packages_); + AddOptionalFlagList("--add-javadoc-annotation", + "Adds a JavaDoc annotation to all generated Java classes.", + &options_.javadoc_annotations); + AddOptionalFlag("--output-text-symbols", + "Generates a text file containing the resource symbols of the R class in\n" + "the specified folder.", + &options_.generate_text_symbols_path); + AddOptionalSwitch("--allow-reserved-package-id", + "Allows the use of a reserved package ID. This should on be used for\n" + "packages with a pre-O min-sdk\n", + &options_.allow_reserved_package_id); + AddOptionalSwitch("--auto-add-overlay", + "Allows the addition of new resources in overlays without\n" + "<add-resource> tags.", + &options_.auto_add_overlay); + AddOptionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml.", + &options_.manifest_fixer_options.rename_manifest_package); + AddOptionalFlag("--rename-instrumentation-target-package", + "Changes the name of the target package for instrumentation. Most useful\n" + "when used in conjunction with --rename-manifest-package.", + &options_.manifest_fixer_options.rename_instrumentation_target_package); + AddOptionalFlagList("-0", "File extensions not to compress.", + &options_.extensions_to_not_compress); + AddOptionalSwitch("--no-compress", "Do not compress any resources.", + &options_.do_not_compress_anything); + AddOptionalSwitch("--warn-manifest-validation", + "Treat manifest validation errors as warnings.", + &options_.manifest_fixer_options.warn_validation); + AddOptionalFlagList("--split", + "Split resources matching a set of configs out to a Split APK.\n" + "Syntax: path/to/output.apk:<config>[,<config>[...]].\n" + "On Windows, use a semicolon ';' separator instead.", + &split_args_); + AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); + AddOptionalSwitch("--debug-mode", + "Inserts android:debuggable=\"true\" in to the application node of the\n" + "manifest, making the application debuggable even on production devices.", + &options_.manifest_fixer_options.debug_mode); + AddOptionalSwitch("--strict-visibility", + "Do not allow overlays with different visibility levels.", + &options_.strict_visibility); + } + + int Action(const std::vector<std::string>& args) override; + + private: + IDiagnostics* diag_; + LinkOptions options_; + + std::vector<std::string> overlay_arg_list_; + std::vector<std::string> extra_java_packages_; + Maybe<std::string> package_id_; + std::vector<std::string> configs_; + Maybe<std::string> preferred_density_; + Maybe<std::string> product_list_; + bool legacy_x_flag_ = false; + bool require_localization_ = false; + bool verbose_ = false; + bool shared_lib_ = false; + bool static_lib_ = false; + bool proto_format_ = false; + Maybe<std::string> stable_id_file_path_; + std::vector<std::string> split_args_; +}; + +}// namespace aapt + +#endif //AAPT2_LINK_H
\ No newline at end of file diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 9c76119f9504..47288ec092de 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "Optimize.h" + #include <memory> #include <vector> @@ -24,7 +26,6 @@ #include "androidfw/StringPiece.h" #include "Diagnostics.h" -#include "Flags.h" #include "LoadedApk.h" #include "ResourceUtils.h" #include "SdkConstants.h" @@ -38,6 +39,7 @@ #include "io/Util.h" #include "optimize/MultiApkGenerator.h" #include "optimize/ResourceDeduper.h" +#include "optimize/ResourceFilter.h" #include "optimize/VersionCollapser.h" #include "split/TableSplitter.h" #include "util/Files.h" @@ -53,33 +55,6 @@ using ::android::base::StringPrintf; namespace aapt { -struct OptimizeOptions { - // Path to the output APK. - Maybe<std::string> output_path; - // Path to the output APK directory for splits. - Maybe<std::string> output_dir; - - // Details of the app extracted from the AndroidManifest.xml - AppInfo app_info; - - // Split APK options. - TableSplitterOptions table_splitter_options; - - // List of output split paths. These are in the same order as `split_constraints`. - std::vector<std::string> split_paths; - - // List of SplitConstraints governing what resources go into each split. Ordered by `split_paths`. - std::vector<SplitConstraints> split_constraints; - - TableFlattenerOptions table_flattener_options; - - Maybe<std::vector<OutputArtifact>> apk_artifacts; - - // Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts - // are kept and will be written as output. - std::unordered_set<std::string> kept_artifacts; -}; - class OptimizeContext : public IAaptContext { public: OptimizeContext() = default; @@ -137,9 +112,9 @@ class OptimizeContext : public IAaptContext { int sdk_version_ = 0; }; -class OptimizeCommand { +class Optimizer { public: - OptimizeCommand(OptimizeContext* context, const OptimizeOptions& options) + Optimizer(OptimizeContext* context, const OptimizeOptions& options) : options_(options), context_(context) { } @@ -147,6 +122,13 @@ class OptimizeCommand { if (context_->IsVerbose()) { context_->GetDiagnostics()->Note(DiagMessage() << "Optimizing APK..."); } + if (!options_.resources_blacklist.empty()) { + ResourceFilter filter(options_.resources_blacklist); + if (!filter.Consume(context_, apk->GetResourceTable())) { + context_->GetDiagnostics()->Error(DiagMessage() << "failed filtering resources"); + return 1; + } + } VersionCollapser collapser; if (!collapser.Consume(context_, apk->GetResourceTable())) { @@ -284,16 +266,62 @@ class OptimizeCommand { OptimizeContext* context_; }; -bool ExtractWhitelistFromConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { +bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { std::string contents; if (!ReadFileToString(path, &contents, true)) { context->GetDiagnostics()->Error(DiagMessage() << "failed to parse whitelist from config file: " << path); return false; } - for (const StringPiece& resource_name : util::Tokenize(contents, ',')) { - options->table_flattener_options.whitelisted_resources.insert(resource_name.to_string()); + for (StringPiece resource_name : util::Tokenize(contents, ',')) { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.to_string()); + } + return true; +} + +bool ExtractConfig(const std::string& path, OptimizeContext* context, + OptimizeOptions* options) { + std::string content; + if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { + context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist"); + return false; + } + + size_t line_no = 0; + for (StringPiece line : util::Tokenize(content, '\n')) { + line_no++; + line = util::TrimWhitespace(line); + if (line.empty()) { + continue; + } + + auto split_line = util::Split(line, '#'); + if (split_line.size() < 2) { + context->GetDiagnostics()->Error(DiagMessage(line) << "No # found in line"); + return false; + } + StringPiece resource_string = split_line[0]; + StringPiece directives = split_line[1]; + ResourceNameRef resource_name; + if (!ResourceUtils::ParseResourceName(resource_string, &resource_name)) { + context->GetDiagnostics()->Error(DiagMessage(line) << "Malformed resource name"); + return false; + } + if (!resource_name.package.empty()) { + context->GetDiagnostics()->Error(DiagMessage(line) + << "Package set for resource. Only use type/name"); + return false; + } + for (StringPiece directive : util::Tokenize(directives, ',')) { + if (directive == "remove") { + options->resources_blacklist.insert(resource_name.ToResourceName()); + } else if (directive == "no_obfuscate") { + options->table_flattener_options.whitelisted_resources.insert( + resource_name.entry.to_string()); + } + } } return true; } @@ -317,76 +345,24 @@ bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk, return true; } -int Optimize(const std::vector<StringPiece>& args) { - OptimizeContext context; - OptimizeOptions options; - Maybe<std::string> config_path; - Maybe<std::string> whitelist_path; - Maybe<std::string> target_densities; - std::vector<std::string> configs; - std::vector<std::string> split_args; - std::unordered_set<std::string> kept_artifacts; - bool verbose = false; - bool print_only = false; - Flags flags = - Flags() - .OptionalFlag("-o", "Path to the output APK.", &options.output_path) - .OptionalFlag("-d", "Path to the output directory (for splits).", &options.output_dir) - .OptionalFlag("-x", "Path to XML configuration file.", &config_path) - .OptionalSwitch("-p", "Print the multi APK artifacts and exit.", &print_only) - .OptionalFlag( - "--target-densities", - "Comma separated list of the screen densities that the APK will be optimized for.\n" - "All the resources that would be unused on devices of the given densities will be \n" - "removed from the APK.", - &target_densities) - .OptionalFlag("--whitelist-config-path", - "Path to the whitelist.cfg file containing whitelisted resources \n" - "whose names should not be altered in final resource tables.", - &whitelist_path) - .OptionalFlagList("-c", - "Comma separated list of configurations to include. The default\n" - "is all configurations.", - &configs) - .OptionalFlagList("--split", - "Split resources matching a set of configs out to a " - "Split APK.\nSyntax: path/to/output.apk;<config>[,<config>[...]].\n" - "On Windows, use a semicolon ';' separator instead.", - &split_args) - .OptionalFlagList("--keep-artifacts", - "Comma separated list of artifacts to keep. If none are specified,\n" - "all artifacts will be kept.", - &kept_artifacts) - .OptionalSwitch("--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.", - &options.table_flattener_options.use_sparse_entries) - .OptionalSwitch("--enable-resource-obfuscation", - "Enables obfuscation of key string pool to single value", - &options.table_flattener_options.collapse_key_stringpool) - .OptionalSwitch("-v", "Enables verbose logging", &verbose); - - if (!flags.Parse("aapt2 optimize", args, &std::cerr)) { - return 1; - } - - if (flags.GetArgs().size() != 1u) { +int OptimizeCommand::Action(const std::vector<std::string>& args) { + if (args.size() != 1u) { std::cerr << "must have one APK as argument.\n\n"; - flags.Usage("aapt2 optimize", &std::cerr); + Usage(&std::cerr); return 1; } - const std::string& apk_path = flags.GetArgs()[0]; - - context.SetVerbose(verbose); + const std::string& apk_path = args[0]; + OptimizeContext context; + context.SetVerbose(verbose_); IDiagnostics* diag = context.GetDiagnostics(); - if (config_path) { - std::string& path = config_path.value(); + if (config_path_) { + std::string& path = config_path_.value(); Maybe<ConfigurationParser> for_path = ConfigurationParser::ForPath(path); if (for_path) { - options.apk_artifacts = for_path.value().WithDiagnostics(diag).Parse(apk_path); - if (!options.apk_artifacts) { + options_.apk_artifacts = for_path.value().WithDiagnostics(diag).Parse(apk_path); + if (!options_.apk_artifacts) { diag->Error(DiagMessage() << "Failed to parse the output artifact list"); return 1; } @@ -396,28 +372,28 @@ int Optimize(const std::vector<StringPiece>& args) { return 1; } - if (print_only) { - for (const OutputArtifact& artifact : options.apk_artifacts.value()) { + if (print_only_) { + for (const OutputArtifact& artifact : options_.apk_artifacts.value()) { std::cout << artifact.name << std::endl; } return 0; } - if (!kept_artifacts.empty()) { - for (const std::string& artifact_str : kept_artifacts) { + if (!kept_artifacts_.empty()) { + for (const std::string& artifact_str : kept_artifacts_) { for (const StringPiece& artifact : util::Tokenize(artifact_str, ',')) { - options.kept_artifacts.insert(artifact.to_string()); + options_.kept_artifacts.insert(artifact.to_string()); } } } // Since we know that we are going to process the APK (not just print targets), make sure we // have somewhere to write them to. - if (!options.output_dir) { + if (!options_.output_dir) { diag->Error(DiagMessage() << "Output directory is required when using a configuration file"); return 1; } - } else if (print_only) { + } else if (print_only_) { diag->Error(DiagMessage() << "Asked to print artifacts without providing a configurations"); return 1; } @@ -427,50 +403,57 @@ int Optimize(const std::vector<StringPiece>& args) { return 1; } - if (target_densities) { + if (target_densities_) { // Parse the target screen densities. - for (const StringPiece& config_str : util::Tokenize(target_densities.value(), ',')) { + for (const StringPiece& config_str : util::Tokenize(target_densities_.value(), ',')) { Maybe<uint16_t> target_density = ParseTargetDensityParameter(config_str, diag); if (!target_density) { return 1; } - options.table_splitter_options.preferred_densities.push_back(target_density.value()); + options_.table_splitter_options.preferred_densities.push_back(target_density.value()); } } std::unique_ptr<IConfigFilter> filter; - if (!configs.empty()) { - filter = ParseConfigFilterParameters(configs, diag); + if (!configs_.empty()) { + filter = ParseConfigFilterParameters(configs_, diag); if (filter == nullptr) { return 1; } - options.table_splitter_options.config_filter = filter.get(); + options_.table_splitter_options.config_filter = filter.get(); } // Parse the split parameters. - for (const std::string& split_arg : split_args) { - options.split_paths.emplace_back(); - options.split_constraints.emplace_back(); - if (!ParseSplitParameter(split_arg, diag, &options.split_paths.back(), - &options.split_constraints.back())) { + for (const std::string& split_arg : split_args_) { + options_.split_paths.emplace_back(); + options_.split_constraints.emplace_back(); + if (!ParseSplitParameter(split_arg, diag, &options_.split_paths.back(), + &options_.split_constraints.back())) { return 1; } } - if (options.table_flattener_options.collapse_key_stringpool) { - if (whitelist_path) { - std::string& path = whitelist_path.value(); - if (!ExtractWhitelistFromConfig(path, &context, &options)) { + if (options_.table_flattener_options.collapse_key_stringpool) { + if (whitelist_path_) { + std::string& path = whitelist_path_.value(); + if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options_)) { return 1; } } } - if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) { + if (resources_config_path_) { + std::string& path = resources_config_path_.value(); + if (!ExtractConfig(path, &context, &options_)) { + return 1; + } + } + + if (!ExtractAppDataFromManifest(&context, apk.get(), &options_)) { return 1; } - OptimizeCommand cmd(&context, options); + Optimizer cmd(&context, options_); return cmd.Run(std::move(apk)); } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h new file mode 100644 index 000000000000..43bc216382fa --- /dev/null +++ b/tools/aapt2/cmd/Optimize.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_OPTIMIZE_H +#define AAPT2_OPTIMIZE_H + +#include "AppInfo.h" +#include "Command.h" +#include "configuration/ConfigurationParser.h" +#include "format/binary/TableFlattener.h" +#include "split/TableSplitter.h" + +namespace aapt { + +struct OptimizeOptions { + friend class OptimizeCommand; + + // Path to the output APK. + Maybe<std::string> output_path; + // Path to the output APK directory for splits. + Maybe<std::string> output_dir; + + // Details of the app extracted from the AndroidManifest.xml + AppInfo app_info; + + // Blacklist of unused resources that should be removed from the apk. + std::unordered_set<ResourceName> resources_blacklist; + + // Split APK options. + TableSplitterOptions table_splitter_options; + + // List of output split paths. These are in the same order as `split_constraints`. + std::vector<std::string> split_paths; + + // List of SplitConstraints governing what resources go into each split. Ordered by `split_paths`. + std::vector<SplitConstraints> split_constraints; + + TableFlattenerOptions table_flattener_options; + + Maybe<std::vector<aapt::configuration::OutputArtifact>> apk_artifacts; + + // Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts + // are kept and will be written as output. + std::unordered_set<std::string> kept_artifacts; +}; + +class OptimizeCommand : public Command { + public: + explicit OptimizeCommand() : Command("optimize") { + SetDescription("Preforms resource optimizations on an apk."); + AddOptionalFlag("-o", "Path to the output APK.", &options_.output_path); + AddOptionalFlag("-d", "Path to the output directory (for splits).", &options_.output_dir); + AddOptionalFlag("-x", "Path to XML configuration file.", &config_path_); + AddOptionalSwitch("-p", "Print the multi APK artifacts and exit.", &print_only_); + AddOptionalFlag( + "--target-densities", + "Comma separated list of the screen densities that the APK will be optimized for.\n" + "All the resources that would be unused on devices of the given densities will be \n" + "removed from the APK.", + &target_densities_); + AddOptionalFlag("--whitelist-path", + "Path to the whitelist.cfg file containing whitelisted resources \n" + "whose names should not be altered in final resource tables.", + &whitelist_path_); + AddOptionalFlag("--resources-config-path", + "Path to the resources.cfg file containing the list of resources and \n" + "directives to each resource. \n" + "Format: type/resource_name#[directive][,directive]", + &resources_config_path_); + AddOptionalFlagList("-c", + "Comma separated list of configurations to include. The default\n" + "is all configurations.", + &configs_); + AddOptionalFlagList("--split", + "Split resources matching a set of configs out to a " + "Split APK.\nSyntax: path/to/output.apk;<config>[,<config>[...]].\n" + "On Windows, use a semicolon ';' separator instead.", + &split_args_); + AddOptionalFlagList("--keep-artifacts", + "Comma separated list of artifacts to keep. If none are specified,\n" + "all artifacts will be kept.", + &kept_artifacts_); + AddOptionalSwitch("--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.", + &options_.table_flattener_options.use_sparse_entries); + AddOptionalSwitch("--enable-resource-obfuscation", + "Enables obfuscation of key string pool to single value", + &options_.table_flattener_options.collapse_key_stringpool); + AddOptionalSwitch("-v", "Enables verbose logging", &verbose_); + } + + int Action(const std::vector<std::string>& args) override; + + private: + OptimizeOptions options_; + + Maybe<std::string> config_path_; + Maybe<std::string> whitelist_path_; + Maybe<std::string> resources_config_path_; + Maybe<std::string> target_densities_; + std::vector<std::string> configs_; + std::vector<std::string> split_args_; + std::unordered_set<std::string> kept_artifacts_; + bool print_only_ = false; + bool verbose_ = false; +}; + +}// namespace aapt + +#endif //AAPT2_OPTIMIZE_H
\ No newline at end of file diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index 8b3a6701b409..c6c82b04ff2e 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -29,6 +29,7 @@ #include "util/Util.h" using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { @@ -145,7 +146,7 @@ static xml::NamespaceDecl CreateAndroidNamespaceDecl() { // // See frameworks/base/core/java/android/content/pm/PackageParser.java which // checks this at runtime. -static std::string MakePackageSafeName(const std::string &name) { +std::string MakePackageSafeName(const std::string &name) { std::string result(name); bool first = true; for (char &c : result) { @@ -168,6 +169,7 @@ static std::string MakePackageSafeName(const std::string &name) { std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, const SplitConstraints& constraints) { const ResourceId kVersionCode(0x0101021b); + const ResourceId kVersionCodeMajor(0x01010576); const ResourceId kRevisionCode(0x010104d5); const ResourceId kHasCode(0x0101000c); @@ -184,6 +186,14 @@ std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, util::make_unique<BinaryPrimitive>(android::Res_value::TYPE_INT_DEC, version_code)}); } + if (app_info.version_code_major) { + const uint32_t version_code_major = app_info.version_code_major.value(); + manifest_el->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, "versionCodeMajor", std::to_string(version_code_major), + CreateAttributeWithId(kVersionCodeMajor), + util::make_unique<BinaryPrimitive>(android::Res_value::TYPE_INT_DEC, version_code_major)}); + } + if (app_info.revision_code) { const uint32_t revision_code = app_info.revision_code.value(); manifest_el->attributes.push_back(xml::Attribute{ @@ -355,6 +365,17 @@ Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, app_info.version_code = maybe_code.value(); } + if (const xml::Attribute* version_code_major_attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor")) { + Maybe<uint32_t> maybe_code = ExtractCompiledInt(*version_code_major_attr, &error_msg); + if (!maybe_code) { + diag->Error(DiagMessage(xml_res.file.source.WithLine(manifest_el->line_number)) + << "invalid android:versionCodeMajor: " << error_msg); + return {}; + } + app_info.version_code_major = maybe_code.value(); + } + if (const xml::Attribute* revision_code_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "revisionCode")) { Maybe<uint32_t> maybe_code = ExtractCompiledInt(*revision_code_attr, &error_msg); @@ -391,4 +412,21 @@ Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, return app_info; } +void SetLongVersionCode(xml::Element* manifest, uint64_t version) { + // Write the low bits of the version code to android:versionCode + auto version_code = manifest->FindOrCreateAttribute(xml::kSchemaAndroid, "versionCode"); + version_code->value = StringPrintf("0x%08x", (uint32_t) (version & 0xffffffff)); + version_code->compiled_value = ResourceUtils::TryParseInt(version_code->value); + + auto version_high = (uint32_t) (version >> 32); + if (version_high != 0) { + // Write the high bits of the version code to android:versionCodeMajor + auto version_major = manifest->FindOrCreateAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + version_major->value = StringPrintf("0x%08x", version_high); + version_major->compiled_value = ResourceUtils::TryParseInt(version_major->value); + } else { + manifest->RemoveAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + } +} + } // namespace aapt diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h index 7611c1526104..cf1443e30e1f 100644 --- a/tools/aapt2/cmd/Util.h +++ b/tools/aapt2/cmd/Util.h @@ -60,6 +60,18 @@ std::unique_ptr<xml::XmlResource> GenerateSplitManifest(const AppInfo& app_info, Maybe<AppInfo> ExtractAppInfoFromBinaryManifest(const xml::XmlResource& xml_res, IDiagnostics* diag); +// Returns a copy of 'name' which conforms to the regex '[a-zA-Z]+[a-zA-Z0-9_]*' by +// replacing nonconforming characters with underscores. +// +// See frameworks/base/core/java/android/content/pm/PackageParser.java which +// checks this at runtime. +std::string MakePackageSafeName(const std::string &name); + +// Sets the versionCode and versionCodeMajor attributes to the version code. Attempts to encode the +// version code using the versionCode attribute only, and encodes using both versionCode and +// versionCodeMajor if the version code requires more than 32 bits. +void SetLongVersionCode(xml::Element* manifest, uint64_t version_code); + } // namespace aapt #endif /* AAPT_SPLIT_UTIL_H */ diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 0c527f6a90dc..b9fb5b2868a7 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -18,6 +18,7 @@ #include "AppInfo.h" #include "split/TableSplitter.h" +#include "test/Builders.h" #include "test/Test.h" namespace aapt { @@ -36,4 +37,51 @@ TEST(UtilTest, SplitNamesAreSanitized) { EXPECT_EQ(root->FindAttribute("", "targetConfig")->value, "b+sr+Latn,en-rUS-land"); } +TEST (UtilTest, LongVersionCodeDefined) { + auto doc = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.aapt.test" android:versionCode="0x1" android:versionCodeMajor="0x1"> + </manifest>)"); + SetLongVersionCode(doc->root.get(), 42); + + auto version_code = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_NE(version_code, nullptr); + EXPECT_EQ(version_code->value, "0x0000002a"); + + ASSERT_NE(version_code->compiled_value, nullptr); + auto compiled_version_code = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + ASSERT_NE(compiled_version_code, nullptr); + EXPECT_EQ(compiled_version_code->value.data, 42U); + + auto version_code_major = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + EXPECT_EQ(version_code_major, nullptr); +} + +TEST (UtilTest, LongVersionCodeUndefined) { + auto doc = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.aapt.test"> + </manifest>)"); + SetLongVersionCode(doc->root.get(), 420000000000); + + auto version_code = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_NE(version_code, nullptr); + EXPECT_EQ(version_code->value, "0xc9f36800"); + + ASSERT_NE(version_code->compiled_value, nullptr); + auto compiled_version_code = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + ASSERT_NE(compiled_version_code, nullptr); + EXPECT_EQ(compiled_version_code->value.data, 0xc9f36800); + + auto version_code_major = doc->root->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_NE(version_code_major, nullptr); + EXPECT_EQ(version_code_major->value, "0x00000061"); + + ASSERT_NE(version_code_major->compiled_value, nullptr); + auto compiled_version_code_major = ValueCast<BinaryPrimitive>( + version_code_major->compiled_value.get()); + ASSERT_NE(compiled_version_code_major, nullptr); + EXPECT_EQ(compiled_version_code_major->value.data, 0x61); +} + } // namespace aapt diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp index febbb2ed11a0..ccaea4e2a771 100644 --- a/tools/aapt2/configuration/ConfigurationParser_test.cpp +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -703,35 +703,24 @@ TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_InvalidVersion) { } TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_NonNumeric) { - static constexpr const char* xml = R"xml( + auto doc = test::BuildXmlDom(R"xml( <android-sdk - label="P" + label="Q" minSdkVersion="25" - targetSdkVersion="%s" - maxSdkVersion="%s"> - </android-sdk>)xml"; - - const auto& dev_sdk = GetDevelopmentSdkCodeNameAndVersion(); - const char* codename = dev_sdk.first.data(); - const ApiVersion& version = dev_sdk.second; - - auto doc = test::BuildXmlDom(StringPrintf(xml, codename, codename)); + targetSdkVersion="Q" + maxSdkVersion="Q"> + </android-sdk>)xml"); PostProcessingConfiguration config; - bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); - ASSERT_TRUE(ok); - + ASSERT_TRUE(AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_)); ASSERT_EQ(1ul, config.android_sdks.size()); - ASSERT_EQ(1u, config.android_sdks.count("P")); - - auto& out = config.android_sdks["P"]; + ASSERT_EQ(1u, config.android_sdks.count("Q")); AndroidSdk sdk; sdk.min_sdk_version = 25; - sdk.target_sdk_version = version; - sdk.max_sdk_version = version; - - ASSERT_EQ(sdk, out); + sdk.target_sdk_version = 10000; + sdk.max_sdk_version = 10000; + ASSERT_EQ(sdk, config.android_sdks["Q"]); } TEST_F(ConfigurationParserTest, GlTextureGroupAction) { diff --git a/tools/aapt2/format/Container.cpp b/tools/aapt2/format/Container.cpp index 739555c5b15d..d4b45717e015 100644 --- a/tools/aapt2/format/Container.cpp +++ b/tools/aapt2/format/Container.cpp @@ -270,7 +270,8 @@ ContainerReader::ContainerReader(io::InputStream* in) } if (magic != kContainerFormatMagic) { - error_ = "magic value doesn't match AAPT"; + error_ = + StringPrintf("magic value is 0x%08x but AAPT expects 0x%08x", magic, kContainerFormatMagic); return; } diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index 8215ddf0461c..3a39a6baeeeb 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -338,10 +338,13 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, const std::string type_str = util::GetString(type_pool_, type->id - 1); - const ResourceType* parsed_type = ParseResourceType(type_str); - if (!parsed_type) { - diag_->Error(DiagMessage(source_) - << "invalid type name '" << type_str << "' for type with ID " << (int)type->id); + // Be lenient on the name of the type if the table is lenient on resource validation. + auto parsed_type = ResourceType::kUnknown; + if (const ResourceType* parsed = ParseResourceType(type_str)) { + parsed_type = *parsed; + } else if (table_->GetValidateResources()) { + diag_->Error(DiagMessage(source_) << "invalid type name '" << type_str << "' for type with ID " + << (int) type->id); return false; } @@ -352,7 +355,7 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, continue; } - const ResourceName name(package->name, *parsed_type, + const ResourceName name(package->name, parsed_type, util::GetString(key_pool_, util::DeviceToHost32(entry->key.index))); const ResourceId res_id(package->id.value(), type->id, static_cast<uint16_t>(it.index())); diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 6fd4c8d2c135..8641a7c93d19 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -133,11 +133,10 @@ class MapFlattenVisitor : public ValueVisitor { } void Visit(Array* array) override { - for (auto& item : array->elements) { - ResTable_map* out_entry = buffer_->NextBlock<ResTable_map>(); - FlattenValue(item.get(), out_entry); - out_entry->value.size = util::HostToDevice16(sizeof(out_entry->value)); - entry_count_++; + const size_t count = array->elements.size(); + for (size_t i = 0; i < count; i++) { + Reference key(android::ResTable_map::ATTR_MIN + i); + FlattenEntry(&key, array->elements[i].get()); } } diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index bab70109fc65..af19b98e528b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -17,7 +17,9 @@ #include "format/binary/TableFlattener.h" #include "android-base/stringprintf.h" +#include "androidfw/TypeWrappers.h" +#include "ResChunkPullParser.h" #include "ResourceUtils.h" #include "SdkConstants.h" #include "format/binary/BinaryResourceParser.h" @@ -236,6 +238,62 @@ TEST_F(TableFlattenerTest, FlattenMinMaxAttributes) { EXPECT_EQ(attr.max_int, actual_attr->max_int); } +TEST_F(TableFlattenerTest, FlattenArray) { + auto array = util::make_unique<Array>(); + array->elements.push_back(util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), + 1u)); + array->elements.push_back(util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), + 2u)); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddValue("android:array/foo", ResourceId(0x01010000), std::move(array)) + .Build(); + + std::string result; + ASSERT_TRUE(Flatten(context_.get(), {}, table.get(), &result)); + + // Parse the flattened resource table + ResChunkPullParser parser(result.data(), result.size()); + ASSERT_TRUE(parser.IsGoodEvent(parser.Next())); + ASSERT_EQ(util::DeviceToHost16(parser.chunk()->type), RES_TABLE_TYPE); + + // Retrieve the package of the entry + ResChunkPullParser table_parser(GetChunkData(parser.chunk()), GetChunkDataLen(parser.chunk())); + const ResChunk_header* package_chunk = nullptr; + while (table_parser.IsGoodEvent(table_parser.Next())) { + if (util::DeviceToHost16(table_parser.chunk()->type) == RES_TABLE_PACKAGE_TYPE) { + package_chunk = table_parser.chunk(); + break; + } + } + + // Retrieve the type that proceeds the array entry + ASSERT_NE(package_chunk, nullptr); + ResChunkPullParser package_parser(GetChunkData(table_parser.chunk()), + GetChunkDataLen(table_parser.chunk())); + const ResChunk_header* type_chunk = nullptr; + while (package_parser.IsGoodEvent(package_parser.Next())) { + if (util::DeviceToHost16(package_parser.chunk()->type) == RES_TABLE_TYPE_TYPE) { + type_chunk = package_parser.chunk(); + break; + } + } + + // Retrieve the array entry + ASSERT_NE(type_chunk, nullptr); + TypeVariant typeVariant((const ResTable_type*) type_chunk); + auto entry = (const ResTable_map_entry*)*typeVariant.beginEntries(); + ASSERT_EQ(util::DeviceToHost16(entry->count), 2u); + + // Check that the value and name of the array entries are correct + auto values = (const ResTable_map*)(((const uint8_t *)entry) + entry->size); + ASSERT_EQ(values->value.data, 1u); + ASSERT_EQ(values->name.ident, android::ResTable_map::ATTR_MIN); + ASSERT_EQ((values+1)->value.data, 2u); + ASSERT_EQ((values+1)->name.ident, android::ResTable_map::ATTR_MIN + 1); +} + static std::unique_ptr<ResourceTable> BuildTableWithSparseEntries( IAaptContext* context, const ConfigDescription& sparse_config, float load) { std::unique_ptr<ResourceTable> table = diff --git a/tools/aapt2/integration-tests/CompileTest/DirInput/res/drawable/image.png b/tools/aapt2/integration-tests/CompileTest/DirInput/res/drawable/image.png Binary files differnew file mode 100644 index 000000000000..1a3731bbc8b8 --- /dev/null +++ b/tools/aapt2/integration-tests/CompileTest/DirInput/res/drawable/image.png diff --git a/tools/aapt2/integration-tests/CompileTest/DirInput/res/layout/layout.xml b/tools/aapt2/integration-tests/CompileTest/DirInput/res/layout/layout.xml new file mode 100644 index 000000000000..e5835ed1a169 --- /dev/null +++ b/tools/aapt2/integration-tests/CompileTest/DirInput/res/layout/layout.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<View xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" /> diff --git a/tools/aapt2/integration-tests/CompileTest/DirInput/res/values/values.xml b/tools/aapt2/integration-tests/CompileTest/DirInput/res/values/values.xml new file mode 100644 index 000000000000..62ab6526ef7e --- /dev/null +++ b/tools/aapt2/integration-tests/CompileTest/DirInput/res/values/values.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/CompileTest/ZipInput/res.zip b/tools/aapt2/integration-tests/CompileTest/ZipInput/res.zip Binary files differnew file mode 100644 index 000000000000..00e396d812c7 --- /dev/null +++ b/tools/aapt2/integration-tests/CompileTest/ZipInput/res.zip diff --git a/tools/aapt2/io/FileSystem.cpp b/tools/aapt2/io/FileSystem.cpp index 1387d2218ed4..16a20f4cb09d 100644 --- a/tools/aapt2/io/FileSystem.cpp +++ b/tools/aapt2/io/FileSystem.cpp @@ -16,6 +16,9 @@ #include "io/FileSystem.h" +#include <dirent.h> + +#include "android-base/errors.h" #include "androidfw/StringPiece.h" #include "utils/FileMap.h" @@ -26,6 +29,7 @@ #include "util/Util.h" using ::android::StringPiece; +using ::android::base::SystemErrorCodeToString; namespace aapt { namespace io { @@ -64,6 +68,50 @@ IFile* FileCollectionIterator::Next() { return result; } +std::unique_ptr<FileCollection> FileCollection::Create(const android::StringPiece& root, + std::string* outError) { + std::unique_ptr<FileCollection> collection = + std::unique_ptr<FileCollection>(new FileCollection()); + + std::unique_ptr<DIR, decltype(closedir) *> d(opendir(root.data()), closedir); + if (!d) { + *outError = "failed to open directory: " + SystemErrorCodeToString(errno); + return nullptr; + } + + while (struct dirent *entry = readdir(d.get())) { + std::string prefix_path = root.to_string(); + file::AppendPath(&prefix_path, entry->d_name); + + // The directory to iterate over looking for files + if (file::GetFileType(prefix_path) != file::FileType::kDirectory + || file::IsHidden(prefix_path)) { + continue; + } + + std::unique_ptr<DIR, decltype(closedir)*> subdir(opendir(prefix_path.data()), closedir); + if (!subdir) { + *outError = "failed to open directory: " + SystemErrorCodeToString(errno); + return nullptr; + } + + while (struct dirent* leaf_entry = readdir(subdir.get())) { + std::string full_path = prefix_path; + file::AppendPath(&full_path, leaf_entry->d_name); + + // Do not add folders to the file collection + if (file::GetFileType(full_path) == file::FileType::kDirectory + || file::IsHidden(full_path)) { + continue; + } + + collection->InsertFile(full_path); + } + } + + return collection; +} + IFile* FileCollection::InsertFile(const StringPiece& path) { return (files_[path.to_string()] = util::make_unique<RegularFile>(Source(path))).get(); } diff --git a/tools/aapt2/io/FileSystem.h b/tools/aapt2/io/FileSystem.h index 6be8807735f1..fb6bf6eeabbc 100644 --- a/tools/aapt2/io/FileSystem.h +++ b/tools/aapt2/io/FileSystem.h @@ -59,6 +59,10 @@ class FileCollection : public IFileCollection { public: FileCollection() = default; + /** Creates a file collection containing all files contained in the specified root directory. */ + static std::unique_ptr<FileCollection> Create(const android::StringPiece& path, + std::string* outError); + // Adds a file located at path. Returns the IFile representation of that file. IFile* InsertFile(const android::StringPiece& path); IFile* FindFile(const android::StringPiece& path) override; diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp index 269b6c5a12e1..8e6d7137640a 100644 --- a/tools/aapt2/io/ZipArchive.cpp +++ b/tools/aapt2/io/ZipArchive.cpp @@ -20,6 +20,7 @@ #include "ziparchive/zip_archive.h" #include "Source.h" +#include "util/Files.h" #include "util/Util.h" using ::android::StringPiece; @@ -121,9 +122,14 @@ std::unique_ptr<ZipFileCollection> ZipFileCollection::Create( std::string zip_entry_path = std::string(reinterpret_cast<const char*>(zip_entry_name.name), zip_entry_name.name_length); - std::string nested_path = path.to_string() + "@" + zip_entry_path; - std::unique_ptr<IFile> file = - util::make_unique<ZipFile>(collection->handle_, zip_data, Source(nested_path)); + + // Do not add folders to the file collection + if (util::EndsWith(zip_entry_path, "/")) { + continue; + } + + std::unique_ptr<IFile> file = util::make_unique<ZipFile>(collection->handle_, zip_data, + Source(zip_entry_path, path.to_string())); collection->files_by_name_[zip_entry_path] = file.get(); collection->files_.push_back(std::move(file)); } @@ -132,6 +138,7 @@ std::unique_ptr<ZipFileCollection> ZipFileCollection::Create( if (out_error) *out_error = ErrorCodeString(result); return {}; } + return collection; } diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 6b07b1e96261..d1a70a75a44e 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -256,9 +256,20 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res styleable_attr.field_name = TransformNestedAttr(attr.name.value(), array_field_name, package_name_to_generate); + Reference ref = attr; + if (attr.name.value().package.empty()) { + + // If the resource does not have a package name, set the package to the unmangled package name + // of the styleable declaration because attributes without package names would have been + // declared in the same package as the styleable. + ref.name = ResourceName(package_name_to_generate, ref.name.value().type, + ref.name.value().entry); + } + // Look up the symbol so that we can write out in the comments what are possible legal values // for this attribute. - const SymbolTable::Symbol* symbol = context_->GetExternalSymbols()->FindByReference(attr); + const SymbolTable::Symbol* symbol = context_->GetExternalSymbols()->FindByReference(ref); + if (symbol && symbol->attribute) { // Copy the symbol data structure because the returned instance can be destroyed. styleable_attr.symbol = *symbol; @@ -303,7 +314,7 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res const ResourceName& attr_name = entry.attr_ref->name.value(); styleable_comment << "<tr><td><code>{@link #" << entry.field_name << " " << (!attr_name.package.empty() ? attr_name.package - : context_->GetCompilationPackage()) + : package_name_to_generate) << ":" << attr_name.entry << "}</code></td>"; // Only use the comment up until the first '.'. This is to stay compatible with @@ -347,7 +358,9 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res } // Add the Styleable array to the Styleable class. - out_class_def->AddMember(std::move(array_def)); + if (out_class_def != nullptr) { + out_class_def->AddMember(std::move(array_def)); + } // Now we emit the indices into the array. for (size_t i = 0; i < attr_count; i++) { @@ -372,7 +385,7 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res StringPiece package_name = attr_name.package; if (package_name.empty()) { - package_name = context_->GetCompilationPackage(); + package_name = package_name_to_generate; } std::unique_ptr<IntMember> index_member = @@ -578,7 +591,6 @@ bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate, if (out_r_txt != nullptr) { r_txt_printer = util::make_unique<Printer>(out_r_txt); } - // Generate an onResourcesLoaded() callback if requested. if (out != nullptr && options_.rewrite_callback_options) { rewrite_method = diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index e449546f9399..fa208be120ed 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -107,6 +107,55 @@ TEST(JavaClassGeneratorTest, CorrectPackageNameIsUsed) { EXPECT_THAT(output, Not(HasSubstr("com_foo$two"))); } +TEST(JavaClassGeneratorTest, StyleableAttributesWithDifferentPackageName) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .SetPackageId("app", 0x7f) + .AddValue("app:attr/foo", ResourceId(0x7f010000), + test::AttributeBuilder().Build()) + .AddValue("app:attr/bar", ResourceId(0x7f010001), + test::AttributeBuilder().Build()) + .AddValue("android:attr/baz", ResourceId(0x01010000), + test::AttributeBuilder().Build()) + .AddValue("app:styleable/MyStyleable", ResourceId(0x7f030000), + test::StyleableBuilder() + .AddItem("app:attr/foo", ResourceId(0x7f010000)) + .AddItem("attr/bar", ResourceId(0x7f010001)) + .AddItem("android:attr/baz", ResourceId(0x01010000)) + .Build()) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(table.get())) + .SetNameManglerPolicy(NameManglerPolicy{"custom"}) + .SetCompilationPackage("custom") + .Build(); + JavaClassGenerator generator(context.get(), table.get(), {}); + + std::string output; + StringOutputStream out(&output); + EXPECT_TRUE(generator.Generate("app", &out)); + out.Flush(); + + EXPECT_THAT(output, Not(HasSubstr("public static final int baz=0x01010000;"))); + EXPECT_THAT(output, HasSubstr("public static final int foo=0x7f010000;")); + EXPECT_THAT(output, HasSubstr("public static final int bar=0x7f010001;")); + + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_android_baz=0;")); + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_foo=1;")); + EXPECT_THAT(output, HasSubstr("public static final int MyStyleable_bar=2;")); + + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_android_baz android:baz")); + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_foo app:foo")); + EXPECT_THAT(output, HasSubstr("@link #MyStyleable_bar app:bar")); + + EXPECT_THAT(output, HasSubstr("@link android.R.attr#baz")); + EXPECT_THAT(output, HasSubstr("@link app.R.attr#foo")); + EXPECT_THAT(output, HasSubstr("@link app.R.attr#bar")); +} + TEST(JavaClassGeneratorTest, AttrPrivateIsWrittenAsAttr) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() @@ -438,4 +487,22 @@ TEST(JavaClassGeneratorTest, GenerateOnResourcesLoadedCallbackForSharedLibrary) EXPECT_THAT(output, HasSubstr("com.boo.R.onResourcesLoaded")); } +TEST(JavaClassGeneratorTest, OnlyGenerateRText) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddValue("android:attr/foo", ResourceId(0x01010000), util::make_unique<Attribute>()) + .AddValue("android:styleable/hey.dude", ResourceId(0x01020000), + test::StyleableBuilder() + .AddItem("android:attr/foo", ResourceId(0x01010000)) + .Build()) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder().SetPackageId(0x01).SetCompilationPackage("android").Build(); + JavaClassGenerator generator(context.get(), table.get(), {}); + + ASSERT_TRUE(generator.Generate("android", nullptr)); +} + } // namespace aapt diff --git a/tools/aapt2/java/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp index ffcef8966654..d40795accf79 100644 --- a/tools/aapt2/java/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -39,7 +39,11 @@ class BaseVisitor : public xml::Visitor { public: using xml::Visitor::Visit; - BaseVisitor(const ResourceFile& file, KeepSet* keep_set) : file_(file), keep_set_(keep_set) { + BaseVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set, "...") { + } + + BaseVisitor(const ResourceFile& file, KeepSet* keep_set, const std::string& ctor_signature) + : file_(file), keep_set_(keep_set), ctor_signature_(ctor_signature) { } void Visit(xml::Element* node) override { @@ -50,11 +54,11 @@ class BaseVisitor : public xml::Visitor { // This is a custom view, let's figure out the class name from this. std::string package = maybe_package.value().package + "." + node->name; if (util::IsJavaClassName(package)) { - AddClass(node->line_number, package); + AddClass(node->line_number, package, ctor_signature_); } } } else if (util::IsJavaClassName(node->name)) { - AddClass(node->line_number, node->name); + AddClass(node->line_number, node->name, ctor_signature_); } for (const auto& child : node->children) { @@ -74,13 +78,18 @@ class BaseVisitor : public xml::Visitor { protected: ResourceFile file_; KeepSet* keep_set_; + std::string ctor_signature_; - virtual void AddClass(size_t line_number, const std::string& class_name) { - keep_set_->AddConditionalClass({file_.name, file_.source.WithLine(line_number)}, class_name); + virtual void AddClass(size_t line_number, const std::string& class_name, + const std::string& ctor_signature) { + keep_set_->AddConditionalClass({file_.name, file_.source.WithLine(line_number)}, + {class_name, ctor_signature}); } - void AddMethod(size_t line_number, const std::string& method_name) { - keep_set_->AddMethod({file_.name, file_.source.WithLine(line_number)}, method_name); + void AddMethod(size_t line_number, const std::string& method_name, + const std::string& method_signature) { + keep_set_->AddMethod({file_.name, file_.source.WithLine(line_number)}, + {method_name, method_signature}); } void AddReference(size_t line_number, Reference* ref) { @@ -100,32 +109,39 @@ class BaseVisitor : public xml::Visitor { class LayoutVisitor : public BaseVisitor { public: - LayoutVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set) { + LayoutVisitor(const ResourceFile& file, KeepSet* keep_set) + : BaseVisitor(file, keep_set, "android.content.Context, android.util.AttributeSet") { } void Visit(xml::Element* node) override { - bool check_class = false; - bool check_name = false; + bool is_view = false; + bool is_fragment = false; if (node->namespace_uri.empty()) { if (node->name == "view") { - check_class = true; + is_view = true; } else if (node->name == "fragment") { - check_class = check_name = true; + is_fragment = true; } } else if (node->namespace_uri == xml::kSchemaAndroid) { - check_name = node->name == "fragment"; + is_fragment = node->name == "fragment"; } for (const auto& attr : node->attributes) { - if (check_class && attr.namespace_uri.empty() && attr.name == "class" && - util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); - } else if (check_name && attr.namespace_uri == xml::kSchemaAndroid && - attr.name == "name" && util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); - } else if (attr.namespace_uri == xml::kSchemaAndroid && - attr.name == "onClick") { - AddMethod(node->line_number, attr.value); + if (attr.namespace_uri.empty() && attr.name == "class") { + if (util::IsJavaClassName(attr.value)) { + if (is_view) { + AddClass(node->line_number, attr.value, + "android.content.Context, android.util.AttributeSet"); + } else if (is_fragment) { + AddClass(node->line_number, attr.value, ""); + } + } + } else if (attr.namespace_uri == xml::kSchemaAndroid && attr.name == "name") { + if (is_fragment && util::IsJavaClassName(attr.value)) { + AddClass(node->line_number, attr.value, ""); + } + } else if (attr.namespace_uri == xml::kSchemaAndroid && attr.name == "onClick") { + AddMethod(node->line_number, attr.value, "android.view.View"); } } @@ -147,9 +163,9 @@ class MenuVisitor : public BaseVisitor { if (attr.namespace_uri == xml::kSchemaAndroid) { if ((attr.name == "actionViewClass" || attr.name == "actionProviderClass") && util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value); + AddClass(node->line_number, attr.value, "android.content.Context"); } else if (attr.name == "onClick") { - AddMethod(node->line_number, attr.value); + AddMethod(node->line_number, attr.value, "android.view.MenuItem"); } } } @@ -178,7 +194,7 @@ class XmlResourceVisitor : public BaseVisitor { xml::Attribute* attr = node->FindAttribute(xml::kSchemaAndroid, "fragment"); if (attr && util::IsJavaClassName(attr->value)) { - AddClass(node->line_number, attr->value); + AddClass(node->line_number, attr->value, ""); } } @@ -189,6 +205,29 @@ class XmlResourceVisitor : public BaseVisitor { DISALLOW_COPY_AND_ASSIGN(XmlResourceVisitor); }; +class NavigationVisitor : public BaseVisitor { + public: + NavigationVisitor(const ResourceFile& file, KeepSet* keep_set, const std::string& package) + : BaseVisitor(file, keep_set), package_(package) { + } + + void Visit(xml::Element* node) override { + const auto& attr = node->FindAttribute(xml::kSchemaAndroid, "name"); + if (attr != nullptr && !attr->value.empty()) { + std::string name = (attr->value[0] == '.') ? package_ + attr->value : attr->value; + if (util::IsJavaClassName(name)) { + AddClass(node->line_number, name, "..."); + } + } + + BaseVisitor::Visit(node); + } + + private: + DISALLOW_COPY_AND_ASSIGN(NavigationVisitor); + const std::string package_; +}; + class TransitionVisitor : public BaseVisitor { public: TransitionVisitor(const ResourceFile& file, KeepSet* keep_set) : BaseVisitor(file, keep_set) { @@ -200,7 +239,8 @@ class TransitionVisitor : public BaseVisitor { if (check_class) { xml::Attribute* attr = node->FindAttribute({}, "class"); if (attr && util::IsJavaClassName(attr->value)) { - AddClass(node->line_number, attr->value); + AddClass(node->line_number, attr->value, + "android.content.Context, android.util.AttributeSet"); } } @@ -231,7 +271,14 @@ class ManifestVisitor : public BaseVisitor { if (attr) { Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); if (result) { - AddClass(node->line_number, result.value()); + AddClass(node->line_number, result.value(), ""); + } + } + attr = node->FindAttribute(xml::kSchemaAndroid, "appComponentFactory"); + if (attr) { + Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); + if (result) { + AddClass(node->line_number, result.value(), ""); } } if (main_dex_only_) { @@ -262,7 +309,7 @@ class ManifestVisitor : public BaseVisitor { if (get_name) { Maybe<std::string> result = util::GetFullyQualifiedClassName(package_, attr->value); if (result) { - AddClass(node->line_number, result.value()); + AddClass(node->line_number, result.value(), ""); } } } @@ -270,7 +317,8 @@ class ManifestVisitor : public BaseVisitor { BaseVisitor::Visit(node); } - virtual void AddClass(size_t line_number, const std::string& class_name) override { + virtual void AddClass(size_t line_number, const std::string& class_name, + const std::string& ctor_signature) override { keep_set_->AddManifestClass({file_.name, file_.source.WithLine(line_number)}, class_name); } @@ -291,7 +339,7 @@ bool CollectProguardRulesForManifest(xml::XmlResource* res, KeepSet* keep_set, b return false; } -bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set) { +bool CollectProguardRules(IAaptContext* context_, xml::XmlResource* res, KeepSet* keep_set) { if (!res->root) { return false; } @@ -309,6 +357,12 @@ bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set) { break; } + case ResourceType::kNavigation: { + NavigationVisitor visitor(res->file, keep_set, context_->GetCompilationPackage()); + res->root->Accept(&visitor); + break; + } + case ResourceType::kTransition: { TransitionVisitor visitor(res->file, keep_set); res->root->Accept(&visitor); @@ -336,7 +390,7 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first).Println(" { <init>(); }"); } for (const auto& entry : keep_set.conditional_class_set_) { @@ -352,13 +406,15 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { printer.Print("-if class **.R$layout { int ") .Print(JavaClassGenerator::TransformToFieldName(location.name.entry)) .Println("; }"); - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first.name).Print(" { <init>(") + .Print(entry.first.signature).Println("); }"); } } else { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keep class ").Print(entry.first).Println(" { <init>(...); }"); + printer.Print("-keep class ").Print(entry.first.name).Print(" { <init>(") + .Print(entry.first.signature).Println("); }"); } printer.Println(); } @@ -367,7 +423,8 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { for (const UsageLocation& location : entry.second) { printer.Print("# Referenced at ").Println(location.source.to_string()); } - printer.Print("-keepclassmembers class * { *** ").Print(entry.first).Println("(...); }"); + printer.Print("-keepclassmembers class * { *** ").Print(entry.first.name) + .Print("(").Print(entry.first.signature).Println("); }"); printer.Println(); } } diff --git a/tools/aapt2/java/ProguardRules.h b/tools/aapt2/java/ProguardRules.h index 46827ee7cf93..01dad0b08aea 100644 --- a/tools/aapt2/java/ProguardRules.h +++ b/tools/aapt2/java/ProguardRules.h @@ -40,6 +40,11 @@ struct UsageLocation { Source source; }; +struct NameAndSignature { + std::string name; + std::string signature; +}; + class KeepSet { public: KeepSet() = default; @@ -51,12 +56,13 @@ class KeepSet { manifest_class_set_[class_name].insert(file); } - inline void AddConditionalClass(const UsageLocation& file, const std::string& class_name) { - conditional_class_set_[class_name].insert(file); + inline void AddConditionalClass(const UsageLocation& file, + const NameAndSignature& class_and_signature) { + conditional_class_set_[class_and_signature].insert(file); } - inline void AddMethod(const UsageLocation& file, const std::string& method_name) { - method_set_[method_name].insert(file); + inline void AddMethod(const UsageLocation& file, const NameAndSignature& name_and_signature) { + method_set_[name_and_signature].insert(file); } inline void AddReference(const UsageLocation& file, const ResourceName& resource_name) { @@ -71,15 +77,15 @@ class KeepSet { bool conditional_keep_rules_ = false; std::map<std::string, std::set<UsageLocation>> manifest_class_set_; - std::map<std::string, std::set<UsageLocation>> method_set_; - std::map<std::string, std::set<UsageLocation>> conditional_class_set_; + std::map<NameAndSignature, std::set<UsageLocation>> method_set_; + std::map<NameAndSignature, std::set<UsageLocation>> conditional_class_set_; std::map<ResourceName, std::set<UsageLocation>> reference_set_; }; bool CollectProguardRulesForManifest(xml::XmlResource* res, KeepSet* keep_set, bool main_dex_only = false); -bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set); +bool CollectProguardRules(IAaptContext* context, xml::XmlResource* res, KeepSet* keep_set); bool CollectResourceReferences(IAaptContext* context, ResourceTable* table, KeepSet* keep_set); @@ -100,6 +106,20 @@ inline int operator<(const UsageLocation& lhs, const UsageLocation& rhs) { return lhs.name.compare(rhs.name); } +// +// NameAndSignature implementation. +// + +inline bool operator<(const NameAndSignature& lhs, const NameAndSignature& rhs) { + if (lhs.name < rhs.name) { + return true; + } + if (lhs.name == rhs.name) { + return lhs.signature < rhs.signature; + } + return false; +} + } // namespace proguard } // namespace aapt diff --git a/tools/aapt2/java/ProguardRules_test.cpp b/tools/aapt2/java/ProguardRules_test.cpp index 37d1a5fbaeb8..83c72d89bb62 100644 --- a/tools/aapt2/java/ProguardRules_test.cpp +++ b/tools/aapt2/java/ProguardRules_test.cpp @@ -34,6 +34,37 @@ std::string GetKeepSetString(const proguard::KeepSet& set) { return out; } +TEST(ProguardRulesTest, ManifestRuleDefaultConstructorOnly) { + std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application + android:appComponentFactory="com.foo.BarAppComponentFactory" + android:backupAgent="com.foo.BarBackupAgent" + android:name="com.foo.BarApplication" + > + <activity android:name="com.foo.BarActivity"/> + <service android:name="com.foo.BarService"/> + <receiver android:name="com.foo.BarReceiver"/> + <provider android:name="com.foo.BarProvider"/> + </application> + <instrumentation android:name="com.foo.BarInstrumentation"/> + </manifest>)"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRulesForManifest(manifest.get(), &set, false)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarAppComponentFactory { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarBackupAgent { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarApplication { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarActivity { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarService { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarReceiver { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarProvider { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarInstrumentation { <init>(); }")); +} + TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"( @@ -42,11 +73,11 @@ TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { @@ -56,11 +87,11 @@ TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { @@ -72,12 +103,40 @@ TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(); }")); +} + +TEST(ProguardRulesTest, NavigationFragmentNameAndClassRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("com.base").Build(); + std::unique_ptr<xml::XmlResource> navigation = test::BuildXmlDom(R"( + <navigation + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <custom android:id="@id/foo" + android:name="com.package.Foo"/> + <fragment android:id="@id/bar" + android:name="com.package.Bar"> + <nested android:id="@id/nested" + android:name=".Nested"/> + </fragment> + </navigation> + )"); + + navigation->file.name = test::ParseNameOrDie("navigation/graph.xml"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), navigation.get(), &set)); + + std::string actual = GetKeepSetString(set); + EXPECT_THAT(actual, HasSubstr("-keep class com.package.Foo { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.package.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.base.Nested { <init>(...); }")); } TEST(ProguardRulesTest, CustomViewRulesAreEmitted) { @@ -89,11 +148,12 @@ TEST(ProguardRulesTest, CustomViewRulesAreEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) { @@ -125,16 +185,16 @@ TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) { ASSERT_TRUE(xml_linker.Consume(context.get(), foo_layout.get())); proguard::KeepSet set = proguard::KeepSet(true); - ASSERT_TRUE(proguard::CollectProguardRules(bar_layout.get(), &set)); - ASSERT_TRUE(proguard::CollectProguardRules(foo_layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), bar_layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), foo_layout.get(), &set)); std::string actual = GetKeepSetString(set); EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); EXPECT_THAT(actual, HasSubstr("int foo")); EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); } TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) { @@ -147,15 +207,15 @@ TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) { proguard::KeepSet set = proguard::KeepSet(true); set.AddReference({test::ParseNameOrDie("layout/bar"), {}}, layout->file.name); - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); EXPECT_THAT(actual, HasSubstr("int foo")); EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); } TEST(ProguardRulesTest, NonLayoutReferencesAreUnconditional) { @@ -168,12 +228,13 @@ TEST(ProguardRulesTest, NonLayoutReferencesAreUnconditional) { proguard::KeepSet set = proguard::KeepSet(true); set.AddReference({test::ParseNameOrDie("style/MyStyle"), {}}, layout->file.name); - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); EXPECT_THAT(actual, Not(HasSubstr("-if"))); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { @@ -184,11 +245,12 @@ TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { layout->file.name = test::ParseNameOrDie("layout/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(layout.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("bar_method")); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** bar_method(android.view.View); }")); } TEST(ProguardRulesTest, MenuRulesAreEmitted) { @@ -203,14 +265,49 @@ TEST(ProguardRulesTest, MenuRulesAreEmitted) { menu->file.name = test::ParseNameOrDie("menu/foo"); proguard::KeepSet set; - ASSERT_TRUE(proguard::CollectProguardRules(menu.get(), &set)); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), menu.get(), &set)); std::string actual = GetKeepSetString(set); - EXPECT_THAT(actual, HasSubstr("on_click")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** on_click(android.view.MenuItem); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(android.content.Context); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(android.content.Context); }")); EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat"))); } +TEST(ProguardRulesTest, TransitionPathMotionRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> transition = test::BuildXmlDom(R"( + <changeBounds> + <pathMotion class="com.foo.Bar"/> + </changeBounds>)"); + transition->file.name = test::ParseNameOrDie("transition/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transition.get(), &set)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); +} + +TEST(ProguardRulesTest, TransitionRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> transitionSet = test::BuildXmlDom(R"( + <transitionSet> + <transition class="com.foo.Bar"/> + </transitionSet>)"); + transitionSet->file.name = test::ParseNameOrDie("transition/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transitionSet.get(), &set)); + + std::string actual = GetKeepSetString(set); + + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); +} + } // namespace aapt diff --git a/tools/aapt2/jni/aapt2_jni.cpp b/tools/aapt2/jni/aapt2_jni.cpp index ad5ad4c336e5..ba9646f9aeb4 100644 --- a/tools/aapt2/jni/aapt2_jni.cpp +++ b/tools/aapt2/jni/aapt2_jni.cpp @@ -25,15 +25,12 @@ #include "ScopedUtfChars.h" #include "Diagnostics.h" +#include "cmd/Compile.h" +#include "cmd/Link.h" #include "util/Util.h" using android::StringPiece; -namespace aapt { -extern int Compile(const std::vector<StringPiece>& args, IDiagnostics* iDiagnostics); -extern int Link(const std::vector<StringPiece>& args, IDiagnostics* iDiagnostics); -} - /* * Converts a java List<String> into C++ vector<ScopedUtfChars>. */ @@ -126,7 +123,7 @@ JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeCompile( list_to_utfchars(env, arguments_obj); std::vector<StringPiece> compile_args = extract_pieces(compile_args_jni); JniDiagnostics diagnostics(env, diagnostics_obj); - return aapt::Compile(compile_args, &diagnostics); + return aapt::CompileCommand(&diagnostics).Execute(compile_args, &std::cerr); } JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeLink(JNIEnv* env, @@ -137,7 +134,7 @@ JNIEXPORT jint JNICALL Java_com_android_tools_aapt2_Aapt2Jni_nativeLink(JNIEnv* list_to_utfchars(env, arguments_obj); std::vector<StringPiece> link_args = extract_pieces(link_args_jni); JniDiagnostics diagnostics(env, diagnostics_obj); - return aapt::Link(link_args, &diagnostics); + return aapt::LinkCommand(&diagnostics).Execute(link_args, &std::cerr); } JNIEXPORT void JNICALL Java_com_android_tools_aapt2_Aapt2Jni_ping( diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index ee4e70288994..c5c78d9d3827 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -282,6 +282,17 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, } } + if (options_.version_code_major_default) { + if (options_.replace_version) { + el->RemoveAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + } + if (el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor") == nullptr) { + el->attributes.push_back( + xml::Attribute{xml::kSchemaAndroid, "versionCodeMajor", + options_.version_code_major_default.value()}); + } + } + if (el->FindAttribute("", "platformBuildVersionCode") == nullptr) { auto versionCode = el->FindAttribute(xml::kSchemaAndroid, "versionCode"); if (versionCode != nullptr) { @@ -346,6 +357,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action["uses-permission"]; manifest_action["uses-permission-sdk-23"]; manifest_action["permission"]; + manifest_action["permission"]["meta-data"] = meta_data_action; manifest_action["permission-tree"]; manifest_action["permission-group"]; manifest_action["uses-configuration"]; @@ -355,6 +367,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action["compatible-screens"]; manifest_action["compatible-screens"]["screen"]; manifest_action["supports-gl-texture"]; + manifest_action["restrict-update"]; + manifest_action["package-verifier"]; manifest_action["meta-data"] = meta_data_action; manifest_action["uses-split"].Action(RequiredNameIsJavaPackage); @@ -376,6 +390,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, uses_static_library_action.Action(RequiredNameIsJavaPackage); uses_static_library_action.Action(RequiredAndroidAttribute("version")); uses_static_library_action.Action(RequiredAndroidAttribute("certDigest")); + uses_static_library_action["additional-certificate"]; if (options_.debug_mode) { application_action.Action([&](xml::Element* el) -> bool { diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h index 98d06fd776d7..3ef57d0d0e42 100644 --- a/tools/aapt2/link/ManifestFixer.h +++ b/tools/aapt2/link/ManifestFixer.h @@ -52,6 +52,10 @@ struct ManifestFixerOptions { // replace_version is set. Maybe<std::string> version_code_default; + // The version code to set if 'android:versionCodeMajor' is not defined in <manifest> or if + // replace_version is set. + Maybe<std::string> version_code_major_default; + // The version of the framework being compiled against to set for 'android:compileSdkVersion' in // the <manifest> tag. Maybe<std::string> compile_sdk_version; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 5bc004d62ff2..adea6273bc8b 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -329,6 +329,7 @@ TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { ManifestFixerOptions options; options.version_name_default = std::string("Beta"); options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" @@ -347,136 +348,199 @@ TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); ASSERT_THAT(attr, NotNull()); EXPECT_THAT(attr->value, StrEq("0x10000000")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } TEST_F(ManifestFixerTest, DontUseDefaultVersionNameAndCode) { -ManifestFixerOptions options; -options.version_name_default = std::string("Beta"); -options.version_code_default = std::string("0x10000000"); + ManifestFixerOptions options; + options.version_name_default = std::string("Beta"); + options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); + + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); +} -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( +TEST_F(ManifestFixerTest, ReplaceVersionNameAndCode) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_name_default = std::string("Beta"); + options.version_code_default = std::string("0x10000000"); + options.version_code_major_default = std::string("0x20000000"); + + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android" - android:versionCode="0x20000000" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Beta")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x10000000")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } -TEST_F(ManifestFixerTest, ReplaceVersionNameAndCode) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_name_default = std::string("Beta"); -options.version_code_default = std::string("0x10000000"); +TEST_F(ManifestFixerTest, ReplaceVersionName) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_name_default = std::string("Beta"); + -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android" - android:versionCode="0x20000000" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Beta")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Beta")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x10000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } -TEST_F(ManifestFixerTest, ReplaceVersionName) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_name_default = std::string("Beta"); +TEST_F(ManifestFixerTest, ReplaceVersionCode) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_code_default = std::string("0x10000000"); -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( - <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Beta")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x10000000")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } -TEST_F(ManifestFixerTest, ReplaceVersionCode) { -ManifestFixerOptions options; -options.replace_version = true; -options.version_code_default = std::string("0x10000000"); +TEST_F(ManifestFixerTest, ReplaceVersionCodeMajor) { + ManifestFixerOptions options; + options.replace_version = true; + options.version_code_major_default = std::string("0x20000000"); -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x10000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); + + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x20000000")); } TEST_F(ManifestFixerTest, DontReplaceVersionNameOrCode) { -ManifestFixerOptions options; -options.replace_version = true; + ManifestFixerOptions options; + options.replace_version = true; -std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android" - android:versionCode="0x20000000" - android:versionName="Alpha" />)EOF", - options); -ASSERT_THAT(doc, NotNull()); + std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" + android:versionCode="0x00000001" + android:versionCodeMajor="0x00000002" + android:versionName="Alpha" />)EOF", + options); + ASSERT_THAT(doc, NotNull()); + + xml::Element* manifest_el = doc->root.get(); + ASSERT_THAT(manifest_el, NotNull()); -xml::Element* manifest_el = doc->root.get(); -ASSERT_THAT(manifest_el, NotNull()); + xml::Attribute* attr = + manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("Alpha")); -xml::Attribute* attr = - manifest_el->FindAttribute(xml::kSchemaAndroid, "versionName"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("Alpha")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000001")); -attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); -ASSERT_THAT(attr, NotNull()); -EXPECT_THAT(attr->value, StrEq("0x20000000")); + attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); + ASSERT_THAT(attr, NotNull()); + EXPECT_THAT(attr->value, StrEq("0x00000002")); } TEST_F(ManifestFixerTest, EnsureManifestAttributesAreTyped) { diff --git a/tools/aapt2/link/NoDefaultResourceRemover.cpp b/tools/aapt2/link/NoDefaultResourceRemover.cpp index cfb4b26aba02..5173b8541943 100644 --- a/tools/aapt2/link/NoDefaultResourceRemover.cpp +++ b/tools/aapt2/link/NoDefaultResourceRemover.cpp @@ -22,17 +22,7 @@ namespace aapt { -static bool IsDefaultConfigRequired(const ConfigDescription& config) { - // We don't want to be overzealous with resource removal, so have strict requirements. - // If a resource defines a value for a locale-only configuration, the default configuration is - // required. - if (ConfigDescription::DefaultConfig().diff(config) == ConfigDescription::CONFIG_LOCALE) { - return true; - } - return false; -} - -static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry) { +static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry, int minSdk) { if (entry->visibility.level == Visibility::Level::kPublic) { // Removing a public API without the developer knowing is bad, so just leave this here for now. return true; @@ -44,22 +34,44 @@ static bool KeepResource(const std::unique_ptr<ResourceEntry>& entry) { } // There is no default value defined, check if removal is required. + bool defaultRequired = false; for (const auto& config_value : entry->values) { - if (IsDefaultConfigRequired(config_value->config)) { - return false; + const int config = ConfigDescription::DefaultConfig().diff(config_value->config); + // If a resource defines a value for a locale-only configuration, the default configuration is + // required. + if (config == ConfigDescription::CONFIG_LOCALE) { + defaultRequired = true; + } + // If a resource defines a version-only config, the config value can be used as a default if + // the version is at most the minimum sdk version + else if (config == ConfigDescription::CONFIG_VERSION + && config_value->config.sdkVersion <= minSdk) { + return true; + } + // If a resource defines a value for a density only configuration, then that value could be used + // as a default and the entry should not be removed + else if (config == ConfigDescription::CONFIG_DENSITY + || (config == (ConfigDescription::CONFIG_DENSITY | ConfigDescription::CONFIG_VERSION) + && config_value->config.sdkVersion <= minSdk)) { + return true; } } - return true; + + return !defaultRequired; } bool NoDefaultResourceRemover::Consume(IAaptContext* context, ResourceTable* table) { - const ConfigDescription default_config = ConfigDescription::DefaultConfig(); for (auto& pkg : table->packages) { for (auto& type : pkg->types) { + // Gather the entries without defaults that must be removed + const int minSdk = context->GetMinSdkVersion(); const auto end_iter = type->entries.end(); - const auto new_end_iter = - std::stable_partition(type->entries.begin(), end_iter, KeepResource); - for (auto iter = new_end_iter; iter != end_iter; ++iter) { + const auto remove_iter = std::stable_partition(type->entries.begin(), end_iter, + [&minSdk](const std::unique_ptr<ResourceEntry>& entry) -> bool { + return KeepResource(entry, minSdk); + }); + + for (auto iter = remove_iter; iter != end_iter; ++iter) { const ResourceName name(pkg->name, type->type, (*iter)->name); IDiagnostics* diag = context->GetDiagnostics(); diag->Warn(DiagMessage() << "removing resource " << name @@ -74,7 +86,7 @@ bool NoDefaultResourceRemover::Consume(IAaptContext* context, ResourceTable* tab } } - type->entries.erase(new_end_iter, type->entries.end()); + type->entries.erase(remove_iter, end_iter); } } return true; diff --git a/tools/aapt2/link/NoDefaultResourceRemover_test.cpp b/tools/aapt2/link/NoDefaultResourceRemover_test.cpp index 943709a2af12..d129c9ac8db7 100644 --- a/tools/aapt2/link/NoDefaultResourceRemover_test.cpp +++ b/tools/aapt2/link/NoDefaultResourceRemover_test.cpp @@ -46,4 +46,64 @@ TEST(NoDefaultResourceRemoverTest, RemoveEntryWithNoDefaultAndOnlyLocales) { EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:string/baz"))); } +TEST(NoDefaultResourceRemoverTest, KeepEntryWithLocalesAndDensities) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(26).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("mdpi")) // v4 + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep2", test::ParseConfigOrDie("xxxhdpi")) // v4 + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("w600dp-xhdpi")) // v13 + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep1"))); + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep2"))); + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + +TEST(NoDefaultResourceRemoverTest, RemoveEntryWithLocalesAndDensitiesLowVersion) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(3).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("mdpi")) // v4 + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + +TEST(NoDefaultResourceRemoverTest, KeepEntryWithVersion) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetMinSdkVersion(8).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("android", 0x01) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("v8")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/keep1", test::ParseConfigOrDie("fr-rFR")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("v9")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("en-rGB")) + .AddSimple("android:drawable/remove1", test::ParseConfigOrDie("fr-rFR")) + .Build(); + + NoDefaultResourceRemover remover; + ASSERT_TRUE(remover.Consume(context.get(), table.get())); + + EXPECT_TRUE(table->FindResource(test::ParseNameOrDie("android:drawable/keep1"))); + EXPECT_FALSE(table->FindResource(test::ParseNameOrDie("android:drawable/remove1"))); +} + } // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index e819f51a5634..afb8ae097449 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -102,7 +102,17 @@ static bool MergeType(IAaptContext* context, const Source& src, ResourceTableTyp } static bool MergeEntry(IAaptContext* context, const Source& src, bool overlay, - ResourceEntry* dst_entry, ResourceEntry* src_entry) { + ResourceEntry* dst_entry, ResourceEntry* src_entry, + bool strict_visibility) { + if (strict_visibility + && dst_entry->visibility.level != Visibility::Level::kUndefined + && src_entry->visibility.level != dst_entry->visibility.level) { + context->GetDiagnostics()->Error( + DiagMessage(src) << "cannot merge resource '" << dst_entry->name << "' with conflicting visibilities: " + << "public and private"); + return false; + } + // Copy over the strongest visibility. if (src_entry->visibility.level > dst_entry->visibility.level) { // Only copy the ID if the source is public, or else the ID is meaningless. @@ -234,7 +244,7 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, continue; } - if (!MergeEntry(context_, src, overlay, dst_entry, src_entry.get())) { + if (!MergeEntry(context_, src, overlay, dst_entry, src_entry.get(), options_.strict_visibility)) { error = true; continue; } @@ -271,8 +281,17 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, dst_config_value->value = std::move(new_file_ref); } else { + Maybe<std::string> original_comment = (dst_config_value->value) + ? dst_config_value->value->GetComment() : Maybe<std::string>(); + dst_config_value->value = std::unique_ptr<Value>( src_config_value->value->Clone(&master_table_->string_pool)); + + // Keep the comment from the original resource and ignore all comments from overlaying + // resources + if (overlay && original_comment) { + dst_config_value->value->SetComment(original_comment.value()); + } } } } diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h index 47e23dded26f..24c5e1329244 100644 --- a/tools/aapt2/link/TableMerger.h +++ b/tools/aapt2/link/TableMerger.h @@ -35,6 +35,8 @@ namespace aapt { struct TableMergerOptions { // If true, resources in overlays can be added without previously having existed. bool auto_add_overlay = false; + // If true, resource overlays with conflicting visibility are not allowed. + bool strict_visibility = false; }; // TableMerger takes resource tables and merges all packages within the tables that have the same diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp index 34461c6b467d..79a734bffabd 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -178,6 +178,49 @@ TEST_F(TableMergerTest, OverrideResourceWithOverlay) { Pointee(Field(&BinaryPrimitive::value, Field(&android::Res_value::data, Eq(0u))))); } +TEST_F(TableMergerTest, DoNotOverrideResourceComment) { + std::unique_ptr<Value> foo_original = ResourceUtils::TryParseBool("true"); + foo_original->SetComment(android::StringPiece("Original foo comment")); + std::unique_ptr<Value> bar_original = ResourceUtils::TryParseBool("true"); + + std::unique_ptr<Value> foo_overlay = ResourceUtils::TryParseBool("false"); + foo_overlay->SetComment(android::StringPiece("Overlay foo comment")); + std::unique_ptr<Value> bar_overlay = ResourceUtils::TryParseBool("false"); + bar_overlay->SetComment(android::StringPiece("Overlay bar comment")); + std::unique_ptr<Value> baz_overlay = ResourceUtils::TryParseBool("false"); + baz_overlay->SetComment(android::StringPiece("Overlay baz comment")); + + std::unique_ptr<ResourceTable> base = + test::ResourceTableBuilder() + .SetPackageId("", 0x00) + .AddValue("bool/foo", std::move(foo_original)) + .AddValue("bool/bar", std::move(bar_original)) + .Build(); + + std::unique_ptr<ResourceTable> overlay = + test::ResourceTableBuilder() + .SetPackageId("", 0x00) + .AddValue("bool/foo", std::move(foo_overlay)) + .AddValue("bool/bar", std::move(bar_overlay)) + .AddValue("bool/baz", std::move(baz_overlay)) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + + ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); + ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); + + BinaryPrimitive* foo = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/foo"); + EXPECT_THAT(foo, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Original foo comment")))); + BinaryPrimitive* bar = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/bar"); + EXPECT_THAT(bar, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("")))); + BinaryPrimitive* baz = test::GetValue<BinaryPrimitive>(&final_table, "com.app.a:bool/baz"); + EXPECT_THAT(baz, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Overlay baz comment")))); +} + TEST_F(TableMergerTest, OverrideSameResourceIdsWithOverlay) { std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() @@ -241,6 +284,37 @@ TEST_F(TableMergerTest, FailToOverrideConflictingEntryIdsWithOverlay) { ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); } +TEST_F(TableMergerTest, FailConflictingVisibility) { + std::unique_ptr<ResourceTable> base = + test::ResourceTableBuilder() + .SetPackageId("", 0x7f) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) + .Build(); + std::unique_ptr<ResourceTable> overlay = + test::ResourceTableBuilder() + .SetPackageId("", 0x7f) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPrivate) + .Build(); + + // It should fail if the "--strict-visibility" flag is set. + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = false; + options.strict_visibility = true; + TableMerger merger(context_.get(), &final_table, options); + + ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); + ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); + + // But it should still pass if the flag is not set. + ResourceTable final_table2; + options.strict_visibility = false; + TableMerger merger2(context_.get(), &final_table2, options); + + ASSERT_TRUE(merger2.Merge({}, base.get(), false /*overlay*/)); + ASSERT_TRUE(merger2.Merge({}, overlay.get(), true /*overlay*/)); +} + TEST_F(TableMergerTest, MergeAddResourceFromOverlay) { std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder().SetPackageId("", 0x7f).Build(); diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp index 588b3316e6fa..e92c121272eb 100644 --- a/tools/aapt2/optimize/MultiApkGenerator.cpp +++ b/tools/aapt2/optimize/MultiApkGenerator.cpp @@ -26,6 +26,7 @@ #include "ResourceUtils.h" #include "ValueVisitor.h" #include "configuration/ConfigurationParser.h" +#include "cmd/Util.h" #include "filter/AbiFilter.h" #include "filter/Filter.h" #include "format/Archive.h" @@ -265,7 +266,7 @@ bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact, // Make sure the first element is <manifest> with package attribute. xml::Element* manifest_el = manifest->root.get(); - if (manifest_el == nullptr) { + if (!manifest_el) { return false; } @@ -274,21 +275,35 @@ bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact, return false; } - // Update the versionCode attribute. - xml::Attribute* versionCode = manifest_el->FindAttribute(kSchemaAndroid, "versionCode"); - if (versionCode == nullptr) { + // Retrieve the versionCode attribute. + auto version_code = manifest_el->FindAttribute(kSchemaAndroid, "versionCode"); + if (!version_code) { diag->Error(DiagMessage(manifest->file.source) << "manifest must have a versionCode attribute"); return false; } - auto* compiled_version = ValueCast<BinaryPrimitive>(versionCode->compiled_value.get()); - if (compiled_version == nullptr) { + auto version_code_value = ValueCast<BinaryPrimitive>(version_code->compiled_value.get()); + if (!version_code_value) { diag->Error(DiagMessage(manifest->file.source) << "versionCode is invalid"); return false; } - int new_version = compiled_version->value.data + artifact.version; - versionCode->compiled_value = ResourceUtils::TryParseInt(std::to_string(new_version)); + // Retrieve the versionCodeMajor attribute. + auto version_code_major = manifest_el->FindAttribute(kSchemaAndroid, "versionCodeMajor"); + BinaryPrimitive* version_code_major_value = nullptr; + if (version_code_major) { + version_code_major_value = ValueCast<BinaryPrimitive>(version_code_major->compiled_value.get()); + if (!version_code_major_value) { + diag->Error(DiagMessage(manifest->file.source) << "versionCodeMajor is invalid"); + return false; + } + } + + // Calculate and set the updated version code + uint64_t major = (version_code_major_value) + ? ((uint64_t) version_code_major_value->value.data) << 32 : 0; + uint64_t new_version = (major | version_code_value->value.data) + artifact.version; + SetLongVersionCode(manifest_el, new_version); // Check to see if the minSdkVersion needs to be updated. if (artifact.android_sdk) { diff --git a/tools/aapt2/optimize/ResourceFilter.cpp b/tools/aapt2/optimize/ResourceFilter.cpp new file mode 100644 index 000000000000..250b65197a7d --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourceFilter.h" + +#include "ResourceTable.h" + +namespace aapt { + +ResourceFilter::ResourceFilter(const std::unordered_set<ResourceName>& blacklist) + : blacklist_(blacklist) { +} + +bool ResourceFilter::Consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto it = type->entries.begin(); it != type->entries.end(); ) { + ResourceName resource = ResourceName({}, type->type, (*it)->name); + if (blacklist_.find(resource) != blacklist_.end()) { + it = type->entries.erase(it); + } else { + ++it; + } + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/optimize/ResourceFilter.h b/tools/aapt2/optimize/ResourceFilter.h new file mode 100644 index 000000000000..d4baf654b0ff --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_OPTIMIZE_RESOURCEFILTER_H +#define AAPT_OPTIMIZE_RESOURCEFILTER_H + +#include "android-base/macros.h" + +#include "process/IResourceTableConsumer.h" + +#include <unordered_set> + +namespace aapt { + +// Removes non-whitelisted entries from resource table. +class ResourceFilter : public IResourceTableConsumer { + public: + explicit ResourceFilter(const std::unordered_set<ResourceName>& blacklist); + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(ResourceFilter); + std::unordered_set<ResourceName> blacklist_; +}; + +} // namespace aapt + +#endif // AAPT_OPTIMIZE_RESOURCEFILTER_H diff --git a/tools/aapt2/optimize/ResourceFilter_test.cpp b/tools/aapt2/optimize/ResourceFilter_test.cpp new file mode 100644 index 000000000000..800b2bfd0403 --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter_test.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourceFilter.h" + +#include "ResourceTable.h" +#include "test/Test.h" + +using ::aapt::test::HasValue; +using ::testing::Not; + +namespace aapt { + +TEST(ResourceFilterTest, SomeValuesAreFilteredOut) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/notblacklisted2", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted2", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set<ResourceName> blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + ResourceName({}, ResourceType::kString, "blacklisted2"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:string/notblacklisted2", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted2", default_config))); +} + +TEST(ResourceFilterTest, TypeIsCheckedBeforeFiltering) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:string/blacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/notblacklisted", ResourceId{}, default_config, "value") + .AddString("android:drawable/blacklisted", ResourceId{}, default_config, "value") + .Build(); + + std::unordered_set<ResourceName> blacklist = { + ResourceName({}, ResourceType::kString, "blacklisted"), + }; + + ASSERT_TRUE(ResourceFilter(blacklist).Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/notblacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/blacklisted", default_config)); + EXPECT_THAT(table, HasValue("android:drawable/notblacklisted", default_config)); + EXPECT_THAT(table, Not(HasValue("android:string/blacklisted", default_config))); +} + +} // namespace aapt diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp index 5a8ff0926483..7cd023bca369 100644 --- a/tools/aapt2/util/Files.cpp +++ b/tools/aapt2/util/Files.cpp @@ -149,6 +149,10 @@ StringPiece GetExtension(const StringPiece& path) { return {}; } +bool IsHidden(const android::StringPiece& path) { + return util::StartsWith(GetFilename(path), "."); +} + void AppendPath(std::string* base, StringPiece part) { CHECK(base != nullptr); const bool base_has_trailing_sep = (!base->empty() && *(base->end() - 1) == sDirSep); diff --git a/tools/aapt2/util/Files.h b/tools/aapt2/util/Files.h index b26e4fa26de6..219e1a07af95 100644 --- a/tools/aapt2/util/Files.h +++ b/tools/aapt2/util/Files.h @@ -70,6 +70,9 @@ android::StringPiece GetFilename(const android::StringPiece& path); // of the path. android::StringPiece GetExtension(const android::StringPiece& path); +// Returns whether or not the name of the file or directory is a hidden file name +bool IsHidden(const android::StringPiece& path); + // Converts a package name (com.android.app) to a path: com/android/app std::string PackageToPath(const android::StringPiece& package); diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index d1c9ca1644d9..9bef54e590c9 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -297,6 +297,53 @@ bool VerifyJavaStringFormat(const StringPiece& str) { return true; } +std::string Utf8ToModifiedUtf8(const std::string& utf8) { + // Java uses Modified UTF-8 which only supports the 1, 2, and 3 byte formats of UTF-8. To encode + // 4 byte UTF-8 codepoints, Modified UTF-8 allows the use of surrogate pairs in the same format + // of CESU-8 surrogate pairs. Calculate the size of the utf8 string with all 4 byte UTF-8 + // codepoints replaced with 2 3 byte surrogate pairs + size_t modified_size = 0; + const size_t size = utf8.size(); + for (size_t i = 0; i < size; i++) { + if (((uint8_t) utf8[i] >> 4) == 0xF) { + modified_size += 6; + i += 3; + } else { + modified_size++; + } + } + + // Early out if no 4 byte codepoints are found + if (size == modified_size) { + return utf8; + } + + std::string output; + output.reserve(modified_size); + for (size_t i = 0; i < size; i++) { + if (((uint8_t) utf8[i] >> 4) == 0xF) { + auto codepoint = (char32_t) utf32_from_utf8_at(utf8.data(), size, i, nullptr); + + // Calculate the high and low surrogates as UTF-16 would + char32_t high = ((codepoint - 0x10000) / 0x400) + 0xD800; + char32_t low = ((codepoint - 0x10000) % 0x400) + 0xDC00; + + // Encode each surrogate in UTF-8 + output.push_back((char) (0xE4 | ((high >> 12) & 0xF))); + output.push_back((char) (0x80 | ((high >> 6) & 0x3F))); + output.push_back((char) (0x80 | (high & 0x3F))); + output.push_back((char) (0xE4 | ((low >> 12) & 0xF))); + output.push_back((char) (0x80 | ((low >> 6) & 0x3F))); + output.push_back((char) (0x80 | (low & 0x3F))); + i += 3; + } else { + output.push_back(utf8[i]); + } + } + + return output; +} + std::u16string Utf8ToUtf16(const StringPiece& utf8) { ssize_t utf16_length = utf8_to_utf16_length( reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length()); diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h index 0eb35d18c06e..36b733376e6f 100644 --- a/tools/aapt2/util/Util.h +++ b/tools/aapt2/util/Util.h @@ -197,6 +197,9 @@ inline StringBuilder::operator bool() const { return error_.empty(); } +// Converts a UTF8 string into Modified UTF8 +std::string Utf8ToModifiedUtf8(const std::string& utf8); + // Converts a UTF8 string to a UTF16 string. std::u16string Utf8ToUtf16(const android::StringPiece& utf8); std::string Utf16ToUtf8(const android::StringPiece16& utf16); diff --git a/tools/aapt2/xml/XmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp index 402e5a459f4e..a023494ad8f7 100644 --- a/tools/aapt2/xml/XmlPullParser.cpp +++ b/tools/aapt2/xml/XmlPullParser.cpp @@ -38,6 +38,7 @@ XmlPullParser::XmlPullParser(InputStream* in) : in_(in), empty_(), depth_(0) { EndNamespaceHandler); XML_SetCharacterDataHandler(parser_, CharacterDataHandler); XML_SetCommentHandler(parser_, CommentDataHandler); + XML_SetCdataSectionHandler(parser_, StartCdataSectionHandler, EndCdataSectionHandler); event_queue_.push(EventData{Event::kStartDocument, 0, depth_++}); } @@ -287,6 +288,22 @@ void XMLCALL XmlPullParser::CommentDataHandler(void* user_data, parser->depth_, comment}); } +void XMLCALL XmlPullParser::StartCdataSectionHandler(void* user_data) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(user_data); + + parser->event_queue_.push(EventData{Event::kCdataStart, + XML_GetCurrentLineNumber(parser->parser_), + parser->depth_ }); +} + +void XMLCALL XmlPullParser::EndCdataSectionHandler(void* user_data) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(user_data); + + parser->event_queue_.push(EventData{Event::kCdataEnd, + XML_GetCurrentLineNumber(parser->parser_), + parser->depth_ }); +} + Maybe<StringPiece> FindAttribute(const XmlPullParser* parser, const StringPiece& name) { auto iter = parser->FindAttribute("", name); diff --git a/tools/aapt2/xml/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h index 63db66f0b2b7..6ebaa285745b 100644 --- a/tools/aapt2/xml/XmlPullParser.h +++ b/tools/aapt2/xml/XmlPullParser.h @@ -52,6 +52,8 @@ class XmlPullParser : public IPackageDeclStack { kEndElement, kText, kComment, + kCdataStart, + kCdataEnd, }; /** @@ -159,6 +161,8 @@ class XmlPullParser : public IPackageDeclStack { static void XMLCALL EndElementHandler(void* user_data, const char* name); static void XMLCALL EndNamespaceHandler(void* user_data, const char* prefix); static void XMLCALL CommentDataHandler(void* user_data, const char* comment); + static void XMLCALL StartCdataSectionHandler(void* user_data); + static void XMLCALL EndCdataSectionHandler(void* user_data); struct EventData { Event event; @@ -223,6 +227,10 @@ inline ::std::ostream& operator<<(::std::ostream& out, return out << "Text"; case XmlPullParser::Event::kComment: return out << "Comment"; + case XmlPullParser::Event::kCdataStart: + return out << "CdataStart"; + case XmlPullParser::Event::kCdataEnd: + return out << "CdataEnd"; } return out; } @@ -240,6 +248,8 @@ inline bool XmlPullParser::NextChildNode(XmlPullParser* parser, size_t start_dep case Event::kText: case Event::kComment: case Event::kStartElement: + case Event::kCdataStart: + case Event::kCdataEnd: return true; default: break; diff --git a/tools/aosp/aosp_sha.sh b/tools/aosp/aosp_sha.sh new file mode 100755 index 000000000000..29bf74c7a8b9 --- /dev/null +++ b/tools/aosp/aosp_sha.sh @@ -0,0 +1,18 @@ +#!/bin/bash +LOCAL_DIR="$( dirname ${BASH_SOURCE} )" + +if git branch -vv | grep "^*" | grep "\[aosp/master" > /dev/null; then + # Change appears to be in AOSP + exit 0 +else + # Change appears to be non-AOSP; search for files + git show --name-only --pretty=format: $1 | grep $2 | while read file; do + echo + echo -e "\033[0;31mThe source of truth for '$file' is in AOSP.\033[0m" + echo + echo "If your change contains no confidential details, please upload and merge" + echo "this change at https://android-review.googlesource.com/." + echo + exit 77 + done +fi diff --git a/tools/apilint/apilint.py b/tools/apilint/apilint.py index 70a47cf42755..018e9c9d5f57 100644 --- a/tools/apilint/apilint.py +++ b/tools/apilint/apilint.py @@ -69,12 +69,19 @@ class Field(): self.raw = raw.strip(" {;") self.blame = blame + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + raw = raw.split() self.split = list(raw) for r in ["field", "volatile", "transient", "public", "protected", "static", "final", "deprecated"]: while r in raw: raw.remove(r) + # ignore annotations for now + raw = [ r for r in raw if not r.startswith("@") ] + self.typ = raw[0] self.name = raw[1].strip(";") if len(raw) >= 4 and raw[2] == "=": @@ -97,25 +104,39 @@ class Method(): self.raw = raw.strip(" {;") self.blame = blame - # drop generics for now - raw = re.sub("<.+?>", "", raw) + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + + # handle each clause differently + raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups() - raw = re.split("[\s(),;]+", raw) + # parse prefixes + raw = re.split("[\s]+", raw_prefix) for r in ["", ";"]: while r in raw: raw.remove(r) self.split = list(raw) - for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default"]: + for r in ["method", "public", "protected", "static", "final", "deprecated", "abstract", "default", "operator"]: while r in raw: raw.remove(r) self.typ = raw[0] self.name = raw[1] + + # parse args self.args = [] + for arg in re.split(",\s*", raw_args): + arg = re.split("\s", arg) + # ignore annotations for now + arg = [ a for a in arg if not a.startswith("@") ] + if len(arg[0]) > 0: + self.args.append(arg[0]) + + # parse throws self.throws = [] - target = self.args - for r in raw[2:]: - if r == "throws": target = self.throws - else: target.append(r) + for throw in re.split(",\s*", raw_throws): + self.throws.append(throw) + self.ident = ident(self.raw) def __hash__(self): @@ -135,12 +156,18 @@ class Class(): self.fields = [] self.methods = [] + # drop generics for now; may need multiple passes + raw = re.sub("<[^<]+?>", "", raw) + raw = re.sub("<[^<]+?>", "", raw) + raw = raw.split() self.split = list(raw) if "class" in raw: self.fullname = raw[raw.index("class")+1] elif "interface" in raw: self.fullname = raw[raw.index("interface")+1] + elif "@interface" in raw: + self.fullname = raw[raw.index("@interface")+1] else: raise ValueError("Funky class type %s" % (self.raw)) @@ -1334,18 +1361,25 @@ def verify_clone(clazz): error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()") +def is_interesting(clazz): + """Test if given class is interesting from an Android PoV.""" + + if clazz.pkg.name.startswith("java"): return False + if clazz.pkg.name.startswith("junit"): return False + if clazz.pkg.name.startswith("org.apache"): return False + if clazz.pkg.name.startswith("org.xml"): return False + if clazz.pkg.name.startswith("org.json"): return False + if clazz.pkg.name.startswith("org.w3c"): return False + if clazz.pkg.name.startswith("android.icu."): return False + return True + + def examine_clazz(clazz): """Find all style issues in the given class.""" notice(clazz) - if clazz.pkg.name.startswith("java"): return - if clazz.pkg.name.startswith("junit"): return - if clazz.pkg.name.startswith("org.apache"): return - if clazz.pkg.name.startswith("org.xml"): return - if clazz.pkg.name.startswith("org.json"): return - if clazz.pkg.name.startswith("org.w3c"): return - if clazz.pkg.name.startswith("android.icu."): return + if not is_interesting(clazz): return verify_constants(clazz) verify_enums(clazz) @@ -1479,6 +1513,7 @@ def show_deprecations_at_birth(cur, prev): # Remove all existing things so we're left with new for prev_clazz in prev.values(): cur_clazz = cur[prev_clazz.fullname] + if not is_interesting(cur_clazz): continue sigs = { i.ident: i for i in prev_clazz.ctors } cur_clazz.ctors = [ i for i in cur_clazz.ctors if i.ident not in sigs ] @@ -1506,6 +1541,38 @@ def show_deprecations_at_birth(cur, prev): print +def show_stats(cur, prev): + """Show API stats.""" + + stats = collections.defaultdict(int) + for cur_clazz in cur.values(): + if not is_interesting(cur_clazz): continue + + if cur_clazz.fullname not in prev: + stats['new_classes'] += 1 + stats['new_ctors'] += len(cur_clazz.ctors) + stats['new_methods'] += len(cur_clazz.methods) + stats['new_fields'] += len(cur_clazz.fields) + else: + prev_clazz = prev[cur_clazz.fullname] + + sigs = { i.ident: i for i in prev_clazz.ctors } + ctors = len([ i for i in cur_clazz.ctors if i.ident not in sigs ]) + sigs = { i.ident: i for i in prev_clazz.methods } + methods = len([ i for i in cur_clazz.methods if i.ident not in sigs ]) + sigs = { i.ident: i for i in prev_clazz.fields } + fields = len([ i for i in cur_clazz.fields if i.ident not in sigs ]) + + if ctors + methods + fields > 0: + stats['extend_classes'] += 1 + stats['extend_ctors'] += ctors + stats['extend_methods'] += methods + stats['extend_fields'] += fields + + print "#", "".join([ k.ljust(20) for k in sorted(stats.keys()) ]) + print " ", "".join([ str(stats[k]).ljust(20) for k in sorted(stats.keys()) ]) + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Enforces common Android public API design \ patterns. It ignores lint messages from a previous API level, if provided.") @@ -1520,6 +1587,8 @@ if __name__ == "__main__": help="Show API changes noticed") parser.add_argument("--show-deprecations-at-birth", action='store_const', const=True, help="Show API deprecations at birth") + parser.add_argument("--show-stats", action='store_const', const=True, + help="Show API stats") args = vars(parser.parse_args()) if args['no_color']: @@ -1539,6 +1608,14 @@ if __name__ == "__main__": show_deprecations_at_birth(cur, prev) sys.exit() + if args['show_stats']: + with current_file as f: + cur = _parse_stream(f) + with previous_file as f: + prev = _parse_stream(f) + show_stats(cur, prev) + sys.exit() + with current_file as f: cur_fail, cur_noticed = examine_stream(f) if not previous_file is None: diff --git a/tools/apilint/apilint_stats.sh b/tools/apilint/apilint_stats.sh new file mode 100755 index 000000000000..052d9a5265fe --- /dev/null +++ b/tools/apilint/apilint_stats.sh @@ -0,0 +1,7 @@ +#!/bin/bash +API=28 +while [ $API -gt 14 ]; do + echo "# Changes in API $((API))" + python tools/apilint/apilint.py --show-stats ../../prebuilts/sdk/$((API))/public/api/android.txt ../../prebuilts/sdk/$((API-1))/public/api/android.txt + let API=API-1 +done diff --git a/tools/incident_section_gen/main.cpp b/tools/incident_section_gen/main.cpp index 3689a8f2981e..639f98062b71 100644 --- a/tools/incident_section_gen/main.cpp +++ b/tools/incident_section_gen/main.cpp @@ -110,9 +110,7 @@ static bool generateIncidentSectionsCpp(Descriptor const* descriptor) N = descriptor->field_count(); for (int i=0; i<N; i++) { const FieldDescriptor* field = descriptor->field(i); - if (field->type() == FieldDescriptor::TYPE_MESSAGE) { - sections[field->name()] = field; - } + sections[field->name()] = field; } printf("IncidentSection const INCIDENT_SECTIONS[] = {\n"); @@ -404,7 +402,7 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { for (int i=0; i<descriptor->field_count(); i++) { const FieldDescriptor* field = descriptor->field(i); - if (field->type() != FieldDescriptor::TYPE_MESSAGE) { + if (field->type() != FieldDescriptor::TYPE_MESSAGE && field->type() != FieldDescriptor::TYPE_STRING) { continue; } const SectionFlags s = getSectionFlags(field); @@ -412,8 +410,7 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { case SECTION_NONE: continue; case SECTION_FILE: - printf(" new FileSection(%d, \"%s\", %s),\n", field->number(), s.args().c_str(), - s.device_specific() ? "true" : "false"); + printf(" new FileSection(%d, \"%s\"),\n", field->number(), s.args().c_str()); break; case SECTION_COMMAND: printf(" new CommandSection(%d,", field->number()); @@ -457,13 +454,13 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { const FieldDescriptor* field = fieldsInOrder[i]; const string fieldName = getFieldName(field); const Destination fieldDest = getFieldDest(field); - const string fieldMessageName = getMessageName(field->message_type(), fieldDest); - - skip[i] = true; - if (field->type() != FieldDescriptor::TYPE_MESSAGE) { + printPrivacy(fieldName, field, "NULL", fieldDest, "NULL"); continue; } + + skip[i] = true; + const string fieldMessageName = getMessageName(field->message_type(), fieldDest); // generate privacy flags for each section. if (generatePrivacyFlags(field->message_type(), fieldDest, variableNames, &parents)) { printPrivacy(fieldName, field, fieldMessageName, fieldDest, "NULL"); diff --git a/tools/stats_log_api_gen/Collation.cpp b/tools/stats_log_api_gen/Collation.cpp index ebdcdfdd6c50..40ee490fc186 100644 --- a/tools/stats_log_api_gen/Collation.cpp +++ b/tools/stats_log_api_gen/Collation.cpp @@ -116,6 +116,9 @@ java_type(const FieldDescriptor* field) if (field->message_type()->full_name() == "android.os.statsd.AttributionNode") { return JAVA_TYPE_ATTRIBUTION_CHAIN; + } else if (field->message_type()->full_name() == + "android.os.statsd.KeyValuePair") { + return JAVA_TYPE_KEY_VALUE_PAIR; } else { return JAVA_TYPE_OBJECT; } @@ -181,6 +184,16 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, expectedNumber++; } + // Skips the key value pair atom. + for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); + it != fields.end(); it++) { + const FieldDescriptor *field = it->second; + java_type_t javaType = java_type(field); + if (javaType == JAVA_TYPE_KEY_VALUE_PAIR) { + return 0; + } + } + // Check that only allowed types are present. Remove any invalid ones. for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); it != fields.end(); it++) { diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h index 5d2c30292c9c..ccdd1458f656 100644 --- a/tools/stats_log_api_gen/Collation.h +++ b/tools/stats_log_api_gen/Collation.h @@ -48,6 +48,7 @@ typedef enum { JAVA_TYPE_DOUBLE = 6, JAVA_TYPE_STRING = 7, JAVA_TYPE_ENUM = 8, + JAVA_TYPE_KEY_VALUE_PAIR = 9, JAVA_TYPE_OBJECT = -1, JAVA_TYPE_BYTE_ARRAY = -2, diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp index e519909aa026..8038a3a4f44e 100644 --- a/tools/stats_log_api_gen/main.cpp +++ b/tools/stats_log_api_gen/main.cpp @@ -349,8 +349,7 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, argIndex++; } fprintf(out, ");\n"); - fprintf(out, " if (ret >= 0) { return retry; }\n"); - + fprintf(out, " if (ret >= 0) { break; }\n"); fprintf(out, " {\n"); fprintf(out, " std::lock_guard<std::mutex> lock(mLogdRetryMutex);\n"); @@ -360,6 +359,9 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, fprintf(out, " }\n"); fprintf(out, " std::this_thread::sleep_for(std::chrono::milliseconds(10));\n"); fprintf(out, " }\n"); + fprintf(out, " if (ret < 0) {\n"); + fprintf(out, " note_log_drop();\n"); + fprintf(out, " }\n"); fprintf(out, " return ret;\n"); fprintf(out, "}\n"); fprintf(out, "\n"); @@ -439,7 +441,7 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, argIndex++; } fprintf(out, ");\n"); - fprintf(out, " if (ret >= 0) { return retry; }\n"); + fprintf(out, " if (ret >= 0) { break; }\n"); fprintf(out, " {\n"); fprintf(out, " std::lock_guard<std::mutex> lock(mLogdRetryMutex);\n"); @@ -450,7 +452,10 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, fprintf(out, " std::this_thread::sleep_for(std::chrono::milliseconds(10));\n"); fprintf(out, " }\n"); - fprintf(out, " return ret;\n"); + fprintf(out, " if (ret < 0) {\n"); + fprintf(out, " note_log_drop();\n"); + fprintf(out, " }\n"); + fprintf(out, " return ret;\n\n"); fprintf(out, "}\n"); fprintf(out, "\n"); diff --git a/tools/stringslint/stringslint.py b/tools/stringslint/stringslint.py index d637ff346c82..03c0b9af66a0 100644 --- a/tools/stringslint/stringslint.py +++ b/tools/stringslint/stringslint.py @@ -20,11 +20,22 @@ a previous strings file, if provided. Usage: stringslint.py strings.xml Usage: stringslint.py strings.xml old_strings.xml + +In general: +* Errors signal issues that must be fixed before submitting, and are only + used when there are no false-positives. +* Warnings signal issues that might need to be fixed, but need manual + inspection due to risk of false-positives. +* Info signal issues that should be fixed to match best-practices, such + as providing comments to aid translation. """ -import re, sys +import re, sys, codecs import lxml.etree as ET +reload(sys) +sys.setdefaultencoding('utf8') + BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): @@ -43,10 +54,10 @@ def format(fg=None, bg=None, bright=False, bold=False, dim=False, reset=False): warnings = None -def warn(tag, msg, actual, expected): +def warn(tag, msg, actual, expected, color=YELLOW): global warnings key = "%s:%d" % (tag.attrib["name"], hash(msg)) - value = "%sLine %d: '%s':%s %s" % (format(fg=YELLOW, bold=True), + value = "%sLine %d: '%s':%s %s" % (format(fg=color, bold=True), tag.sourceline, tag.attrib["name"], format(reset=True), @@ -59,6 +70,46 @@ def warn(tag, msg, actual, expected): format(reset=True)) warnings[key] = value + +def error(tag, msg, actual, expected): + warn(tag, msg, actual, expected, RED) + +def info(tag, msg, actual, expected): + warn(tag, msg, actual, expected, CYAN) + +# Escaping logic borrowed from https://stackoverflow.com/a/24519338 +ESCAPE_SEQUENCE_RE = re.compile(r''' + ( \\U........ # 8-digit hex escapes + | \\u.... # 4-digit hex escapes + | \\x.. # 2-digit hex escapes + | \\[0-7]{1,3} # Octal escapes + | \\N\{[^}]+\} # Unicode characters by name + | \\[\\'"abfnrtv] # Single-character escapes + )''', re.UNICODE | re.VERBOSE) + +def decode_escapes(s): + def decode_match(match): + return codecs.decode(match.group(0), 'unicode-escape') + + s = re.sub(r"\n\s*", " ", s) + s = ESCAPE_SEQUENCE_RE.sub(decode_match, s) + s = re.sub(r"%(\d+\$)?[a-z]", "____", s) + s = re.sub(r"\^\d+", "____", s) + s = re.sub(r"<br/?>", "\n", s) + s = re.sub(r"</?[a-z]+>", "", s) + return s + +def sample_iter(tag): + if not isinstance(tag, ET._Comment) and re.match("{.*xliff.*}g", tag.tag) and "example" in tag.attrib: + yield tag.attrib["example"] + elif tag.text: + yield decode_escapes(tag.text) + for e in tag: + for v in sample_iter(e): + yield v + if e.tail: + yield decode_escapes(e.tail) + def lint(path): global warnings warnings = {} @@ -80,35 +131,45 @@ def lint(path): comment = last_comment last_comment = None + # Prepare string for analysis + text = "".join(child.itertext()) + sample = "".join(sample_iter(child)).strip().strip("'\"") + # Validate comment if comment is None: - warn(child, "Missing string comment to aid translation", + info(child, "Missing string comment to aid translation", None, None) continue if "do not translate" in comment.text.lower(): continue if "translatable" in child.attrib and child.attrib["translatable"].lower() == "false": continue - if re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text) is None: - warn(child, "Missing CHAR LIMIT to aid translation", + + limit = re.search("CHAR[ _-]LIMIT=(\d+|NONE|none)", comment.text) + if limit is None: + info(child, "Missing CHAR LIMIT to aid translation", repr(comment), "<!-- Description of string [CHAR LIMIT=32] -->") + elif re.match("\d+", limit.group(1)): + limit = int(limit.group(1)) + if len(sample) > limit: + warn(child, "Expanded string length is larger than CHAR LIMIT", + sample, None) # Look for common mistakes/substitutions - text = "".join(child.itertext()).strip() if "'" in text: - warn(child, "Turned quotation mark glyphs are more polished", + error(child, "Turned quotation mark glyphs are more polished", text, "This doesn\u2019t need to \u2018happen\u2019 today") if '"' in text and not text.startswith('"') and text.endswith('"'): - warn(child, "Turned quotation mark glyphs are more polished", + error(child, "Turned quotation mark glyphs are more polished", text, "This needs to \u201chappen\u201d today") if "..." in text: - warn(child, "Ellipsis glyph is more polished", + error(child, "Ellipsis glyph is more polished", text, "Loading\u2026") if "wi-fi" in text.lower(): - warn(child, "Non-breaking glyph is more polished", + error(child, "Non-breaking glyph is more polished", text, "Wi\u2011Fi") if "wifi" in text.lower(): - warn(child, "Using non-standard spelling", + error(child, "Using non-standard spelling", text, "Wi\u2011Fi") if re.search("\d-\d", text): warn(child, "Ranges should use en dash glyph", @@ -119,11 +180,17 @@ def lint(path): if ". " in text: warn(child, "Only use single space between sentences", text, "First idea. Second idea.") + if re.match(r"^[A-Z\s]{5,}$", text): + warn(child, "Actions should use android:textAllCaps in layout; ignore if acronym", + text, "Refresh data") + if " phone " in text and "product" not in child.attrib: + warn(child, "Strings mentioning phones should have variants for tablets", + text, None) # When more than one substitution, require indexes if len(re.findall("%[^%]", text)) > 1: if len(re.findall("%[^\d]", text)) > 0: - warn(child, "Substitutions must be indexed", + error(child, "Substitutions must be indexed", text, "Add %1$s to %2$s") # Require xliff substitutions @@ -132,15 +199,15 @@ def lint(path): if gc.tail and re.search("%[^%]", gc.tail): badsub = True if re.match("{.*xliff.*}g", gc.tag): if "id" not in gc.attrib: - warn(child, "Substitutions must define id attribute", + error(child, "Substitutions must define id attribute", None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") if "example" not in gc.attrib: - warn(child, "Substitutions must define example attribute", + error(child, "Substitutions must define example attribute", None, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") else: if gc.text and re.search("%[^%]", gc.text): badsub = True if badsub: - warn(child, "Substitutions must be inside xliff tags", + error(child, "Substitutions must be inside xliff tags", text, "<xliff:g id=\"domain\" example=\"example.com\">%1$s</xliff:g>") return warnings |