diff options
Diffstat (limited to 'tools')
109 files changed, 8082 insertions, 1818 deletions
diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp index d631f3531127..f06643dcf784 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -28,7 +28,7 @@ static const char* kExcludeExtension = ".EXCLUDE"; /* these formats are already compressed, or don't compress well */ static const char* kNoCompressExt[] = { - ".jpg", ".jpeg", ".png", ".gif", + ".jpg", ".jpeg", ".png", ".gif", ".opus", ".wav", ".mp2", ".mp3", ".ogg", ".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", 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 750fb56b2792..ba498e19f837 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", @@ -82,6 +83,7 @@ cc_library_host_static { "compile/Pseudolocalizer.cpp", "compile/XmlIdCollector.cpp", "configuration/ConfigurationParser.cpp", + "dump/DumpManifest.cpp", "filter/AbiFilter.cpp", "filter/ConfigFilter.cpp", "format/Archive.cpp", @@ -111,6 +113,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", @@ -122,7 +125,6 @@ cc_library_host_static { "util/Util.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..0bc5221245ca 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -306,8 +306,29 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& break; } - if (entry->overlayable) { - printer->Print(" OVERLAYABLE"); + for (size_t i = 0; i < entry->overlayable_declarations.size(); i++) { + printer->Print((i == 0) ? " " : "|"); + printer->Print("OVERLAYABLE"); + + if (entry->overlayable_declarations[i].policy) { + switch (entry->overlayable_declarations[i].policy.value()) { + case Overlayable::Policy::kProduct: + printer->Print("_PRODUCT"); + break; + case Overlayable::Policy::kProductServices: + printer->Print("_PRODUCT_SERVICES"); + break; + case Overlayable::Policy::kSystem: + printer->Print("_SYSTEM"); + break; + case Overlayable::Policy::kVendor: + printer->Print("_VENDOR"); + break; + case Overlayable::Policy::kPublic: + printer->Print("_PUBLIC"); + break; + } + } } printer->Println(); @@ -408,6 +429,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/Diagnostics.h b/tools/aapt2/Diagnostics.h index 50e8b33ab9b1..30deb5555b21 100644 --- a/tools/aapt2/Diagnostics.h +++ b/tools/aapt2/Diagnostics.h @@ -133,11 +133,19 @@ class SourcePathDiagnostics : public IDiagnostics { void Log(Level level, DiagMessageActual& actual_msg) override { actual_msg.source.path = source_.path; diag_->Log(level, actual_msg); + if (level == Level::Error) { + error = true; + } + } + + bool HadError() { + return error; } private: Source source_; IDiagnostics* diag_; + bool error = false; DISALLOW_COPY_AND_ASSIGN(SourcePathDiagnostics); }; 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..b353ff00a23f 100644 --- a/tools/aapt2/LoadedApk.cpp +++ b/tools/aapt2/LoadedApk.cpp @@ -35,6 +35,43 @@ using ::std::unique_ptr; namespace aapt { +static ApkFormat DetermineApkFormat(io::IFileCollection* apk) { + if (apk->FindFile(kApkResourceTablePath) != nullptr) { + return ApkFormat::kBinary; + } else if (apk->FindFile(kProtoResourceTablePath) != nullptr) { + return ApkFormat::kProto; + } else { + // If the resource table is not present, attempt to read the manifest. + io::IFile* manifest_file = apk->FindFile(kAndroidManifestPath); + if (manifest_file == nullptr) { + return ApkFormat::kUnknown; + } + + // First try in proto format. + std::unique_ptr<io::InputStream> manifest_in = manifest_file->OpenInputStream(); + if (manifest_in != nullptr) { + pb::XmlNode pb_node; + io::ProtoInputStreamReader proto_reader(manifest_in.get()); + if (proto_reader.ReadMessage(&pb_node)) { + return ApkFormat::kProto; + } + } + + // If it didn't work, try in binary format. + std::unique_ptr<io::IData> manifest_data = manifest_file->OpenAsData(); + if (manifest_data != nullptr) { + std::string error; + std::unique_ptr<xml::XmlResource> manifest = + xml::Inflate(manifest_data->data(), manifest_data->size(), &error); + if (manifest != nullptr) { + return ApkFormat::kBinary; + } + } + + return ApkFormat::kUnknown; + } +} + std::unique_ptr<LoadedApk> LoadedApk::LoadApkFromPath(const StringPiece& path, IDiagnostics* diag) { Source source(path); std::string error; @@ -69,14 +106,14 @@ std::unique_ptr<LoadedApk> LoadedApk::LoadProtoApkFromFileCollection( return {}; } - io::ZeroCopyInputAdaptor adaptor(in.get()); - if (!pb_table.ParseFromZeroCopyStream(&adaptor)) { + io::ProtoInputStreamReader proto_reader(in.get()); + if (!proto_reader.ReadMessage(&pb_table)) { diag->Error(DiagMessage(source) << "failed to read " << kProtoResourceTablePath); return {}; } 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); @@ -97,8 +134,8 @@ std::unique_ptr<LoadedApk> LoadedApk::LoadProtoApkFromFileCollection( } pb::XmlNode pb_node; - io::ZeroCopyInputAdaptor manifest_adaptor(manifest_in.get()); - if (!pb_node.ParseFromZeroCopyStream(&manifest_adaptor)) { + io::ProtoInputStreamReader proto_reader(manifest_in.get()); + if (!proto_reader.ReadMessage(&pb_node)) { diag->Error(DiagMessage(source) << "failed to read proto " << kAndroidManifestPath); return {}; } @@ -120,7 +157,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 +221,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,41 +291,51 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table return true; } -ApkFormat LoadedApk::DetermineApkFormat(io::IFileCollection* apk) { - if (apk->FindFile(kApkResourceTablePath) != nullptr) { - return ApkFormat::kBinary; - } else if (apk->FindFile(kProtoResourceTablePath) != nullptr) { - return ApkFormat::kProto; - } else { - // If the resource table is not present, attempt to read the manifest. - io::IFile* manifest_file = apk->FindFile(kAndroidManifestPath); - if (manifest_file == nullptr) { - return ApkFormat::kUnknown; +std::unique_ptr<xml::XmlResource> LoadedApk::LoadXml(const std::string& file_path, + IDiagnostics* diag) const { + 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; } - // First try in proto format. - std::unique_ptr<io::InputStream> manifest_in = manifest_file->OpenInputStream(); - if (manifest_in != nullptr) { - pb::XmlNode pb_node; - io::ZeroCopyInputAdaptor manifest_adaptor(manifest_in.get()); - if (pb_node.ParseFromZeroCopyStream(&manifest_adaptor)) { - return ApkFormat::kProto; - } + pb::XmlNode pb_node; + io::ProtoInputStreamReader proto_reader(in.get()); + if (!proto_reader.ReadMessage(&pb_node)) { + diag->Error(DiagMessage() << "failed to parse file as proto XML"); + return nullptr; } - // If it didn't work, try in binary format. - std::unique_ptr<io::IData> manifest_data = manifest_file->OpenAsData(); - if (manifest_data != nullptr) { - std::string error; - std::unique_ptr<xml::XmlResource> manifest = - xml::Inflate(manifest_data->data(), manifest_data->size(), &error); - if (manifest != nullptr) { - return ApkFormat::kBinary; - } + 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; } - return ApkFormat::kUnknown; + 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; } } // namespace aapt diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h index 41f879d0cdc3..5b6f45ebb38d 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) const; private: DISALLOW_COPY_AND_ASSIGN(LoadedApk); @@ -115,8 +121,6 @@ class LoadedApk { std::unique_ptr<ResourceTable> table_; std::unique_ptr<xml::XmlResource> manifest_; ApkFormat format_; - - static ApkFormat DetermineApkFormat(io::IFileCollection* apk); }; } // namespace aapt diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 808b29cfd844..adf85b0ea8e8 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -29,6 +29,14 @@ #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 "io/FileStream.h" #include "util/Files.h" #include "util/Util.h" @@ -43,114 +51,132 @@ 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(text::Printer* printer, IDiagnostics* diagnostics) + : Command("aapt2"), diagnostics_(diagnostics) { + AddOptionalSubcommand(util::make_unique<CompileCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<LinkCommand>(diagnostics)); + AddOptionalSubcommand(util::make_unique<DumpCommand>(printer, 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(io::FileOutputStream* out, IDiagnostics* diagnostics) + : Command("daemon", "m"), out_(out), 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 { + text::Printer printer(out_); + 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()); + int result = MainCommand(&printer, diagnostics_).Execute(args, &std::cerr); + out_->Flush(); + if (result != 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: + io::FileOutputStream* out_; + 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; - } + // Use a smaller buffer so that there is less latency for printing to stdout. + constexpr size_t kStdOutBufferSize = 1024u; + aapt::io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); + aapt::text::Printer printer(&fout); + + aapt::StdErrDiagnostics diagnostics; + auto main_command = new aapt::MainCommand(&printer, &diagnostics); - aapt::RunDaemon(&diagnostics); - return 0; + // Add the daemon subcommand here so it cannot be called while executing the daemon + main_command->AddOptionalSubcommand( + aapt::util::make_unique<aapt::DaemonCommand>(&fout, &diagnostics)); + 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 1c1aeddf158a..dd5c751967b8 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 39ca80bbcf51..4f25e0968c0e 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -99,7 +99,7 @@ struct ParsedResource { ResourceId id; Visibility::Level visibility_level = Visibility::Level::kUndefined; bool allow_new = false; - bool overlayable = false; + std::vector<Overlayable> overlayable_declarations; std::string comment; std::unique_ptr<Value> value; @@ -133,11 +133,8 @@ static bool AddResourcesToTable(ResourceTable* table, IDiagnostics* diag, Parsed } } - if (res->overlayable) { - Overlayable overlayable; - overlayable.source = res->source; - overlayable.comment = res->comment; - if (!table->SetOverlayable(res->name, overlayable, diag)) { + for (auto& overlayable : res->overlayable_declarations) { + if (!table->AddOverlayable(res->name, overlayable, diag)) { return false; } } @@ -209,6 +206,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: @@ -245,6 +251,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; @@ -254,11 +261,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; @@ -334,6 +345,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; @@ -448,6 +469,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")) { @@ -646,7 +670,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, if (can_be_bag) { const auto bag_iter = elToBagMap.find(resource_type); if (bag_iter != elToBagMap.end()) { - // Ensure we have a name (unless this is a <public-group>). + // Ensure we have a name (unless this is a <public-group> or <overlayable>). if (resource_type != "public-group" && resource_type != "overlayable") { if (!maybe_name) { diag_->Error(DiagMessage(out_resource->source) @@ -775,7 +799,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 {}; } @@ -837,6 +862,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"); @@ -879,6 +910,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 @@ -1000,6 +1037,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 <" @@ -1017,69 +1059,135 @@ bool ResourceParser::ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource* out_resource) { if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) - << "ignoring configuration '" << out_resource->config << "' for <overlayable> tag"); + << "ignoring configuration '" << out_resource->config + << "' for <overlayable> tag"); } - if (Maybe<StringPiece> maybe_policy = xml::FindNonEmptyAttribute(parser, "policy")) { - const StringPiece& policy = maybe_policy.value(); - if (policy != "system") { - diag_->Error(DiagMessage(out_resource->source) - << "<overlayable> has invalid policy '" << policy << "'"); - return false; - } - } + std::string comment; + std::vector<Overlayable::Policy> policies; bool error = false; - const size_t depth = parser->depth(); - while (xml::XmlPullParser::NextChildNode(parser, depth)) { - if (parser->event() != xml::XmlPullParser::Event::kStartElement) { - // Skip text/comments. + const size_t start_depth = parser->depth(); + while (xml::XmlPullParser::IsGoodEvent(parser->Next())) { + xml::XmlPullParser::Event event = parser->event(); + if (event == xml::XmlPullParser::Event::kEndElement && parser->depth() == start_depth) { + // Break the loop when exiting the overyabale element + break; + } else if (event == xml::XmlPullParser::Event::kEndElement + && parser->depth() == start_depth + 1) { + // Clear the current policies when exiting the policy element + policies.clear(); + continue; + } else if (event == xml::XmlPullParser::Event::kComment) { + // Get the comment of individual item elements + comment = parser->comment(); + continue; + } else if (event != xml::XmlPullParser::Event::kStartElement) { + // Skip to the next element continue; } const Source item_source = source_.WithLine(parser->line_number()); - const std::string& element_namespace = parser->element_namespace(); const std::string& element_name = parser->element_name(); + const std::string& element_namespace = parser->element_namespace(); + if (element_namespace.empty() && element_name == "item") { - Maybe<StringPiece> maybe_name = xml::FindNonEmptyAttribute(parser, "name"); - if (!maybe_name) { - diag_->Error(DiagMessage(item_source) - << "<item> within an <overlayable> tag must have a 'name' attribute"); + if (!ParseOverlayableItem(parser, policies, comment, out_resource)) { error = true; - continue; } - - Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type"); - if (!maybe_type) { - diag_->Error(DiagMessage(item_source) - << "<item> within an <overlayable> tag must have a 'type' attribute"); + } else if (element_namespace.empty() && element_name == "policy") { + if (!policies.empty()) { + // If the policy list is not empty, then we are currently inside a policy element + diag_->Error(DiagMessage(item_source) << "<policy> blocks cannot be recursively nested"); error = true; - continue; + break; + } else if (Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { + // Parse the polices separated by vertical bar characters to allow for specifying multiple + // policies at once + for (StringPiece part : util::Tokenize(maybe_type.value(), '|')) { + StringPiece trimmed_part = util::TrimWhitespace(part); + if (trimmed_part == "public") { + policies.push_back(Overlayable::Policy::kPublic); + } else if (trimmed_part == "product") { + policies.push_back(Overlayable::Policy::kProduct); + } else if (trimmed_part == "product_services") { + policies.push_back(Overlayable::Policy::kProductServices); + } else if (trimmed_part == "system") { + policies.push_back(Overlayable::Policy::kSystem); + } else if (trimmed_part == "vendor") { + policies.push_back(Overlayable::Policy::kVendor); + } else { + diag_->Error(DiagMessage(out_resource->source) + << "<policy> has unsupported type '" << trimmed_part << "'"); + error = true; + continue; + } + } } + } else if (!ShouldIgnoreElement(element_namespace, element_name)) { + diag_->Error(DiagMessage(item_source) << "invalid element <" << element_name << "> in " + << " <overlayable>"); + error = true; + break; + } + } - const ResourceType* type = ParseResourceType(maybe_type.value()); - if (type == nullptr) { - diag_->Error(DiagMessage(out_resource->source) + return !error; +} + +bool ResourceParser::ParseOverlayableItem(xml::XmlPullParser* parser, + const std::vector<Overlayable::Policy>& policies, + const std::string& comment, + ParsedResource* out_resource) { + const Source item_source = source_.WithLine(parser->line_number()); + + Maybe<StringPiece> maybe_name = xml::FindNonEmptyAttribute(parser, "name"); + if (!maybe_name) { + diag_->Error(DiagMessage(item_source) + << "<item> within an <overlayable> tag must have a 'name' attribute"); + return false; + } + + Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type"); + if (!maybe_type) { + diag_->Error(DiagMessage(item_source) + << "<item> within an <overlayable> tag must have a 'type' attribute"); + return false; + } + + const ResourceType* type = ParseResourceType(maybe_type.value()); + if (type == nullptr) { + diag_->Error(DiagMessage(out_resource->source) << "invalid resource type '" << maybe_type.value() << "' in <item> within an <overlayable>"); - error = true; - continue; - } + return false; + } - ParsedResource child_resource; - child_resource.name.type = *type; - child_resource.name.entry = maybe_name.value().to_string(); - child_resource.source = item_source; - child_resource.overlayable = true; - out_resource->child_resources.push_back(std::move(child_resource)); + ParsedResource child_resource; + child_resource.name.type = *type; + child_resource.name.entry = maybe_name.value().to_string(); + child_resource.source = item_source; - xml::XmlPullParser::SkipCurrentElement(parser); - } else if (!ShouldIgnoreElement(element_namespace, element_name)) { - diag_->Error(DiagMessage(item_source) << ":" << element_name << ">"); - error = true; + if (policies.empty()) { + Overlayable overlayable; + overlayable.source = item_source; + overlayable.comment = comment; + child_resource.overlayable_declarations.push_back(overlayable); + } else { + for (Overlayable::Policy policy : policies) { + Overlayable overlayable; + overlayable.policy = policy; + overlayable.source = item_source; + overlayable.comment = comment; + child_resource.overlayable_declarations.push_back(overlayable); } } - return !error; + + if (options_.visibility) { + child_resource.visibility_level = options_.visibility.value(); + } + out_resource->child_resources.push_back(std::move(child_resource)); + return true; } bool ResourceParser::ParseAddResource(xml::XmlPullParser* parser, ParsedResource* out_resource) { @@ -1213,6 +1321,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)); @@ -1590,6 +1701,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 6cc7b76c1ad1..ebacd6f1280e 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; }; /* @@ -92,6 +96,10 @@ class ResourceParser { bool ParseSymbolImpl(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseOverlayable(xml::XmlPullParser* parser, ParsedResource* out_resource); + bool ParseOverlayableItem(xml::XmlPullParser* parser, + const std::vector<Overlayable::Policy>& policies, + const std::string& comment, + ParsedResource* out_resource); bool ParseAddResource(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseAttr(xml::XmlPullParser* parser, ParsedResource* out_resource); bool ParseAttrImpl(xml::XmlPullParser* parser, ParsedResource* out_resource, bool weak); diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 9de43c02fdca..c6f29ac53ca6 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -498,6 +498,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"/>)")); @@ -873,56 +891,162 @@ TEST_F(ResourceParserTest, ParsePlatformIndependentNewline) { ASSERT_TRUE(TestParse(R"(<string name="foo">%1$s %n %2$s</string>)")); } -TEST_F(ResourceParserTest, ParseOverlayableTagWithSystemPolicy) { - std::string input = R"( - <overlayable policy="illegal_policy"> +TEST_F(ResourceParserTest, ParseOverlayable) { + std::string input = R"(<overlayable />)"; + EXPECT_TRUE(TestParse(input)); + + input = R"( + <overlayable> <item type="string" name="foo" /> + <item type="drawable" name="bar" /> </overlayable>)"; - EXPECT_FALSE(TestParse(input)); + ASSERT_TRUE(TestParse(input)); + + auto search_result = table_.FindResource(test::ParseNameOrDie("string/foo")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_FALSE(search_result.value().entry->overlayable_declarations[0].policy); + + search_result = table_.FindResource(test::ParseNameOrDie("drawable/bar")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_FALSE(search_result.value().entry->overlayable_declarations[0].policy); +} + +TEST_F(ResourceParserTest, ParseOverlayablePolicy) { + std::string input = R"(<overlayable />)"; + EXPECT_TRUE(TestParse(input)); input = R"( - <overlayable policy="system"> - <item name="foo" /> + <overlayable> + <item type="string" name="foo" /> + <policy type="product"> + <item type="string" name="bar" /> + </policy> + <policy type="product_services"> + <item type="string" name="baz" /> + </policy> + <policy type="system"> + <item type="string" name="fiz" /> + </policy> + <policy type="vendor"> + <item type="string" name="fuz" /> + </policy> + <policy type="public"> + <item type="string" name="faz" /> + </policy> </overlayable>)"; - EXPECT_FALSE(TestParse(input)); + ASSERT_TRUE(TestParse(input)); - input = R"( - <overlayable policy="system"> - <item type="attr" /> + auto search_result = table_.FindResource(test::ParseNameOrDie("string/foo")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_FALSE(search_result.value().entry->overlayable_declarations[0].policy); + + search_result = table_.FindResource(test::ParseNameOrDie("string/bar")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProduct)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/baz")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProductServices)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/fiz")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kSystem)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/fuz")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kVendor)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/faz")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kPublic)); +} + +TEST_F(ResourceParserTest, ParseOverlayableBadPolicyError) { + std::string input = R"( + <overlayable> + <policy type="illegal_policy"> + <item type="string" name="foo" /> + </policy> </overlayable>)"; EXPECT_FALSE(TestParse(input)); input = R"( - <overlayable policy="system"> - <item type="bad_type" name="foo" /> + <overlayable> + <policy type="product"> + <item name="foo" /> + </policy> </overlayable>)"; EXPECT_FALSE(TestParse(input)); - input = R"(<overlayable policy="system" />)"; - EXPECT_TRUE(TestParse(input)); - - input = R"(<overlayable />)"; - EXPECT_TRUE(TestParse(input)); - input = R"( - <overlayable policy="system"> - <item type="string" name="foo" /> - <item type="dimen" name="foo" /> + <overlayable> + <policy type="vendor"> + <item type="string" /> + </policy> </overlayable>)"; - ASSERT_TRUE(TestParse(input)); + EXPECT_FALSE(TestParse(input)); +} - input = R"( +TEST_F(ResourceParserTest, ParseOverlayableMultiplePolicy) { + std::string input = R"( <overlayable> - <item type="string" name="bar" /> + <policy type="vendor|product_services"> + <item type="string" name="foo" /> + </policy> + <policy type="product|system"> + <item type="string" name="bar" /> + </policy> </overlayable>)"; ASSERT_TRUE(TestParse(input)); - Maybe<ResourceTable::SearchResult> search_result = - table_.FindResource(test::ParseNameOrDie("string/bar")); + auto search_result = table_.FindResource(test::ParseNameOrDie("string/foo")); ASSERT_TRUE(search_result); ASSERT_THAT(search_result.value().entry, NotNull()); EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); - EXPECT_TRUE(search_result.value().entry->overlayable); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(2)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kVendor)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kProductServices)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/bar")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(2)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProduct)); + EXPECT_THAT(search_result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kSystem)); } TEST_F(ResourceParserTest, DuplicateOverlayableIsError) { @@ -932,6 +1056,85 @@ TEST_F(ResourceParserTest, DuplicateOverlayableIsError) { <item type="string" name="foo" /> </overlayable>)"; EXPECT_FALSE(TestParse(input)); + + input = R"( + <overlayable> + <item type="string" name="foo" /> + </overlayable> + <overlayable> + <item type="string" name="foo" /> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); + + input = R"( + <overlayable"> + <policy type="product"> + <item type="string" name="foo" /> + <item type="string" name="foo" /> + </policy> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); + + input = R"( + <overlayable> + <policy type="product"> + <item type="string" name="foo" /> + </policy> + </overlayable> + + <overlayable> + <policy type="product"> + <item type="string" name="foo" /> + </policy> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); +} + +TEST_F(ResourceParserTest, PolicyAndNonPolicyOverlayableError) { + std::string input = R"( + <overlayable policy="product"> + <item type="string" name="foo" /> + </overlayable> + <overlayable policy=""> + <item type="string" name="foo" /> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); + + input = R"( + <overlayable policy=""> + <item type="string" name="foo" /> + </overlayable> + <overlayable policy="product"> + <item type="string" name="foo" /> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); +} + +TEST_F(ResourceParserTest, DuplicateOverlayableMultiplePolicyError) { + std::string input = R"( + <overlayable> + <policy type="vendor|product"> + <item type="string" name="foo" /> + </policy> + </overlayable> + <overlayable> + <policy type="product_services|vendor"> + <item type="string" name="foo" /> + </policy> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); +} + +TEST_F(ResourceParserTest, NestPolicyInOverlayableError) { + std::string input = R"( + <overlayable> + <policy type="vendor|product"> + <policy type="product_services"> + <item type="string" name="foo" /> + </policy> + </policy> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); } TEST_F(ResourceParserTest, ParseIdItem) { @@ -972,4 +1175,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 c2274d04cc8c..bc8a4d1f85b8 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -26,6 +26,7 @@ #include "androidfw/ConfigDescription.h" #include "androidfw/ResourceTypes.h" +#include "Debug.h" #include "NameMangler.h" #include "ResourceValues.h" #include "ValueVisitor.h" @@ -39,8 +40,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> @@ -50,9 +52,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 { @@ -116,42 +118,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) { @@ -303,9 +315,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; @@ -322,15 +340,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, @@ -338,14 +358,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, @@ -364,7 +388,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, @@ -372,7 +396,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, @@ -399,37 +423,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); @@ -451,17 +495,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, @@ -485,28 +534,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; } @@ -562,17 +625,17 @@ bool ResourceTable::SetAllowNewImpl(const ResourceNameRef& name, const AllowNew& return true; } -bool ResourceTable::SetOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, +bool ResourceTable::AddOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, IDiagnostics* diag) { - return SetOverlayableImpl(name, overlayable, ResourceNameValidator, diag); + return AddOverlayableImpl(name, overlayable, ResourceNameValidator, diag); } -bool ResourceTable::SetOverlayableMangled(const ResourceNameRef& name, +bool ResourceTable::AddOverlayableMangled(const ResourceNameRef& name, const Overlayable& overlayable, IDiagnostics* diag) { - return SetOverlayableImpl(name, overlayable, SkipNameValidator, diag); + return AddOverlayableImpl(name, overlayable, SkipNameValidator, diag); } -bool ResourceTable::SetOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, +bool ResourceTable::AddOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, NameValidator name_validator, IDiagnostics* diag) { CHECK(diag != nullptr); @@ -583,13 +646,28 @@ bool ResourceTable::SetOverlayableImpl(const ResourceNameRef& name, const Overla ResourceTablePackage* package = FindOrCreatePackage(name.package); ResourceTableType* type = package->FindOrCreateType(name.type); ResourceEntry* entry = type->FindOrCreateEntry(name.entry); - if (entry->overlayable) { - diag->Error(DiagMessage(overlayable.source) - << "duplicate overlayable declaration for resource '" << name << "'"); - diag->Error(DiagMessage(entry->overlayable.value().source) << "previous declaration here"); - return false; + + for (auto& overlayable_declaration : entry->overlayable_declarations) { + // An overlayable resource cannot be declared twice with the same policy + if (overlayable.policy == overlayable_declaration.policy) { + diag->Error(DiagMessage(overlayable.source) + << "duplicate overlayable declaration for resource '" << name << "'"); + diag->Error(DiagMessage(overlayable_declaration.source) << "previous declaration here"); + return false; + } + + // An overlayable resource cannot be declared once with a policy and without a policy because + // the policy becomes unused + if (!overlayable.policy || !overlayable_declaration.policy) { + diag->Error(DiagMessage(overlayable.source) + << "overlayable resource '" << name << "'" + << " declared once with a policy and once with no policy"); + diag->Error(DiagMessage(overlayable_declaration.source) << "previous declaration here"); + return false; + } } - entry->overlayable = overlayable; + + entry->overlayable_declarations.push_back(overlayable); return true; } @@ -625,7 +703,7 @@ std::unique_ptr<ResourceTable> ResourceTable::Clone() const { new_entry->id = entry->id; new_entry->visibility = entry->visibility; new_entry->allow_new = entry->allow_new; - new_entry->overlayable = entry->overlayable; + new_entry->overlayable_declarations = entry->overlayable_declarations; for (const auto& config_value : entry->values) { ResourceConfigValue* new_value = diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index 7b19a3188a76..3dd0a769d944 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -57,8 +57,27 @@ struct AllowNew { std::string comment; }; -// The policy dictating whether an entry is overlayable at runtime by RROs. +// Represents a declaration that a resource is overayable at runtime. struct Overlayable { + // Represents the types overlays that are allowed to overlay the resource. + enum class Policy { + // The resource can be overlaid by any overlay. + kPublic, + + // The resource can be overlaid by any overlay on the system partition. + kSystem, + + // The resource can be overlaid by any overlay on the vendor partition. + kVendor, + + // The resource can be overlaid by any overlay on the product partition. + kProduct, + + // The resource can be overlaid by any overlay on the product services partition. + kProductServices, + }; + + Maybe<Policy> policy; Source source; std::string comment; }; @@ -96,7 +115,8 @@ class ResourceEntry { Maybe<AllowNew> allow_new; - Maybe<Overlayable> overlayable; + // The declarations of this resource as overlayable for RROs + std::vector<Overlayable> overlayable_declarations; // The resource's values for each configuration. std::vector<std::unique_ptr<ResourceConfigValue>> values; @@ -146,8 +166,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 +185,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 +197,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 android::ConfigDescription& config, const android::StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag); @@ -209,6 +236,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); @@ -217,9 +246,9 @@ class ResourceTable { bool SetVisibilityWithIdMangled(const ResourceNameRef& name, const Visibility& visibility, const ResourceId& res_id, IDiagnostics* diag); - bool SetOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, + bool AddOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, IDiagnostics* diag); - bool SetOverlayableMangled(const ResourceNameRef& name, const Overlayable& overlayable, + bool AddOverlayableMangled(const ResourceNameRef& name, const Overlayable& overlayable, IDiagnostics* diag); bool SetAllowNew(const ResourceNameRef& name, const AllowNew& allow_new, IDiagnostics* diag); @@ -294,13 +323,16 @@ class ResourceTable { bool SetAllowNewImpl(const ResourceNameRef& name, const AllowNew& allow_new, NameValidator name_validator, IDiagnostics* diag); - bool SetOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, + bool AddOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, NameValidator name_validator, IDiagnostics* diag); bool SetSymbolStateImpl(const ResourceNameRef& name, const ResourceId& res_id, 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 1a1f73fa4bb9..7c28f07d0f66 100644 --- a/tools/aapt2/ResourceTable_test.cpp +++ b/tools/aapt2/ResourceTable_test.cpp @@ -242,21 +242,109 @@ TEST(ResourceTableTest, SetAllowNew) { ASSERT_THAT(result.value().entry->allow_new.value().comment, StrEq("second")); } -TEST(ResourceTableTest, SetOverlayable) { +TEST(ResourceTableTest, AddOverlayable) { ResourceTable table; const ResourceName name = test::ParseNameOrDie("android:string/foo"); Overlayable overlayable; - + overlayable.policy = Overlayable::Policy::kProduct; overlayable.comment = "first"; - ASSERT_TRUE(table.SetOverlayable(name, overlayable, test::GetDiagnostics())); + ASSERT_TRUE(table.AddOverlayable(name, overlayable, test::GetDiagnostics())); Maybe<ResourceTable::SearchResult> result = table.FindResource(name); ASSERT_TRUE(result); - ASSERT_TRUE(result.value().entry->overlayable); - ASSERT_THAT(result.value().entry->overlayable.value().comment, StrEq("first")); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(1)); + ASSERT_THAT(result.value().entry->overlayable_declarations[0].comment, StrEq("first")); + ASSERT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProduct)); + + Overlayable overlayable2; + overlayable2.comment = "second"; + overlayable2.policy = Overlayable::Policy::kProductServices; + ASSERT_TRUE(table.AddOverlayable(name, overlayable2, test::GetDiagnostics())); + result = table.FindResource(name); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(2)); + ASSERT_THAT(result.value().entry->overlayable_declarations[0].comment, StrEq("first")); + ASSERT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProduct)); + ASSERT_THAT(result.value().entry->overlayable_declarations[1].comment, StrEq("second")); + ASSERT_THAT(result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kProductServices)); +} + +TEST(ResourceTableTest, AddDuplicateOverlayableFail) { + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + Overlayable overlayable; + overlayable.policy = Overlayable::Policy::kProduct; + ASSERT_TRUE(table.AddOverlayable(name, overlayable, test::GetDiagnostics())); + + Overlayable overlayable2; + overlayable2.policy = Overlayable::Policy::kProduct; + ASSERT_FALSE(table.AddOverlayable(name, overlayable2, test::GetDiagnostics())); +} + +TEST(ResourceTableTest, AddOverlayablePolicyAndNoneFirstFail) { + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + ASSERT_TRUE(table.AddOverlayable(name, {}, test::GetDiagnostics())); + + Overlayable overlayable2; + overlayable2.policy = Overlayable::Policy::kProduct; + ASSERT_FALSE(table.AddOverlayable(name, overlayable2, test::GetDiagnostics())); +} + +TEST(ResourceTableTest, AddOverlayablePolicyAndNoneLastFail) { + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + Overlayable overlayable; + overlayable.policy = Overlayable::Policy::kProduct; + ASSERT_TRUE(table.AddOverlayable(name, overlayable, test::GetDiagnostics())); + + ASSERT_FALSE(table.AddOverlayable(name, {}, test::GetDiagnostics())); +} - overlayable.comment = "second"; - 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 82d9e041b41a..dbe5ac5adb98 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -39,6 +39,8 @@ using ::android::base::StringPrintf; namespace aapt { namespace ResourceUtils { +constexpr int32_t kNonBreakingSpace = 0xa0; + Maybe<ResourceName> ToResourceName( const android::ResTable::resource_name& name_in) { ResourceName name_out; @@ -798,16 +800,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_ && codepoint != kNonBreakingSpace && iswspace(codepoint)) { if (!last_codepoint_was_space_) { // Emit a space if it's the first. xml_string_.text += ' '; @@ -828,7 +834,6 @@ StringBuilder& StringBuilder::AppendText(const std::string& text) { case U't': xml_string_.text += '\t'; break; - case U'n': xml_string_.text += '\n'; break; @@ -856,12 +861,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 e2f1c89bfd4c..e282fd58d261 100644 --- a/tools/aapt2/ResourceUtils.h +++ b/tools/aapt2/ResourceUtils.h @@ -269,8 +269,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/Resources.proto b/tools/aapt2/Resources.proto index d7a377176fc5..bf9fe49da2d6 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -133,13 +133,25 @@ message AllowNew { string comment = 2; } -// Whether a resource is overlayable by runtime resource overlays (RRO). +// Represents a declaration that a resource is overayable at runtime. message Overlayable { + enum Policy { + NONE = 0; + PUBLIC = 1; + SYSTEM = 2; + VENDOR = 3; + PRODUCT = 4; + PRODUCT_SERVICES = 5; + } + // Where this declaration was defined in source. Source source = 1; // Any comment associated with the declaration. string comment = 2; + + // The policy of the overlayable declaration + Policy policy = 3; } // An entry ID in the range [0x0000, 0xffff]. @@ -169,7 +181,7 @@ message Entry { AllowNew allow_new = 4; // Whether this resource can be overlaid by a runtime resource overlay (RRO). - Overlayable overlayable = 5; + repeated Overlayable overlayable = 5; // The set of values defined for this entry, each corresponding to a different // configuration/variant. 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..9a7238b584ba 100644 --- a/tools/aapt2/StringPool_test.cpp +++ b/tools/aapt2/StringPool_test.cpp @@ -303,6 +303,37 @@ TEST(StringPoolTest, Flatten) { } } +TEST(StringPoolTest, ModifiedUTF8) { + 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 codepoints are encoded using two three-byte surrogate pairs + ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), NO_ERROR); + size_t len; + const char* str = test.string8At(0, &len); + ASSERT_THAT(str, NotNull()); + EXPECT_THAT(std::string(str, len), Eq("\xED\xA0\x81\xED\xB0\x80")); + str = test.string8At(1, &len); + ASSERT_THAT(str, NotNull()); + EXPECT_THAT(std::string(str, len), Eq("foo \xED\xA0\x81\xED\xB0\xB7 bar")); + str = test.string8At(2, &len); + ASSERT_THAT(str, NotNull()); + EXPECT_THAT(std::string(str, len), Eq("\xED\xA0\x81\xED\xB0\x80\xED\xA0\x81\xED\xB0\xB7")); + + // Check that retrieving the strings returns the original UTF-8 character bytes + EXPECT_THAT(util::GetString(test, 0), Eq("\xF0\x90\x90\x80")); + EXPECT_THAT(util::GetString(test, 1), Eq("foo \xF0\x90\x90\xB7 bar")); + EXPECT_THAT(util::GetString(test, 2), Eq("\xF0\x90\x90\x80\xF0\x90\x90\xB7")); +} TEST(StringPoolTest, MaxEncodingLength) { StdErrDiagnostics diag; diff --git a/tools/aapt2/Flags.cpp b/tools/aapt2/cmd/Command.cpp index 84977ab424cc..bdee5c9d4909 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,117 @@ 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, bool experimental) { + subcommand->fullname_ = name_ + " " + subcommand->name_; + if (experimental) { + experimental_subcommands_.push_back(std::move(subcommand)); + } else { + 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 +148,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 +160,35 @@ 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 subcommand 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); + } + } + for (auto& subcommand : experimental_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 +198,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 +213,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..16949883d123 --- /dev/null +++ b/tools/aapt2/cmd/Command.h @@ -0,0 +1,91 @@ +/* + * 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, bool experimental = false); + + 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_; + std::vector<std::unique_ptr<Command>> experimental_subcommands_; +}; + +} // namespace aapt + +#endif // AAPT_COMMAND_H diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 411ad747faa3..fc9514a691d2 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 "google/protobuf/io/zero_copy_stream_impl_lite.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" @@ -71,9 +74,9 @@ struct ResourcePathData { }; // Resource file paths are expected to look like: [--/res/]type[-config]/name -static Maybe<ResourcePathData> ExtractResourcePathData(const std::string& path, +static Maybe<ResourcePathData> ExtractResourcePathData(const std::string& path, const char dir_sep, std::string* out_error) { - std::vector<std::string> parts = util::Split(path, file::sDirSep); + std::vector<std::string> parts = util::Split(path, dir_sep); if (parts.size() < 2) { if (out_error) *out_error = "bad resource path"; return {}; @@ -121,16 +124,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; @@ -145,81 +138,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; @@ -227,8 +159,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; } @@ -294,36 +230,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); + } } } } @@ -402,7 +350,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"); @@ -410,18 +358,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); @@ -502,7 +449,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"); @@ -516,15 +463,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); @@ -592,7 +541,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, {})) { @@ -606,41 +555,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 { @@ -695,79 +634,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, inputs->GetDirSeparator(), &err_str)) { + path_data = maybe_path_data.value(); + } else { + context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << err_str); error = true; continue; } @@ -785,13 +678,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; } @@ -801,17 +694,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..c0c05cda35e7 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -17,80 +17,156 @@ #include "Compile.h" #include "android-base/file.h" +#include "android-base/stringprintf.h" +#include "android-base/utf8.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) { +std::string BuildPath(std::vector<std::string> args) { + std::string out; + if (args.empty()) { + return out; + } + out = args[0]; + for (int i = 1; i < args.size(); i++) { + file::AppendPath(&out, args[i]); + } + return out; +} + +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) { StdErrDiagnostics diag; std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); - const std::string kResDir = android::base::Dirname(android::base::GetExecutablePath()) - + "/integration-tests/CompileTest/res"; + const std::string kResDir = BuildPath({android::base::Dirname(android::base::GetExecutablePath()), + "integration-tests", "CompileTest", "res"}); // Resource files without periods in the file name should not throw errors - const std::string path0 = kResDir + "/values/values.xml"; - const std::string path0_out = kResDir + "/values_values.arsc.flat"; - - remove(path0_out.c_str()); + const std::string path0 = BuildPath({kResDir, "values", "values.xml"}); + const std::string path0_out = BuildPath({kResDir, "values_values.arsc.flat"}); + ::android::base::utf8::unlink(path0_out.c_str()); ASSERT_EQ(TestCompile(path0, kResDir, /** legacy */ false, diag), 0); - ASSERT_EQ(remove(path0_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path0_out.c_str()), 0); ASSERT_EQ(TestCompile(path0, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path0_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path0_out.c_str()), 0); - const std::string path1 = kResDir + "/drawable/image.png"; - const std::string path1_out = kResDir + "/drawable_image.png.flat"; - remove(path1_out.c_str()); + const std::string path1 = BuildPath({kResDir, "drawable", "image.png"}); + const std::string path1_out = BuildPath({kResDir, "drawable_image.png.flat"}); + ::android::base::utf8::unlink(path1_out.c_str()); ASSERT_EQ(TestCompile(path1, kResDir, /** legacy */ false, diag), 0); - ASSERT_EQ(remove(path1_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path1_out.c_str()), 0); ASSERT_EQ(TestCompile(path1, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path1_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path1_out.c_str()), 0); - const std::string path2 = kResDir + "/drawable/image.9.png"; - const std::string path2_out = kResDir + "/drawable_image.9.png.flat"; - remove(path2_out.c_str()); + const std::string path2 = BuildPath({kResDir, "drawable", "image.9.png"}); + const std::string path2_out = BuildPath({kResDir, "drawable_image.9.png.flat"}); + ::android::base::utf8::unlink(path2_out.c_str()); ASSERT_EQ(TestCompile(path2, kResDir, /** legacy */ false, diag), 0); - ASSERT_EQ(remove(path2_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path2_out.c_str()), 0); ASSERT_EQ(TestCompile(path2, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path2_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path2_out.c_str()), 0); // Resource files with periods in the file name should fail on non-legacy compilations - const std::string path3 = kResDir + "/values/values.all.xml"; - const std::string path3_out = kResDir + "/values_values.all.arsc.flat"; - remove(path3_out.c_str()); + const std::string path3 = BuildPath({kResDir, "values", "values.all.xml"}); + const std::string path3_out = BuildPath({kResDir, "values_values.all.arsc.flat"}); + ::android::base::utf8::unlink(path3_out.c_str()); ASSERT_NE(TestCompile(path3, kResDir, /** legacy */ false, diag), 0); - ASSERT_NE(remove(path3_out.c_str()), 0); + ASSERT_NE(::android::base::utf8::unlink(path3_out.c_str()), 0); ASSERT_EQ(TestCompile(path3, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path3_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path3_out.c_str()), 0); - const std::string path4 = kResDir + "/drawable/image.small.png"; - const std::string path4_out = (kResDir + std::string("/drawable_image.small.png.flat")).c_str(); - remove(path4_out.c_str()); + const std::string path4 = BuildPath({kResDir, "drawable", "image.small.png"}); + const std::string path4_out = BuildPath({kResDir, "drawable_image.small.png.flat"}); + ::android::base::utf8::unlink(path4_out.c_str()); ASSERT_NE(TestCompile(path4, kResDir, /** legacy */ false, diag), 0); - ASSERT_NE(remove(path4_out.c_str()), 0); + ASSERT_NE(::android::base::utf8::unlink(path4_out.c_str()), 0); ASSERT_EQ(TestCompile(path4, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path4_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path4_out.c_str()), 0); - const std::string path5 = kResDir + "/drawable/image.small.9.png"; - const std::string path5_out = (kResDir + std::string("/drawable_image.small.9.png.flat")).c_str(); - remove(path5_out.c_str()); + const std::string path5 = BuildPath({kResDir, "drawable", "image.small.9.png"}); + const std::string path5_out = BuildPath({kResDir, "drawable_image.small.9.png.flat"}); + ::android::base::utf8::unlink(path5_out.c_str()); ASSERT_NE(TestCompile(path5, kResDir, /** legacy */ false, diag), 0); - ASSERT_NE(remove(path5_out.c_str()), 0); + ASSERT_NE(::android::base::utf8::unlink(path5_out.c_str()), 0); ASSERT_EQ(TestCompile(path5, kResDir, /** legacy */ true, diag), 0); - ASSERT_EQ(remove(path5_out.c_str()), 0); + ASSERT_EQ(::android::base::utf8::unlink(path5_out.c_str()), 0); +} + +TEST(CompilerTest, DirInput) { + StdErrDiagnostics diag; + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const std::string kResDir = BuildPath({android::base::Dirname(android::base::GetExecutablePath()), + "integration-tests", "CompileTest", "DirInput", "res"}); + const std::string kOutputFlata = + BuildPath({android::base::Dirname(android::base::GetExecutablePath()), "integration-tests", + "CompileTest", "DirInput", "compiled.flata"}); + ::android::base::utf8::unlink(kOutputFlata.c_str()); + + std::vector<android::StringPiece> args; + args.push_back("--dir"); + args.push_back(kResDir); + args.push_back("-o"); + args.push_back(kOutputFlata); + args.push_back("-v"); + 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(::android::base::utf8::unlink(kOutputFlata.c_str()), 0); +} + +TEST(CompilerTest, ZipInput) { + StdErrDiagnostics diag; + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const std::string kResZip = + BuildPath({android::base::Dirname(android::base::GetExecutablePath()), "integration-tests", + "CompileTest", "ZipInput", "res.zip"}); + const std::string kOutputFlata = + BuildPath({android::base::Dirname(android::base::GetExecutablePath()), "integration-tests", + "CompileTest", "ZipInput", "compiled.flata"}); + + ::android::base::utf8::unlink(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(::android::base::utf8::unlink(kOutputFlata.c_str()), 0); } -}
\ No newline at end of file +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index eb307fb1ddce..3ea17552ea7c 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" @@ -45,7 +46,7 @@ class IApkSerializer { IApkSerializer(IAaptContext* context, const Source& source) : context_(context), source_(source) {} virtual bool SerializeXml(const xml::XmlResource* xml, const std::string& path, bool utf16, - IArchiveWriter* writer) = 0; + IArchiveWriter* writer, uint32_t compression_flags) = 0; virtual bool SerializeTable(ResourceTable* table, IArchiveWriter* writer) = 0; virtual bool SerializeFile(FileReference* file, IArchiveWriter* writer) = 0; @@ -58,7 +59,10 @@ class IApkSerializer { bool ConvertApk(IAaptContext* context, unique_ptr<LoadedApk> apk, IApkSerializer* serializer, IArchiveWriter* writer) { - if (!serializer->SerializeXml(apk->GetManifest(), kAndroidManifestPath, true /*utf16*/, writer)) { + io::IFile* manifest = apk->GetFileCollection()->FindFile(kAndroidManifestPath); + if (!serializer->SerializeXml(apk->GetManifest(), kAndroidManifestPath, true /*utf16*/, writer, + (manifest != nullptr && manifest->WasCompressed()) + ? ArchiveEntry::kCompress : 0u)) { context->GetDiagnostics()->Error(DiagMessage(apk->GetSource()) << "failed to serialize AndroidManifest.xml"); return false; @@ -104,10 +108,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 || @@ -135,7 +136,7 @@ class BinaryApkSerializer : public IApkSerializer { : IApkSerializer(context, source), tableFlattenerOptions_(options) {} bool SerializeXml(const xml::XmlResource* xml, const std::string& path, bool utf16, - IArchiveWriter* writer) override { + IArchiveWriter* writer, uint32_t compression_flags) override { BigBuffer buffer(4096); XmlFlattenerOptions options = {}; options.use_utf16 = utf16; @@ -146,8 +147,7 @@ class BinaryApkSerializer : public IApkSerializer { } io::BigBufferInputStream input_stream(&buffer); - return io::CopyInputStreamToArchive(context_, &input_stream, path, ArchiveEntry::kCompress, - writer); + return io::CopyInputStreamToArchive(context_, &input_stream, path, compression_flags, writer); } bool SerializeTable(ResourceTable* table, IArchiveWriter* writer) override { @@ -172,8 +172,8 @@ class BinaryApkSerializer : public IApkSerializer { } pb::XmlNode pb_node; - io::ZeroCopyInputAdaptor adaptor(in.get()); - if (!pb_node.ParseFromZeroCopyStream(&adaptor)) { + io::ProtoInputStreamReader proto_reader(in.get()); + if (!proto_reader.ReadMessage(&pb_node)) { context_->GetDiagnostics()->Error(DiagMessage(source_) << "failed to parse proto XML " << *file->path); return false; @@ -188,7 +188,8 @@ class BinaryApkSerializer : public IApkSerializer { return false; } - if (!SerializeXml(xml.get(), *file->path, false /*utf16*/, writer)) { + if (!SerializeXml(xml.get(), *file->path, false /*utf16*/, writer, + file->file->WasCompressed() ? ArchiveEntry::kCompress : 0u)) { context_->GetDiagnostics()->Error(DiagMessage(source_) << "failed to serialize to binary XML: " << *file->path); return false; @@ -218,10 +219,10 @@ class ProtoApkSerializer : public IApkSerializer { : IApkSerializer(context, source) {} bool SerializeXml(const xml::XmlResource* xml, const std::string& path, bool utf16, - IArchiveWriter* writer) override { + IArchiveWriter* writer, uint32_t compression_flags) override { pb::XmlNode pb_node; SerializeXmlResourceToPb(*xml, &pb_node); - return io::CopyProtoToArchive(context_, &pb_node, path, ArchiveEntry::kCompress, writer); + return io::CopyProtoToArchive(context_, &pb_node, path, compression_flags, writer); } bool SerializeTable(ResourceTable* table, IArchiveWriter* writer) override { @@ -248,7 +249,8 @@ class ProtoApkSerializer : public IApkSerializer { return false; } - if (!SerializeXml(xml.get(), *file->path, false /*utf16*/, writer)) { + if (!SerializeXml(xml.get(), *file->path, false /*utf16*/, writer, + file->file->WasCompressed() ? ArchiveEntry::kCompress : 0u)) { context_->GetDiagnostics()->Error(DiagMessage(source_) << "failed to serialize to proto XML: " << *file->path); return false; @@ -321,37 +323,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 +350,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..a23a6a46cf0f 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -14,18 +14,22 @@ * limitations under the License. */ +#include "Dump.h" + #include <cinttypes> #include <vector> #include "android-base/stringprintf.h" +#include "androidfw/ConfigDescription.h" #include "androidfw/StringPiece.h" #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 +43,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,210 +74,6 @@ 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; - } - - 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; - } - - 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; - } - - 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"); - } - - Debug::DumpXml(*doc, printer); - return true; -} - -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); - - 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; - - std::unique_ptr<io::IData> data = file->OpenAsData(); - if (data == nullptr) { - context->GetDiagnostics()->Error(DiagMessage(file_path) << "failed to open resources.pb"); - return false; - } - - pb::ResourceTable pb_table; - if (!pb_table.ParseFromArray(data->data(), data->size())) { - context->GetDiagnostics()->Error(DiagMessage(file_path) << "invalid resources.pb"); - return false; - } - - 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; - } - - BinaryResourceParser parser(context->GetDiagnostics(), &table, Source(file_path), - data->data(), data->size()); - if (!parser.Parse()) { - return 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; - } - - 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"); - return false; - } - return DumpXmlFile(context, file, proto, &printer); - } - - err.clear(); - - io::FileInputStream input(file_path); - if (input.HadError()) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to open file: " << input.GetError()); - return false; - } - - // 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; - } - - 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; - } - - ResourceTable table; - err.clear(); - if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &err)) { - context->GetDiagnostics()->Error(DiagMessage(file_path) - << "failed to parse table: " << err); - continue; - } - - 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; - } - - 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; - } - - printer.Indent(); - DumpCompiledFile(file, Source(file_path), offset, length, &printer); - printer.Undent(); - } - } - return true; -} - -static bool DumpPackageName(IAaptContext* context, const std::string& file_path) { - auto loaded_apk = LoadedApk::LoadApkFromPath(file_path, context->GetDiagnostics()); - if (!loaded_apk) { - return false; - } - - 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; - } - - xml::Attribute* attr = manifest_el->FindAttribute({}, "package"); - if (!attr) { - context->GetDiagnostics()->Error(DiagMessage() << "No package name."); - return false; - } - printer.Println(StringPrintf("%s", attr->value.c_str())); - - return true; -} - namespace { class DumpContext : public IAaptContext { @@ -332,44 +125,423 @@ class DumpContext : public IAaptContext { } // namespace -// 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)) { +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; + } + + bool error = false; + for (auto container : args) { + io::FileInputStream input(container); + if (input.HadError()) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to open file: " << input.GetError()); + error = true; + continue; + } + + // Try as a compiled file. + ContainerReader reader(&input); + if (reader.HadError()) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to read container: " << reader.GetError()); + error = true; + continue; + } + + 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()); + error = true; + continue; + } + + ResourceTable table; + error.clear(); + if (!DeserializeTableFromPb(pb_table, nullptr /*files*/, &table, &error)) { + context.GetDiagnostics()->Error(DiagMessage(container) + << "failed to parse table: " << error); + error = true; + 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()); + error = true; + continue; + } + + ResourceFile file; + if (!DeserializeCompiledFileFromPb(pb_compiled_file, &file, &error)) { + context.GetDiagnostics()->Warn(DiagMessage(container) + << "failed to parse compiled file: " << error); + error = true; + continue; + } + + printer_->Indent(); + DumpCompiledFile(file, Source(container), offset, length, printer_); + printer_->Undent(); + } + } + } + + return (error) ? 1 : 0; +} + +int DumpBadgerCommand::Action(const std::vector<std::string>& args) { + printer_->Print(StringPrintf("%s", kBadgerData)); + printer_->Print("Did you mean \"aapt2 dump badging\"?\n"); + return 1; +} + +int DumpConfigsCommand::Dump(LoadedApk* apk) { + ResourceTable* table = apk->GetResourceTable(); + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + // Comparison function used to order configurations + auto compare = [](android::ConfigDescription c1, android::ConfigDescription c2) -> bool { + return c1.compare(c2) < 0; + }; + + // Insert the configurations into a set in order to keep every configuarion seen + std::set<android::ConfigDescription, decltype(compare)> configs(compare); + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + for (auto& value : entry->values) { + configs.insert(value->config); + } + } + } + } + + // Print the configurations in order + for (auto& config : configs) { + GetPrinter()->Print(StringPrintf("%s\n", config.to_string().data())); + } + return 0; +} + +int DumpPackageNameCommand::Dump(LoadedApk* apk) { + Maybe<std::string> package_name = GetPackageName(apk); + if (!package_name) { + return 1; + } + + GetPrinter()->Println(package_name.value()); + return 0; +} + +int DumpStringsCommand::Dump(LoadedApk* apk) { + ResourceTable* table = apk->GetResourceTable(); + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + // Load the run-time xml string pool using the flattened data + BigBuffer buffer(4096); + StringPool::FlattenUtf8(&buffer, table->string_pool, GetDiagnostics()); + auto data = buffer.to_string(); + android::ResStringPool pool(data.data(), data.size(), false); + Debug::DumpResStringPool(&pool, GetPrinter()); + return 0; +} + +int DumpStyleParentCommand::Dump(LoadedApk* apk) { + Maybe<std::string> package_name = GetPackageName(apk); + if (!package_name) { + return 1; + } + + const auto target_style = ResourceName(package_name.value(), ResourceType::kStyle, style_); + const auto table = apk->GetResourceTable(); + + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + Maybe<ResourceTable::SearchResult> target = table->FindResource(target_style); + if (!target) { + GetDiagnostics()->Error( + DiagMessage() << "Target style \"" << target_style.entry << "\" does not exist"); return 1; } + Debug::PrintStyleGraph(table, target_style); + return 0; +} + +int DumpTableCommand::Dump(LoadedApk* apk) { + if (apk->GetApkFormat() == ApkFormat::kProto) { + GetPrinter()->Println("Proto APK"); + } else { + GetPrinter()->Println("Binary APK"); + } + + ResourceTable* table = apk->GetResourceTable(); + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + DebugPrintTableOptions print_options; + print_options.show_sources = true; + print_options.show_values = !no_values_; + Debug::PrintTable(*table, print_options, GetPrinter()); + return 0; +} + +int DumpXmlStringsCommand::Dump(LoadedApk* apk) { DumpContext context; - context.SetVerbose(verbose); - - 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)) { - return 1; + bool error = false; + for (auto xml_file : files_) { + android::ResXMLTree tree; + + if (apk->GetApkFormat() == ApkFormat::kProto) { + auto xml = apk->LoadXml(xml_file, GetDiagnostics()); + if (!xml) { + error = true; + continue; + } + + // 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())) { + error = true; + continue; + } + + // Load the run-time xml tree using the flattened data + std::string data = buffer.to_string(); + tree.setTo(data.data(), data.size(), /** copyData */ true); + + } else if (apk->GetApkFormat() == ApkFormat::kBinary) { + io::IFile* file = apk->GetFileCollection()->FindFile(xml_file); + if (!file) { + GetDiagnostics()->Error(DiagMessage(xml_file) + << "File '" << xml_file << "' not found in APK"); + error = true; + continue; + } + + std::unique_ptr<io::IData> data = file->OpenAsData(); + if (!data) { + GetDiagnostics()->Error(DiagMessage() << "Failed to open " << xml_file); + error = true; + continue; } + + // Load the run-time xml tree from the file data + tree.setTo(data->data(), data->size(), /** copyData */ true); + } else { + GetDiagnostics()->Error(DiagMessage(apk->GetSource()) << "Unknown APK format"); + error = true; + continue; } - return 0; + + Debug::DumpResStringPool(&tree.getStrings(), GetPrinter()); } + return (error) ? 1 : 0; +} - options.print_options.show_sources = true; - options.print_options.show_values = !no_values; - for (const std::string& arg : parsedArgs) { - if (!TryDumpFile(&context, arg, options)) { +int DumpXmlTreeCommand::Dump(LoadedApk* apk) { + for (auto file : files_) { + auto xml = apk->LoadXml(file, GetDiagnostics()); + if (!xml) { return 1; } + Debug::DumpXml(*xml, GetPrinter()); } return 0; } +const char DumpBadgerCommand::kBadgerData[2925] = { + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 95, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 63, 86, 35, 40, 46, 46, + 95, 95, 95, 95, 97, 97, 44, 32, 46, 124, 42, 33, 83, 62, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 58, 46, 58, 59, 61, 59, 61, 81, 81, 81, 81, 66, 96, 61, 61, 58, 46, + 46, 46, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 58, 106, 81, 81, + 81, 81, 102, 59, 61, 59, 59, 61, 61, 61, 58, 46, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 61, 59, 59, 59, 58, 109, 81, 81, 81, 81, 61, 59, 59, 59, 59, 59, 58, 59, 59, + 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 60, 81, 81, 81, 81, 87, 58, + 59, 59, 59, 59, 59, 59, 61, 119, 44, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 47, 61, 59, 59, + 58, 100, 81, 81, 81, 81, 35, 58, 59, 59, 59, 59, 59, 58, 121, 81, 91, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 46, 109, 58, 59, 59, 61, 81, 81, 81, 81, 81, 109, 58, 59, 59, 59, + 59, 61, 109, 81, 81, 76, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 41, 87, 59, 61, 59, 41, 81, 81, + 81, 81, 81, 81, 59, 61, 59, 59, 58, 109, 81, 81, 87, 39, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 60, 81, 91, 59, 59, 61, 81, 81, 81, 81, 81, 87, 43, 59, 58, 59, 60, 81, 81, + 81, 76, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 52, 91, 58, 45, 59, 87, 81, 81, 81, 81, + 70, 58, 58, 58, 59, 106, 81, 81, 81, 91, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 93, 40, + 32, 46, 59, 100, 81, 81, 81, 81, 40, 58, 46, 46, 58, 100, 81, 81, 68, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 46, 46, 32, 46, + 46, 46, 32, 46, 32, 46, 45, 91, 59, 61, 58, 109, 81, 81, 81, 87, 46, 58, 61, + 59, 60, 81, 81, 80, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 46, 46, + 61, 59, 61, 61, 61, 59, 61, 61, 59, 59, 59, 58, 58, 46, 46, 41, 58, 59, 58, + 81, 81, 81, 81, 69, 58, 59, 59, 60, 81, 81, 68, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, + 32, 32, 32, 32, 58, 59, 61, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 61, 61, 46, 61, 59, 93, 81, 81, 81, 81, 107, 58, 59, 58, 109, 87, 68, + 96, 32, 32, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 10, 32, 32, 32, 46, 60, 61, 61, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 58, 58, 58, 115, 109, 68, 41, 36, + 81, 109, 46, 61, 61, 81, 69, 96, 46, 58, 58, 46, 58, 46, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 46, 32, 95, 81, 67, + 61, 61, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 58, 68, 39, 61, 105, 61, 63, 81, 119, 58, 106, 80, 32, 58, 61, 59, 59, + 61, 59, 61, 59, 61, 46, 95, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 10, 32, 32, 36, 81, 109, 105, 59, 61, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 46, 58, 37, 73, 108, 108, 62, 52, 81, + 109, 34, 32, 61, 59, 59, 59, 59, 59, 59, 59, 59, 59, 61, 59, 61, 61, 46, 46, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 46, 45, 57, 101, 43, 43, 61, + 61, 59, 59, 59, 59, 59, 59, 61, 59, 59, 59, 59, 59, 59, 59, 59, 59, 58, 97, + 46, 61, 108, 62, 126, 58, 106, 80, 96, 46, 61, 61, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 61, 61, 97, 103, 97, 32, 32, 32, 32, 32, 32, 32, 10, + 32, 32, 32, 32, 45, 46, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, + 58, 59, 59, 59, 59, 61, 119, 81, 97, 124, 105, 124, 124, 39, 126, 95, 119, 58, 61, + 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 61, 119, 81, 81, + 99, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 59, 59, 58, 106, 81, 81, 81, 109, 119, + 119, 119, 109, 109, 81, 81, 122, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 58, 115, 81, 87, 81, 102, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 58, + 59, 61, 81, 81, 81, 81, 81, 81, 87, 87, 81, 81, 81, 81, 81, 58, 59, 59, 59, + 59, 59, 59, 59, 59, 58, 45, 45, 45, 59, 59, 59, 41, 87, 66, 33, 32, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 58, 59, 59, 93, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 40, 58, 59, 59, 59, 58, 45, 32, 46, 32, 32, 32, 32, 32, 46, + 32, 126, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 59, 58, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 40, 58, 59, 59, 59, 58, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 58, 59, 59, 58, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 40, 58, 59, 59, 59, 46, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 59, 60, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 59, 61, 59, 59, 61, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 58, 59, 59, 93, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 40, 59, + 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 58, 106, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 76, 58, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 58, 58, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 87, 58, 59, 59, 59, 59, + 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 58, 59, 61, 41, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 87, 59, 61, 58, 59, 59, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 58, 61, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 107, 58, 59, 59, 59, 59, 58, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 58, 59, 59, 58, 51, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 102, 94, + 59, 59, 59, 59, 59, 61, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 59, 59, 59, 43, 63, 36, 81, + 81, 81, 87, 64, 86, 102, 58, 59, 59, 59, 59, 59, 59, 59, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 61, + 59, 59, 59, 59, 59, 59, 59, 43, 33, 58, 126, 126, 58, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 58, 45, 58, 61, 59, 58, 58, 58, 61, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 58, 32, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 59, 59, + 58, 95, 32, 45, 61, 59, 61, 59, 59, 59, 59, 59, 59, 59, 45, 58, 59, 59, 59, + 59, 61, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 58, 61, 59, 59, 59, 59, 59, 61, 59, 61, 46, 46, 32, 45, 45, 45, 59, 58, + 45, 45, 46, 58, 59, 59, 59, 59, 59, 59, 61, 46, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 46, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 61, 59, 46, 32, 32, 46, 32, 46, 32, 58, 61, 59, 59, 59, 59, 59, 59, 59, 59, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, + 59, 59, 59, 59, 59, 59, 59, 59, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, + 59, 59, 59, 59, 59, 59, 59, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 59, 59, 59, 59, 32, 46, 32, + 32, 32, 32, 32, 32, 61, 46, 61, 59, 59, 59, 59, 59, 59, 58, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 59, 59, 59, + 59, 59, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 32, 46, 61, 58, 59, 59, + 59, 59, 59, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 58, 59, 59, 59, 59, 59, 59, 59, 59, 46, 46, 32, 32, 32, 32, + 32, 32, 32, 61, 59, 59, 59, 59, 59, 59, 59, 45, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 32, 45, 61, 59, 59, 59, 59, + 59, 58, 32, 46, 32, 32, 32, 32, 32, 32, 32, 58, 59, 59, 59, 59, 59, 58, 45, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 45, 45, 45, 45, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 45, 61, 59, 58, 45, 45, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 32, 32, 46, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10}; + } // namespace aapt diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h new file mode 100644 index 000000000000..89d19cf4ba08 --- /dev/null +++ b/tools/aapt2/cmd/Dump.h @@ -0,0 +1,276 @@ +/* + * 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" +#include "LoadedApk.h" +#include "dump/DumpManifest.h" + +namespace aapt { + +/** + * The base command for dumping information about apks. When the command is executed, the command + * performs the DumpApkCommand::Dump() operation on each apk provided as a file argument. + **/ +class DumpApkCommand : public Command { + public: + explicit DumpApkCommand(const std::string&& name, text::Printer* printer, IDiagnostics* diag) + : Command(name), printer_(printer), diag_(diag) { + } + + text::Printer* GetPrinter() { + return printer_; + } + + IDiagnostics* GetDiagnostics() { + return diag_; + } + + Maybe<std::string> GetPackageName(LoadedApk* apk) { + xml::Element* manifest_el = apk->GetManifest()->root.get(); + if (!manifest_el) { + GetDiagnostics()->Error(DiagMessage() << "No AndroidManifest."); + return Maybe<std::string>(); + } + + xml::Attribute* attr = manifest_el->FindAttribute({}, "package"); + if (!attr) { + GetDiagnostics()->Error(DiagMessage() << "No package name."); + return Maybe<std::string>(); + } + return attr->value; + } + + /** Perform the dump operation on the apk. */ + virtual int Dump(LoadedApk* apk) = 0; + + int Action(const std::vector<std::string>& args) final { + if (args.size() < 1) { + diag_->Error(DiagMessage() << "No dump apk specified."); + return 1; + } + + bool error = false; + for (auto apk : args) { + auto loaded_apk = LoadedApk::LoadApkFromPath(apk, diag_); + if (!loaded_apk) { + error = true; + continue; + } + + error |= Dump(loaded_apk.get()); + } + + return error; + } + + private: + text::Printer* printer_; + IDiagnostics* diag_; +}; + +/** Command that prints contents of files generated from the compilation stage. */ +class DumpAPCCommand : public Command { + public: + explicit DumpAPCCommand(text::Printer* printer, IDiagnostics* diag) + : Command("apc"), printer_(printer), 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: + text::Printer* printer_; + IDiagnostics* diag_; + bool no_values_ = false; + bool verbose_ = false; +}; + +/** Easter egg command shown when users enter "badger" instead of "badging". */ +class DumpBadgerCommand : public Command { + public: + explicit DumpBadgerCommand(text::Printer* printer) : Command("badger"), printer_(printer) { + } + + int Action(const std::vector<std::string>& args) override; + + private: + text::Printer* printer_; + const static char kBadgerData[2925]; +}; + +class DumpBadgingCommand : public DumpApkCommand { + public: + explicit DumpBadgingCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("badging", printer, diag) { + SetDescription("Print information extracted from the manifest of the APK."); + AddOptionalSwitch("--include-meta-data", "Include meta-data information.", + &options_.include_meta_data); + } + + int Dump(LoadedApk* apk) override { + return DumpManifest(apk, options_, GetPrinter(), GetDiagnostics()); + } + + private: + DumpManifestOptions options_; +}; + +class DumpConfigsCommand : public DumpApkCommand { + public: + explicit DumpConfigsCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("configurations", printer, diag) { + SetDescription("Print every configuration used by a resource in the APK."); + } + + int Dump(LoadedApk* apk) override; +}; + +class DumpPackageNameCommand : public DumpApkCommand { + public: + explicit DumpPackageNameCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("packagename", printer, diag) { + SetDescription("Print the package name of the APK."); + } + + int Dump(LoadedApk* apk) override; +}; + +class DumpPermissionsCommand : public DumpApkCommand { + public: + explicit DumpPermissionsCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("permissions", printer, diag) { + SetDescription("Print the permissions extracted from the manifest of the APK."); + } + + int Dump(LoadedApk* apk) override { + DumpManifestOptions options; + options.only_permissions = true; + return DumpManifest(apk, options, GetPrinter(), GetDiagnostics()); + } +}; + +class DumpStringsCommand : public DumpApkCommand { + public: + explicit DumpStringsCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("strings", printer, diag) { + SetDescription("Print the contents of the resource table string pool in the APK."); + } + + int Dump(LoadedApk* apk) override; +}; + +/** Prints the graph of parents of a style in an APK. */ +class DumpStyleParentCommand : public DumpApkCommand { + public: + explicit DumpStyleParentCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("styleparents", printer, diag) { + SetDescription("Print the parents of a style in an APK."); + AddRequiredFlag("--style", "The name of the style to print", &style_); + } + + int Dump(LoadedApk* apk) override; + + private: + std::string style_; +}; + +class DumpTableCommand : public DumpApkCommand { + public: + explicit DumpTableCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("resources", printer, 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 Dump(LoadedApk* apk) override; + + private: + bool no_values_ = false; + bool verbose_ = false; +}; + +class DumpXmlStringsCommand : public DumpApkCommand { + public: + explicit DumpXmlStringsCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("xmlstrings", printer, diag) { + SetDescription("Print the string pool of a compiled xml in an APK."); + AddRequiredFlagList("--file", "A compiled xml file to print", &files_); + } + + int Dump(LoadedApk* apk) override; + + private: + std::vector<std::string> files_; +}; + +class DumpXmlTreeCommand : public DumpApkCommand { + public: + explicit DumpXmlTreeCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("xmltree", printer, diag) { + SetDescription("Print the tree of a compiled xml in an APK."); + AddRequiredFlagList("--file", "A compiled xml file to print", &files_); + } + + int Dump(LoadedApk* apk) override; + + private: + std::vector<std::string> files_; +}; + +/** The default dump command. Performs no action because a subcommand is required. */ +class DumpCommand : public Command { + public: + explicit DumpCommand(text::Printer* printer, IDiagnostics* diag) + : Command("dump", "d"), diag_(diag) { + AddOptionalSubcommand(util::make_unique<DumpAPCCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpBadgingCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpConfigsCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpPackageNameCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpPermissionsCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpStringsCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpStyleParentCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpTableCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpXmlStringsCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpXmlTreeCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpBadgerCommand>(printer), /* hidden */ true); + } + + int Action(const std::vector<std::string>& args) override { + if (args.size() == 0) { + diag_->Error(DiagMessage() << "no subcommand specified"); + } else { + diag_->Error(DiagMessage() << "unknown subcommand '" << args[0] << "'"); + } + Usage(&std::cerr); + return 1; + } + + private: + IDiagnostics* diag_; +}; + +} // namespace aapt + +#endif // AAPT2_DUMP_H diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 60cab5db1b1f..6a7da0c57df3 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> @@ -29,7 +32,6 @@ #include "AppInfo.h" #include "Debug.h" -#include "Flags.h" #include "LoadedApk.h" #include "NameMangler.h" #include "ResourceUtils.h" @@ -74,71 +76,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_resource_removal = 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) @@ -488,7 +425,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 {}; } @@ -534,6 +471,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; @@ -631,6 +572,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()) { @@ -773,9 +728,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_(), @@ -963,6 +918,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); @@ -1294,7 +1261,7 @@ class LinkCommand { return false; } - proguard::WriteKeepSet(keep_set, &fout); + proguard::WriteKeepSet(keep_set, &fout, options_.generate_minimal_proguard_rules); fout.Flush(); if (fout.HadError()) { @@ -1704,6 +1671,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()) { @@ -1877,9 +1845,15 @@ class LinkCommand { } else { // Adjust the SplitConstraints so that their SDK version is stripped if it is less than or // equal to the minSdk. + const size_t origConstraintSize = options_.split_constraints.size(); options_.split_constraints = AdjustSplitConstraintsForMinSdk(context_->GetMinSdkVersion(), options_.split_constraints); + if (origConstraintSize != options_.split_constraints.size()) { + context_->GetDiagnostics()->Warn(DiagMessage() + << "requested to split resources prior to min sdk of " + << context_->GetMinSdkVersion()); + } TableSplitter table_splitter(options_.split_constraints, options_.table_splitter_options); if (!table_splitter.VerifySplitConstraints(context_)) { return 1; @@ -2029,192 +2003,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("--no-resource-removal", - "Disables automatic removal of resources without defaults. Use this only\n" - "when building runtime resource overlay packages.", - &options.no_resource_removal) - .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; @@ -2228,27 +2022,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; } @@ -2256,26 +2050,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; } @@ -2283,7 +2077,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)); @@ -2293,71 +2087,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..950dac204dde --- /dev/null +++ b/tools/aapt2/cmd/Link.h @@ -0,0 +1,287 @@ +/* + * 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_minimal_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_resource_removal = 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("--proguard-minimal-keep-rules", + "Generate a minimal set of Proguard keep rules.", + &options_.generate_minimal_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("--no-resource-removal", "Disables automatic removal of resources without\n" + "defaults. Use this only when building runtime resource overlay packages.", + &options_.no_resource_removal); + 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 diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 45297a7f8997..328b0beda372 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> @@ -25,7 +27,6 @@ #include "androidfw/StringPiece.h" #include "Diagnostics.h" -#include "Flags.h" #include "LoadedApk.h" #include "ResourceUtils.h" #include "SdkConstants.h" @@ -39,6 +40,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" @@ -55,33 +57,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; @@ -139,9 +114,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) { } @@ -149,6 +124,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())) { @@ -286,16 +268,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; } @@ -319,76 +347,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; } @@ -398,28 +374,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; } @@ -429,50 +405,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 25010c52235c..792120e449ae 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -31,6 +31,7 @@ using ::android::ConfigDescription; using ::android::LocaleValue; using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { @@ -74,6 +75,7 @@ bool ParseSplitParameter(const StringPiece& arg, IDiagnostics* diag, std::string } *out_path = parts[0]; + out_split->name = parts[1]; for (const StringPiece& config_str : util::Tokenize(parts[1], ',')) { ConfigDescription config; if (!ConfigDescription::Parse(config_str, &config)) { @@ -120,12 +122,15 @@ std::vector<SplitConstraints> AdjustSplitConstraintsForMinSdk( for (const SplitConstraints& constraints : split_constraints) { SplitConstraints constraint; for (const ConfigDescription& config : constraints.configs) { - if (config.sdkVersion <= min_sdk) { - constraint.configs.insert(config.CopyWithoutSdkVersion()); - } else { - constraint.configs.insert(config); + const ConfigDescription &configToInsert = (config.sdkVersion <= min_sdk) + ? config.CopyWithoutSdkVersion() + : config; + // only add the config if it actually selects something + if (configToInsert != ConfigDescription::DefaultConfig()) { + constraint.configs.insert(configToInsert); } } + constraint.name = constraints.name; adjusted_constraints.push_back(std::move(constraint)); } return adjusted_constraints; @@ -147,7 +152,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) { @@ -170,6 +175,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); @@ -186,6 +192,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{ @@ -357,6 +371,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); @@ -393,4 +418,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..f92f1e3c4c7e 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -16,12 +16,29 @@ #include "Util.h" +#include "android-base/stringprintf.h" + #include "AppInfo.h" #include "split/TableSplitter.h" +#include "test/Builders.h" #include "test/Test.h" +#include "util/Files.h" + +using ::android::ConfigDescription; namespace aapt { +#ifdef _WIN32 +#define CREATE_PATH(path) android::base::StringPrintf(";%s", path) +#else +#define CREATE_PATH(path) android::base::StringPrintf(":%s", path) +#endif + +#define EXPECT_CONFIG_EQ(constraints, config) \ + EXPECT_EQ(constraints.configs.size(), 1); \ + EXPECT_EQ(*constraints.configs.begin(), config); \ + constraints.configs.clear(); + TEST(UtilTest, SplitNamesAreSanitized) { AppInfo app_info{"com.pkg"}; SplitConstraints split_constraints{ @@ -36,4 +53,334 @@ 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); +} + + +TEST (UtilTest, ParseSplitParameters) { + IDiagnostics* diagnostics = test::ContextBuilder().Build().get()->GetDiagnostics(); + std::string path; + SplitConstraints constraints; + ConfigDescription expected_configuration; + + // ========== Test IMSI ========== + // mcc: 'mcc[0-9]{3}' + // mnc: 'mnc[0-9]{1,3}' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("mcc310"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setMcc(0x0136) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("mcc310-mnc004"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setMcc(0x0136) + .setMnc(0x0004) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("mcc310-mnc000"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setMcc(0x0136) + .setMnc(0xFFFF) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test LOCALE ========== + // locale: '[a-z]{2,3}(-r[a-z]{2})?' + // locale: 'b+[a-z]{2,3}(+[a-z[0-9]]{2})?' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("es"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setLanguage(0x6573) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("fr-rCA"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setLanguage(0x6672) + .setCountry(0x4341) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("b+es+419"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setLanguage(0x6573) + .setCountry(0xA424) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test SCREEN_TYPE ========== + // orientation: '(port|land|square)' + // touchscreen: '(notouch|stylus|finger)' + // density" '(anydpi|nodpi|ldpi|mdpi|tvdpi|hdpi|xhdpi|xxhdpi|xxxhdpi|[0-9]*dpi)' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("square"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setOrientation(0x03) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("stylus"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setTouchscreen(0x02) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("xxxhdpi"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setDensity(0x0280) + .setSdkVersion(0x0004) // version [any density requires donut] + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("land-xhdpi-finger"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setOrientation(0x02) + .setTouchscreen(0x03) + .setDensity(0x0140) + .setSdkVersion(0x0004) // version [any density requires donut] + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test INPUT ========== + // keyboard: '(nokeys|qwerty|12key)' + // navigation: '(nonav|dpad|trackball|wheel)' + // inputFlags: '(keysexposed|keyshidden|keyssoft)' + // inputFlags: '(navexposed|navhidden)' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("qwerty"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setKeyboard(0x02) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("dpad"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setNavigation(0x02) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("keyssoft-navhidden"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setInputFlags(0x0B) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("keyshidden-nokeys-navexposed-trackball"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setKeyboard(0x01) + .setNavigation(0x03) + .setInputFlags(0x06) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test SCREEN_SIZE ========== + // screenWidth/screenHeight: '[0-9]+x[0-9]+' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("1920x1080"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenWidth(0x0780) + .setScreenHeight(0x0438) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test VERSION ========== + // version 'v[0-9]+' + + // ========== Test SCREEN_CONFIG ========== + // screenLayout [direction]: '(ldltr|ldrtl)' + // screenLayout [size]: '(small|normal|large|xlarge)' + // screenLayout [long]: '(long|notlong)' + // uiMode [type]: '(desk|car|television|appliance|watch|vrheadset)' + // uiMode [night]: '(night|notnight)' + // smallestScreenWidthDp: 'sw[0-9]dp' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("ldrtl"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenLayout(0x80) + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("small"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenLayout(0x01) + .setSdkVersion(0x0004) // screenLayout (size) requires donut + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("notlong"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenLayout(0x10) + .setSdkVersion(0x0004) // screenLayout (long) requires donut + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("ldltr-normal-long"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenLayout(0x62) + .setSdkVersion(0x0004) // screenLayout (size|long) requires donut + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("car"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setUiMode(0x03) + .setSdkVersion(0x0008) // uiMode requires froyo + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("vrheadset"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setUiMode(0x07) + .setSdkVersion(0x001A) // uiMode 'vrheadset' requires oreo + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("television-night"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setUiMode(0x24) + .setSdkVersion(0x0008) // uiMode requires froyo + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("sw1920dp"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setSmallestScreenWidthDp(0x0780) + .setSdkVersion(0x000D) // smallestScreenWidthDp requires honeycomb mr2 + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test SCREEN_SIZE_DP ========== + // screenWidthDp: 'w[0-9]dp' + // screenHeightDp: 'h[0-9]dp' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("w1920dp"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenWidthDp(0x0780) + .setSdkVersion(0x000D) // screenWidthDp requires honeycomb mr2 + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("h1080dp"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenHeightDp(0x0438) + .setSdkVersion(0x000D) // screenHeightDp requires honeycomb mr2 + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + // ========== Test SCREEN_CONFIG_2 ========== + // screenLayout2: '(round|notround)' + // colorMode: '(widecg|nowidecg)' + // colorMode: '(highhdr|lowdr)' + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("round"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setScreenLayout2(0x02) + .setSdkVersion(0x0017) // screenLayout2 (round) requires marshmallow + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); + + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("widecg-highdr"), + diagnostics, &path, &constraints)); + expected_configuration = test::ConfigDescriptionBuilder() + .setColorMode(0x0A) + .setSdkVersion(0x001A) // colorMode (hdr|colour gamut) requires oreo + .Build(); + EXPECT_CONFIG_EQ(constraints, expected_configuration); +} + +TEST (UtilTest, AdjustSplitConstraintsForMinSdk) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + IDiagnostics* diagnostics = context.get()->GetDiagnostics(); + std::vector<SplitConstraints> test_constraints; + std::string path; + + test_constraints.push_back({}); + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("v7"), + diagnostics, &path, &test_constraints.back())); + test_constraints.push_back({}); + ASSERT_TRUE(ParseSplitParameter(CREATE_PATH("xhdpi"), + diagnostics, &path, &test_constraints.back())); + EXPECT_EQ(test_constraints.size(), 2); + EXPECT_EQ(test_constraints[0].name, "v7"); + EXPECT_EQ(test_constraints[0].configs.size(), 1); + EXPECT_NE(*test_constraints[0].configs.begin(), ConfigDescription::DefaultConfig()); + EXPECT_EQ(test_constraints[1].name, "xhdpi"); + EXPECT_EQ(test_constraints[1].configs.size(), 1); + EXPECT_NE(*test_constraints[0].configs.begin(), ConfigDescription::DefaultConfig()); + + auto adjusted_contraints = AdjustSplitConstraintsForMinSdk(26, test_constraints); + EXPECT_EQ(adjusted_contraints.size(), 2); + EXPECT_EQ(adjusted_contraints[0].name, "v7"); + EXPECT_EQ(adjusted_contraints[0].configs.size(), 0); + EXPECT_EQ(adjusted_contraints[1].name, "xhdpi"); + EXPECT_EQ(adjusted_contraints[1].configs.size(), 1); + EXPECT_NE(*adjusted_contraints[1].configs.begin(), ConfigDescription::DefaultConfig()); +} + } // namespace aapt diff --git a/tools/aapt2/compile/XmlIdCollector.cpp b/tools/aapt2/compile/XmlIdCollector.cpp index d61a15af0d85..2199d003bccb 100644 --- a/tools/aapt2/compile/XmlIdCollector.cpp +++ b/tools/aapt2/compile/XmlIdCollector.cpp @@ -21,6 +21,7 @@ #include "ResourceUtils.h" #include "ResourceValues.h" +#include "text/Unicode.h" #include "xml/XmlDom.h" namespace aapt { @@ -35,8 +36,9 @@ struct IdCollector : public xml::Visitor { public: using xml::Visitor::Visit; - explicit IdCollector(std::vector<SourcedResourceName>* out_symbols) - : out_symbols_(out_symbols) {} + explicit IdCollector(std::vector<SourcedResourceName>* out_symbols, + SourcePathDiagnostics* source_diag) : out_symbols_(out_symbols), + source_diag_(source_diag) {} void Visit(xml::Element* element) override { for (xml::Attribute& attr : element->attributes) { @@ -44,12 +46,16 @@ struct IdCollector : public xml::Visitor { bool create = false; if (ResourceUtils::ParseReference(attr.value, &name, &create, nullptr)) { if (create && name.type == ResourceType::kId) { - auto iter = std::lower_bound(out_symbols_->begin(), - out_symbols_->end(), name, cmp_name); - if (iter == out_symbols_->end() || iter->name != name) { - out_symbols_->insert(iter, - SourcedResourceName{name.ToResourceName(), - element->line_number}); + if (!text::IsValidResourceEntryName(name.entry)) { + source_diag_->Error(DiagMessage(element->line_number) + << "id '" << name << "' has an invalid entry name"); + } else { + auto iter = std::lower_bound(out_symbols_->begin(), + out_symbols_->end(), name, cmp_name); + if (iter == out_symbols_->end() || iter->name != name) { + out_symbols_->insert(iter, SourcedResourceName{name.ToResourceName(), + element->line_number}); + } } } } @@ -60,15 +66,17 @@ struct IdCollector : public xml::Visitor { private: std::vector<SourcedResourceName>* out_symbols_; + SourcePathDiagnostics* source_diag_; }; } // namespace bool XmlIdCollector::Consume(IAaptContext* context, xml::XmlResource* xmlRes) { xmlRes->file.exported_symbols.clear(); - IdCollector collector(&xmlRes->file.exported_symbols); + SourcePathDiagnostics source_diag(xmlRes->file.source, context->GetDiagnostics()); + IdCollector collector(&xmlRes->file.exported_symbols, &source_diag); xmlRes->root->Accept(&collector); - return true; + return !source_diag.HadError(); } } // namespace aapt diff --git a/tools/aapt2/compile/XmlIdCollector_test.cpp b/tools/aapt2/compile/XmlIdCollector_test.cpp index 98da56d03ae3..d49af3bf903c 100644 --- a/tools/aapt2/compile/XmlIdCollector_test.cpp +++ b/tools/aapt2/compile/XmlIdCollector_test.cpp @@ -64,4 +64,14 @@ TEST(XmlIdCollectorTest, DontCollectNonIds) { EXPECT_TRUE(doc->file.exported_symbols.empty()); } +TEST(XmlIdCollectorTest, ErrorOnInvalidIds) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + std::unique_ptr<xml::XmlResource> doc = + test::BuildXmlDom("<View foo=\"@+id/foo$bar\"/>"); + + XmlIdCollector collector; + ASSERT_FALSE(collector.Consume(context.get(), doc.get())); +} + } // namespace aapt diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp index 960880a8daa6..3a71e836aa38 100644 --- a/tools/aapt2/configuration/ConfigurationParser_test.cpp +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -705,35 +705,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/development.md b/tools/aapt2/development.md new file mode 100644 index 000000000000..8ee873a0e40f --- /dev/null +++ b/tools/aapt2/development.md @@ -0,0 +1,11 @@ +# AAPT2 development + +## Building +All build targets can be found in `Android.bp` file. The main ones are `make -j aapt2` and `make -j aapt2_tests` + +`make -j aapt2` will create an aapt2 executable in `out/host/linux-x86/bin/aapt2` (on Linux). This `aapt2` executable will then be used for all the apps in the platform. + +Static version of the tool (without shared libraries) can be built with `make -j static_sdk_tools dist DIST_DIR=$OUTPUT_DIRECTORY BUILD_HOST_static=1`. Note, in addition to aapt2 this command will also output other statically built tools to the `$OUTPUT_DIRECTORY`. + +## Running tests +Build `make -j aapt2_tests` and then (on Linux) execute `out/host/linux-x86/nativetest64/aapt2_tests/aapt2_tests`
\ No newline at end of file diff --git a/tools/aapt2/dump/DumpManifest.cpp b/tools/aapt2/dump/DumpManifest.cpp new file mode 100644 index 000000000000..11a4074cd3cd --- /dev/null +++ b/tools/aapt2/dump/DumpManifest.cpp @@ -0,0 +1,2275 @@ +/* + * 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 "DumpManifest.h" + +#include <algorithm> + +#include "LoadedApk.h" +#include "SdkConstants.h" +#include "ValueVisitor.h" +#include "io/File.h" +#include "io/FileStream.h" +#include "process/IResourceTableConsumer.h" +#include "xml/XmlDom.h" + +#include "androidfw/ConfigDescription.h" + +using ::android::base::StringPrintf; +using ::android::ConfigDescription; + +namespace aapt { + +/** + * These are attribute resource constants for the platform, as found in android.R.attr. + */ +enum { + LABEL_ATTR = 0x01010001, + ICON_ATTR = 0x01010002, + NAME_ATTR = 0x01010003, + PERMISSION_ATTR = 0x01010006, + EXPORTED_ATTR = 0x01010010, + GRANT_URI_PERMISSIONS_ATTR = 0x0101001b, + RESOURCE_ATTR = 0x01010025, + DEBUGGABLE_ATTR = 0x0101000f, + VALUE_ATTR = 0x01010024, + VERSION_CODE_ATTR = 0x0101021b, + VERSION_NAME_ATTR = 0x0101021c, + SCREEN_ORIENTATION_ATTR = 0x0101001e, + MIN_SDK_VERSION_ATTR = 0x0101020c, + MAX_SDK_VERSION_ATTR = 0x01010271, + REQ_TOUCH_SCREEN_ATTR = 0x01010227, + REQ_KEYBOARD_TYPE_ATTR = 0x01010228, + REQ_HARD_KEYBOARD_ATTR = 0x01010229, + REQ_NAVIGATION_ATTR = 0x0101022a, + REQ_FIVE_WAY_NAV_ATTR = 0x01010232, + TARGET_SDK_VERSION_ATTR = 0x01010270, + TEST_ONLY_ATTR = 0x01010272, + ANY_DENSITY_ATTR = 0x0101026c, + GL_ES_VERSION_ATTR = 0x01010281, + SMALL_SCREEN_ATTR = 0x01010284, + NORMAL_SCREEN_ATTR = 0x01010285, + LARGE_SCREEN_ATTR = 0x01010286, + XLARGE_SCREEN_ATTR = 0x010102bf, + REQUIRED_ATTR = 0x0101028e, + INSTALL_LOCATION_ATTR = 0x010102b7, + SCREEN_SIZE_ATTR = 0x010102ca, + SCREEN_DENSITY_ATTR = 0x010102cb, + REQUIRES_SMALLEST_WIDTH_DP_ATTR = 0x01010364, + COMPATIBLE_WIDTH_LIMIT_DP_ATTR = 0x01010365, + LARGEST_WIDTH_LIMIT_DP_ATTR = 0x01010366, + PUBLIC_KEY_ATTR = 0x010103a6, + CATEGORY_ATTR = 0x010103e8, + BANNER_ATTR = 0x10103f2, + ISGAME_ATTR = 0x10103f4, + VERSION_ATTR = 0x01010519, + CERT_DIGEST_ATTR = 0x01010548, + REQUIRED_FEATURE_ATTR = 0x1010557, + REQUIRED_NOT_FEATURE_ATTR = 0x1010558, + COMPILE_SDK_VERSION_ATTR = 0x01010572, + COMPILE_SDK_VERSION_CODENAME_ATTR = 0x01010573, + VERSION_MAJOR_ATTR = 0x01010577, + PACKAGE_TYPE_ATTR = 0x01010587, +}; + +const std::string& kAndroidNamespace = "http://schemas.android.com/apk/res/android"; + +/** Retrieves the attribute of the element with the specified attribute resource id. */ +static xml::Attribute* FindAttribute(xml::Element *el, uint32_t resd_id) { + for (auto& a : el->attributes) { + if (a.compiled_attribute && a.compiled_attribute.value().id) { + if (a.compiled_attribute.value().id.value() == resd_id) { + return std::move(&a); + } + } + } + return nullptr; +} + +/** Retrieves the attribute of the element that has the specified namespace and attribute name. */ +static xml::Attribute* FindAttribute(xml::Element *el, const std::string &package, + const std::string &name) { + return el->FindAttribute(package, name); +} + +class CommonFeatureGroup; + +class ManifestExtractor { + public: + + explicit ManifestExtractor(LoadedApk* apk, DumpManifestOptions& options) + : apk_(apk), options_(options) { } + + class Element { + public: + Element() = default; + virtual ~Element() = default; + + static std::unique_ptr<Element> Inflate(ManifestExtractor* extractor, xml::Element* el); + + /** Writes out the extracted contents of the element. */ + virtual void Print(text::Printer* printer) { } + + /** Adds an element to the list of children of the element. */ + void AddChild(std::unique_ptr<Element>& child) { children_.push_back(std::move(child)); } + + /** Retrieves the list of children of the element. */ + const std::vector<std::unique_ptr<Element>>& children() const { + return children_; + } + + /** Retrieves the extracted xml element tag. */ + const std::string tag() const { + return tag_; + } + + protected: + ManifestExtractor* extractor() const { + return extractor_; + } + + /** Retrieves and stores the information extracted from the xml element. */ + virtual void Extract(xml::Element* el) { } + + /* + * Retrieves a configuration value of the resource entry that best matches the specified + * configuration. + */ + static Value* BestConfigValue(ResourceEntry* entry, + const ConfigDescription& match) { + if (!entry) { + return nullptr; + } + + // Determine the config that best matches the desired config + ResourceConfigValue* best_value = nullptr; + for (auto& value : entry->values) { + if (!value->config.match(match)) { + continue; + } + + if (best_value != nullptr) { + if (!value->config.isBetterThan(best_value->config, &match)) { + if (value->config.compare(best_value->config) != 0) { + continue; + } + } + } + + best_value = value.get(); + } + + // The entry has no values + if (!best_value) { + return nullptr; + } + + return best_value->value.get(); + } + + /** Retrieves the resource assigned to the specified resource id if one exists. */ + Value* FindValueById(const ResourceTable* table, const ResourceId& res_id, + const ConfigDescription& config = DummyConfig()) { + if (table) { + for (auto& package : table->packages) { + if (package->id && package->id.value() == res_id.package_id()) { + for (auto& type : package->types) { + if (type->id && type->id.value() == res_id.type_id()) { + for (auto& entry : type->entries) { + if (entry->id && entry->id.value() == res_id.entry_id()) { + if (auto value = BestConfigValue(entry.get(), config)) { + return value; + } + } + } + } + } + } + } + } + return nullptr; + } + + /** Attempts to resolve the reference to a non-reference value. */ + Value* ResolveReference(Reference* ref, const ConfigDescription& config = DummyConfig()) { + const int kMaxIterations = 40; + int i = 0; + while (ref && ref->id && i++ < kMaxIterations) { + auto table = extractor_->apk_->GetResourceTable(); + if (auto value = FindValueById(table, ref->id.value(), config)) { + if (ValueCast<Reference>(value)) { + ref = ValueCast<Reference>(value); + } else { + return value; + } + } + } + return nullptr; + } + + /** + * Retrieves the integer value of the attribute . If the value of the attribute is a reference, + * this will attempt to resolve the reference to an integer value. + **/ + int32_t* GetAttributeInteger(xml::Attribute* attr, + const ConfigDescription& config = DummyConfig()) { + if (attr != nullptr) { + if (attr->compiled_value) { + // Resolve references using the dummy configuration + Value* value = attr->compiled_value.get(); + if (ValueCast<Reference>(value)) { + value = ResolveReference(ValueCast<Reference>(value), config); + } else { + value = attr->compiled_value.get(); + } + // Retrieve the integer data if possible + if (value != nullptr) { + if (BinaryPrimitive* intValue = ValueCast<BinaryPrimitive>(value)) { + return (int32_t*) &intValue->value.data; + } + } + } + } + return nullptr; + } + + /** + * A version of GetAttributeInteger that returns a default integer if the attribute does not + * exist or cannot be resolved to an integer value. + **/ + int32_t GetAttributeIntegerDefault(xml::Attribute* attr, int32_t def, + const ConfigDescription& config = DummyConfig()) { + auto value = GetAttributeInteger(attr, config); + if (value) { + return *value; + } + return def; + } + + /** + * Retrieves the string value of the attribute. If the value of the attribute is a reference, + * this will attempt to resolve the reference to a string value. + **/ + const std::string* GetAttributeString(xml::Attribute* attr, + const ConfigDescription& config = DummyConfig()) { + if (attr != nullptr) { + if (attr->compiled_value) { + // Resolve references using the dummy configuration + Value* value = attr->compiled_value.get(); + if (ValueCast<Reference>(value)) { + value = ResolveReference(ValueCast<Reference>(value), config); + } else { + value = attr->compiled_value.get(); + } + + // Retrieve the string data of the value if possible + if (value != nullptr) { + if (String* intValue = ValueCast<String>(value)) { + return &(*intValue->value); + } else if (RawString* rawValue = ValueCast<RawString>(value)) { + return &(*rawValue->value); + } else if (FileReference* strValue = ValueCast<FileReference>(value)) { + return &(*strValue->path); + } + } + } + return &attr->value; + } + return nullptr; + } + + /** + * A version of GetAttributeString that returns a default string if the attribute does not + * exist or cannot be resolved to an string value. + **/ + std::string GetAttributeStringDefault(xml::Attribute* attr, std::string def, + const ConfigDescription& config = DummyConfig()) { + auto value = GetAttributeString(attr, config); + if (value) { + return *value; + } + return def; + } + + private: + ManifestExtractor* extractor_; + std::vector<std::unique_ptr<Element>> children_; + std::string tag_; + }; + + friend Element; + + /** Creates a default configuration used to retrieve resources. */ + static ConfigDescription DummyConfig() { + ConfigDescription config; + config.orientation = android::ResTable_config::ORIENTATION_PORT; + config.density = android::ResTable_config::DENSITY_MEDIUM; + config.sdkVersion = 10000; // Very high. + config.screenWidthDp = 320; + config.screenHeightDp = 480; + config.smallestScreenWidthDp = 320; + config.screenLayout |= android::ResTable_config::SCREENSIZE_NORMAL; + return config; + } + + bool Dump(text::Printer* printer, IDiagnostics* diag); + + /** Recursively visit the xml element tree and return a processed badging element tree. */ + std::unique_ptr<Element> Visit(xml::Element* element); + + /** Raises the target sdk value if the min target is greater than the current target. */ + void RaiseTargetSdk(int32_t min_target) { + if (min_target > target_sdk_) { + target_sdk_ = min_target; + } + } + + /** + * Retrieves the default feature group that features are added into when <uses-feature> + * are not in a <feature-group> element. + **/ + CommonFeatureGroup* GetCommonFeatureGroup() { + return commonFeatureGroup_.get(); + } + + /** + * Retrieves a mapping of density values to Configurations for retrieving resources that would be + * used for that density setting. + **/ + const std::map<uint16_t, ConfigDescription> densities() const { + return densities_; + } + + /** + * Retrieves a mapping of locale BCP 47 strings to Configurations for retrieving resources that + * would be used for that locale setting. + **/ + const std::map<std::string, ConfigDescription> locales() const { + return locales_; + } + + /** Retrieves the current stack of parent during data extraction. */ + const std::vector<Element*> parent_stack() const { + return parent_stack_; + } + + int32_t target_sdk() const { + return target_sdk_; + } + + LoadedApk* const apk_; + DumpManifestOptions& options_; + + private: + std::unique_ptr<CommonFeatureGroup> commonFeatureGroup_ = util::make_unique<CommonFeatureGroup>(); + std::map<std::string, ConfigDescription> locales_; + std::map<uint16_t, ConfigDescription> densities_; + std::vector<Element*> parent_stack_; + int32_t target_sdk_ = 0; +}; + +template<typename T> T* ElementCast(ManifestExtractor::Element* element); + +/** Recurs through the children of the specified root in depth-first order. */ +static void ForEachChild(ManifestExtractor::Element* root, + std::function<void(ManifestExtractor::Element*)> f) { + for (auto& child : root->children()) { + f(child.get()); + ForEachChild(child.get(), f); + } +} + +/** + * Checks the element and its recursive children for an element that makes the specified + * conditional function return true. Returns the first element that makes the conditional function + * return true. + **/ +static ManifestExtractor::Element* FindElement(ManifestExtractor::Element* root, + std::function<bool(ManifestExtractor::Element*)> f) { + if (f(root)) { + return root; + } + for (auto& child : root->children()) { + if (auto b2 = FindElement(child.get(), f)) { + return b2; + } + } + return nullptr; +} + +/** Represents the <manifest> elements **/ +class Manifest : public ManifestExtractor::Element { + public: + Manifest() = default; + std::string package; + int32_t versionCode; + std::string versionName; + const std::string* split = nullptr; + const std::string* platformVersionName = nullptr; + const std::string* platformVersionCode = nullptr; + const int32_t* compilesdkVersion = nullptr; + const std::string* compilesdkVersionCodename = nullptr; + const int32_t* installLocation = nullptr; + + void Extract(xml::Element* manifest) override { + package = GetAttributeStringDefault(FindAttribute(manifest, {}, "package"), ""); + versionCode = GetAttributeIntegerDefault(FindAttribute(manifest, VERSION_CODE_ATTR), 0); + versionName = GetAttributeStringDefault(FindAttribute(manifest, VERSION_NAME_ATTR), ""); + split = GetAttributeString(FindAttribute(manifest, {}, "split")); + + // Extract the platform build info + platformVersionName = GetAttributeString(FindAttribute(manifest, {}, + "platformBuildVersionName")); + platformVersionCode = GetAttributeString(FindAttribute(manifest, {}, + "platformBuildVersionCode")); + + // Extract the compile sdk info + compilesdkVersion = GetAttributeInteger(FindAttribute(manifest, COMPILE_SDK_VERSION_ATTR)); + compilesdkVersionCodename = GetAttributeString( + FindAttribute(manifest, COMPILE_SDK_VERSION_CODENAME_ATTR)); + installLocation = GetAttributeInteger(FindAttribute(manifest, INSTALL_LOCATION_ATTR)); + } + + void Print(text::Printer* printer) override { + printer->Print(StringPrintf("package: name='%s' ", package.data())); + printer->Print(StringPrintf("versionCode='%s' ", + (versionCode > 0) ? std::to_string(versionCode).data() : "")); + printer->Print(StringPrintf("versionName='%s'", versionName.data())); + + if (split) { + printer->Print(StringPrintf(" split='%s'", split->data())); + } + if (platformVersionName) { + printer->Print(StringPrintf(" platformBuildVersionName='%s'", platformVersionName->data())); + } + if (platformVersionCode) { + printer->Print(StringPrintf(" platformBuildVersionCode='%s'", platformVersionCode->data())); + } + if (compilesdkVersion) { + printer->Print(StringPrintf(" compileSdkVersion='%d'", *compilesdkVersion)); + } + if (compilesdkVersionCodename) { + printer->Print(StringPrintf(" compileSdkVersionCodename='%s'", + compilesdkVersionCodename->data())); + } + printer->Print("\n"); + + if (installLocation) { + switch (*installLocation) { + case 0: + printer->Print("install-location:'auto'\n"); + break; + case 1: + printer->Print("install-location:'internalOnly'\n"); + break; + case 2: + printer->Print("install-location:'preferExternal'\n"); + break; + default: + break; + } + } + } +}; + +/** Represents <application> elements. **/ +class Application : public ManifestExtractor::Element { + public: + Application() = default; + std::string label; + std::string icon; + std::string banner; + int32_t is_game; + int32_t debuggable; + int32_t test_only; + bool has_multi_arch; + + /** Mapping from locales to app names. */ + std::map<std::string, std::string> locale_labels; + + /** Mapping from densities to app icons. */ + std::map<uint16_t, std::string> density_icons; + + void Extract(xml::Element* element) override { + label = GetAttributeStringDefault(FindAttribute(element, LABEL_ATTR), ""); + icon = GetAttributeStringDefault(FindAttribute(element, ICON_ATTR), ""); + test_only = GetAttributeIntegerDefault(FindAttribute(element, TEST_ONLY_ATTR), 0); + banner = GetAttributeStringDefault(FindAttribute(element, BANNER_ATTR), ""); + is_game = GetAttributeIntegerDefault(FindAttribute(element, ISGAME_ATTR), 0); + debuggable = GetAttributeIntegerDefault(FindAttribute(element, DEBUGGABLE_ATTR), 0); + + // We must search by name because the multiArch flag hasn't been API + // frozen yet. + has_multi_arch = (GetAttributeIntegerDefault( + FindAttribute(element, kAndroidNamespace, "multiArch"), 0) != 0); + + // Retrieve the app names for every locale the app supports + auto attr = FindAttribute(element, LABEL_ATTR); + for (auto& config : extractor()->locales()) { + if (auto label = GetAttributeString(attr, config.second)) { + if (label) { + locale_labels.insert(std::make_pair(config.first, *label)); + } + } + } + + // Retrieve the icons for the densities the app supports + attr = FindAttribute(element, ICON_ATTR); + for (auto& config : extractor()->densities()) { + if (auto resource = GetAttributeString(attr, config.second)) { + if (resource) { + density_icons.insert(std::make_pair(config.first, *resource)); + } + } + } + } + + void Print(text::Printer* printer) override { + // Print the labels for every locale + for (auto p : locale_labels) { + if (p.first.empty()) { + printer->Print(StringPrintf("application-label:'%s'\n", + android::ResTable::normalizeForOutput(p.second.data()) + .c_str())); + } else { + printer->Print(StringPrintf("application-label-%s:'%s'\n", p.first.data(), + android::ResTable::normalizeForOutput(p.second.data()) + .c_str())); + } + } + + // Print the icon paths for every density + for (auto p : density_icons) { + printer->Print(StringPrintf("application-icon-%d:'%s'\n", p.first, p.second.data())); + } + + // Print the application info + printer->Print(StringPrintf("application: label='%s' ", + android::ResTable::normalizeForOutput(label.data()).c_str())); + printer->Print(StringPrintf("icon='%s'", icon.data())); + if (!banner.empty()) { + printer->Print(StringPrintf(" banner='%s'", banner.data())); + } + printer->Print("\n"); + + if (test_only != 0) { + printer->Print(StringPrintf("testOnly='%d'\n", test_only)); + } + if (is_game != 0) { + printer->Print("application-isGame\n"); + } + if (debuggable != 0) { + printer->Print("application-debuggable\n"); + } + } +}; + +/** Represents <uses-sdk> elements. **/ +class UsesSdkBadging : public ManifestExtractor::Element { + public: + UsesSdkBadging() = default; + const int32_t* min_sdk = nullptr; + const std::string* min_sdk_name = nullptr; + const int32_t* max_sdk = nullptr; + const int32_t* target_sdk = nullptr; + const std::string* target_sdk_name = nullptr; + + void Extract(xml::Element* element) override { + min_sdk = GetAttributeInteger(FindAttribute(element, MIN_SDK_VERSION_ATTR)); + min_sdk_name = GetAttributeString(FindAttribute(element, MIN_SDK_VERSION_ATTR)); + max_sdk = GetAttributeInteger(FindAttribute(element, MAX_SDK_VERSION_ATTR)); + target_sdk = GetAttributeInteger(FindAttribute(element, TARGET_SDK_VERSION_ATTR)); + target_sdk_name = GetAttributeString(FindAttribute(element, TARGET_SDK_VERSION_ATTR)); + + // Detect the target sdk of the element + if ((min_sdk_name && *min_sdk_name == "Donut") + || (target_sdk_name && *target_sdk_name == "Donut")) { + extractor()->RaiseTargetSdk(4); + } + if (min_sdk) { + extractor()->RaiseTargetSdk(*min_sdk); + } + if (target_sdk) { + extractor()->RaiseTargetSdk(*target_sdk); + } + } + + void Print(text::Printer* printer) override { + if (min_sdk) { + printer->Print(StringPrintf("sdkVersion:'%d'\n", *min_sdk)); + } else if (min_sdk_name) { + printer->Print(StringPrintf("sdkVersion:'%s'\n", min_sdk_name->data())); + } + if (max_sdk) { + printer->Print(StringPrintf("maxSdkVersion:'%d'\n", *max_sdk)); + } + if (target_sdk) { + printer->Print(StringPrintf("targetSdkVersion:'%d'\n", *target_sdk)); + } else if (target_sdk_name) { + printer->Print(StringPrintf("targetSdkVersion:'%s'\n", target_sdk_name->data())); + } + } +}; + +/** Represents <uses-configuration> elements. **/ +class UsesConfiguarion : public ManifestExtractor::Element { + public: + UsesConfiguarion() = default; + int32_t req_touch_screen = 0; + int32_t req_keyboard_type = 0; + int32_t req_hard_keyboard = 0; + int32_t req_navigation = 0; + int32_t req_five_way_nav = 0; + + void Extract(xml::Element* element) override { + req_touch_screen = GetAttributeIntegerDefault( + FindAttribute(element, REQ_TOUCH_SCREEN_ATTR), 0); + req_keyboard_type = GetAttributeIntegerDefault( + FindAttribute(element, REQ_KEYBOARD_TYPE_ATTR), 0); + req_hard_keyboard = GetAttributeIntegerDefault( + FindAttribute(element, REQ_HARD_KEYBOARD_ATTR), 0); + req_navigation = GetAttributeIntegerDefault( + FindAttribute(element, REQ_NAVIGATION_ATTR), 0); + req_five_way_nav = GetAttributeIntegerDefault( + FindAttribute(element, REQ_FIVE_WAY_NAV_ATTR), 0); + } + + void Print(text::Printer* printer) override { + printer->Print("uses-configuration:"); + if (req_touch_screen != 0) { + printer->Print(StringPrintf(" reqTouchScreen='%d'", req_touch_screen)); + } + if (req_keyboard_type != 0) { + printer->Print(StringPrintf(" reqKeyboardType='%d'", req_keyboard_type)); + } + if (req_hard_keyboard != 0) { + printer->Print(StringPrintf(" reqHardKeyboard='%d'", req_hard_keyboard)); + } + if (req_navigation != 0) { + printer->Print(StringPrintf(" reqNavigation='%d'", req_navigation)); + } + if (req_five_way_nav != 0) { + printer->Print(StringPrintf(" reqFiveWayNav='%d'", req_five_way_nav)); + } + printer->Print("\n"); + } +}; + +/** Represents <supports-screen> elements. **/ +class SupportsScreen : public ManifestExtractor::Element { + public: + SupportsScreen() = default; + int32_t small_screen = 1; + int32_t normal_screen = 1; + int32_t large_screen = 1; + int32_t xlarge_screen = 1; + int32_t any_density = 1; + int32_t requires_smallest_width_dp = 0; + int32_t compatible_width_limit_dp = 0; + int32_t largest_width_limit_dp = 0; + + void Extract(xml::Element* element) override { + small_screen = GetAttributeIntegerDefault(FindAttribute(element, SMALL_SCREEN_ATTR), 1); + normal_screen = GetAttributeIntegerDefault(FindAttribute(element, NORMAL_SCREEN_ATTR), 1); + large_screen = GetAttributeIntegerDefault(FindAttribute(element, LARGE_SCREEN_ATTR), 1); + xlarge_screen = GetAttributeIntegerDefault(FindAttribute(element, XLARGE_SCREEN_ATTR), 1); + any_density = GetAttributeIntegerDefault(FindAttribute(element, ANY_DENSITY_ATTR), 1); + + requires_smallest_width_dp = GetAttributeIntegerDefault( + FindAttribute(element, REQUIRES_SMALLEST_WIDTH_DP_ATTR), 0); + compatible_width_limit_dp = GetAttributeIntegerDefault( + FindAttribute(element, COMPATIBLE_WIDTH_LIMIT_DP_ATTR), 0); + largest_width_limit_dp = GetAttributeIntegerDefault( + FindAttribute(element, LARGEST_WIDTH_LIMIT_DP_ATTR), 0); + + // For modern apps, if screen size buckets haven't been specified + // but the new width ranges have, then infer the buckets from them. + if (small_screen > 0 && normal_screen > 0 && large_screen > 0 && xlarge_screen > 0 + && requires_smallest_width_dp > 0) { + int32_t compat_width = (compatible_width_limit_dp > 0) ? compatible_width_limit_dp + : requires_smallest_width_dp; + small_screen = (requires_smallest_width_dp <= 240 && compat_width >= 240) ? -1 : 0; + normal_screen = (requires_smallest_width_dp <= 320 && compat_width >= 320) ? -1 : 0; + large_screen = (requires_smallest_width_dp <= 480 && compat_width >= 480) ? -1 : 0; + xlarge_screen = (requires_smallest_width_dp <= 720 && compat_width >= 720) ? -1 : 0; + } + } + + void PrintScreens(text::Printer* printer, int32_t target_sdk) { + int32_t small_screen_temp = small_screen; + int32_t normal_screen_temp = normal_screen; + int32_t large_screen_temp = large_screen; + int32_t xlarge_screen_temp = xlarge_screen; + int32_t any_density_temp = any_density; + + // Determine default values for any unspecified screen sizes, + // based on the target SDK of the package. As of 4 (donut) + // the screen size support was introduced, so all default to + // enabled. + if (small_screen_temp > 0) { + small_screen_temp = target_sdk >= 4 ? -1 : 0; + } + if (normal_screen_temp > 0) { + normal_screen_temp = -1; + } + if (large_screen_temp > 0) { + large_screen_temp = target_sdk >= 4 ? -1 : 0; + } + if (xlarge_screen_temp > 0) { + // Introduced in Gingerbread. + xlarge_screen_temp = target_sdk >= 9 ? -1 : 0; + } + if (any_density_temp > 0) { + any_density_temp = (target_sdk >= 4 || requires_smallest_width_dp > 0 + || compatible_width_limit_dp > 0) ? -1 : 0; + } + + // Print the formatted screen info + printer->Print("supports-screens:"); + if (small_screen_temp != 0) { + printer->Print(" 'small'"); + } + if (normal_screen_temp != 0) { + printer->Print(" 'normal'"); + } + if (large_screen_temp != 0) { + printer->Print(" 'large'"); + } + if (xlarge_screen_temp != 0) { + printer->Print(" 'xlarge'"); + } + printer->Print("\n"); + printer->Print(StringPrintf("supports-any-density: '%s'\n", + (any_density_temp ) ? "true" : "false")); + if (requires_smallest_width_dp > 0) { + printer->Print(StringPrintf("requires-smallest-width:'%d'\n", requires_smallest_width_dp)); + } + if (compatible_width_limit_dp > 0) { + printer->Print(StringPrintf("compatible-width-limit:'%d'\n", compatible_width_limit_dp)); + } + if (largest_width_limit_dp > 0) { + printer->Print(StringPrintf("largest-width-limit:'%d'\n", largest_width_limit_dp)); + } + } +}; + +/** Represents <feature-group> elements. **/ +class FeatureGroup : public ManifestExtractor::Element { + public: + FeatureGroup() = default; + std::string label; + int32_t open_gles_version = 0; + + void Extract(xml::Element* element) override { + label = GetAttributeStringDefault(FindAttribute(element, LABEL_ATTR), ""); + } + + virtual void PrintGroup(text::Printer* printer) { + printer->Print(StringPrintf("feature-group: label='%s'\n", label.data())); + if (open_gles_version > 0) { + printer->Print(StringPrintf(" uses-gl-es: '0x%x'\n", open_gles_version)); + } + + for (auto feature : features_) { + printer->Print(StringPrintf(" uses-feature%s: name='%s'", + (feature.second.required ? "" : "-not-required"), + feature.first.data())); + if (feature.second.version > 0) { + printer->Print(StringPrintf(" version='%d'", feature.second.version)); + } + printer->Print("\n"); + } + } + + /** Adds a feature to the feature group. */ + void AddFeature(const std::string& name, bool required = true, int32_t version = -1) { + features_.insert(std::make_pair(name, Feature{ required, version })); + if (required) { + if (name == "android.hardware.camera.autofocus" || + name == "android.hardware.camera.flash") { + AddFeature("android.hardware.camera", true); + } else if (name == "android.hardware.location.gps" || + name == "android.hardware.location.network") { + AddFeature("android.hardware.location", true); + } else if (name == "android.hardware.faketouch.multitouch") { + AddFeature("android.hardware.faketouch", true); + } else if (name == "android.hardware.faketouch.multitouch.distinct" || + name == "android.hardware.faketouch.multitouch.jazzhands") { + AddFeature("android.hardware.faketouch.multitouch", true); + AddFeature("android.hardware.faketouch", true); + } else if (name == "android.hardware.touchscreen.multitouch") { + AddFeature("android.hardware.touchscreen", true); + } else if (name == "android.hardware.touchscreen.multitouch.distinct" || + name == "android.hardware.touchscreen.multitouch.jazzhands") { + AddFeature("android.hardware.touchscreen.multitouch", true); + AddFeature("android.hardware.touchscreen", true); + } else if (name == "android.hardware.opengles.aep") { + const int kOpenGLESVersion31 = 0x00030001; + if (kOpenGLESVersion31 > open_gles_version) { + open_gles_version = kOpenGLESVersion31; + } + } + } + } + + /** Returns true if the feature group has the given feature. */ + virtual bool HasFeature(const std::string& name) { + return features_.find(name) != features_.end(); + } + + /** Merges the features of another feature group into this group. */ + void Merge(FeatureGroup* group) { + open_gles_version = std::max(open_gles_version, group->open_gles_version); + for (auto& feature : group->features_) { + features_.insert(feature); + } + } + + protected: + struct Feature { + public: + bool required = false; + int32_t version = -1; + }; + + /* Mapping of feature names to their properties. */ + std::map<std::string, Feature> features_; +}; + +/** + * Represents the default feature group for the application if no <feature-group> elements are + * present in the manifest. + **/ +class CommonFeatureGroup : public FeatureGroup { + public: + CommonFeatureGroup() = default; + void PrintGroup(text::Printer* printer) override { + FeatureGroup::PrintGroup(printer); + + // Also print the implied features + for (auto feature : implied_features_) { + if (features_.find(feature.first) == features_.end()) { + const char* sdk23 = feature.second.implied_from_sdk_k23 ? "-sdk-23" : ""; + printer->Print(StringPrintf(" uses-feature%s: name='%s'\n", sdk23, feature.first.data())); + printer->Print(StringPrintf(" uses-implied-feature%s: name='%s' reason='", sdk23, + feature.first.data())); + + // Print the reasons as a sentence + size_t count = 0; + for (auto reason : feature.second.reasons) { + printer->Print(reason); + if (count + 2 < feature.second.reasons.size()) { + printer->Print(", "); + } else if (count + 1 < feature.second.reasons.size()) { + printer->Print(", and "); + } + count++; + } + printer->Print("'\n"); + } + } + } + + /** Returns true if the feature group has the given feature. */ + bool HasFeature(const std::string& name) override { + return FeatureGroup::HasFeature(name) + || implied_features_.find(name) != implied_features_.end(); + } + + /** Adds a feature to a set of implied features not explicitly requested in the manifest. */ + void addImpliedFeature(const std::string& name, const std::string& reason, bool sdk23 = false) { + auto entry = implied_features_.find(name); + if (entry == implied_features_.end()) { + implied_features_.insert(std::make_pair(name, ImpliedFeature(sdk23))); + entry = implied_features_.find(name); + } + + // A non-sdk 23 implied feature takes precedence. + if (entry->second.implied_from_sdk_k23 && !sdk23) { + entry->second.implied_from_sdk_k23 = false; + } + + entry->second.reasons.insert(reason); + } + + /** + * Adds a feature to a set of implied features for all features that are implied by the presence + * of the permission. + **/ + void addImpliedFeaturesForPermission(int32_t targetSdk, const std::string& name, bool sdk23) { + if (name == "android.permission.CAMERA") { + addImpliedFeature("android.hardware.camera", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.ACCESS_FINE_LOCATION") { + if (targetSdk < SDK_LOLLIPOP) { + addImpliedFeature("android.hardware.location.gps", + StringPrintf("requested %s permission", name.data()), + sdk23); + addImpliedFeature("android.hardware.location.gps", + StringPrintf("targetSdkVersion < %d", SDK_LOLLIPOP), + sdk23); + } + addImpliedFeature("android.hardware.location", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.ACCESS_COARSE_LOCATION") { + if (targetSdk < SDK_LOLLIPOP) { + addImpliedFeature("android.hardware.location.network", + StringPrintf("requested %s permission", name.data()), + sdk23); + addImpliedFeature("android.hardware.location.network", + StringPrintf("targetSdkVersion < %d", SDK_LOLLIPOP), + sdk23); + } + addImpliedFeature("android.hardware.location", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.ACCESS_MOCK_LOCATION" || + name == "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" || + name == "android.permission.INSTALL_LOCATION_PROVIDER") { + addImpliedFeature("android.hardware.location", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.BLUETOOTH" || + name == "android.permission.BLUETOOTH_ADMIN") { + if (targetSdk > SDK_DONUT) { + addImpliedFeature("android.hardware.bluetooth", + StringPrintf("requested %s permission", name.data()), + sdk23); + addImpliedFeature("android.hardware.bluetooth", + StringPrintf("targetSdkVersion > %d", SDK_DONUT), + sdk23); + } + + } else if (name == "android.permission.RECORD_AUDIO") { + addImpliedFeature("android.hardware.microphone", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.ACCESS_WIFI_STATE" || + name == "android.permission.CHANGE_WIFI_STATE" || + name == "android.permission.CHANGE_WIFI_MULTICAST_STATE") { + addImpliedFeature("android.hardware.wifi", + StringPrintf("requested %s permission", name.data()), + sdk23); + + } else if (name == "android.permission.CALL_PHONE" || + name == "android.permission.CALL_PRIVILEGED" || + name == "android.permission.MODIFY_PHONE_STATE" || + name == "android.permission.PROCESS_OUTGOING_CALLS" || + name == "android.permission.READ_SMS" || + name == "android.permission.RECEIVE_SMS" || + name == "android.permission.RECEIVE_MMS" || + name == "android.permission.RECEIVE_WAP_PUSH" || + name == "android.permission.SEND_SMS" || + name == "android.permission.WRITE_APN_SETTINGS" || + name == "android.permission.WRITE_SMS") { + addImpliedFeature("android.hardware.telephony", + "requested a telephony permission", + sdk23); + } + } + + private: + /** + * Represents a feature that has been automatically added due to a pre-requisite or for some + * other reason. + */ + struct ImpliedFeature { + explicit ImpliedFeature(bool sdk23 = false) : implied_from_sdk_k23(sdk23) {} + + /** List of human-readable reasons for why this feature was implied. */ + std::set<std::string> reasons; + + // Was this implied by a permission from SDK 23 (<uses-permission-sdk-23 />) + bool implied_from_sdk_k23; + }; + + /* Mapping of implied feature names to their properties. */ + std::map<std::string, ImpliedFeature> implied_features_; +}; + +/** Represents <uses-feature> elements. **/ +class UsesFeature : public ManifestExtractor::Element { + public: + UsesFeature() = default; + void Extract(xml::Element* element) override { + const std::string* name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + int32_t* gl = GetAttributeInteger(FindAttribute(element, GL_ES_VERSION_ATTR)); + bool required = GetAttributeIntegerDefault( + FindAttribute(element, REQUIRED_ATTR), true) != 0; + int32_t version = GetAttributeIntegerDefault( + FindAttribute(element, kAndroidNamespace, "version"), 0); + + // Add the feature to the parent feature group element if one exists; otherwise, add it to the + // common feature group + FeatureGroup* feature_group = ElementCast<FeatureGroup>(extractor()->parent_stack()[0]); + if (!feature_group) { + feature_group = extractor()->GetCommonFeatureGroup(); + } else { + // All features in side of <feature-group> elements are required. + required = true; + } + + if (name) { + feature_group->AddFeature(*name, required, version); + } else if (gl) { + feature_group->open_gles_version = std::max(feature_group->open_gles_version, *gl); + } + } +}; + +/** Represents <uses-permission> elements. **/ +class UsesPermission : public ManifestExtractor::Element { + public: + UsesPermission() = default; + std::string name; + std::string requiredFeature; + std::string requiredNotFeature; + int32_t required = true; + int32_t maxSdkVersion = -1; + + void Extract(xml::Element* element) override { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + requiredFeature = GetAttributeStringDefault( + FindAttribute(element, REQUIRED_FEATURE_ATTR), ""); + requiredNotFeature = GetAttributeStringDefault( + FindAttribute(element, REQUIRED_NOT_FEATURE_ATTR), ""); + required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1); + maxSdkVersion = GetAttributeIntegerDefault( + FindAttribute(element, MAX_SDK_VERSION_ATTR), -1); + + if (!name.empty()) { + CommonFeatureGroup* common = extractor()->GetCommonFeatureGroup(); + common->addImpliedFeaturesForPermission(extractor()->target_sdk(), name, false); + } + } + + void Print(text::Printer* printer) override { + if (!name.empty()) { + printer->Print(StringPrintf("uses-permission: name='%s'", name.data())); + if (maxSdkVersion >= 0) { + printer->Print(StringPrintf(" maxSdkVersion='%d'", maxSdkVersion)); + } + if (!requiredFeature.empty()) { + printer->Print(StringPrintf(" requiredFeature='%s'", requiredFeature.data())); + } + if (!requiredNotFeature.empty()) { + printer->Print(StringPrintf(" requiredNotFeature='%s'", requiredNotFeature.data())); + } + printer->Print("\n"); + if (required == 0) { + printer->Print(StringPrintf("optional-permission: name='%s'", name.data())); + if (maxSdkVersion >= 0) { + printer->Print(StringPrintf(" maxSdkVersion='%d'", maxSdkVersion)); + } + printer->Print("\n"); + } + } + } + + void PrintImplied(text::Printer* printer, const std::string& reason) { + printer->Print(StringPrintf("uses-implied-permission: name='%s'", name.data())); + if (maxSdkVersion >= 0) { + printer->Print(StringPrintf(" maxSdkVersion='%d'", maxSdkVersion)); + } + printer->Print(StringPrintf(" reason='%s'\n", reason.data())); + } +}; + +/** Represents <uses-permission-sdk-23> elements. **/ +class UsesPermissionSdk23 : public ManifestExtractor::Element { + public: + UsesPermissionSdk23() = default; + const std::string* name = nullptr; + const int32_t* maxSdkVersion = nullptr; + + void Extract(xml::Element* element) override { + name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + maxSdkVersion = GetAttributeInteger(FindAttribute(element, MAX_SDK_VERSION_ATTR)); + + if (name) { + CommonFeatureGroup* common = extractor()->GetCommonFeatureGroup(); + common->addImpliedFeaturesForPermission(extractor()->target_sdk(), *name, true); + } + } + + void Print(text::Printer* printer) override { + if (name) { + printer->Print(StringPrintf("uses-permission-sdk-23: name='%s'", name->data())); + if (maxSdkVersion) { + printer->Print(StringPrintf(" maxSdkVersion='%d'", *maxSdkVersion)); + } + printer->Print("\n"); + } + } +}; + +/** Represents <permission> elements. These elements are only printing when dumping permissions. **/ +class Permission : public ManifestExtractor::Element { + public: + Permission() = default; + std::string name; + + void Extract(xml::Element* element) override { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + } + + void Print(text::Printer* printer) override { + if (extractor()->options_.only_permissions && !name.empty()) { + printer->Print(StringPrintf("permission: %s\n", name.data())); + } + } +}; + +/** Represents <activity> elements. **/ +class Activity : public ManifestExtractor::Element { + public: + Activity() = default; + std::string name; + std::string icon; + std::string label; + std::string banner; + + bool has_component_ = false; + bool has_launcher_category = false; + bool has_leanback_launcher_category = false; + bool has_main_action = false; + + void Extract(xml::Element* element) override { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + label = GetAttributeStringDefault(FindAttribute(element, LABEL_ATTR), ""); + icon = GetAttributeStringDefault(FindAttribute(element, ICON_ATTR), ""); + banner = GetAttributeStringDefault(FindAttribute(element, BANNER_ATTR), ""); + + // Retrieve the package name from the manifest + std::string package; + for (auto& parent : extractor()->parent_stack()) { + if (auto manifest = ElementCast<Manifest>(parent)) { + package = manifest->package; + break; + } + } + + // Fully qualify the activity name + ssize_t idx = name.find("."); + if (idx == 0) { + name = package + name; + } else if (idx < 0) { + name = package + "." + name; + } + + auto orientation = GetAttributeInteger(FindAttribute(element, SCREEN_ORIENTATION_ATTR)); + if (orientation) { + CommonFeatureGroup* common = extractor()->GetCommonFeatureGroup(); + int orien = *orientation; + if (orien == 0 || orien == 6 || orien == 8) { + // Requests landscape, sensorLandscape, or reverseLandscape. + common->addImpliedFeature("android.hardware.screen.landscape", + "one or more activities have specified a landscape orientation", + false); + } else if (orien == 1 || orien == 7 || orien == 9) { + // Requests portrait, sensorPortrait, or reversePortrait. + common->addImpliedFeature("android.hardware.screen.portrait", + "one or more activities have specified a portrait orientation", + false); + } + } + } + + void Print(text::Printer* printer) override { + // Print whether the activity has the HOME category and a the MAIN action + if (has_main_action && has_launcher_category) { + printer->Print("launchable-activity:"); + if (!name.empty()) { + printer->Print(StringPrintf(" name='%s' ", name.data())); + } + printer->Print(StringPrintf(" label='%s' icon='%s'\n", + android::ResTable::normalizeForOutput(label.data()).c_str(), + icon.data())); + } + + // Print wether the activity has the HOME category and a the MAIN action + if (has_leanback_launcher_category) { + printer->Print("leanback-launchable-activity:"); + if (!name.empty()) { + printer->Print(StringPrintf(" name='%s' ", name.data())); + } + printer->Print(StringPrintf(" label='%s' icon='%s' banner='%s'\n", + android::ResTable::normalizeForOutput(label.data()).c_str(), + icon.data(), banner.data())); + } + } +}; + +/** Represents <intent-filter> elements. */ +class IntentFilter : public ManifestExtractor::Element { + public: + IntentFilter() = default; +}; + +/** Represents <category> elements. */ +class Category : public ManifestExtractor::Element { + public: + Category() = default; + std::string component = ""; + + void Extract(xml::Element* element) override { + const std::string* category = GetAttributeString(FindAttribute(element, NAME_ATTR)); + + auto parent_stack = extractor()->parent_stack(); + if (category && ElementCast<IntentFilter>(parent_stack[0]) + && ElementCast<Activity>(parent_stack[1])) { + Activity* activity = ElementCast<Activity>(parent_stack[1]); + + if (*category == "android.intent.category.LAUNCHER") { + activity->has_launcher_category = true; + } else if (*category == "android.intent.category.LEANBACK_LAUNCHER") { + activity->has_leanback_launcher_category = true; + } else if (*category == "android.intent.category.HOME") { + component = "launcher"; + } + } + } +}; + +/** + * Represents <provider> elements. The elements may have an <intent-filter> which may have <action> + * elements nested within. + **/ +class Provider : public ManifestExtractor::Element { + public: + Provider() = default; + bool has_required_saf_attributes = false; + + void Extract(xml::Element* element) override { + const int32_t* exported = GetAttributeInteger(FindAttribute(element, EXPORTED_ATTR)); + const int32_t* grant_uri_permissions = GetAttributeInteger( + FindAttribute(element, GRANT_URI_PERMISSIONS_ATTR)); + const std::string* permission = GetAttributeString( + FindAttribute(element, PERMISSION_ATTR)); + + has_required_saf_attributes = ((exported && *exported != 0) + && (grant_uri_permissions && *grant_uri_permissions != 0) + && (permission && *permission == "android.permission.MANAGE_DOCUMENTS")); + } +}; + +/** Represents <receiver> elements. **/ +class Receiver : public ManifestExtractor::Element { + public: + Receiver() = default; + const std::string* permission = nullptr; + bool has_component = false; + + void Extract(xml::Element* element) override { + permission = GetAttributeString(FindAttribute(element, PERMISSION_ATTR)); + } +}; + +/**Represents <service> elements. **/ +class Service : public ManifestExtractor::Element { + public: + Service() = default; + const std::string* permission = nullptr; + bool has_component = false; + + void Extract(xml::Element* element) override { + permission = GetAttributeString(FindAttribute(element, PERMISSION_ATTR)); + } +}; + +/** Represents <uses-library> elements. **/ +class UsesLibrary : public ManifestExtractor::Element { + public: + UsesLibrary() = default; + std::string name; + int required; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1); + } + } + + void Print(text::Printer* printer) override { + if (!name.empty()) { + printer->Print(StringPrintf("uses-library%s:'%s'\n", + (required == 0) ? "-not-required" : "", name.data())); + } + } +}; + +/** Represents <static-library> elements. **/ +class StaticLibrary : public ManifestExtractor::Element { + public: + StaticLibrary() = default; + std::string name; + int version; + int versionMajor; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0); + versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0); + } + } + + void Print(text::Printer* printer) override { + printer->Print(StringPrintf( + "static-library: name='%s' version='%d' versionMajor='%d'\n", + name.data(), version, versionMajor)); + } +}; + +/** Represents <uses-static-library> elements. **/ +class UsesStaticLibrary : public ManifestExtractor::Element { + public: + UsesStaticLibrary() = default; + std::string name; + int version; + int versionMajor; + std::vector<std::string> certDigests; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0); + versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0); + AddCertDigest(element); + } + } + + void AddCertDigest(xml::Element* element) { + std::string digest = GetAttributeStringDefault(FindAttribute(element, CERT_DIGEST_ATTR), ""); + // We allow ":" delimiters in the SHA declaration as this is the format + // emitted by the certtool making it easy for developers to copy/paste. + digest.erase(std::remove(digest.begin(), digest.end(), ':'), digest.end()); + if (!digest.empty()) { + certDigests.push_back(digest); + } + } + + void Print(text::Printer* printer) override { + printer->Print(StringPrintf( + "uses-static-library: name='%s' version='%d' versionMajor='%d'", + name.data(), version, versionMajor)); + for (size_t i = 0; i < certDigests.size(); i++) { + printer->Print(StringPrintf(" certDigest='%s'", certDigests[i].data())); + } + printer->Print("\n"); + } +}; + +/** + * Represents <meta-data> elements. These tags are only printed when a flag is passed in to + * explicitly enable meta data printing. + **/ +class MetaData : public ManifestExtractor::Element { + public: + MetaData() = default; + std::string name; + std::string value; + const int* value_int; + std::string resource; + const int* resource_int; + + void Extract(xml::Element* element) override { + name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + value = GetAttributeStringDefault(FindAttribute(element, VALUE_ATTR), ""); + value_int = GetAttributeInteger(FindAttribute(element, VALUE_ATTR)); + resource = GetAttributeStringDefault(FindAttribute(element, RESOURCE_ATTR), ""); + resource_int = GetAttributeInteger(FindAttribute(element, RESOURCE_ATTR)); + } + + void Print(text::Printer* printer) override { + if (extractor()->options_.include_meta_data && !name.empty()) { + printer->Print(StringPrintf("meta-data: name='%s' ", name.data())); + if (!value.empty()) { + printer->Print(StringPrintf("value='%s' ", value.data())); + } else if (value_int) { + printer->Print(StringPrintf("value='%d' ", *value_int)); + } else { + if (!resource.empty()) { + printer->Print(StringPrintf("resource='%s' ", resource.data())); + } else if (resource_int) { + printer->Print(StringPrintf("resource='%d' ", *resource_int)); + } + } + printer->Print("\n"); + } + } +}; + +/** + * Represents <action> elements. Detects the presence of certain activity, provider, receiver, and + * service components. + **/ +class Action : public ManifestExtractor::Element { + public: + Action() = default; + std::string component = ""; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + std::string action = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), ""); + + if (ElementCast<IntentFilter>(parent_stack[0])) { + if (ElementCast<Activity>(parent_stack[1])) { + // Detects the presence of a particular type of activity. + Activity* activity = ElementCast<Activity>(parent_stack[1]); + auto map = std::map<std::string, std::string>({ + { "android.intent.action.MAIN" , "main" }, + { "android.intent.action.VIDEO_CAMERA" , "camera" }, + { "android.intent.action.STILL_IMAGE_CAMERA_SECURE" , "camera-secure" }, + }); + + auto entry = map.find(action); + if (entry != map.end()) { + component = entry->second; + activity->has_component_ = true; + } + + if (action == "android.intent.action.MAIN") { + activity->has_main_action = true; + } + + } else if (ElementCast<Receiver>(parent_stack[1])) { + // Detects the presence of a particular type of receiver. If the action requires a + // permission, then the receiver element is checked for the permission. + Receiver* receiver = ElementCast<Receiver>(parent_stack[1]); + auto map = std::map<std::string, std::string>({ + { "android.appwidget.action.APPWIDGET_UPDATE" , "app-widget" }, + { "android.app.action.DEVICE_ADMIN_ENABLED" , "device-admin" }, + }); + + auto permissions = std::map<std::string, std::string>({ + { "android.app.action.DEVICE_ADMIN_ENABLED" , "android.permission.BIND_DEVICE_ADMIN" }, + }); + + auto entry = map.find(action); + auto permission = permissions.find(action); + if (entry != map.end() && (permission == permissions.end() + || (receiver->permission && permission->second == *receiver->permission))) { + receiver->has_component = true; + component = entry->second; + } + + } else if (ElementCast<Service>(parent_stack[1])) { + // Detects the presence of a particular type of service. If the action requires a + // permission, then the service element is checked for the permission. + Service* service = ElementCast<Service>(parent_stack[1]); + auto map = std::map<std::string, std::string>({ + { "android.view.InputMethod" , "ime" }, + { "android.service.wallpaper.WallpaperService" , "wallpaper" }, + { "android.accessibilityservice.AccessibilityService" , "accessibility" }, + { "android.printservice.PrintService" , "print-service" }, + { "android.nfc.cardemulation.action.HOST_APDU_SERVICE" , "host-apdu" }, + { "android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE" , "offhost-apdu" }, + { "android.service.notification.NotificationListenerService" ,"notification-listener" }, + { "android.service.dreams.DreamService" , "dream" }, + }); + + auto permissions = std::map<std::string, std::string>({ + { "android.accessibilityservice.AccessibilityService" , + "android.permission.BIND_ACCESSIBILITY_SERVICE" }, + { "android.printservice.PrintService" , "android.permission.BIND_PRINT_SERVICE" }, + { "android.nfc.cardemulation.action.HOST_APDU_SERVICE" , + "android.permission.BIND_NFC_SERVICE" }, + { "android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE" , + "android.permission.BIND_NFC_SERVICE" }, + { "android.service.notification.NotificationListenerService" , + "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" }, + { "android.service.dreams.DreamService" , "android.permission.BIND_DREAM_SERVICE" }, + }); + + auto entry = map.find(action); + auto permission = permissions.find(action); + if (entry != map.end() && (permission == permissions.end() + || (service->permission && permission->second == *service->permission))) { + service->has_component= true; + component = entry->second; + } + + } else if (ElementCast<Provider>(parent_stack[1])) { + // Detects the presence of a particular type of receiver. If the provider requires a + // permission, then the provider element is checked for the permission. + // Detect whether this action + Provider* provider = ElementCast<Provider>(parent_stack[1]); + if (action == "android.content.action.DOCUMENTS_PROVIDER" + && provider->has_required_saf_attributes) { + component = "document-provider"; + } + } + } + + // Represents a searchable interface + if (action == "android.intent.action.SEARCH") { + component = "search"; + } + } +}; + +/** + * Represents <supports-input> elements. The element may have <input-type> elements nested within. + **/ +class SupportsInput : public ManifestExtractor::Element { + public: + SupportsInput() = default; + std::vector<std::string> inputs; + + void Print(text::Printer* printer) override { + const size_t size = inputs.size(); + if (size > 0) { + printer->Print("supports-input: '"); + for (size_t i = 0; i < size; i++) { + printer->Print(StringPrintf("value='%s' ", inputs[i].data())); + } + printer->Print("\n"); + } + } +}; + +/** Represents <input-type> elements. **/ +class InputType : public ManifestExtractor::Element { + public: + InputType() = default; + void Extract(xml::Element* element) override { + auto name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + auto parent_stack = extractor()->parent_stack(); + + // Add the input to the set of supported inputs + if (name && ElementCast<SupportsInput>(parent_stack[0])) { + SupportsInput* supports = ElementCast<SupportsInput>(parent_stack[0]); + supports->inputs.push_back(*name); + } + } +}; + +/** Represents <original-package> elements. **/ +class OriginalPackage : public ManifestExtractor::Element { + public: + OriginalPackage() = default; + const std::string* name = nullptr; + + void Extract(xml::Element* element) override { + name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + } + + void Print(text::Printer* printer) override { + if (name) { + printer->Print(StringPrintf("original-package:'%s'\n", name->data())); + } + } +}; + +/** * Represents <package-verifier> elements. **/ +class PackageVerifier : public ManifestExtractor::Element { + public: + PackageVerifier() = default; + const std::string* name = nullptr; + const std::string* public_key = nullptr; + + void Extract(xml::Element* element) override { + name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + public_key = GetAttributeString(FindAttribute(element, PUBLIC_KEY_ATTR)); + } + + void Print(text::Printer* printer) override { + if (name && public_key) { + printer->Print(StringPrintf("package-verifier: name='%s' publicKey='%s'\n", + name->data(), public_key->data())); + } + } +}; + +/** Represents <uses-package> elements. **/ +class UsesPackage : public ManifestExtractor::Element { + public: + UsesPackage() = default; + const std::string* packageType = nullptr; + const std::string* name = nullptr; + int version; + int versionMajor; + std::vector<std::string> certDigests; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) { + packageType = GetAttributeString(FindAttribute(element, PACKAGE_TYPE_ATTR)); + name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0); + versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0); + AddCertDigest(element); + } + } + + void AddCertDigest(xml::Element* element) { + std::string digest = GetAttributeStringDefault(FindAttribute(element, CERT_DIGEST_ATTR), ""); + // We allow ":" delimiters in the SHA declaration as this is the format + // emitted by the certtool making it easy for developers to copy/paste. + digest.erase(std::remove(digest.begin(), digest.end(), ':'), digest.end()); + if (!digest.empty()) { + certDigests.push_back(digest); + } + } + + void Print(text::Printer* printer) override { + if (name) { + if (packageType) { + printer->Print(StringPrintf( + "uses-typed-package: type='%s' name='%s' version='%d' versionMajor='%d'", + packageType->data(), name->data(), version, versionMajor)); + for (size_t i = 0; i < certDigests.size(); i++) { + printer->Print(StringPrintf(" certDigest='%s'", certDigests[i].data())); + } + printer->Print("\n"); + } else { + printer->Print(StringPrintf("uses-package:'%s'\n", name->data())); + } + } + } +}; + +/** Represents <additional-certificate> elements. **/ +class AdditionalCertificate : public ManifestExtractor::Element { + public: + AdditionalCertificate() = default; + + void Extract(xml::Element* element) override { + auto parent_stack = extractor()->parent_stack(); + if (parent_stack.size() > 0) { + if (ElementCast<UsesPackage>(parent_stack[0])) { + UsesPackage* uses = ElementCast<UsesPackage>(parent_stack[0]); + uses->AddCertDigest(element); + } else if (ElementCast<UsesStaticLibrary>(parent_stack[0])) { + UsesStaticLibrary* uses = ElementCast<UsesStaticLibrary>(parent_stack[0]); + uses->AddCertDigest(element); + } + } + } +}; + +/** Represents <screen> elements found in <compatible-screens> elements. */ +class Screen : public ManifestExtractor::Element { + public: + Screen() = default; + const int32_t* size = nullptr; + const int32_t* density = nullptr; + + void Extract(xml::Element* element) override { + size = GetAttributeInteger(FindAttribute(element, SCREEN_SIZE_ATTR)); + density = GetAttributeInteger(FindAttribute(element, SCREEN_DENSITY_ATTR)); + } +}; + +/** + * Represents <compatible-screens> elements. These elements have <screen> elements nested within + * that each denote a supported screen size and screen density. + **/ +class CompatibleScreens : public ManifestExtractor::Element { + public: + CompatibleScreens() = default; + void Print(text::Printer* printer) override { + printer->Print("compatible-screens:"); + + bool first = true; + ForEachChild(this, [&printer, &first](ManifestExtractor::Element* el){ + if (auto screen = ElementCast<Screen>(el)) { + if (first) { + first = false; + } else { + printer->Print(","); + } + + if (screen->size && screen->density) { + printer->Print(StringPrintf("'%d/%d'", *screen->size, *screen->density)); + } + } + }); + printer->Print("\n"); + } +}; + +/** Represents <supports-gl-texture> elements. **/ +class SupportsGlTexture : public ManifestExtractor::Element { + public: + SupportsGlTexture() = default; + const std::string* name = nullptr; + + void Extract(xml::Element* element) override { + name = GetAttributeString(FindAttribute(element, NAME_ATTR)); + } + + void Print(text::Printer* printer) override { + if (name) { + printer->Print(StringPrintf("supports-gl-texture:'%s'\n", name->data())); + } + } +}; + +/** Recursively prints the extracted badging element. */ +static void Print(ManifestExtractor::Element* el, text::Printer* printer) { + el->Print(printer); + for (auto &child : el->children()) { + Print(child.get(), printer); + } +} + +bool ManifestExtractor::Dump(text::Printer* printer, IDiagnostics* diag) { + // Load the manifest + std::unique_ptr<xml::XmlResource> doc = apk_->LoadXml("AndroidManifest.xml", diag); + if (doc == nullptr) { + diag->Error(DiagMessage() << "failed to find AndroidManifest.xml"); + return false; + } + + xml::Element* element = doc->root.get(); + if (element->name != "manifest") { + diag->Error(DiagMessage() << "manifest does not start with <manifest> tag"); + return false; + } + + // Print only the <uses-permission>, <uses-permission-sdk23>, and <permission> elements if + // printing only permission elements is requested + if (options_.only_permissions) { + std::unique_ptr<ManifestExtractor::Element> manifest_element = + ManifestExtractor::Element::Inflate(this, element); + + if (auto manifest = ElementCast<Manifest>(manifest_element.get())) { + for (xml::Element* child : element->GetChildElements()) { + if (child->name == "uses-permission" || child->name == "uses-permission-sdk-23" + || child->name == "permission") { + auto permission_element = ManifestExtractor::Element::Inflate(this, child); + manifest->AddChild(permission_element); + } + } + + printer->Print(StringPrintf("package: %s\n", manifest->package.data())); + ForEachChild(manifest, [&printer](ManifestExtractor::Element* el) -> void { + el->Print(printer); + }); + + return true; + } + + return false; + } + + // Collect information about the resource configurations + if (apk_->GetResourceTable()) { + for (auto &package : apk_->GetResourceTable()->packages) { + for (auto &type : package->types) { + for (auto &entry : type->entries) { + for (auto &value : entry->values) { + std::string locale_str = value->config.GetBcp47LanguageTag(); + + // Collect all the unique locales of the apk + if (locales_.find(locale_str) == locales_.end()) { + ConfigDescription config = ManifestExtractor::DummyConfig(); + config.setBcp47Locale(locale_str.data()); + locales_.insert(std::make_pair(locale_str, config)); + } + + // Collect all the unique density of the apk + uint16_t density = (value->config.density == 0) ? (uint16_t) 160 + : value->config.density; + if (densities_.find(density) == densities_.end()) { + ConfigDescription config = ManifestExtractor::DummyConfig(); + config.density = density; + densities_.insert(std::make_pair(density, config)); + } + } + } + } + } + } + + // Extract badging information + auto root = Visit(element); + + // Print the elements in order seen + Print(root.get(), printer); + + /** Recursively checks the extracted elements for the specified permission. **/ + auto FindPermission = [&](ManifestExtractor::Element* root, + const std::string& name) -> ManifestExtractor::Element* { + return FindElement(root, [&](ManifestExtractor::Element* el) -> bool { + if (UsesPermission* permission = ElementCast<UsesPermission>(el)) { + return permission->name == name; + } + return false; + }); + }; + + auto PrintPermission = [&printer](const std::string& name, const std::string& reason, + int32_t max_sdk_version) -> void { + auto permission = util::make_unique<UsesPermission>(); + permission->name = name; + permission->maxSdkVersion = max_sdk_version; + permission->Print(printer); + permission->PrintImplied(printer, reason); + }; + + // Implied permissions + // Pre-1.6 implicitly granted permission compatibility logic + CommonFeatureGroup* common_feature_group = GetCommonFeatureGroup(); + bool insert_write_external = false; + auto write_external_permission = ElementCast<UsesPermission>( + FindPermission(root.get(), "android.permission.WRITE_EXTERNAL_STORAGE")); + + if (target_sdk() < 4) { + if (!write_external_permission) { + PrintPermission("android.permission.WRITE_EXTERNAL_STORAGE", "targetSdkVersion < 4", -1); + insert_write_external = true; + } + + if (!FindPermission(root.get(), "android.permission.READ_PHONE_STATE")) { + PrintPermission("android.permission.READ_PHONE_STATE", "targetSdkVersion < 4", -1); + } + } + + // If the application has requested WRITE_EXTERNAL_STORAGE, we will + // force them to always take READ_EXTERNAL_STORAGE as well. We always + // do this (regardless of target API version) because we can't have + // an app with write permission but not read permission. + auto read_external = FindPermission(root.get(), "android.permission.READ_EXTERNAL_STORAGE"); + if (!read_external && (insert_write_external || write_external_permission)) { + PrintPermission("android.permission.READ_EXTERNAL_STORAGE", + "requested WRITE_EXTERNAL_STORAGE", + (write_external_permission) ? write_external_permission->maxSdkVersion : -1); + } + + // Pre-JellyBean call log permission compatibility. + if (target_sdk() < 16) { + if (!FindPermission(root.get(), "android.permission.READ_CALL_LOG") + && FindPermission(root.get(), "android.permission.READ_CONTACTS")) { + PrintPermission("android.permission.READ_CALL_LOG", + "targetSdkVersion < 16 and requested READ_CONTACTS", -1); + } + + if (!FindPermission(root.get(), "android.permission.WRITE_CALL_LOG") + && FindPermission(root.get(), "android.permission.WRITE_CONTACTS")) { + PrintPermission("android.permission.WRITE_CALL_LOG", + "targetSdkVersion < 16 and requested WRITE_CONTACTS", -1); + } + } + + // If the app hasn't declared the touchscreen as a feature requirement (either + // directly or implied, required or not), then the faketouch feature is implied. + if (!common_feature_group->HasFeature("android.hardware.touchscreen")) { + common_feature_group->addImpliedFeature("android.hardware.faketouch", + "default feature for all apps", false); + } + + // Only print the common feature group if no feature group is defined + std::vector<FeatureGroup*> feature_groups; + ForEachChild(root.get(), [&feature_groups](ManifestExtractor::Element* el) -> void { + if (auto feature_group = ElementCast<FeatureGroup>(el)) { + feature_groups.push_back(feature_group); + } + }); + + if (feature_groups.empty()) { + common_feature_group->PrintGroup(printer); + } else { + // Merge the common feature group into the feature group + for (auto& feature_group : feature_groups) { + feature_group->open_gles_version = std::max(feature_group->open_gles_version, + common_feature_group->open_gles_version); + feature_group->Merge(common_feature_group); + feature_group->PrintGroup(printer); + } + }; + + // Collect the component types of the application + std::set<std::string> components; + ForEachChild(root.get(), [&components](ManifestExtractor::Element* el) -> void { + if (ElementCast<Action>(el)) { + auto action = ElementCast<Action>(el); + if (!action->component.empty()) { + components.insert(action->component); + return; + } + } + + if (ElementCast<Category>(el)) { + auto category = ElementCast<Category>(el); + if (!category->component.empty()) { + components.insert(category->component); + return; + } + } + }); + + // Check for the payment component + auto apk = apk_; + ForEachChild(root.get(), [&apk, &components, &diag](ManifestExtractor::Element* el) -> void { + if (auto service = ElementCast<Service>(el)) { + auto host_apdu_action = ElementCast<Action>(FindElement(service, + [&](ManifestExtractor::Element* el) -> bool { + if (auto action = ElementCast<Action>(el)) { + return (action->component == "host-apdu"); + } + return false; + })); + + auto offhost_apdu_action = ElementCast<Action>(FindElement(service, + [&](ManifestExtractor::Element* el) -> bool { + if (auto action = ElementCast<Action>(el)) { + return (action->component == "offhost-apdu"); + } + return false; + })); + + ForEachChild(service, [&apk, &components, &diag, &host_apdu_action, + &offhost_apdu_action](ManifestExtractor::Element* el) -> void { + if (auto meta_data = ElementCast<MetaData>(el)) { + if ((meta_data->name == "android.nfc.cardemulation.host_apdu_service" && host_apdu_action) + || (meta_data->name == "android.nfc.cardemulation.off_host_apdu_service" + && offhost_apdu_action)) { + + // Attempt to load the resource file + if (!meta_data->resource.empty()) { + return; + } + auto resource = apk->LoadXml(meta_data->resource, diag); + if (!resource) { + return; + } + + // Look for the payment category on an <aid-group> element + auto& root = resource.get()->root; + if ((host_apdu_action && root->name == "host-apdu-service") + || (offhost_apdu_action && root->name == "offhost-apdu-service")) { + + for (auto& child : root->GetChildElements()) { + if (child->name == "aid-group") { + auto category = FindAttribute(child, CATEGORY_ATTR); + if (category && category->value == "payment") { + components.insert("payment"); + return; + } + } + } + } + } + } + }); + } + }); + + // Print the components types if they are present + auto PrintComponent = [&components, &printer](const std::string& component) -> void { + if (components.find(component) != components.end()) { + printer->Print(StringPrintf("provides-component:'%s'\n", component.data())); + } + }; + + PrintComponent("app-widget"); + PrintComponent("device-admin"); + PrintComponent("ime"); + PrintComponent("wallpaper"); + PrintComponent("accessibility"); + PrintComponent("print-service"); + PrintComponent("payment"); + PrintComponent("search"); + PrintComponent("document-provider"); + PrintComponent("launcher"); + PrintComponent("notification-listener"); + PrintComponent("dream"); + PrintComponent("camera"); + PrintComponent("camera-secure"); + + // Print presence of main activity + if (components.find("main") != components.end()) { + printer->Print("main\n"); + } + + // Print presence of activities, recivers, and services with no special components + FindElement(root.get(), [&printer](ManifestExtractor::Element* el) -> bool { + if (auto activity = ElementCast<Activity>(el)) { + if (!activity->has_component_) { + printer->Print("other-activities\n"); + return true; + } + } + return false; + }); + + FindElement(root.get(), [&printer](ManifestExtractor::Element* el) -> bool { + if (auto receiver = ElementCast<Receiver>(el)) { + if (!receiver->has_component) { + printer->Print("other-receivers\n"); + return true; + } + } + return false; + }); + + FindElement(root.get(), [&printer](ManifestExtractor::Element* el) -> bool { + if (auto service = ElementCast<Service>(el)) { + if (!service->has_component) { + printer->Print("other-services\n"); + return true; + } + } + return false; + }); + + // Print the supported screens + SupportsScreen* screen = ElementCast<SupportsScreen>(FindElement(root.get(), + [&](ManifestExtractor::Element* el) -> bool { + return ElementCast<SupportsScreen>(el) != nullptr; + })); + + if (screen) { + screen->PrintScreens(printer, target_sdk_); + } else { + // Print the default supported screens + SupportsScreen default_screens; + default_screens.PrintScreens(printer, target_sdk_); + } + + // Print all the unique locales of the apk + printer->Print("locales:"); + for (auto& config : locales_) { + if (config.first.empty()) { + printer->Print(" '--_--'"); + } else { + printer->Print(StringPrintf(" '%s'", config.first.data())); + } + } + printer->Print("\n"); + + // Print all the densities locales of the apk + printer->Print("densities:"); + for (auto& config : densities_) { + printer->Print(StringPrintf(" '%d'", config.first)); + } + printer->Print("\n"); + + // Print the supported architectures of the app + std::set<std::string> architectures; + auto it = apk_->GetFileCollection()->Iterator(); + while (it->HasNext()) { + auto file_path = it->Next()->GetSource().path; + + + size_t pos = file_path.find("lib/"); + if (pos != std::string::npos) { + file_path = file_path.substr(pos + 4); + pos = file_path.find("/"); + if (pos != std::string::npos) { + file_path = file_path.substr(0, pos); + } + + architectures.insert(file_path); + } + } + + // Determine if the application has multiArch supports + auto has_multi_arch = FindElement(root.get(), [&](ManifestExtractor::Element* el) -> bool { + if (auto application = ElementCast<Application>(el)) { + return application->has_multi_arch; + } + return false; + }); + + bool output_alt_native_code = false; + // A multiArch package is one that contains 64-bit and + // 32-bit versions of native code and expects 3rd-party + // apps to load these native code libraries. Since most + // 64-bit systems also support 32-bit apps, the apps + // loading this multiArch package's code may be either + if (has_multi_arch) { + // If this is a multiArch package, report the 64-bit + // version only. Then as a separate entry, report the + // rest. + // + // If we report the 32-bit architecture, this APK will + // be installed on a 32-bit device, causing a large waste + // of bandwidth and disk space. This assumes that + // the developer of the multiArch package has also + // made a version that is 32-bit only. + const std::string kIntel64 = "x86_64"; + const std::string kArm64 = "arm64-v8a"; + + auto arch = architectures.find(kIntel64); + if (arch == architectures.end()) { + arch = architectures.find(kArm64); + } + + if (arch != architectures.end()) { + printer->Print(StringPrintf("native-code: '%s'\n", arch->data())); + architectures.erase(arch); + output_alt_native_code = true; + } + } + + if (architectures.size() > 0) { + if (output_alt_native_code) { + printer->Print("alt-"); + } + printer->Print("native-code:"); + for (auto& arch : architectures) { + printer->Print(StringPrintf(" '%s'", arch.data())); + } + printer->Print("\n"); + } + + return true; +} + +/** + * Returns the element casted to the type if the element is of that type. Otherwise, returns a null + * pointer. + **/ +template<typename T> +T* ElementCast(ManifestExtractor::Element* element) { + if (element == nullptr) { + return nullptr; + } + + const std::unordered_map<std::string, bool> kTagCheck = { + {"action", std::is_base_of<Action, T>::value}, + {"activity", std::is_base_of<Activity, T>::value}, + {"application", std::is_base_of<Application, T>::value}, + {"category", std::is_base_of<Category, T>::value}, + {"compatible-screens", std::is_base_of<CompatibleScreens, T>::value}, + {"feature-group", std::is_base_of<FeatureGroup, T>::value}, + {"input-type", std::is_base_of<InputType, T>::value}, + {"intent-filter", std::is_base_of<IntentFilter, T>::value}, + {"meta-data", std::is_base_of<MetaData, T>::value}, + {"manifest", std::is_base_of<Manifest, T>::value}, + {"original-package", std::is_base_of<OriginalPackage, T>::value}, + {"package-verifier", std::is_base_of<PackageVerifier, T>::value}, + {"permission", std::is_base_of<Permission, T>::value}, + {"provider", std::is_base_of<Provider, T>::value}, + {"receiver", std::is_base_of<Receiver, T>::value}, + {"screen", std::is_base_of<Screen, T>::value}, + {"service", std::is_base_of<Service, T>::value}, + {"supports-gl-texture", std::is_base_of<SupportsGlTexture, T>::value}, + {"supports-input", std::is_base_of<SupportsInput, T>::value}, + {"supports-screens", std::is_base_of<SupportsScreen, T>::value}, + {"uses-configuration", std::is_base_of<UsesConfiguarion, T>::value}, + {"uses-feature", std::is_base_of<UsesFeature, T>::value}, + {"uses-permission", std::is_base_of<UsesPermission, T>::value}, + {"uses-permission-sdk-23", std::is_base_of<UsesPermissionSdk23, T>::value}, + {"uses-library", std::is_base_of<UsesLibrary, T>::value}, + {"uses-package", std::is_base_of<UsesPackage, T>::value}, + {"static-library", std::is_base_of<StaticLibrary, T>::value}, + {"uses-static-library", std::is_base_of<UsesStaticLibrary, T>::value}, + {"additional-certificate", std::is_base_of<AdditionalCertificate, T>::value}, + {"uses-sdk", std::is_base_of<UsesSdkBadging, T>::value}, + }; + + auto check = kTagCheck.find(element->tag()); + if (check != kTagCheck.end() && check->second) { + return static_cast<T*>(element); + } + return nullptr; +} + +template<typename T> +std::unique_ptr<T> CreateType() { + return std::move(util::make_unique<T>()); +} + +std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Element::Inflate( + ManifestExtractor* extractor, xml::Element* el) { + const std::unordered_map<std::string, + std::function<std::unique_ptr<ManifestExtractor::Element>()>> + kTagCheck = { + {"action", &CreateType<Action>}, + {"activity", &CreateType<Activity>}, + {"application", &CreateType<Application>}, + {"category", &CreateType<Category>}, + {"compatible-screens", &CreateType<CompatibleScreens>}, + {"feature-group", &CreateType<FeatureGroup>}, + {"input-type", &CreateType<InputType>}, + {"intent-filter",&CreateType<IntentFilter>}, + {"manifest", &CreateType<Manifest>}, + {"meta-data", &CreateType<MetaData>}, + {"original-package", &CreateType<OriginalPackage>}, + {"package-verifier", &CreateType<PackageVerifier>}, + {"permission", &CreateType<Permission>}, + {"provider", &CreateType<Provider>}, + {"receiver", &CreateType<Receiver>}, + {"screen", &CreateType<Screen>}, + {"service", &CreateType<Service>}, + {"supports-gl-texture", &CreateType<SupportsGlTexture>}, + {"supports-input", &CreateType<SupportsInput>}, + {"supports-screens", &CreateType<SupportsScreen>}, + {"uses-configuration", &CreateType<UsesConfiguarion>}, + {"uses-feature", &CreateType<UsesFeature>}, + {"uses-permission", &CreateType<UsesPermission>}, + {"uses-permission-sdk-23", &CreateType<UsesPermissionSdk23>}, + {"uses-library", &CreateType<UsesLibrary>}, + {"static-library", &CreateType<StaticLibrary>}, + {"uses-static-library", &CreateType<UsesStaticLibrary>}, + {"uses-package", &CreateType<UsesPackage>}, + {"additional-certificate", &CreateType<AdditionalCertificate>}, + {"uses-sdk", &CreateType<UsesSdkBadging>}, + }; + + // Attempt to map the xml tag to a element inflater + std::unique_ptr<ManifestExtractor::Element> element; + auto check = kTagCheck.find(el->name); + if (check != kTagCheck.end()) { + element = check->second(); + } else { + element = util::make_unique<ManifestExtractor::Element>(); + } + + element->extractor_ = extractor; + element->tag_ = el->name; + element->Extract(el); + return element; +} + +std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Visit(xml::Element* el) { + auto element = ManifestExtractor::Element::Inflate(this, el); + parent_stack_.insert(parent_stack_.begin(), element.get()); + + // Process the element and recursively visit the children + for (xml::Element* child : el->GetChildElements()) { + auto v = Visit(child); + element->AddChild(v); + } + + parent_stack_.erase(parent_stack_.begin()); + return element; +} + + +int DumpManifest(LoadedApk* apk, DumpManifestOptions& options, text::Printer* printer, + IDiagnostics* diag) { + ManifestExtractor extractor(apk, options); + return extractor.Dump(printer, diag); +} + +} // namespace aapt diff --git a/tools/aapt2/dump/DumpManifest.h b/tools/aapt2/dump/DumpManifest.h new file mode 100644 index 000000000000..daf22ed57a84 --- /dev/null +++ b/tools/aapt2/dump/DumpManifest.h @@ -0,0 +1,39 @@ +/* + * 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_MANIFEST_H +#define AAPT2_DUMP_MANIFEST_H + +#include "Diagnostics.h" +#include "LoadedApk.h" +#include "text/Printer.h" + +namespace aapt { + +struct DumpManifestOptions { + /** Include meta information from <meta-data> elements in the output. */ + bool include_meta_data = false; + /** Only output permission information. */ + bool only_permissions = false; +}; + +/** Print information extracted from the manifest of the APK. */ +int DumpManifest(LoadedApk* apk, DumpManifestOptions& options, text::Printer* printer, + IDiagnostics* diag); + +} // namespace aapt + +#endif // AAPT2_DUMP_MANIFEST_H
\ No newline at end of file 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..ed70fb3c57d6 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())); @@ -395,7 +398,7 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, if (type_spec_flags & ResTable_typeSpec::SPEC_OVERLAYABLE) { Overlayable overlayable; overlayable.source = source_.WithLine(0); - if (!table_->SetOverlayableMangled(name, overlayable, diag_)) { + if (!table_->AddOverlayableMangled(name, overlayable, diag_)) { return false; } } diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 6fd4c8d2c135..8a86f63a30c1 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()); } } @@ -447,7 +446,7 @@ class PackageFlattener { config_masks[entry->id.value()] |= util::HostToDevice32(ResTable_typeSpec::SPEC_PUBLIC); } - if (entry->overlayable) { + if (!entry->overlayable_declarations.empty()) { config_masks[entry->id.value()] |= util::HostToDevice32(ResTable_typeSpec::SPEC_OVERLAYABLE); } diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index bab70109fc65..cd1414c7e628 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 = @@ -576,7 +634,7 @@ TEST_F(TableFlattenerTest, FlattenOverlayable) { .AddSimple("com.app.test:integer/overlayable", ResourceId(0x7f020000)) .Build(); - ASSERT_TRUE(table->SetOverlayable(test::ParseNameOrDie("com.app.test:integer/overlayable"), + ASSERT_TRUE(table->AddOverlayable(test::ParseNameOrDie("com.app.test:integer/overlayable"), Overlayable{}, test::GetDiagnostics())); ResTable res_table; diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index d1b2fdb84afc..f612914269de 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -437,15 +437,37 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr entry->allow_new = std::move(allow_new); } - if (pb_entry.has_overlayable()) { - const pb::Overlayable& pb_overlayable = pb_entry.overlayable(); - + for (const pb::Overlayable& pb_overlayable : pb_entry.overlayable()) { Overlayable overlayable; + switch (pb_overlayable.policy()) { + case pb::Overlayable::NONE: + overlayable.policy = {}; + break; + case pb::Overlayable::PUBLIC: + overlayable.policy = Overlayable::Policy::kPublic; + break; + case pb::Overlayable::PRODUCT: + overlayable.policy = Overlayable::Policy::kProduct; + break; + case pb::Overlayable::PRODUCT_SERVICES: + overlayable.policy = Overlayable::Policy::kProductServices; + break; + case pb::Overlayable::SYSTEM: + overlayable.policy = Overlayable::Policy::kSystem; + break; + case pb::Overlayable::VENDOR: + overlayable.policy = Overlayable::Policy::kVendor; + break; + default: + *out_error = "unknown overlayable policy"; + return false; + } + if (pb_overlayable.has_source()) { DeserializeSourceFromPb(pb_overlayable.source(), src_pool, &overlayable.source); } overlayable.comment = pb_overlayable.comment(); - entry->overlayable = std::move(overlayable); + entry->overlayable_declarations.push_back(overlayable); } ResourceId resid(pb_package.package_id().id(), pb_type.type_id().id(), diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index 7e35ea7bb7a3..f1e96d61191b 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -310,11 +310,31 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table pb_allow_new->set_comment(entry->allow_new.value().comment); } - if (entry->overlayable) { - pb::Overlayable* pb_overlayable = pb_entry->mutable_overlayable(); - SerializeSourceToPb(entry->overlayable.value().source, &source_pool, + for (const Overlayable& overlayable : entry->overlayable_declarations) { + pb::Overlayable* pb_overlayable = pb_entry->add_overlayable(); + if (overlayable.policy) { + switch (overlayable.policy.value()) { + case Overlayable::Policy::kPublic: + pb_overlayable->set_policy(pb::Overlayable::PUBLIC); + break; + case Overlayable::Policy::kProduct: + pb_overlayable->set_policy(pb::Overlayable::PRODUCT); + break; + case Overlayable::Policy::kProductServices: + pb_overlayable->set_policy(pb::Overlayable::PRODUCT_SERVICES); + break; + case Overlayable::Policy::kSystem: + pb_overlayable->set_policy(pb::Overlayable::SYSTEM); + break; + case Overlayable::Policy::kVendor: + pb_overlayable->set_policy(pb::Overlayable::VENDOR); + break; + } + } + + SerializeSourceToPb(overlayable.source, &source_pool, pb_overlayable->mutable_source()); - pb_overlayable->set_comment(entry->overlayable.value().comment); + pb_overlayable->set_comment(overlayable.comment); } for (const std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp index a0d92f7b9308..95dbbeb58a5d 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -34,6 +34,7 @@ class MockFileCollection : public io::IFileCollection { public: MOCK_METHOD1(FindFile, io::IFile*(const StringPiece& path)); MOCK_METHOD0(Iterator, std::unique_ptr<io::IFileCollectionIterator>()); + MOCK_METHOD0(GetDirSeparator, char()); }; TEST(ProtoSerializeTest, SerializeSinglePackage) { @@ -92,7 +93,7 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { util::make_unique<Reference>(expected_ref), context->GetDiagnostics())); // Make an overlayable resource. - ASSERT_TRUE(table->SetOverlayable(test::ParseNameOrDie("com.app.a:integer/overlayable"), + ASSERT_TRUE(table->AddOverlayable(test::ParseNameOrDie("com.app.a:integer/overlayable"), Overlayable{}, test::GetDiagnostics())); pb::ResourceTable pb_table; @@ -105,7 +106,7 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { ResourceTable new_table; std::string error; - ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error)); + ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error)) << error; EXPECT_THAT(error, IsEmpty()); Id* new_id = test::GetValue<Id>(&new_table, "com.app.a:id/foo"); @@ -159,7 +160,8 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { new_table.FindResource(test::ParseNameOrDie("com.app.a:integer/overlayable")); ASSERT_TRUE(search_result); ASSERT_THAT(search_result.value().entry, NotNull()); - EXPECT_TRUE(search_result.value().entry->overlayable); + EXPECT_THAT(search_result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_FALSE(search_result.value().entry->overlayable_declarations[0].policy); } TEST(ProtoSerializeTest, SerializeAndDeserializeXml) { @@ -463,4 +465,59 @@ TEST(ProtoSerializeTest, SerializeDeserializeConfiguration) { "night-xhdpi-stylus-keysexposed-qwerty-navhidden-dpad-300x200-v23"); } +TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddOverlayable("com.app.a:bool/foo", Overlayable::Policy::kSystem) + .AddOverlayable("com.app.a:bool/foo", Overlayable::Policy::kProduct) + .AddOverlayable("com.app.a:bool/bar", Overlayable::Policy::kProductServices) + .AddOverlayable("com.app.a:bool/bar", Overlayable::Policy::kVendor) + .AddOverlayable("com.app.a:bool/baz", Overlayable::Policy::kPublic) + .AddOverlayable("com.app.a:bool/biz", {}) + .AddValue("com.app.a:bool/fiz", ResourceUtils::TryParseBool("true")) + .Build(); + + pb::ResourceTable pb_table; + SerializeTableToPb(*table, &pb_table, context->GetDiagnostics()); + + MockFileCollection files; + ResourceTable new_table; + std::string error; + ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error)); + EXPECT_THAT(error, IsEmpty()); + + Maybe<ResourceTable::SearchResult> result = + new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/foo")); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(2)); + EXPECT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kSystem)); + EXPECT_THAT(result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kProduct)); + + result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/bar")); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(2)); + EXPECT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProductServices)); + EXPECT_THAT(result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kVendor)); + + result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/baz")); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kPublic)); + + result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/biz")); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(1)); + EXPECT_FALSE(result.value().entry->overlayable_declarations[0].policy); + + result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/fiz")); + ASSERT_TRUE(result); + EXPECT_THAT(result.value().entry->overlayable_declarations.size(), Eq(0)); +} + } // namespace aapt 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/File.h b/tools/aapt2/io/File.h index f06e28c79c7b..565aad6f2284 100644 --- a/tools/aapt2/io/File.h +++ b/tools/aapt2/io/File.h @@ -25,6 +25,7 @@ #include "Source.h" #include "io/Data.h" +#include "util/Files.h" #include "util/Util.h" namespace aapt { @@ -103,6 +104,7 @@ class IFileCollection { virtual IFile* FindFile(const android::StringPiece& path) = 0; virtual std::unique_ptr<IFileCollectionIterator> Iterator() = 0; + virtual char GetDirSeparator() = 0; }; } // namespace io diff --git a/tools/aapt2/io/FileSystem.cpp b/tools/aapt2/io/FileSystem.cpp index 1387d2218ed4..51cc9032fb3e 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(); } @@ -80,5 +128,9 @@ std::unique_ptr<IFileCollectionIterator> FileCollection::Iterator() { return util::make_unique<FileCollectionIterator>(this); } +char FileCollection::GetDirSeparator() { + return file::sDirSep; +} + } // namespace io } // namespace aapt diff --git a/tools/aapt2/io/FileSystem.h b/tools/aapt2/io/FileSystem.h index 6be8807735f1..04c6fa15bc85 100644 --- a/tools/aapt2/io/FileSystem.h +++ b/tools/aapt2/io/FileSystem.h @@ -59,10 +59,15 @@ 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; std::unique_ptr<IFileCollectionIterator> Iterator() override; + char GetDirSeparator() override; private: DISALLOW_COPY_AND_ASSIGN(FileCollection); diff --git a/tools/aapt2/io/Util.h b/tools/aapt2/io/Util.h index b07fb5399313..5f978a8e2c35 100644 --- a/tools/aapt2/io/Util.h +++ b/tools/aapt2/io/Util.h @@ -20,6 +20,7 @@ #include <string> #include "google/protobuf/message_lite.h" +#include "google/protobuf/io/coded_stream.h" #include "format/Archive.h" #include "io/File.h" @@ -122,6 +123,23 @@ class ZeroCopyInputAdaptor : public ::google::protobuf::io::ZeroCopyInputStream io::InputStream* in_; }; +class ProtoInputStreamReader { + public: + explicit ProtoInputStreamReader(io::InputStream* in) : in_(in) { } + + /** Deserializes a MessageLite proto from the current position in the input stream.*/ + template <typename T> bool ReadMessage(T *message_lite) { + ZeroCopyInputAdaptor adapter(in_); + google::protobuf::io::CodedInputStream coded_stream(&adapter); + coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max(), + coded_stream.BytesUntilTotalBytesLimit()); + return message_lite->ParseFromCodedStream(&coded_stream); + } + + private: + io::InputStream* in_; +}; + } // namespace io } // namespace aapt diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp index 269b6c5a12e1..427dc92505d4 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; @@ -32,6 +33,11 @@ ZipFile::ZipFile(ZipArchiveHandle handle, const ZipEntry& entry, : zip_handle_(handle), zip_entry_(entry), source_(source) {} std::unique_ptr<IData> ZipFile::OpenAsData() { + // The file will fail to be mmaped if it is empty + if (zip_entry_.uncompressed_length == 0) { + return util::make_unique<EmptyData>(); + } + if (zip_entry_.method == kCompressStored) { int fd = GetFileDescriptor(zip_handle_); @@ -121,9 +127,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 +143,7 @@ std::unique_ptr<ZipFileCollection> ZipFileCollection::Create( if (out_error) *out_error = ErrorCodeString(result); return {}; } + return collection; } @@ -147,6 +159,13 @@ std::unique_ptr<IFileCollectionIterator> ZipFileCollection::Iterator() { return util::make_unique<ZipFileCollectionIterator>(this); } +char ZipFileCollection::GetDirSeparator() { + // According to the zip file specification, section 4.4.17.1: + // "All slashes MUST be forward slashes '/' as opposed to backwards slashes '\' for compatibility + // with Amiga and UNIX file systems etc." + return '/'; +} + ZipFileCollection::~ZipFileCollection() { if (handle_) { CloseArchive(handle_); diff --git a/tools/aapt2/io/ZipArchive.h b/tools/aapt2/io/ZipArchive.h index 8381259d77c5..b283e57d4011 100644 --- a/tools/aapt2/io/ZipArchive.h +++ b/tools/aapt2/io/ZipArchive.h @@ -66,6 +66,7 @@ class ZipFileCollection : public IFileCollection { io::IFile* FindFile(const android::StringPiece& path) override; std::unique_ptr<IFileCollectionIterator> Iterator() override; + char GetDirSeparator() override; ~ZipFileCollection() override; 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..52e168ed47aa 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); @@ -330,13 +384,13 @@ bool CollectProguardRules(xml::XmlResource* res, KeepSet* keep_set) { return true; } -void WriteKeepSet(const KeepSet& keep_set, OutputStream* out) { +void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep) { Printer printer(out); for (const auto& entry : keep_set.manifest_class_set_) { 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,19 @@ 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>("); + printer.Print((minimal_keep) ? entry.first.signature : "..."); + printer.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>("); + printer.Print((minimal_keep) ? entry.first.signature : "..."); + printer.Println("); }"); } printer.Println(); } @@ -367,7 +427,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..38b4860d1d61 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) { @@ -64,26 +70,26 @@ class KeepSet { } private: - friend void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out); + friend void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep); friend bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set, std::set<UsageLocation>* locations); 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); -void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out); +void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep); bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set, std::set<UsageLocation>* locations); @@ -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 f774e3a1245a..da24907417fa 100644 --- a/tools/aapt2/java/ProguardRules_test.cpp +++ b/tools/aapt2/java/ProguardRules_test.cpp @@ -27,14 +27,54 @@ using ::testing::Not; namespace aapt { -std::string GetKeepSetString(const proguard::KeepSet& set) { +std::string GetKeepSetString(const proguard::KeepSet& set, bool minimal_rules) { std::string out; StringOutputStream sout(&out); - proguard::WriteKeepSet(set, &sout); + proguard::WriteKeepSet(set, &sout, minimal_rules); sout.Flush(); 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, /** minimal_rules */ false); + 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>(); }")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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"( @@ -43,11 +83,13 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { @@ -57,11 +99,13 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }")); } TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { @@ -73,12 +117,48 @@ 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, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(...); }")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + 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>(...); }")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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) { @@ -90,11 +170,14 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) { @@ -126,16 +209,21 @@ 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)); - - std::string actual = GetKeepSetString(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, /** minimal_rules */ false); EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); EXPECT_THAT(actual, HasSubstr("int foo")); EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); + 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")); } TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) { @@ -148,15 +236,21 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-if class **.R$layout")); + EXPECT_THAT(actual, HasSubstr("int foo")); + EXPECT_THAT(actual, HasSubstr("int bar")); - EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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) { @@ -169,12 +263,16 @@ 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)); - - std::string actual = GetKeepSetString(set); + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set)); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); EXPECT_THAT(actual, Not(HasSubstr("-if"))); EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, Not(HasSubstr("-if"))); + EXPECT_THAT(actual, HasSubstr( + "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }")); } TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { @@ -185,11 +283,15 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** bar_method(android.view.View); }")); - EXPECT_THAT(actual, HasSubstr("bar_method")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** bar_method(android.view.View); }")); } TEST(ProguardRulesTest, MenuRulesAreEmitted) { @@ -204,14 +306,59 @@ 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); + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr( + "-keepclassmembers class * { *** on_click(android.view.MenuItem); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(...); }")); + EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat"))); - EXPECT_THAT(actual, HasSubstr("on_click")); - EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); - EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }")); + + actual = GetKeepSetString(set, /** minimal_rules */ true); + 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 7180a9918abe..85bf6f218ba0 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -283,6 +283,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) { @@ -382,6 +393,10 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, uses_static_library_action.Action(RequiredAndroidAttribute("certDigest")); uses_static_library_action["additional-certificate"]; + xml::XmlNodeAction& uses_package_action = application_action["uses-package"]; + uses_package_action.Action(RequiredNameIsJavaPackage); + uses_package_action["additional-certificate"]; + if (options_.debug_mode) { application_action.Action([&](xml::Element* el) -> bool { xml::Attribute *attr = el->FindOrCreateAttribute(xml::kSchemaAndroid, "debuggable"); 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 13054bf14c78..05990de6a9b3 100644 --- a/tools/aapt2/link/NoDefaultResourceRemover.cpp +++ b/tools/aapt2/link/NoDefaultResourceRemover.cpp @@ -26,17 +26,7 @@ using android::ConfigDescription; 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; @@ -48,22 +38,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 @@ -78,7 +90,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..d777e22fa4b7 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -101,8 +101,18 @@ static bool MergeType(IAaptContext* context, const Source& src, ResourceTableTyp return true; } -static bool MergeEntry(IAaptContext* context, const Source& src, bool overlay, - ResourceEntry* dst_entry, ResourceEntry* src_entry) { +static bool MergeEntry(IAaptContext* context, const Source& src, + 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. @@ -124,17 +134,35 @@ static bool MergeEntry(IAaptContext* context, const Source& src, bool overlay, dst_entry->allow_new = std::move(src_entry->allow_new); } - if (src_entry->overlayable) { - if (dst_entry->overlayable && !overlay) { - context->GetDiagnostics()->Error(DiagMessage(src_entry->overlayable.value().source) - << "duplicate overlayable declaration for resource '" - << src_entry->name << "'"); - context->GetDiagnostics()->Error(DiagMessage(dst_entry->overlayable.value().source) - << "previous declaration here"); - return false; + for (auto& src_overlayable : src_entry->overlayable_declarations) { + for (auto& dst_overlayable : dst_entry->overlayable_declarations) { + // An overlayable resource cannot be declared twice with the same policy + if (src_overlayable.policy == dst_overlayable.policy) { + context->GetDiagnostics()->Error(DiagMessage(src_overlayable.source) + << "duplicate overlayable declaration for resource '" + << src_entry->name << "'"); + context->GetDiagnostics()->Error(DiagMessage(dst_overlayable.source) + << "previous declaration here"); + return false; + } + + // An overlayable resource cannot be declared once with a policy and without a policy because + // the policy becomes unused + if (!src_overlayable.policy || !dst_overlayable.policy) { + context->GetDiagnostics()->Error(DiagMessage(src_overlayable.source) + << "overlayable resource '" << src_entry->name + << "' declared once with a policy and once with no " + << "policy"); + context->GetDiagnostics()->Error(DiagMessage(dst_overlayable.source) + << "previous declaration here"); + return false; + } } - dst_entry->overlayable = std::move(src_entry->overlayable); } + + dst_entry->overlayable_declarations.insert(dst_entry->overlayable_declarations.end(), + src_entry->overlayable_declarations.begin(), + src_entry->overlayable_declarations.end()); return true; } @@ -234,7 +262,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, dst_entry, src_entry.get(), options_.strict_visibility)) { error = true; continue; } @@ -271,8 +299,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..d6579d37b452 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(); @@ -362,4 +436,97 @@ TEST_F(TableMergerTest, OverlaidStyleablesAndStylesShouldBeMerged) { Eq(make_value(Reference(test::ParseNameOrDie("com.app.a:style/OverlayParent"))))); } +TEST_F(TableMergerTest, AddOverlayable) { + std::unique_ptr<ResourceTable> table_a = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProduct) + .Build(); + + std::unique_ptr<ResourceTable> table_b = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProductServices) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); + ASSERT_TRUE(merger.Merge({}, table_b.get(), false /*overlay*/)); + + const ResourceName name = test::ParseNameOrDie("com.app.a:bool/foo"); + Maybe<ResourceTable::SearchResult> result = final_table.FindResource(name); + ASSERT_TRUE(result); + ASSERT_THAT(result.value().entry->overlayable_declarations.size(), Eq(2)); + ASSERT_THAT(result.value().entry->overlayable_declarations[0].policy, + Eq(Overlayable::Policy::kProduct)); + ASSERT_THAT(result.value().entry->overlayable_declarations[1].policy, + Eq(Overlayable::Policy::kProductServices)); +} + +TEST_F(TableMergerTest, AddDuplicateOverlayableFail) { + std::unique_ptr<ResourceTable> table_a = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProduct) + .Build(); + + std::unique_ptr<ResourceTable> table_b = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProduct) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); + ASSERT_FALSE(merger.Merge({}, table_b.get(), false /*overlay*/)); +} + +TEST_F(TableMergerTest, AddOverlayablePolicyAndNoneFirstFail) { + std::unique_ptr<ResourceTable> table_a = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", {}) + .Build(); + + std::unique_ptr<ResourceTable> table_b = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProduct) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); + ASSERT_FALSE(merger.Merge({}, table_b.get(), false /*overlay*/)); +} + +TEST_F(TableMergerTest, AddOverlayablePolicyAndNoneLastFail) { + std::unique_ptr<ResourceTable> table_a = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", Overlayable::Policy::kProduct) + .Build(); + + std::unique_ptr<ResourceTable> table_b = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddOverlayable("bool/foo", {}) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + TableMerger merger(context_.get(), &final_table, options); + ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); + ASSERT_FALSE(merger.Merge({}, table_b.get(), false /*overlay*/)); +} + } // namespace aapt diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp index aa9f9ab4798f..8c9c43409569 100644 --- a/tools/aapt2/optimize/MultiApkGenerator.cpp +++ b/tools/aapt2/optimize/MultiApkGenerator.cpp @@ -27,6 +27,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" @@ -267,7 +268,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; } @@ -276,21 +277,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..ef57f9c56dab --- /dev/null +++ b/tools/aapt2/optimize/ResourceFilter_test.cpp @@ -0,0 +1,75 @@ +/* + * 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 ::android::ConfigDescription; +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/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp index 1dd5502202b2..2e717ff2bc3b 100644 --- a/tools/aapt2/split/TableSplitter.cpp +++ b/tools/aapt2/split/TableSplitter.cpp @@ -156,6 +156,12 @@ static void MarkNonPreferredDensitiesAsClaimed( bool TableSplitter::VerifySplitConstraints(IAaptContext* context) { bool error = false; for (size_t i = 0; i < split_constraints_.size(); i++) { + if (split_constraints_[i].configs.size() == 0) { + // For now, treat this as a warning. We may consider aborting processing. + context->GetDiagnostics()->Warn(DiagMessage() + << "no configurations for constraint '" + << split_constraints_[i].name << "'"); + } for (size_t j = i + 1; j < split_constraints_.size(); j++) { for (const ConfigDescription& config : split_constraints_[i].configs) { if (split_constraints_[j].configs.find(config) != split_constraints_[j].configs.end()) { @@ -242,6 +248,7 @@ void TableSplitter::SplitTable(ResourceTable* original_table) { if (!split_entry->id) { split_entry->id = entry->id; split_entry->visibility = entry->visibility; + split_entry->overlayable_declarations = entry->overlayable_declarations; } // Copy the selected values into the new Split Entry. diff --git a/tools/aapt2/split/TableSplitter.h b/tools/aapt2/split/TableSplitter.h index 91afaa37e2d4..cb1395f3132b 100644 --- a/tools/aapt2/split/TableSplitter.h +++ b/tools/aapt2/split/TableSplitter.h @@ -30,6 +30,7 @@ namespace aapt { struct SplitConstraints { std::set<android::ConfigDescription> configs; + std::string name; }; struct TableSplitterOptions { diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp index f33ae3155192..03b59e033402 100644 --- a/tools/aapt2/test/Builders.cpp +++ b/tools/aapt2/test/Builders.cpp @@ -135,6 +135,15 @@ ResourceTableBuilder& ResourceTableBuilder::SetSymbolState(const StringPiece& na return *this; } +ResourceTableBuilder& ResourceTableBuilder::AddOverlayable(const StringPiece& name, + const Maybe<Overlayable::Policy> p) { + ResourceName res_name = ParseNameOrDie(name); + Overlayable overlayable; + overlayable.policy = p; + CHECK(table_->AddOverlayable(res_name, overlayable, GetDiagnostics())); + return *this; +} + StringPool* ResourceTableBuilder::string_pool() { return &table_->string_pool; } diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h index a88b11e1334e..d68c24ddc665 100644 --- a/tools/aapt2/test/Builders.h +++ b/tools/aapt2/test/Builders.h @@ -73,6 +73,8 @@ class ResourceTableBuilder { const ResourceId& id, std::unique_ptr<Value> value); ResourceTableBuilder& SetSymbolState(const android::StringPiece& name, const ResourceId& id, Visibility::Level level, bool allow_new = false); + ResourceTableBuilder& AddOverlayable(const android::StringPiece& name, + Maybe<Overlayable::Policy> policy); StringPool* string_pool(); std::unique_ptr<ResourceTable> Build(); @@ -208,6 +210,112 @@ class PostProcessingConfigurationBuilder { configuration::PostProcessingConfiguration config_; }; +class ConfigDescriptionBuilder { + public: + ConfigDescriptionBuilder() = default; + + ConfigDescriptionBuilder& setMcc(uint16_t mcc) { + config_.mcc = mcc; + return *this; + } + ConfigDescriptionBuilder& setMnc(uint16_t mnc) { + config_.mnc = mnc; + return *this; + } + ConfigDescriptionBuilder& setLanguage(uint16_t language) { + config_.language[0] = language >> 8; + config_.language[1] = language & 0xff; + return *this; + } + ConfigDescriptionBuilder& setCountry(uint16_t country) { + config_.country[0] = country >> 8; + config_.country[1] = country & 0xff; + return *this; + } + ConfigDescriptionBuilder& setOrientation(uint8_t orientation) { + config_.orientation = orientation; + return *this; + } + ConfigDescriptionBuilder& setTouchscreen(uint8_t touchscreen) { + config_.touchscreen = touchscreen; + return *this; + } + ConfigDescriptionBuilder& setDensity(uint16_t density) { + config_.density = density; + return *this; + } + ConfigDescriptionBuilder& setKeyboard(uint8_t keyboard) { + config_.keyboard = keyboard; + return *this; + } + ConfigDescriptionBuilder& setNavigation(uint8_t navigation) { + config_.navigation = navigation; + return *this; + } + ConfigDescriptionBuilder& setInputFlags(uint8_t inputFlags) { + config_.inputFlags = inputFlags; + return *this; + } + ConfigDescriptionBuilder& setInputPad0(uint8_t inputPad0) { + config_.inputPad0 = inputPad0; + return *this; + } + ConfigDescriptionBuilder& setScreenWidth(uint16_t screenWidth) { + config_.screenWidth = screenWidth; + return *this; + } + ConfigDescriptionBuilder& setScreenHeight(uint16_t screenHeight) { + config_.screenHeight = screenHeight; + return *this; + } + ConfigDescriptionBuilder& setSdkVersion(uint16_t sdkVersion) { + config_.sdkVersion = sdkVersion; + return *this; + } + ConfigDescriptionBuilder& setMinorVersion(uint16_t minorVersion) { + config_.minorVersion = minorVersion; + return *this; + } + ConfigDescriptionBuilder& setScreenLayout(uint8_t screenLayout) { + config_.screenLayout = screenLayout; + return *this; + } + ConfigDescriptionBuilder& setUiMode(uint8_t uiMode) { + config_.uiMode = uiMode; + return *this; + } + ConfigDescriptionBuilder& setSmallestScreenWidthDp(uint16_t smallestScreenWidthDp) { + config_.smallestScreenWidthDp = smallestScreenWidthDp; + return *this; + } + ConfigDescriptionBuilder& setScreenWidthDp(uint16_t screenWidthDp) { + config_.screenWidthDp = screenWidthDp; + return *this; + } + ConfigDescriptionBuilder& setScreenHeightDp(uint16_t screenHeightDp) { + config_.screenHeightDp = screenHeightDp; + return *this; + } + ConfigDescriptionBuilder& setScreenLayout2(uint8_t screenLayout2) { + config_.screenLayout2 = screenLayout2; + return *this; + } + ConfigDescriptionBuilder& setColorMode(uint8_t colorMode) { + config_.colorMode = colorMode; + return *this; + } + ConfigDescriptionBuilder& setScreenConfigPad2(uint16_t screenConfigPad2) { + config_.screenConfigPad2 = screenConfigPad2; + return *this; + } + android::ConfigDescription Build() { + return config_; + } + + private: + android::ConfigDescription config_; +}; + } // namespace test } // namespace aapt diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp index 5a8ff0926483..73105e16559b 100644 --- a/tools/aapt2/util/Files.cpp +++ b/tools/aapt2/util/Files.cpp @@ -102,12 +102,25 @@ FileType GetFileType(const std::string& path) { #endif bool mkdirs(const std::string& path) { - constexpr const mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP; + #ifdef _WIN32 + // Start after the drive path if present. Calling mkdir with only the drive will cause an error. + size_t current_pos = 1u; + if (path.size() >= 3 && path[1] == ':' && + (path[2] == '\\' || path[2] == '/')) { + current_pos = 3u; + } + #else // Start after the first character so that we don't consume the root '/'. // This is safe to do with unicode because '/' will never match with a continuation character. size_t current_pos = 1u; + #endif + constexpr const mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP; while ((current_pos = path.find(sDirSep, current_pos)) != std::string::npos) { std::string parent_path = path.substr(0, current_pos); + if (parent_path.empty()) { + continue; + } + int result = ::android::base::utf8::mkdir(parent_path.c_str(), mode); if (result < 0 && errno != EEXIST) { return false; @@ -149,6 +162,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/Files_test.cpp b/tools/aapt2/util/Files_test.cpp index 219c18397358..202cc261ad89 100644 --- a/tools/aapt2/util/Files_test.cpp +++ b/tools/aapt2/util/Files_test.cpp @@ -18,11 +18,21 @@ #include <sstream> +#include "android-base/stringprintf.h" + #include "test/Test.h" +using ::android::base::StringPrintf; + namespace aapt { namespace file { +#ifdef _WIN32 +constexpr const char sTestDirSep = '\\'; +#else +constexpr const char sTestDirSep = '/'; +#endif + class FilesTest : public ::testing::Test { public: void SetUp() override { @@ -42,16 +52,16 @@ TEST_F(FilesTest, AppendPath) { } TEST_F(FilesTest, AppendPathWithLeadingOrTrailingSeparators) { - std::string base = "hello/"; + std::string base = StringPrintf("hello%c", sTestDirSep); AppendPath(&base, "there"); EXPECT_EQ(expected_path_, base); base = "hello"; - AppendPath(&base, "/there"); + AppendPath(&base, StringPrintf("%cthere", sTestDirSep)); EXPECT_EQ(expected_path_, base); - base = "hello/"; - AppendPath(&base, "/there"); + base = StringPrintf("hello%c", sTestDirSep); + AppendPath(&base, StringPrintf("%cthere", sTestDirSep)); EXPECT_EQ(expected_path_, base); } diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index d1c9ca1644d9..59b7fff0f9e3 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -297,6 +297,107 @@ 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) { + int32_t codepoint = utf32_from_utf8_at(utf8.data(), size, i, nullptr); + + // Calculate the high and low surrogates as UTF-16 would + int32_t high = ((codepoint - 0x10000) / 0x400) + 0xD800; + int32_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::string ModifiedUtf8ToUtf8(const std::string& modified_utf8) { + // The UTF-8 representation will have a byte length less than or equal to the Modified UTF-8 + // representation. + std::string output; + output.reserve(modified_utf8.size()); + + size_t index = 0; + const size_t modified_size = modified_utf8.size(); + while (index < modified_size) { + size_t next_index; + int32_t high_surrogate = utf32_from_utf8_at(modified_utf8.data(), modified_size, index, + &next_index); + if (high_surrogate < 0) { + return {}; + } + + // Check that the first codepoint is within the high surrogate range + if (high_surrogate >= 0xD800 && high_surrogate <= 0xDB7F) { + int32_t low_surrogate = utf32_from_utf8_at(modified_utf8.data(), modified_size, next_index, + &next_index); + if (low_surrogate < 0) { + return {}; + } + + // Check that the second codepoint is within the low surrogate range + if (low_surrogate >= 0xDC00 && low_surrogate <= 0xDFFF) { + const char32_t codepoint = (char32_t) (((high_surrogate - 0xD800) * 0x400) + + (low_surrogate - 0xDC00) + 0x10000); + + // The decoded codepoint should represent a 4 byte, UTF-8 character + const size_t utf8_length = (size_t) utf32_to_utf8_length(&codepoint, 1); + if (utf8_length != 4) { + return {}; + } + + // Encode the UTF-8 representation of the codepoint into the string + char* start = &output[output.size()]; + output.resize(output.size() + utf8_length); + utf32_to_utf8((char32_t*) &codepoint, 1, start, utf8_length + 1); + + index = next_index; + continue; + } + } + + // Append non-surrogate pairs to the output string + for (size_t i = index; i < next_index; i++) { + output.push_back(modified_utf8[i]); + } + index = next_index; + } + 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()); @@ -422,7 +523,7 @@ std::string GetString(const android::ResStringPool& pool, size_t idx) { size_t len; const char* str = pool.string8At(idx, &len); if (str != nullptr) { - return std::string(str, len); + return ModifiedUtf8ToUtf8(std::string(str, len)); } return Utf16ToUtf8(GetString16(pool, idx)); } diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h index 0eb35d18c06e..c6e8e6ef7619 100644 --- a/tools/aapt2/util/Util.h +++ b/tools/aapt2/util/Util.h @@ -197,6 +197,10 @@ inline StringBuilder::operator bool() const { return error_.empty(); } +// Converts a UTF8 string into Modified UTF8 +std::string Utf8ToModifiedUtf8(const std::string& utf8); +std::string ModifiedUtf8ToUtf8(const std::string& modified_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/apilint/apilint.py b/tools/apilint/apilint.py index 70a47cf42755..91cd1cba964d 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) - raw = re.split("[\s(),;]+", raw) + # handle each clause differently + raw_prefix, raw_args, _, raw_throws = re.match(r"(.*?)\((.*?)\)( throws )?(.*?);$", raw).groups() + + # 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)) @@ -465,6 +492,7 @@ def verify_parcelable(clazz): def verify_protected(clazz): """Verify that no protected methods or fields are allowed.""" for m in clazz.methods: + if m.name == "finalize": continue if "protected" in m.split: error(clazz, m, "M7", "Protected methods not allowed; must be public") for f in clazz.fields: @@ -670,8 +698,8 @@ def verify_layering(clazz): "android.provider", ["android.content","android.graphics.drawable"], "android.database", - "android.graphics", "android.text", + "android.graphics", "android.os", "android.util" ] @@ -998,6 +1026,10 @@ def verify_resource_names(clazz): # Resources defined by files are foo_bar_baz if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]: for f in clazz.fields: + if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue + if f.name.startswith("config_"): + error(clazz, f, None, "Expected config name to be config_fooBarBaz style") + if re.match("[a-z1-9_]+$", f.name): continue error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style") @@ -1334,18 +1366,79 @@ def verify_clone(clazz): error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()") +def verify_pfd(clazz): + """Verify that android APIs use PFD over FD.""" + examine = clazz.ctors + clazz.methods + for m in examine: + if m.typ == "java.io.FileDescriptor": + error(clazz, m, "FW11", "Must use ParcelFileDescriptor") + if m.typ == "int": + if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name: + error(clazz, m, "FW11", "Must use ParcelFileDescriptor") + for arg in m.args: + if arg == "java.io.FileDescriptor": + error(clazz, m, "FW11", "Must use ParcelFileDescriptor") + + for f in clazz.fields: + if f.typ == "java.io.FileDescriptor": + error(clazz, f, "FW11", "Must use ParcelFileDescriptor") + + +def verify_numbers(clazz): + """Discourage small numbers types like short and byte.""" + + discouraged = ["short","byte"] + + for c in clazz.ctors: + for arg in c.args: + if arg in discouraged: + warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead") + + for f in clazz.fields: + if f.typ in discouraged: + warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead") + + for m in clazz.methods: + if m.typ in discouraged: + warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead") + for arg in m.args: + if arg in discouraged: + warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead") + + +def verify_singleton(clazz): + """Catch singleton objects with constructors.""" + + singleton = False + for m in clazz.methods: + if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw: + singleton = True + + if singleton: + for c in clazz.ctors: + error(clazz, c, None, "Singleton classes should use getInstance() methods") + + + +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) @@ -1397,6 +1490,9 @@ def examine_clazz(clazz): verify_tense(clazz) verify_icu(clazz) verify_clone(clazz) + verify_pfd(clazz) + verify_numbers(clazz) + verify_singleton(clazz) def examine_stream(stream): @@ -1479,6 +1575,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 +1603,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 +1649,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 +1670,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/fonts/add_additional_fonts.py b/tools/fonts/add_additional_fonts.py deleted file mode 100644 index bf4af2b1c56e..000000000000 --- a/tools/fonts/add_additional_fonts.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -# 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. -# - -import sys - -def main(argv): - original_file = 'frameworks/base/data/fonts/fonts.xml' - - if len(argv) == 3: - output_file_path = argv[1] - override_file_path = argv[2] - else: - raise ValueError("Wrong number of arguments %s" % len(argv)) - - fallbackPlaceholderFound = False - with open(original_file, 'r') as input_file: - with open(output_file_path, 'w') as output_file: - for line in input_file: - # If we've found the spot to add additional fonts, add them. - if line.strip() == '<!-- fallback fonts -->': - fallbackPlaceholderFound = True - with open(override_file_path) as override_file: - for override_line in override_file: - output_file.write(override_line) - output_file.write(line) - if not fallbackPlaceholderFound: - raise ValueError('<!-- fallback fonts --> not found in source file: %s' % original_file) - -if __name__ == '__main__': - main(sys.argv) diff --git a/tools/incident_section_gen/main.cpp b/tools/incident_section_gen/main.cpp index 4b5677a75b70..0cf1046ef376 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..257043b30704 100644 --- a/tools/stats_log_api_gen/Collation.cpp +++ b/tools/stats_log_api_gen/Collation.cpp @@ -47,7 +47,8 @@ AtomDecl::AtomDecl(const AtomDecl& that) fields(that.fields), primaryFields(that.primaryFields), exclusiveField(that.exclusiveField), - uidField(that.uidField) {} + uidField(that.uidField), + binaryFields(that.binaryFields) {} AtomDecl::AtomDecl(int c, const string& n, const string& m) :code(c), @@ -116,6 +117,12 @@ 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 if (field->options().GetExtension(os::statsd::log_mode) == + os::statsd::LogMode::MODE_BYTES) { + return JAVA_TYPE_BYTE_ARRAY; } else { return JAVA_TYPE_OBJECT; } @@ -185,6 +192,8 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); it != fields.end(); it++) { const FieldDescriptor *field = it->second; + bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == + os::statsd::LogMode::MODE_BYTES; java_type_t javaType = java_type(field); @@ -192,17 +201,26 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, print_error(field, "Unkown type for field: %s\n", field->name().c_str()); errorCount++; continue; - } else if (javaType == JAVA_TYPE_OBJECT) { - // Allow attribution chain, but only at position 1. - print_error(field, "Message type not allowed for field: %s\n", - field->name().c_str()); - errorCount++; - continue; - } else if (javaType == JAVA_TYPE_BYTE_ARRAY) { - print_error(field, "Raw bytes type not allowed for field: %s\n", - field->name().c_str()); - errorCount++; - continue; + } else if (javaType == JAVA_TYPE_OBJECT && + atomDecl->code < PULL_ATOM_START_ID) { + // Allow attribution chain, but only at position 1. + print_error(field, + "Message type not allowed for field in pushed atoms: %s\n", + field->name().c_str()); + errorCount++; + continue; + } else if (javaType == JAVA_TYPE_BYTE_ARRAY && !isBinaryField) { + print_error(field, "Raw bytes type not allowed for field: %s\n", + field->name().c_str()); + errorCount++; + continue; + } + + if (isBinaryField && javaType != JAVA_TYPE_BYTE_ARRAY) { + print_error(field, "Cannot mark field %s as bytes.\n", + field->name().c_str()); + errorCount++; + continue; } } @@ -228,18 +246,29 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, it != fields.end(); it++) { const FieldDescriptor *field = it->second; java_type_t javaType = java_type(field); + bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == + os::statsd::LogMode::MODE_BYTES; AtomField atField(field->name(), javaType); + // Generate signature for pushed atoms + if (atomDecl->code < PULL_ATOM_START_ID) { + if (javaType == JAVA_TYPE_ENUM) { + // All enums are treated as ints when it comes to function signatures. + signature->push_back(JAVA_TYPE_INT); + collate_enums(*field->enum_type(), &atField); + } else if (javaType == JAVA_TYPE_OBJECT && isBinaryField) { + signature->push_back(JAVA_TYPE_BYTE_ARRAY); + } else { + signature->push_back(javaType); + } + } if (javaType == JAVA_TYPE_ENUM) { // All enums are treated as ints when it comes to function signatures. - signature->push_back(JAVA_TYPE_INT); collate_enums(*field->enum_type(), &atField); - } else { - signature->push_back(javaType); } atomDecl->fields.push_back(atField); - if (field->options().GetExtension(os::statsd::stateFieldOption).option() == + if (field->options().GetExtension(os::statsd::state_field_option).option() == os::statsd::StateField::PRIMARY) { if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || @@ -249,7 +278,7 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, atomDecl->primaryFields.push_back(it->first); } - if (field->options().GetExtension(os::statsd::stateFieldOption).option() == + if (field->options().GetExtension(os::statsd::state_field_option).option() == os::statsd::StateField::EXCLUSIVE) { if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || @@ -275,6 +304,10 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, errorCount++; } } + // Binary field validity is already checked above. + if (isBinaryField) { + atomDecl->binaryFields.push_back(it->first); + } } return errorCount; diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h index 5d2c30292c9c..450b30547c21 100644 --- a/tools/stats_log_api_gen/Collation.h +++ b/tools/stats_log_api_gen/Collation.h @@ -34,6 +34,8 @@ using std::vector; using google::protobuf::Descriptor; using google::protobuf::FieldDescriptor; +const int PULL_ATOM_START_ID = 10000; + /** * The types for atom parameters. */ @@ -48,6 +50,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, @@ -86,6 +89,8 @@ struct AtomDecl { int uidField = 0; + vector<int> binaryFields; + AtomDecl(); AtomDecl(const AtomDecl& that); AtomDecl(int code, const string& name, const string& message); diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp index e519909aa026..8585ae9f3f61 100644 --- a/tools/stats_log_api_gen/main.cpp +++ b/tools/stats_log_api_gen/main.cpp @@ -18,8 +18,6 @@ using namespace std; namespace android { namespace stats_log_api_gen { -const int PULL_ATOM_START_ID = 1000; - int maxPushedAtomId = 2; using android::os::statsd::Atom; @@ -68,6 +66,8 @@ cpp_type_name(java_type_t type) return "double"; case JAVA_TYPE_STRING: return "char const*"; + case JAVA_TYPE_BYTE_ARRAY: + return "char const*"; default: return "UNKNOWN"; } @@ -90,6 +90,8 @@ java_type_name(java_type_t type) return "double"; case JAVA_TYPE_STRING: return "java.lang.String"; + case JAVA_TYPE_BYTE_ARRAY: + return "byte[]"; default: return "UNKNOWN"; } @@ -200,13 +202,40 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, } fprintf(out, " return options;\n"); - fprintf(out, " }\n"); + fprintf(out, "}\n"); fprintf(out, "const std::map<int, StateAtomFieldOptions> " "AtomsInfo::kStateAtomsFieldOptions = " "getStateAtomFieldOptions();\n"); + fprintf(out, + "static std::map<int, std::vector<int>> " + "getBinaryFieldAtoms() {\n"); + fprintf(out, " std::map<int, std::vector<int>> options;\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + if (atom->binaryFields.size() == 0) { + continue; + } + fprintf(out, + "\n // Adding binary fields for atom " + "(%d)%s\n", + atom->code, atom->name.c_str()); + + for (const auto& field : atom->binaryFields) { + fprintf(out, " options[static_cast<int>(%s)].push_back(%d);\n", + make_constant_name(atom->name).c_str(), field); + } + } + + fprintf(out, " return options;\n"); + fprintf(out, "}\n"); + + fprintf(out, + "const std::map<int, std::vector<int>> " + "AtomsInfo::kBytesFieldAtoms = " + "getBinaryFieldAtoms();\n"); fprintf(out, "int64_t lastRetryTimestampNs = -1;\n"); fprintf(out, "const int64_t kMinRetryIntervalNs = NS_PER_SEC * 60 * 20; // 20 minutes\n"); @@ -235,6 +264,12 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " + "const std::map<int, int64_t>& arg%d_2, " + "const std::map<int, char const*>& arg%d_3, " + "const std::map<int, float>& arg%d_4", + argIndex, argIndex, argIndex, argIndex); } else { fprintf(out, ", %s arg%d", cpp_type_name(*arg), argIndex); } @@ -277,6 +312,37 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, fprintf(out, " event.end();\n"); fprintf(out, " }\n"); fprintf(out, " event.end();\n\n"); + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, " event.begin();\n\n"); + fprintf(out, " for (const auto& it : arg%d_1) {\n", argIndex); + fprintf(out, " event.begin();\n"); + fprintf(out, " event << it.first;\n"); + fprintf(out, " event << it.second;\n"); + fprintf(out, " event.end();\n"); + fprintf(out, " }\n"); + + fprintf(out, " for (const auto& it : arg%d_2) {\n", argIndex); + fprintf(out, " event.begin();\n"); + fprintf(out, " event << it.first;\n"); + fprintf(out, " event << it.second;\n"); + fprintf(out, " event.end();\n"); + fprintf(out, " }\n"); + + fprintf(out, " for (const auto& it : arg%d_3) {\n", argIndex); + fprintf(out, " event.begin();\n"); + fprintf(out, " event << it.first;\n"); + fprintf(out, " event << it.second;\n"); + fprintf(out, " event.end();\n"); + fprintf(out, " }\n"); + + fprintf(out, " for (const auto& it : arg%d_4) {\n", argIndex); + fprintf(out, " event.begin();\n"); + fprintf(out, " event << it.first;\n"); + fprintf(out, " event << it.second;\n"); + fprintf(out, " event.end();\n"); + fprintf(out, " }\n"); + + fprintf(out, " event.end();\n\n"); } else { if (*arg == JAVA_TYPE_STRING) { fprintf(out, " if (arg%d == NULL) {\n", argIndex); @@ -300,7 +366,7 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, signature != atoms.signatures.end(); signature++) { int argIndex; - fprintf(out, "int \n"); + fprintf(out, "int\n"); fprintf(out, "stats_write(int32_t code"); argIndex = 1; for (vector<java_type_t>::const_iterator arg = signature->begin(); @@ -317,6 +383,12 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " + "const std::map<int, int64_t>& arg%d_2, " + "const std::map<int, char const*>& arg%d_3, " + "const std::map<int, float>& arg%d_4", + argIndex, argIndex, argIndex, argIndex); } else { fprintf(out, ", %s arg%d", cpp_type_name(*arg), argIndex); } @@ -343,14 +415,16 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, chainField.name.c_str(), chainField.name.c_str()); } } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", arg%d_1, arg%d_2, arg%d_3, arg%d_4", + argIndex, argIndex, argIndex, argIndex); } else { fprintf(out, ", arg%d", argIndex); } 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 +434,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(ret);\n"); + fprintf(out, " }\n"); fprintf(out, " return ret;\n"); fprintf(out, "}\n"); fprintf(out, "\n"); @@ -439,7 +516,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 +527,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(ret);\n"); + fprintf(out, " }\n"); + fprintf(out, " return ret;\n\n"); fprintf(out, "}\n"); fprintf(out, "\n"); @@ -491,6 +571,15 @@ static void write_cpp_usage( chainField.name.c_str(), chainField.name.c_str()); } } + } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", const std::map<int, int32_t>& %s_int" + ", const std::map<int, int64_t>& %s_long" + ", const std::map<int, char const*>& %s_str" + ", const std::map<int, float>& %s_float", + field->name.c_str(), + field->name.c_str(), + field->name.c_str(), + field->name.c_str()); } else { fprintf(out, ", %s %s", cpp_type_name(field->javaType), field->name.c_str()); } @@ -503,7 +592,7 @@ static void write_cpp_method_header( const AtomDecl &attributionDecl) { for (set<vector<java_type_t>>::const_iterator signature = signatures.begin(); signature != signatures.end(); signature++) { - fprintf(out, "int %s(int32_t code ", method_name.c_str()); + fprintf(out, "int %s(int32_t code", method_name.c_str()); int argIndex = 1; for (vector<java_type_t>::const_iterator arg = signature->begin(); arg != signature->end(); arg++) { @@ -518,6 +607,12 @@ static void write_cpp_method_header( chainField.name.c_str(), chainField.name.c_str()); } } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " + "const std::map<int, int64_t>& arg%d_2, " + "const std::map<int, char const*>& arg%d_3, " + "const std::map<int, float>& arg%d_4", + argIndex, argIndex, argIndex, argIndex); } else { fprintf(out, ", %s arg%d", cpp_type_name(*arg), argIndex); } @@ -600,6 +695,9 @@ write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributio fprintf(out, " const static std::map<int, StateAtomFieldOptions> " "kStateAtomsFieldOptions;\n"); + fprintf(out, + " const static std::map<int, std::vector<int>> " + "kBytesFieldAtoms;"); fprintf(out, "};\n"); fprintf(out, "const static int kMaxPushedAtomId = %d;\n\n", @@ -632,6 +730,10 @@ static void write_java_usage(FILE* out, const string& method_name, const string& field != atom.fields.end(); field++) { if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { fprintf(out, ", android.os.WorkSource workSource"); + } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", SparseArray<Object> value_map"); + } else if (field->javaType == JAVA_TYPE_BYTE_ARRAY) { + fprintf(out, ", byte[] %s", field->name.c_str()); } else { fprintf(out, ", %s %s", java_type_name(field->javaType), field->name.c_str()); } @@ -653,6 +755,8 @@ static void write_java_method( fprintf(out, ", %s[] %s", java_type_name(chainField.javaType), chainField.name.c_str()); } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", SparseArray<Object> value_map"); } else { fprintf(out, ", %s arg%d", java_type_name(*arg), argIndex); } @@ -741,6 +845,7 @@ write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl &attributionD fprintf(out, "package android.util;\n"); fprintf(out, "\n"); fprintf(out, "import android.os.WorkSource;\n"); + fprintf(out, "import android.util.SparseArray;\n"); fprintf(out, "import java.util.ArrayList;\n"); fprintf(out, "\n"); fprintf(out, "\n"); @@ -821,6 +926,8 @@ jni_type_name(java_type_t type) return "jdouble"; case JAVA_TYPE_STRING: return "jstring"; + case JAVA_TYPE_BYTE_ARRAY: + return "jbyteArray"; default: return "UNKNOWN"; } @@ -832,6 +939,8 @@ jni_array_type_name(java_type_t type) switch (type) { case JAVA_TYPE_INT: return "jintArray"; + case JAVA_TYPE_FLOAT: + return "jfloatArray"; case JAVA_TYPE_STRING: return "jobjectArray"; default: @@ -868,6 +977,12 @@ jni_function_name(const string& method_name, const vector<java_type_t>& signatur case JAVA_TYPE_ATTRIBUTION_CHAIN: result += "_AttributionChain"; break; + case JAVA_TYPE_KEY_VALUE_PAIR: + result += "_KeyValuePairs"; + break; + case JAVA_TYPE_BYTE_ARRAY: + result += "_bytes"; + break; default: result += "_UNKNOWN"; break; @@ -893,6 +1008,8 @@ java_type_signature(java_type_t type) return "D"; case JAVA_TYPE_STRING: return "Ljava/lang/String;"; + case JAVA_TYPE_BYTE_ARRAY: + return "[B"; default: return "UNKNOWN"; } @@ -909,6 +1026,8 @@ jni_function_signature(const vector<java_type_t>& signature, const AtomDecl &att result += "["; result += java_type_signature(chainField.javaType); } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + result += "Landroid/util/SparseArray;"; } else { result += java_type_signature(*arg); } @@ -917,6 +1036,48 @@ jni_function_signature(const vector<java_type_t>& signature, const AtomDecl &att return result; } +static void write_key_value_map_jni(FILE* out) { + fprintf(out, " std::map<int, int32_t> int32_t_map;\n"); + fprintf(out, " std::map<int, int64_t> int64_t_map;\n"); + fprintf(out, " std::map<int, float> float_map;\n"); + fprintf(out, " std::map<int, char const*> string_map;\n\n"); + + fprintf(out, " jclass jmap_class = env->FindClass(\"android/util/SparseArray\");\n"); + + fprintf(out, " jmethodID jget_size_method = env->GetMethodID(jmap_class, \"size\", \"()I\");\n"); + fprintf(out, " jmethodID jget_key_method = env->GetMethodID(jmap_class, \"keyAt\", \"(I)I\");\n"); + fprintf(out, " jmethodID jget_value_method = env->GetMethodID(jmap_class, \"valueAt\", \"(I)Ljava/lang/Object;\");\n\n"); + + + fprintf(out, " std::vector<std::unique_ptr<ScopedUtfChars>> scoped_ufs;\n\n"); + + fprintf(out, " jclass jint_class = env->FindClass(\"java/lang/Integer\");\n"); + fprintf(out, " jclass jlong_class = env->FindClass(\"java/lang/Long\");\n"); + fprintf(out, " jclass jfloat_class = env->FindClass(\"java/lang/Float\");\n"); + fprintf(out, " jclass jstring_class = env->FindClass(\"java/lang/String\");\n"); + fprintf(out, " jmethodID jget_int_method = env->GetMethodID(jint_class, \"intValue\", \"()I\");\n"); + fprintf(out, " jmethodID jget_long_method = env->GetMethodID(jlong_class, \"longValue\", \"()J\");\n"); + fprintf(out, " jmethodID jget_float_method = env->GetMethodID(jfloat_class, \"floatValue\", \"()F\");\n\n"); + + fprintf(out, " jint jsize = env->CallIntMethod(value_map, jget_size_method);\n"); + fprintf(out, " for(int i = 0; i < jsize; i++) {\n"); + fprintf(out, " jint key = env->CallIntMethod(value_map, jget_key_method, i);\n"); + fprintf(out, " jobject jvalue_obj = env->CallObjectMethod(value_map, jget_value_method, i);\n"); + fprintf(out, " if (jvalue_obj == NULL) { continue; }\n"); + fprintf(out, " if (env->IsInstanceOf(jvalue_obj, jint_class)) {\n"); + fprintf(out, " int32_t_map[key] = env->CallIntMethod(jvalue_obj, jget_int_method);\n"); + fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jlong_class)) {\n"); + fprintf(out, " int64_t_map[key] = env->CallLongMethod(jvalue_obj, jget_long_method);\n"); + fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jfloat_class)) {\n"); + fprintf(out, " float_map[key] = env->CallFloatMethod(jvalue_obj, jget_float_method);\n"); + fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jstring_class)) {\n"); + fprintf(out, " std::unique_ptr<ScopedUtfChars> utf(new ScopedUtfChars(env, (jstring)jvalue_obj));\n"); + fprintf(out, " if (utf->c_str() != NULL) { string_map[key] = utf->c_str(); }\n"); + fprintf(out, " scoped_ufs.push_back(std::move(utf));\n"); + fprintf(out, " }\n"); + fprintf(out, " }\n"); +} + static int write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp_method_name, const set<vector<java_type_t>>& signatures, const AtomDecl &attributionDecl) @@ -937,6 +1098,8 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp fprintf(out, ", %s %s", jni_array_type_name(chainField.javaType), chainField.name.c_str()); } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", jobject value_map"); } else { fprintf(out, ", %s arg%d", jni_type_name(*arg), argIndex); } @@ -949,6 +1112,7 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp // Prepare strings argIndex = 1; bool hadStringOrChain = false; + bool isKeyValuePairAtom = false; for (vector<java_type_t>::const_iterator arg = signature->begin(); arg != signature->end(); arg++) { if (*arg == JAVA_TYPE_STRING) { @@ -960,6 +1124,25 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp fprintf(out, " } else {\n"); fprintf(out, " str%d = NULL;\n", argIndex); fprintf(out, " }\n"); + } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { + hadStringOrChain = true; + fprintf(out, " jbyte* jbyte_array%d;\n", argIndex); + fprintf(out, " const char* str%d;\n", argIndex); + fprintf(out, " if (arg%d != NULL) {\n", argIndex); + fprintf(out, + " jbyte_array%d = " + "env->GetByteArrayElements(arg%d, NULL);\n", + argIndex, argIndex); + fprintf(out, + " str%d = " + "reinterpret_cast<char*>(env->GetByteArrayElements(arg%" + "d, NULL));\n", + argIndex, argIndex); + fprintf(out, " } else {\n"); + fprintf(out, " jbyte_array%d = NULL;\n", argIndex); + fprintf(out, " str%d = NULL;\n", argIndex); + fprintf(out, " }\n"); + } else if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { hadStringOrChain = true; for (auto chainField : attributionDecl.fields) { @@ -1001,18 +1184,23 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp } fprintf(out, "\n"); } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + isKeyValuePairAtom = true; } argIndex++; } // Emit this to quiet the unused parameter warning if there were no strings or attribution // chains. - if (!hadStringOrChain) { + if (!hadStringOrChain && !isKeyValuePairAtom) { fprintf(out, " (void)env;\n"); } + if (isKeyValuePairAtom) { + write_key_value_map_jni(out); + } // stats_write call argIndex = 1; - fprintf(out, " int ret = android::util::%s(code", cpp_method_name.c_str()); + fprintf(out, "\n int ret = android::util::%s(code", cpp_method_name.c_str()); for (vector<java_type_t>::const_iterator arg = signature->begin(); arg != signature->end(); arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { @@ -1025,8 +1213,13 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp fprintf(out, ", %s_vec", chainField.name.c_str()); } } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", int32_t_map, int64_t_map, string_map, float_map"); } else { - const char *argName = (*arg == JAVA_TYPE_STRING) ? "str" : "arg"; + const char* argName = (*arg == JAVA_TYPE_STRING || + *arg == JAVA_TYPE_BYTE_ARRAY) + ? "str" + : "arg"; fprintf(out, ", (%s)%s%d", cpp_type_name(*arg), argName, argIndex); } argIndex++; @@ -1043,6 +1236,13 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp fprintf(out, " env->ReleaseStringUTFChars(arg%d, str%d);\n", argIndex, argIndex); fprintf(out, " }\n"); + } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { + fprintf(out, " if (str%d != NULL) { \n", argIndex); + fprintf(out, + " env->ReleaseByteArrayElements(arg%d, " + "jbyte_array%d, 0);\n", + argIndex, argIndex); + fprintf(out, " }\n"); } else if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (auto chainField : attributionDecl.fields) { if (chainField.javaType == JAVA_TYPE_INT) { @@ -1058,6 +1258,7 @@ write_stats_log_jni(FILE* out, const string& java_method_name, const string& cpp } argIndex++; } + fprintf(out, " return ret;\n"); fprintf(out, "}\n"); diff --git a/tools/stats_log_api_gen/test.proto b/tools/stats_log_api_gen/test.proto index 264a865e3b39..3be87d95df15 100644 --- a/tools/stats_log_api_gen/test.proto +++ b/tools/stats_log_api_gen/test.proto @@ -109,6 +109,28 @@ message BadAttributionNodePosition { oneof event { BadAttributionNodePositionAtom bad = 1; } } +message GoodEventWithBinaryFieldAtom { + oneof event { GoodBinaryFieldAtom field1 = 1; } +} + +message ComplexField { + optional string str = 1; +} + +message GoodBinaryFieldAtom { + optional int32 field1 = 1; + optional ComplexField bf = 2 [(android.os.statsd.log_mode) = MODE_BYTES]; +} + +message BadEventWithBinaryFieldAtom { + oneof event { BadBinaryFieldAtom field1 = 1; } +} + +message BadBinaryFieldAtom { + optional int32 field1 = 1; + optional ComplexField bf = 2; +} + message BadStateAtoms { oneof event { BadStateAtom1 bad1 = 1; @@ -127,33 +149,33 @@ message GoodStateAtoms { // The atom has only primary field but no exclusive state field. message BadStateAtom1 { optional int32 uid = 1 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; } // Only primative types can be annotated. message BadStateAtom2 { repeated android.os.statsd.AttributionNode attribution = 1 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; optional int32 state = 2 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; } // Having 2 exclusive state field in the atom means the atom is badly designed. // E.g., putting bluetooth state and wifi state in the same atom. message BadStateAtom3 { optional int32 uid = 1 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; optional int32 state = 2 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; optional int32 state2 = 3 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; } message GoodStateAtom1 { optional int32 uid = 1 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; optional int32 state = 2 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; } // Atoms can have exclusive state field, but no primary field. That means @@ -161,16 +183,16 @@ message GoodStateAtom1 { message GoodStateAtom2 { optional int32 uid = 1; optional int32 state = 2 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; } // We can have more than one primary fields. That means their combination is a // primary key. message GoodStateAtom3 { optional int32 uid = 1 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; optional int32 tid = 2 - [(android.os.statsd.stateFieldOption).option = PRIMARY]; + [(android.os.statsd.state_field_option).option = PRIMARY]; optional int32 state = 3 - [(android.os.statsd.stateFieldOption).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).option = EXCLUSIVE]; }
\ No newline at end of file diff --git a/tools/stats_log_api_gen/test_collation.cpp b/tools/stats_log_api_gen/test_collation.cpp index 1936d9667948..ad3bffacd442 100644 --- a/tools/stats_log_api_gen/test_collation.cpp +++ b/tools/stats_log_api_gen/test_collation.cpp @@ -212,5 +212,19 @@ TEST(CollationTest, PassOnGoodStateAtomOptions) { EXPECT_EQ(0, errorCount); } +TEST(CollationTest, PassOnGoodBinaryFieldAtom) { + Atoms atoms; + int errorCount = + collate_atoms(GoodEventWithBinaryFieldAtom::descriptor(), &atoms); + EXPECT_EQ(0, errorCount); +} + +TEST(CollationTest, FailOnBadBinaryFieldAtom) { + Atoms atoms; + int errorCount = + collate_atoms(BadEventWithBinaryFieldAtom::descriptor(), &atoms); + EXPECT_TRUE(errorCount > 0); +} + } // namespace stats_log_api_gen } // namespace android
\ No newline at end of file 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 diff --git a/tools/validatekeymaps/Android.bp b/tools/validatekeymaps/Android.bp index 6fb278c83f0a..819e75ba1fed 100644 --- a/tools/validatekeymaps/Android.bp +++ b/tools/validatekeymaps/Android.bp @@ -15,6 +15,7 @@ cc_binary_host { ], static_libs: [ + "libbase", "libinput", "libutils", "libcutils", diff --git a/tools/validatekeymaps/Main.cpp b/tools/validatekeymaps/Main.cpp index bbfcba6272b2..f31f771004bd 100644 --- a/tools/validatekeymaps/Main.cpp +++ b/tools/validatekeymaps/Main.cpp @@ -18,7 +18,6 @@ #include <input/KeyLayoutMap.h> #include <input/VirtualKeyMap.h> #include <utils/PropertyMap.h> -#include <utils/String8.h> #include <stdarg.h> #include <stdio.h> @@ -98,7 +97,7 @@ static bool validateFile(const char* filename) { case FILETYPE_KEYLAYOUT: { sp<KeyLayoutMap> map; - status_t status = KeyLayoutMap::load(String8(filename), &map); + status_t status = KeyLayoutMap::load(filename, &map); if (status) { error("Error %d parsing key layout file.\n\n", status); return false; @@ -108,7 +107,7 @@ static bool validateFile(const char* filename) { case FILETYPE_KEYCHARACTERMAP: { sp<KeyCharacterMap> map; - status_t status = KeyCharacterMap::load(String8(filename), + status_t status = KeyCharacterMap::load(filename, KeyCharacterMap::FORMAT_ANY, &map); if (status) { error("Error %d parsing key character map file.\n\n", status); @@ -130,7 +129,7 @@ static bool validateFile(const char* filename) { case FILETYPE_VIRTUALKEYDEFINITION: { VirtualKeyMap* map; - status_t status = VirtualKeyMap::load(String8(filename), &map); + status_t status = VirtualKeyMap::load(filename, &map); if (status) { error("Error %d parsing virtual key definition file.\n\n", status); return false; |