diff options
Diffstat (limited to 'tools/aapt2')
-rw-r--r-- | tools/aapt2/Android.mk | 7 | ||||
-rw-r--r-- | tools/aapt2/BinaryXmlPullParser.cpp | 259 | ||||
-rw-r--r-- | tools/aapt2/BinaryXmlPullParser.h | 76 | ||||
-rw-r--r-- | tools/aapt2/Main.cpp | 188 | ||||
-rw-r--r-- | tools/aapt2/ManifestMerger.cpp | 376 | ||||
-rw-r--r-- | tools/aapt2/ManifestMerger.h | 45 | ||||
-rw-r--r-- | tools/aapt2/ManifestMerger_test.cpp | 121 | ||||
-rw-r--r-- | tools/aapt2/SdkConstants.cpp | 50 | ||||
-rw-r--r-- | tools/aapt2/SdkConstants.h | 5 | ||||
-rw-r--r-- | tools/aapt2/XmlDom.cpp | 431 | ||||
-rw-r--r-- | tools/aapt2/XmlDom.h | 154 | ||||
-rw-r--r-- | tools/aapt2/XmlDom_test.cpp | 49 | ||||
-rw-r--r-- | tools/aapt2/XmlFlattener.cpp | 818 | ||||
-rw-r--r-- | tools/aapt2/XmlFlattener.h | 69 | ||||
-rw-r--r-- | tools/aapt2/XmlFlattener_test.cpp | 60 | ||||
-rw-r--r-- | tools/aapt2/data/lib/AndroidManifest.xml | 5 |
16 files changed, 1869 insertions, 844 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index 23d679d..d311cd9 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -27,7 +27,6 @@ main := Main.cpp sources := \ BigBuffer.cpp \ BinaryResourceParser.cpp \ - BinaryXmlPullParser.cpp \ BindingXmlPullParser.cpp \ ConfigDescription.cpp \ Debug.cpp \ @@ -37,6 +36,7 @@ sources := \ Linker.cpp \ Locale.cpp \ Logger.cpp \ + ManifestMerger.cpp \ ManifestParser.cpp \ ManifestValidator.cpp \ Png.cpp \ @@ -53,6 +53,7 @@ sources := \ ScopedXmlPullParser.cpp \ SourceXmlPullParser.cpp \ XliffXmlPullParser.cpp \ + XmlDom.cpp \ XmlFlattener.cpp \ ZipEntry.cpp \ ZipFile.cpp @@ -65,6 +66,7 @@ testSources := \ JavaClassGenerator_test.cpp \ Linker_test.cpp \ Locale_test.cpp \ + ManifestMerger_test.cpp \ ManifestParser_test.cpp \ Maybe_test.cpp \ NameMangler_test.cpp \ @@ -76,6 +78,7 @@ testSources := \ StringPool_test.cpp \ Util_test.cpp \ XliffXmlPullParser_test.cpp \ + XmlDom_test.cpp \ XmlFlattener_test.cpp cIncludes := \ @@ -101,7 +104,7 @@ else endif cFlags := -Wall -Werror -Wno-unused-parameter -UNDEBUG -cppFlags := -std=c++11 -Wno-missing-field-initializers +cppFlags := -std=c++11 -Wno-missing-field-initializers -Wno-unused-private-field # ========================================================== # Build the host static library: libaapt2 diff --git a/tools/aapt2/BinaryXmlPullParser.cpp b/tools/aapt2/BinaryXmlPullParser.cpp deleted file mode 100644 index 476a215..0000000 --- a/tools/aapt2/BinaryXmlPullParser.cpp +++ /dev/null @@ -1,259 +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. - */ - -#include "BinaryXmlPullParser.h" -#include "Maybe.h" -#include "Util.h" - -#include <androidfw/ResourceTypes.h> -#include <memory> -#include <string> -#include <vector> - -namespace aapt { - -static XmlPullParser::Event codeToEvent(android::ResXMLParser::event_code_t code) { - switch (code) { - case android::ResXMLParser::START_DOCUMENT: - return XmlPullParser::Event::kStartDocument; - case android::ResXMLParser::END_DOCUMENT: - return XmlPullParser::Event::kEndDocument; - case android::ResXMLParser::START_NAMESPACE: - return XmlPullParser::Event::kStartNamespace; - case android::ResXMLParser::END_NAMESPACE: - return XmlPullParser::Event::kEndNamespace; - case android::ResXMLParser::START_TAG: - return XmlPullParser::Event::kStartElement; - case android::ResXMLParser::END_TAG: - return XmlPullParser::Event::kEndElement; - case android::ResXMLParser::TEXT: - return XmlPullParser::Event::kText; - default: - break; - } - return XmlPullParser::Event::kBadDocument; -} - -BinaryXmlPullParser::BinaryXmlPullParser(const std::shared_ptr<android::ResXMLTree>& parser) - : mParser(parser), mEvent(Event::kStartDocument), mHasComment(false), sEmpty(), sEmpty8(), - mDepth(0) { -} - -XmlPullParser::Event BinaryXmlPullParser::next() { - mStr1.clear(); - mStr2.clear(); - mAttributes.clear(); - - android::ResXMLParser::event_code_t code; - if (mHasComment) { - mHasComment = false; - code = mParser->getEventType(); - } else { - code = mParser->next(); - if (code != android::ResXMLParser::BAD_DOCUMENT) { - size_t len; - const char16_t* comment = mParser->getComment(&len); - if (comment) { - mHasComment = true; - mStr1.assign(comment, len); - return XmlPullParser::Event::kComment; - } - } - } - - size_t len; - const char16_t* data; - mEvent = codeToEvent(code); - switch (mEvent) { - case Event::kStartNamespace: - case Event::kEndNamespace: { - data = mParser->getNamespacePrefix(&len); - if (data) { - mStr1.assign(data, len); - } else { - mStr1.clear(); - } - data = mParser->getNamespaceUri(&len); - if (data) { - mStr2.assign(data, len); - } else { - mStr2.clear(); - } - - Maybe<std::u16string> result = util::extractPackageFromNamespace(mStr2); - if (result) { - if (mEvent == Event::kStartNamespace) { - mPackageAliases.emplace_back(mStr1, result.value()); - } else { - assert(mPackageAliases.back().second == result.value()); - mPackageAliases.pop_back(); - } - } - break; - } - - case Event::kStartElement: - copyAttributes(); - // fallthrough - - case Event::kEndElement: - data = mParser->getElementNamespace(&len); - if (data) { - mStr1.assign(data, len); - } else { - mStr1.clear(); - } - data = mParser->getElementName(&len); - if (data) { - mStr2.assign(data, len); - } else { - mStr2.clear(); - } - break; - - case Event::kText: - data = mParser->getText(&len); - if (data) { - mStr1.assign(data, len); - } else { - mStr1.clear(); - } - break; - - default: - break; - } - return mEvent; -} - -XmlPullParser::Event BinaryXmlPullParser::getEvent() const { - if (mHasComment) { - return XmlPullParser::Event::kComment; - } - return mEvent; -} - -const std::string& BinaryXmlPullParser::getLastError() const { - return sEmpty8; -} - -const std::u16string& BinaryXmlPullParser::getComment() const { - if (mHasComment) { - return mStr1; - } - return sEmpty; -} - -size_t BinaryXmlPullParser::getLineNumber() const { - return mParser->getLineNumber(); -} - -size_t BinaryXmlPullParser::getDepth() const { - return mDepth; -} - -const std::u16string& BinaryXmlPullParser::getText() const { - if (!mHasComment && mEvent == XmlPullParser::Event::kText) { - return mStr1; - } - return sEmpty; -} - -const std::u16string& BinaryXmlPullParser::getNamespacePrefix() const { - if (!mHasComment && (mEvent == XmlPullParser::Event::kStartNamespace || - mEvent == XmlPullParser::Event::kEndNamespace)) { - return mStr1; - } - return sEmpty; -} - -const std::u16string& BinaryXmlPullParser::getNamespaceUri() const { - if (!mHasComment && (mEvent == XmlPullParser::Event::kStartNamespace || - mEvent == XmlPullParser::Event::kEndNamespace)) { - return mStr2; - } - return sEmpty; -} - -bool BinaryXmlPullParser::applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const { - const auto endIter = mPackageAliases.rend(); - for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { - if (iter->first == *package) { - if (iter->second.empty()) { - *package = defaultPackage; - } else { - *package = iter->second; - } - return true; - } - } - return false; -} - -const std::u16string& BinaryXmlPullParser::getElementNamespace() const { - if (!mHasComment && (mEvent == XmlPullParser::Event::kStartElement || - mEvent == XmlPullParser::Event::kEndElement)) { - return mStr1; - } - return sEmpty; -} - -const std::u16string& BinaryXmlPullParser::getElementName() const { - if (!mHasComment && (mEvent == XmlPullParser::Event::kStartElement || - mEvent == XmlPullParser::Event::kEndElement)) { - return mStr2; - } - return sEmpty; -} - -size_t BinaryXmlPullParser::getAttributeCount() const { - return mAttributes.size(); -} - -XmlPullParser::const_iterator BinaryXmlPullParser::beginAttributes() const { - return mAttributes.begin(); -} - -XmlPullParser::const_iterator BinaryXmlPullParser::endAttributes() const { - return mAttributes.end(); -} - -void BinaryXmlPullParser::copyAttributes() { - const size_t attrCount = mParser->getAttributeCount(); - if (attrCount > 0) { - mAttributes.reserve(attrCount); - for (size_t i = 0; i < attrCount; i++) { - XmlPullParser::Attribute attr; - size_t len; - const char16_t* str = mParser->getAttributeNamespace(i, &len); - if (str) { - attr.namespaceUri.assign(str, len); - } - str = mParser->getAttributeName(i, &len); - if (str) { - attr.name.assign(str, len); - } - str = mParser->getAttributeStringValue(i, &len); - if (str) { - attr.value.assign(str, len); - } - mAttributes.push_back(std::move(attr)); - } - } -} - -} // namespace aapt diff --git a/tools/aapt2/BinaryXmlPullParser.h b/tools/aapt2/BinaryXmlPullParser.h deleted file mode 100644 index 16fc8b7..0000000 --- a/tools/aapt2/BinaryXmlPullParser.h +++ /dev/null @@ -1,76 +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_BINARY_XML_PULL_PARSER_H -#define AAPT_BINARY_XML_PULL_PARSER_H - -#include "XmlPullParser.h" - -#include <androidfw/ResourceTypes.h> -#include <memory> -#include <string> -#include <vector> - -namespace aapt { - -/** - * Wraps a ResTable into the canonical XmlPullParser interface. - */ -class BinaryXmlPullParser : public XmlPullParser { -public: - BinaryXmlPullParser(const std::shared_ptr<android::ResXMLTree>& parser); - BinaryXmlPullParser(const BinaryXmlPullParser& rhs) = delete; - - Event getEvent() const override; - const std::string& getLastError() const override; - Event next() override; - - const std::u16string& getComment() const override; - size_t getLineNumber() const override; - size_t getDepth() const override; - - const std::u16string& getText() const override; - - const std::u16string& getNamespacePrefix() const override; - const std::u16string& getNamespaceUri() const override; - bool applyPackageAlias(std::u16string* package, const std::u16string& defaultpackage) - const override; - - const std::u16string& getElementNamespace() const override; - const std::u16string& getElementName() const override; - - const_iterator beginAttributes() const override; - const_iterator endAttributes() const override; - size_t getAttributeCount() const override; - -private: - void copyAttributes(); - - std::shared_ptr<android::ResXMLTree> mParser; - std::u16string mStr1; - std::u16string mStr2; - std::vector<Attribute> mAttributes; - Event mEvent; - bool mHasComment; - const std::u16string sEmpty; - const std::string sEmpty8; - size_t mDepth; - std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; -}; - -} // namespace aapt - -#endif // AAPT_BINARY_XML_PULL_PARSER_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 91639c5..de2dafc 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -17,13 +17,13 @@ #include "AppInfo.h" #include "BigBuffer.h" #include "BinaryResourceParser.h" -#include "BinaryXmlPullParser.h" #include "BindingXmlPullParser.h" #include "Debug.h" #include "Files.h" #include "Flag.h" #include "JavaClassGenerator.h" #include "Linker.h" +#include "ManifestMerger.h" #include "ManifestParser.h" #include "ManifestValidator.h" #include "NameMangler.h" @@ -57,6 +57,20 @@ constexpr const char* kAaptVersionStr = "2.0-alpha"; using namespace aapt; /** + * Used with smart pointers to free malloc'ed memory. + */ +struct DeleteMalloc { + void operator()(void* ptr) { + free(ptr); + } +}; + +struct StaticLibraryData { + Source source; + std::unique_ptr<ZipFile> apk; +}; + +/** * Collect files from 'root', filtering out any files that do not * match the FileFilter 'filter'. */ @@ -128,7 +142,7 @@ void versionStylesForCompat(const std::shared_ptr<ResourceTable>& table) { auto iter = style.entries.begin(); while (iter != style.entries.end()) { if (iter->key.name.package == u"android") { - size_t sdkLevel = findAttributeSdkLevel(iter->key.name.entry); + size_t sdkLevel = findAttributeSdkLevel(iter->key.name); if (sdkLevel > 1 && sdkLevel > configValue.config.sdkVersion) { // Record that we are about to strip this. stripped.emplace_back(std::move(*iter)); @@ -300,6 +314,42 @@ struct AaptOptions { ResourceName dumpStyleTarget; }; +struct IdCollector : public xml::Visitor { + IdCollector(const Source& source, const std::shared_ptr<ResourceTable>& table) : + mSource(source), mTable(table) { + } + + virtual void visit(xml::Text* node) override {} + + virtual void visit(xml::Namespace* node) override { + for (const auto& child : node->children) { + child->accept(this); + } + } + + virtual void visit(xml::Element* node) override { + for (const xml::Attribute& attr : node->attributes) { + bool create = false; + bool priv = false; + ResourceNameRef nameRef; + if (ResourceParser::tryParseReference(attr.value, &nameRef, &create, &priv)) { + if (create) { + mTable->addResource(nameRef, {}, mSource.line(node->lineNumber), + util::make_unique<Id>()); + } + } + } + + for (const auto& child : node->children) { + child->accept(this); + } + } + +private: + Source mSource; + std::shared_ptr<ResourceTable> mTable; +}; + bool compileXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, const CompileItem& item, ZipFile* outApk) { std::ifstream in(item.source.path, std::ifstream::binary); @@ -308,20 +358,19 @@ bool compileXml(const AaptOptions& options, const std::shared_ptr<ResourceTable> return false; } - BigBuffer outBuffer(1024); - - // No resolver, since we are not compiling attributes here. - XmlFlattener flattener(table, {}); - - XmlFlattener::Options xmlOptions; - xmlOptions.defaultPackage = table->getPackage(); - xmlOptions.keepRawValues = true; + SourceLogger logger(item.source); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + if (!root) { + return false; + } - std::shared_ptr<XmlPullParser> parser = std::make_shared<SourceXmlPullParser>(in); + // Collect any resource ID's declared here. + IdCollector idCollector(item.source, table); + root->accept(&idCollector); - Maybe<size_t> minStrippedSdk = flattener.flatten(item.source, parser, &outBuffer, - xmlOptions); - if (!minStrippedSdk) { + BigBuffer outBuffer(1024); + if (!xml::flatten(root.get(), options.appInfo.package, &outBuffer)) { + logger.error() << "failed to encode XML." << std::endl; return false; } @@ -369,19 +418,13 @@ bool shouldGenerateVersionedResource(const std::shared_ptr<const ResourceTable>& bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, const std::shared_ptr<IResolver>& resolver, const LinkItem& item, const void* data, size_t dataLen, ZipFile* outApk, std::queue<LinkItem>* outQueue) { - std::shared_ptr<android::ResXMLTree> tree = std::make_shared<android::ResXMLTree>(); - if (tree->setTo(data, dataLen, false) != android::NO_ERROR) { + SourceLogger logger(item.source); + std::unique_ptr<xml::Node> root = xml::inflate(data, dataLen, &logger); + if (!root) { return false; } - std::shared_ptr<XmlPullParser> parser = std::make_shared<BinaryXmlPullParser>(tree); - - BigBuffer outBuffer(1024); - XmlFlattener flattener({}, resolver); - - XmlFlattener::Options xmlOptions; - xmlOptions.defaultPackage = item.originalPackage; - + xml::FlattenOptions xmlOptions; if (options.packageType == AaptOptions::PackageType::StaticLibrary) { xmlOptions.keepRawValues = true; } @@ -392,16 +435,12 @@ bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& t xmlOptions.maxSdkAttribute = item.config.sdkVersion ? item.config.sdkVersion : 1; } - std::shared_ptr<BindingXmlPullParser> binding; - if (item.name.type == ResourceType::kLayout) { - // Layouts may have defined bindings, so we need to make sure they get processed. - binding = std::make_shared<BindingXmlPullParser>(parser); - parser = binding; - } - - Maybe<size_t> minStrippedSdk = flattener.flatten(item.source, parser, &outBuffer, - xmlOptions); + BigBuffer outBuffer(1024); + Maybe<size_t> minStrippedSdk = xml::flattenAndLink(item.source, root.get(), + item.originalPackage, resolver, + xmlOptions, &outBuffer); if (!minStrippedSdk) { + logger.error() << "failed to encode XML." << std::endl; return false; } @@ -431,30 +470,6 @@ bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& t << buildFileReference(item) << "' to apk." << std::endl; return false; } - - if (binding && !options.bindingOutput.path.empty()) { - // We generated a binding xml file, write it out. - Source bindingOutput = options.bindingOutput; - appendPath(&bindingOutput.path, buildFileReference(item)); - - if (!mkdirs(bindingOutput.path)) { - Logger::error(bindingOutput) << strerror(errno) << std::endl; - return false; - } - - appendPath(&bindingOutput.path, "bind.xml"); - - std::ofstream bout(bindingOutput.path); - if (!bout) { - Logger::error(bindingOutput) << strerror(errno) << std::endl; - return false; - } - - if (!binding->writeToFile(bout)) { - Logger::error(bindingOutput) << strerror(errno) << std::endl; - return false; - } - } return true; } @@ -493,6 +508,7 @@ bool copyFile(const AaptOptions& options, const CompileItem& item, ZipFile* outA } bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver>& resolver, + const std::map<std::shared_ptr<ResourceTable>, StaticLibraryData>& libApks, const android::ResTable& table, ZipFile* outApk) { if (options.verbose) { Logger::note(options.manifest) << "compiling AndroidManifest.xml." << std::endl; @@ -504,13 +520,46 @@ bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver return false; } - BigBuffer outBuffer(1024); - std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); - XmlFlattener flattener({}, resolver); + SourceLogger logger(options.manifest); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + if (!root) { + return false; + } - XmlFlattener::Options xmlOptions; - xmlOptions.defaultPackage = options.appInfo.package; - if (!flattener.flatten(options.manifest, xmlParser, &outBuffer, xmlOptions)) { + ManifestMerger merger({}); + if (!merger.setAppManifest(options.manifest, options.appInfo.package, std::move(root))) { + return false; + } + + for (const auto& entry : libApks) { + ZipFile* libApk = entry.second.apk.get(); + const std::u16string& libPackage = entry.first->getPackage(); + const Source& libSource = entry.second.source; + + ZipEntry* zipEntry = libApk->getEntryByName("AndroidManifest.xml"); + if (!zipEntry) { + continue; + } + + std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( + libApk->uncompress(zipEntry)); + assert(uncompressedData); + + SourceLogger logger(libSource); + std::unique_ptr<xml::Node> libRoot = xml::inflate(uncompressedData.get(), + zipEntry->getUncompressedLen(), &logger); + if (!libRoot) { + return false; + } + + if (!merger.mergeLibraryManifest(libSource, libPackage, std::move(libRoot))) { + return false; + } + } + + BigBuffer outBuffer(1024); + if (!xml::flattenAndLink(options.manifest, merger.getMergedXml(), options.appInfo.package, + resolver, {}, &outBuffer)) { return false; } @@ -665,17 +714,6 @@ static void addApkFilesToLinkQueue(const std::u16string& package, const Source& static constexpr int kOpenFlags = ZipFile::kOpenCreate | ZipFile::kOpenTruncate | ZipFile::kOpenReadWrite; -struct DeleteMalloc { - void operator()(void* ptr) { - free(ptr); - } -}; - -struct StaticLibraryData { - Source source; - std::unique_ptr<ZipFile> apk; -}; - bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outTable, const std::shared_ptr<IResolver>& resolver) { std::map<std::shared_ptr<ResourceTable>, StaticLibraryData> apkFiles; @@ -768,7 +806,7 @@ bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outT } android::ResTable binTable; - if (!compileManifest(options, resolver, binTable, &outApk)) { + if (!compileManifest(options, resolver, apkFiles, binTable, &outApk)) { return false; } diff --git a/tools/aapt2/ManifestMerger.cpp b/tools/aapt2/ManifestMerger.cpp new file mode 100644 index 0000000..71d3424 --- /dev/null +++ b/tools/aapt2/ManifestMerger.cpp @@ -0,0 +1,376 @@ +#include "ManifestMerger.h" +#include "Maybe.h" +#include "ResourceParser.h" +#include "Source.h" +#include "Util.h" +#include "XmlPullParser.h" + +#include <iostream> +#include <memory> +#include <set> +#include <string> + +namespace aapt { + +constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; + +static xml::Element* findManifest(xml::Node* root) { + if (!root) { + return nullptr; + } + + while (root->type == xml::NodeType::kNamespace) { + if (root->children.empty()) { + break; + } + root = root->children[0].get(); + } + + if (root && root->type == xml::NodeType::kElement) { + xml::Element* el = static_cast<xml::Element*>(root); + if (el->namespaceUri.empty() && el->name == u"manifest") { + return el; + } + } + return nullptr; +} + +static xml::Element* findChildWithSameName(xml::Element* parent, xml::Element* src) { + xml::Attribute* attrKey = src->findAttribute(kSchemaAndroid, u"name"); + if (!attrKey) { + return nullptr; + } + return parent->findChildWithAttribute(src->namespaceUri, src->name, attrKey); +} + +static bool attrLess(const xml::Attribute& lhs, const xml::Attribute& rhs) { + return std::tie(lhs.namespaceUri, lhs.name, lhs.value) + < std::tie(rhs.namespaceUri, rhs.name, rhs.value); +} + +static int compare(xml::Element* lhs, xml::Element* rhs) { + int diff = lhs->attributes.size() - rhs->attributes.size(); + if (diff != 0) { + return diff; + } + + std::set<xml::Attribute, decltype(&attrLess)> lhsAttrs(&attrLess); + lhsAttrs.insert(lhs->attributes.begin(), lhs->attributes.end()); + for (auto& attr : rhs->attributes) { + if (lhsAttrs.erase(attr) == 0) { + // The rhs attribute is not in the left. + return -1; + } + } + + if (!lhsAttrs.empty()) { + // The lhs has attributes not in the rhs. + return 1; + } + return 0; +} + +ManifestMerger::ManifestMerger(const Options& options) : + mOptions(options), mAppLogger({}), mLogger({}) { +} + +bool ManifestMerger::setAppManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> root) { + + mAppLogger = SourceLogger{ source }; + mRoot = std::move(root); + return true; +} + +bool ManifestMerger::checkEqual(xml::Element* elA, xml::Element* elB) { + if (compare(elA, elB) != 0) { + mLogger.error(elB->lineNumber) + << "library tag '" << elB->name << "' conflicts with app tag." + << std::endl; + mAppLogger.note(elA->lineNumber) + << "app tag '" << elA->name << "' defined here." + << std::endl; + return false; + } + + std::vector<xml::Element*> childrenA = elA->getChildElements(); + std::vector<xml::Element*> childrenB = elB->getChildElements(); + + if (childrenA.size() != childrenB.size()) { + mLogger.error(elB->lineNumber) + << "library tag '" << elB->name << "' children conflict with app tag." + << std::endl; + mAppLogger.note(elA->lineNumber) + << "app tag '" << elA->name << "' defined here." + << std::endl; + return false; + } + + auto cmp = [](xml::Element* lhs, xml::Element* rhs) -> bool { + return compare(lhs, rhs) < 0; + }; + + std::sort(childrenA.begin(), childrenA.end(), cmp); + std::sort(childrenB.begin(), childrenB.end(), cmp); + + for (size_t i = 0; i < childrenA.size(); i++) { + if (!checkEqual(childrenA[i], childrenB[i])) { + return false; + } + } + return true; +} + +bool ManifestMerger::mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB) { + if (!elA) { + parentA->addChild(elB->clone()); + return true; + } + return checkEqual(elA, elB); +} + +bool ManifestMerger::mergePreferRequired(xml::Element* parentA, xml::Element* elA, + xml::Element* elB) { + if (!elA) { + parentA->addChild(elB->clone()); + return true; + } + + xml::Attribute* reqA = elA->findAttribute(kSchemaAndroid, u"required"); + xml::Attribute* reqB = elB->findAttribute(kSchemaAndroid, u"required"); + bool requiredA = !reqA || (reqA->value != u"false" && reqA->value != u"FALSE"); + bool requiredB = !reqB || (reqB->value != u"false" && reqB->value != u"FALSE"); + if (!requiredA && requiredB) { + if (reqA) { + *reqA = xml::Attribute{ kSchemaAndroid, u"required", u"true" }; + } else { + elA->attributes.push_back(xml::Attribute{ kSchemaAndroid, u"required", u"true" }); + } + } + return true; +} + +static int findIntegerValue(xml::Attribute* attr, int defaultValue) { + if (attr) { + std::unique_ptr<BinaryPrimitive> integer = ResourceParser::tryParseInt(attr->value); + if (integer) { + return integer->value.data; + } + } + return defaultValue; +} + +bool ManifestMerger::mergeUsesSdk(xml::Element* elA, xml::Element* elB) { + bool error = false; + xml::Attribute* minAttrA = nullptr; + xml::Attribute* minAttrB = nullptr; + if (elA) { + minAttrA = elA->findAttribute(kSchemaAndroid, u"minSdkVersion"); + } + + if (elB) { + minAttrB = elB->findAttribute(kSchemaAndroid, u"minSdkVersion"); + } + + int minSdkA = findIntegerValue(minAttrA, 1); + int minSdkB = findIntegerValue(minAttrB, 1); + + if (minSdkA < minSdkB) { + std::ostream* out; + if (minAttrA) { + out = &(mAppLogger.error(elA->lineNumber) << "app declares "); + } else if (elA) { + out = &(mAppLogger.error(elA->lineNumber) << "app has implied "); + } else { + out = &(mAppLogger.error() << "app has implied "); + } + + *out << "minSdkVersion=" << minSdkA << " but library expects a higher SDK version." + << std::endl; + + // elB is valid because minSdkB wouldn't be greater than minSdkA if it wasn't. + mLogger.note(elB->lineNumber) + << "library declares minSdkVersion=" << minSdkB << "." + << std::endl; + error = true; + } + + xml::Attribute* targetAttrA = nullptr; + xml::Attribute* targetAttrB = nullptr; + + if (elA) { + targetAttrA = elA->findAttribute(kSchemaAndroid, u"targetSdkVersion"); + } + + if (elB) { + targetAttrB = elB->findAttribute(kSchemaAndroid, u"targetSdkVersion"); + } + + int targetSdkA = findIntegerValue(targetAttrA, minSdkA); + int targetSdkB = findIntegerValue(targetAttrB, minSdkB); + + if (targetSdkA < targetSdkB) { + std::ostream* out; + if (targetAttrA) { + out = &(mAppLogger.warn(elA->lineNumber) << "app declares "); + } else if (elA) { + out = &(mAppLogger.warn(elA->lineNumber) << "app has implied "); + } else { + out = &(mAppLogger.warn() << "app has implied "); + } + + *out << "targetSdkVerion=" << targetSdkA << " but library expects target SDK " + << targetSdkB << "." << std::endl; + + mLogger.note(elB->lineNumber) + << "library declares targetSdkVersion=" << targetSdkB << "." + << std::endl; + error = true; + } + return !error; +} + +bool ManifestMerger::mergeApplication(xml::Element* applicationA, xml::Element* applicationB) { + if (!applicationA || !applicationB) { + return true; + } + + bool error = false; + + // First make sure that the names are identical. + xml::Attribute* nameA = applicationA->findAttribute(kSchemaAndroid, u"name"); + xml::Attribute* nameB = applicationB->findAttribute(kSchemaAndroid, u"name"); + if (nameB) { + if (!nameA) { + applicationA->attributes.push_back(*nameB); + } else if (nameA->value != nameB->value) { + mLogger.error(applicationB->lineNumber) + << "conflicting application name '" + << nameB->value + << "'." << std::endl; + mAppLogger.note(applicationA->lineNumber) + << "application defines application name '" + << nameA->value + << "'." << std::endl; + error = true; + } + } + + // Now we descend into the activity/receiver/service/provider tags + for (xml::Element* elB : applicationB->getChildElements()) { + if (!elB->namespaceUri.empty()) { + continue; + } + + if (elB->name == u"activity" || elB->name == u"activity-alias" + || elB->name == u"service" || elB->name == u"receiver" + || elB->name == u"provider" || elB->name == u"meta-data") { + xml::Element* elA = findChildWithSameName(applicationA, elB); + error |= !mergeNewOrEqual(applicationA, elA, elB); + } else if (elB->name == u"uses-library") { + xml::Element* elA = findChildWithSameName(applicationA, elB); + error |= !mergePreferRequired(applicationA, elA, elB); + } + } + return !error; +} + +bool ManifestMerger::mergeLibraryManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> libRoot) { + mLogger = SourceLogger{ source }; + xml::Element* manifestA = findManifest(mRoot.get()); + xml::Element* manifestB = findManifest(libRoot.get()); + if (!manifestA) { + mAppLogger.error() << "missing manifest tag." << std::endl; + return false; + } + + if (!manifestB) { + mLogger.error() << "library missing manifest tag." << std::endl; + return false; + } + + bool error = false; + + // Do <application> first. + xml::Element* applicationA = manifestA->findChild({}, u"application"); + xml::Element* applicationB = manifestB->findChild({}, u"application"); + error |= !mergeApplication(applicationA, applicationB); + + // Do <uses-sdk> next. + xml::Element* usesSdkA = manifestA->findChild({}, u"uses-sdk"); + xml::Element* usesSdkB = manifestB->findChild({}, u"uses-sdk"); + error |= !mergeUsesSdk(usesSdkA, usesSdkB); + + for (xml::Element* elB : manifestB->getChildElements()) { + if (!elB->namespaceUri.empty()) { + continue; + } + + if (elB->name == u"uses-permission" || elB->name == u"permission" + || elB->name == u"permission-group" || elB->name == u"permission-tree") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !mergeNewOrEqual(manifestA, elA, elB); + } else if (elB->name == u"uses-feature") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !mergePreferRequired(manifestA, elA, elB); + } else if (elB->name == u"uses-configuration" || elB->name == u"supports-screen" + || elB->name == u"compatible-screens" || elB->name == u"supports-gl-texture") { + xml::Element* elA = findChildWithSameName(manifestA, elB); + error |= !checkEqual(elA, elB); + } + } + return !error; +} + +static void printMerged(xml::Node* node, int depth) { + std::string indent; + for (int i = 0; i < depth; i++) { + indent += " "; + } + + switch (node->type) { + case xml::NodeType::kNamespace: + std::cerr << indent << "N: " + << "xmlns:" << static_cast<xml::Namespace*>(node)->namespacePrefix + << "=\"" << static_cast<xml::Namespace*>(node)->namespaceUri + << "\"\n"; + break; + + case xml::NodeType::kElement: + std::cerr << indent << "E: " + << static_cast<xml::Element*>(node)->namespaceUri + << ":" << static_cast<xml::Element*>(node)->name + << "\n"; + for (const auto& attr : static_cast<xml::Element*>(node)->attributes) { + std::cerr << indent << " A: " + << attr.namespaceUri + << ":" << attr.name + << "=\"" << attr.value << "\"\n"; + } + break; + + case xml::NodeType::kText: + std::cerr << indent << "T: \"" << static_cast<xml::Text*>(node)->text << "\"\n"; + break; + } + + for (auto& child : node->children) { + printMerged(child.get(), depth + 1); + } +} + +xml::Node* ManifestMerger::getMergedXml() { + return mRoot.get(); +} + +bool ManifestMerger::printMerged() { + if (!mRoot) { + return false; + } + + ::aapt::printMerged(mRoot.get(), 0); + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestMerger.h b/tools/aapt2/ManifestMerger.h new file mode 100644 index 0000000..c6219db --- /dev/null +++ b/tools/aapt2/ManifestMerger.h @@ -0,0 +1,45 @@ +#ifndef AAPT_MANIFEST_MERGER_H +#define AAPT_MANIFEST_MERGER_H + +#include "Logger.h" +#include "Source.h" +#include "XmlDom.h" + +#include <memory> +#include <string> + +namespace aapt { + +class ManifestMerger { +public: + struct Options { + }; + + ManifestMerger(const Options& options); + + bool setAppManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> root); + + bool mergeLibraryManifest(const Source& source, const std::u16string& package, + std::unique_ptr<xml::Node> libRoot); + + xml::Node* getMergedXml(); + + bool printMerged(); + +private: + bool mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB); + bool mergePreferRequired(xml::Element* parentA, xml::Element* elA, xml::Element* elB); + bool checkEqual(xml::Element* elA, xml::Element* elB); + bool mergeApplication(xml::Element* applicationA, xml::Element* applicationB); + bool mergeUsesSdk(xml::Element* elA, xml::Element* elB); + + Options mOptions; + std::unique_ptr<xml::Node> mRoot; + SourceLogger mAppLogger; + SourceLogger mLogger; +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_MERGER_H diff --git a/tools/aapt2/ManifestMerger_test.cpp b/tools/aapt2/ManifestMerger_test.cpp new file mode 100644 index 0000000..6838253 --- /dev/null +++ b/tools/aapt2/ManifestMerger_test.cpp @@ -0,0 +1,121 @@ +/* + * 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. + */ + +#include "ManifestMerger.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kAppManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="21" /> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-feature android:name="android.hardware.GPS" android:required="false" /> + <application android:name="com.android.library.Application"> + <activity android:name="com.android.example.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC" /> + </intent-filter> + </service> + </application> +</manifest> +)EOF"; + +constexpr const char* kLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="21" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-feature android:name="android.hardware.GPS" /> + <uses-permission android:name="android.permission.GPS" /> + <application android:name="com.android.library.Application"> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC" /> + </intent-filter> + </service> + <provider android:name="com.android.library.DocumentProvider" + android:authorities="com.android.library.documents" + android:grantUriPermission="true" + android:exported="true" + android:permission="android.permission.MANAGE_DOCUMENTS" + android:enabled="@bool/atLeastKitKat"> + <intent-filter> + <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> + </intent-filter> + </provider> + </application> +</manifest> +)EOF"; + +constexpr const char* kBadLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="22" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-feature android:name="android.hardware.GPS" /> + <uses-permission android:name="android.permission.GPS" /> + <application android:name="com.android.library.Application2"> + <service android:name="com.android.library.Service"> + <intent-filter> + <action android:name="com.android.library.intent.action.SYNC_ACTION" /> + </intent-filter> + </service> + </application> +</manifest> +)EOF"; + +TEST(ManifestMergerTest, MergeManifestsSuccess) { + std::stringstream inA(kAppManifest); + std::stringstream inB(kLibManifest); + + const Source sourceA = { "AndroidManifest.xml" }; + const Source sourceB = { "lib.apk/AndroidManifest.xml" }; + SourceLogger loggerA(sourceA); + SourceLogger loggerB(sourceB); + + ManifestMerger merger({}); + EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", + xml::inflate(&inA, &loggerA))); + EXPECT_TRUE(merger.mergeLibraryManifest(sourceB, u"com.android.library", + xml::inflate(&inB, &loggerB))); +} + +TEST(ManifestMergerTest, MergeManifestFail) { + std::stringstream inA(kAppManifest); + std::stringstream inB(kBadLibManifest); + + const Source sourceA = { "AndroidManifest.xml" }; + const Source sourceB = { "lib.apk/AndroidManifest.xml" }; + SourceLogger loggerA(sourceA); + SourceLogger loggerB(sourceB); + + ManifestMerger merger({}); + EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", + xml::inflate(&inA, &loggerA))); + EXPECT_FALSE(merger.mergeLibraryManifest(sourceB, u"com.android.library", + xml::inflate(&inB, &loggerB))); +} + +} // namespace aapt diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index 3f156a6..9bdae49 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -14,11 +14,51 @@ * limitations under the License. */ +#include "SdkConstants.h" + +#include <algorithm> #include <string> #include <unordered_map> +#include <vector> namespace aapt { +static const std::vector<std::pair<uint16_t, size_t>> sAttrIdMap = { + { 0x021c, 1 }, + { 0x021d, 2 }, + { 0x0269, SDK_CUPCAKE }, + { 0x028d, SDK_DONUT }, + { 0x02ad, SDK_ECLAIR }, + { 0x02b3, SDK_ECLAIR_0_1 }, + { 0x02b5, SDK_ECLAIR_MR1 }, + { 0x02bd, SDK_FROYO }, + { 0x02cb, SDK_GINGERBREAD }, + { 0x0361, SDK_HONEYCOMB }, + { 0x0366, SDK_HONEYCOMB_MR1 }, + { 0x03a6, SDK_HONEYCOMB_MR2 }, + { 0x03ae, SDK_JELLY_BEAN }, + { 0x03cc, SDK_JELLY_BEAN_MR1 }, + { 0x03da, SDK_JELLY_BEAN_MR2 }, + { 0x03f1, SDK_KITKAT }, + { 0x03f6, SDK_KITKAT_WATCH }, + { 0x04ce, SDK_LOLLIPOP }, +}; + +static bool lessEntryId(const std::pair<uint16_t, size_t>& p, uint16_t entryId) { + return p.first < entryId; +} + +size_t findAttributeSdkLevel(ResourceId id) { + if (id.packageId() != 0x01 && id.typeId() != 0x01) { + return 0; + } + auto iter = std::lower_bound(sAttrIdMap.begin(), sAttrIdMap.end(), id.entryId(), lessEntryId); + if (iter == sAttrIdMap.end()) { + return SDK_LOLLIPOP_MR1; + } + return iter->second; +} + static const std::unordered_map<std::u16string, size_t> sAttrMap = { { u"marqueeRepeatLimit", 2 }, { u"windowNoDisplay", 3 }, @@ -682,12 +722,16 @@ static const std::unordered_map<std::u16string, size_t> sAttrMap = { { u"colorEdgeEffect", 21 } }; -size_t findAttributeSdkLevel(const std::u16string& name) { - auto iter = sAttrMap.find(name); +size_t findAttributeSdkLevel(const ResourceName& name) { + if (name.package != u"android" && name.type != ResourceType::kAttr) { + return 0; + } + + auto iter = sAttrMap.find(name.entry); if (iter != sAttrMap.end()) { return iter->second; } - return 0; + return SDK_LOLLIPOP_MR1; } } // namespace aapt diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h index 469c355..803da03 100644 --- a/tools/aapt2/SdkConstants.h +++ b/tools/aapt2/SdkConstants.h @@ -19,8 +19,6 @@ #include "Resource.h" -#include <string> - namespace aapt { enum { @@ -46,7 +44,8 @@ enum { SDK_LOLLIPOP_MR1 = 22, }; -size_t findAttributeSdkLevel(const std::u16string& name); +size_t findAttributeSdkLevel(ResourceId id); +size_t findAttributeSdkLevel(const ResourceName& name); } // namespace aapt diff --git a/tools/aapt2/XmlDom.cpp b/tools/aapt2/XmlDom.cpp new file mode 100644 index 0000000..763029f --- /dev/null +++ b/tools/aapt2/XmlDom.cpp @@ -0,0 +1,431 @@ +/* + * 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. + */ + +#include "Logger.h" +#include "Util.h" +#include "XmlDom.h" +#include "XmlPullParser.h" + +#include <cassert> +#include <memory> +#include <stack> +#include <string> +#include <tuple> + +namespace aapt { +namespace xml { + +constexpr char kXmlNamespaceSep = 1; + +struct Stack { + std::unique_ptr<xml::Node> root; + std::stack<xml::Node*> nodeStack; + std::u16string pendingComment; +}; + +/** + * Extracts the namespace and name of an expanded element or attribute name. + */ +static void splitName(const char* name, std::u16string* outNs, std::u16string* outName) { + const char* p = name; + while (*p != 0 && *p != kXmlNamespaceSep) { + p++; + } + + if (*p == 0) { + outNs->clear(); + *outName = util::utf8ToUtf16(name); + } else { + *outNs = util::utf8ToUtf16(StringPiece(name, (p - name))); + *outName = util::utf8ToUtf16(p + 1); + } +} + +static void addToStack(Stack* stack, XML_Parser parser, std::unique_ptr<Node> node) { + node->lineNumber = XML_GetCurrentLineNumber(parser); + node->columnNumber = XML_GetCurrentColumnNumber(parser); + + Node* thisNode = node.get(); + if (!stack->nodeStack.empty()) { + stack->nodeStack.top()->addChild(std::move(node)); + } else { + stack->root = std::move(node); + } + + if (thisNode->type != NodeType::kText) { + stack->nodeStack.push(thisNode); + } +} + +static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + std::unique_ptr<Namespace> ns = util::make_unique<Namespace>(); + if (prefix) { + ns->namespacePrefix = util::utf8ToUtf16(prefix); + } + + if (uri) { + ns->namespaceUri = util::utf8ToUtf16(uri); + } + + addToStack(stack, parser, std::move(ns)); +} + +static void XMLCALL endNamespaceHandler(void* userData, const char* prefix) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + assert(!stack->nodeStack.empty()); + stack->nodeStack.pop(); +} + +static bool lessAttribute(const Attribute& lhs, const Attribute& rhs) { + return std::tie(lhs.namespaceUri, lhs.name, lhs.value) < + std::tie(rhs.namespaceUri, rhs.name, rhs.value); +} + +static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + std::unique_ptr<Element> el = util::make_unique<Element>(); + splitName(name, &el->namespaceUri, &el->name); + + while (*attrs) { + Attribute attribute; + splitName(*attrs++, &attribute.namespaceUri, &attribute.name); + attribute.value = util::utf8ToUtf16(*attrs++); + + // Insert in sorted order. + auto iter = std::lower_bound(el->attributes.begin(), el->attributes.end(), attribute, + lessAttribute); + el->attributes.insert(iter, std::move(attribute)); + } + + el->comment = std::move(stack->pendingComment); + addToStack(stack, parser, std::move(el)); +} + +static void XMLCALL endElementHandler(void* userData, const char* name) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + assert(!stack->nodeStack.empty()); + stack->nodeStack.top()->comment = std::move(stack->pendingComment); + stack->nodeStack.pop(); +} + +static void XMLCALL characterDataHandler(void* userData, const char* s, int len) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + if (!s || len <= 0) { + return; + } + + // See if we can just append the text to a previous text node. + if (!stack->nodeStack.empty()) { + Node* currentParent = stack->nodeStack.top(); + if (!currentParent->children.empty()) { + Node* lastChild = currentParent->children.back().get(); + if (lastChild->type == NodeType::kText) { + Text* text = static_cast<Text*>(lastChild); + text->text += util::utf8ToUtf16(StringPiece(s, len)); + return; + } + } + } + + std::unique_ptr<Text> text = util::make_unique<Text>(); + text->text = util::utf8ToUtf16(StringPiece(s, len)); + addToStack(stack, parser, std::move(text)); +} + +static void XMLCALL commentDataHandler(void* userData, const char* comment) { + XML_Parser parser = reinterpret_cast<XML_Parser>(userData); + Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); + + if (!stack->pendingComment.empty()) { + stack->pendingComment += '\n'; + } + stack->pendingComment += util::utf8ToUtf16(comment); +} + +std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger) { + Stack stack; + + XML_Parser parser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); + XML_SetUserData(parser, &stack); + XML_UseParserAsHandlerArg(parser); + XML_SetElementHandler(parser, startElementHandler, endElementHandler); + XML_SetNamespaceDeclHandler(parser, startNamespaceHandler, endNamespaceHandler); + XML_SetCharacterDataHandler(parser, characterDataHandler); + XML_SetCommentHandler(parser, commentDataHandler); + + char buffer[1024]; + while (!in->eof()) { + in->read(buffer, sizeof(buffer) / sizeof(buffer[0])); + if (in->bad() && !in->eof()) { + stack.root = {}; + logger->error() << strerror(errno) << std::endl; + break; + } + + if (XML_Parse(parser, buffer, in->gcount(), in->eof()) == XML_STATUS_ERROR) { + stack.root = {}; + logger->error(XML_GetCurrentLineNumber(parser)) + << XML_ErrorString(XML_GetErrorCode(parser)) << std::endl; + break; + } + } + + XML_ParserFree(parser); + return std::move(stack.root); +} + +static void copyAttributes(Element* el, android::ResXMLParser* parser) { + const size_t attrCount = parser->getAttributeCount(); + if (attrCount > 0) { + el->attributes.reserve(attrCount); + for (size_t i = 0; i < attrCount; i++) { + Attribute attr; + size_t len; + const char16_t* str16 = parser->getAttributeNamespace(i, &len); + if (str16) { + attr.namespaceUri.assign(str16, len); + } + + str16 = parser->getAttributeName(i, &len); + if (str16) { + attr.name.assign(str16, len); + } + + str16 = parser->getAttributeStringValue(i, &len); + if (str16) { + attr.value.assign(str16, len); + } + el->attributes.push_back(std::move(attr)); + } + } +} + +std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger) { + std::unique_ptr<Node> root; + std::stack<Node*> nodeStack; + + android::ResXMLTree tree; + if (tree.setTo(data, dataLen) != android::NO_ERROR) { + return {}; + } + + android::ResXMLParser::event_code_t code; + while ((code = tree.next()) != android::ResXMLParser::BAD_DOCUMENT && + code != android::ResXMLParser::END_DOCUMENT) { + std::unique_ptr<Node> newNode; + switch (code) { + case android::ResXMLParser::START_NAMESPACE: { + std::unique_ptr<Namespace> node = util::make_unique<Namespace>(); + size_t len; + const char16_t* str16 = tree.getNamespacePrefix(&len); + if (str16) { + node->namespacePrefix.assign(str16, len); + } + + str16 = tree.getNamespaceUri(&len); + if (str16) { + node->namespaceUri.assign(str16, len); + } + newNode = std::move(node); + break; + } + + case android::ResXMLParser::START_TAG: { + std::unique_ptr<Element> node = util::make_unique<Element>(); + size_t len; + const char16_t* str16 = tree.getElementNamespace(&len); + if (str16) { + node->namespaceUri.assign(str16, len); + } + + str16 = tree.getElementName(&len); + if (str16) { + node->name.assign(str16, len); + } + + copyAttributes(node.get(), &tree); + + newNode = std::move(node); + break; + } + + case android::ResXMLParser::TEXT: { + std::unique_ptr<Text> node = util::make_unique<Text>(); + size_t len; + const char16_t* str16 = tree.getText(&len); + if (str16) { + node->text.assign(str16, len); + } + newNode = std::move(node); + break; + } + + case android::ResXMLParser::END_NAMESPACE: + case android::ResXMLParser::END_TAG: + assert(!nodeStack.empty()); + nodeStack.pop(); + break; + + default: + assert(false); + break; + } + + if (newNode) { + newNode->lineNumber = tree.getLineNumber(); + + Node* thisNode = newNode.get(); + if (!root) { + assert(nodeStack.empty()); + root = std::move(newNode); + } else { + assert(!nodeStack.empty()); + nodeStack.top()->addChild(std::move(newNode)); + } + + if (thisNode->type != NodeType::kText) { + nodeStack.push(thisNode); + } + } + } + return std::move(root); +} + +Node::Node(NodeType type) : type(type), parent(nullptr), lineNumber(0), columnNumber(0) { +} + +void Node::addChild(std::unique_ptr<Node> child) { + child->parent = this; + children.push_back(std::move(child)); +} + +Namespace::Namespace() : BaseNode(NodeType::kNamespace) { +} + +std::unique_ptr<Node> Namespace::clone() const { + Namespace* ns = new Namespace(); + ns->lineNumber = lineNumber; + ns->columnNumber = columnNumber; + ns->comment = comment; + ns->namespacePrefix = namespacePrefix; + ns->namespaceUri = namespaceUri; + for (auto& child : children) { + ns->addChild(child->clone()); + } + return std::unique_ptr<Node>(ns); +} + +Element::Element() : BaseNode(NodeType::kElement) { +} + +std::unique_ptr<Node> Element::clone() const { + Element* el = new Element(); + el->lineNumber = lineNumber; + el->columnNumber = columnNumber; + el->comment = comment; + el->namespaceUri = namespaceUri; + el->name = name; + el->attributes = attributes; + for (auto& child : children) { + el->addChild(child->clone()); + } + return std::unique_ptr<Node>(el); +} + +Attribute* Element::findAttribute(const StringPiece16& ns, const StringPiece16& name) { + for (auto& attr : attributes) { + if (ns == attr.namespaceUri && name == attr.name) { + return &attr; + } + } + return nullptr; +} + +Element* Element::findChild(const StringPiece16& ns, const StringPiece16& name) { + return findChildWithAttribute(ns, name, nullptr); +} + +Element* Element::findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, + const Attribute* reqAttr) { + for (auto& childNode : children) { + Node* child = childNode.get(); + while (child->type == NodeType::kNamespace) { + if (child->children.empty()) { + break; + } + child = child->children[0].get(); + } + + if (child->type == NodeType::kElement) { + Element* el = static_cast<Element*>(child); + if (ns == el->namespaceUri && name == el->name) { + if (!reqAttr) { + return el; + } + + Attribute* attrName = el->findAttribute(reqAttr->namespaceUri, reqAttr->name); + if (attrName && attrName->value == reqAttr->value) { + return el; + } + } + } + } + return nullptr; +} + +std::vector<Element*> Element::getChildElements() { + std::vector<Element*> elements; + for (auto& childNode : children) { + Node* child = childNode.get(); + while (child->type == NodeType::kNamespace) { + if (child->children.empty()) { + break; + } + child = child->children[0].get(); + } + + if (child->type == NodeType::kElement) { + elements.push_back(static_cast<Element*>(child)); + } + } + return elements; +} + +Text::Text() : BaseNode(NodeType::kText) { +} + +std::unique_ptr<Node> Text::clone() const { + Text* el = new Text(); + el->lineNumber = lineNumber; + el->columnNumber = columnNumber; + el->comment = comment; + el->text = text; + return std::unique_ptr<Node>(el); +} + +} // namespace xml +} // namespace aapt diff --git a/tools/aapt2/XmlDom.h b/tools/aapt2/XmlDom.h new file mode 100644 index 0000000..6931884 --- /dev/null +++ b/tools/aapt2/XmlDom.h @@ -0,0 +1,154 @@ +/* + * 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_XML_DOM_H +#define AAPT_XML_DOM_H + +#include "Logger.h" +#include "StringPiece.h" + +#include <istream> +#include <libexpat/expat.h> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { +namespace xml { + +struct Visitor; + +/** + * The type of node. Can be used to downcast to the concrete XML node + * class. + */ +enum class NodeType { + kNamespace, + kElement, + kText, +}; + +/** + * Base class for all XML nodes. + */ +struct Node { + NodeType type; + Node* parent; + size_t lineNumber; + size_t columnNumber; + std::u16string comment; + std::vector<std::unique_ptr<Node>> children; + + Node(NodeType type); + void addChild(std::unique_ptr<Node> child); + virtual std::unique_ptr<Node> clone() const = 0; + virtual void accept(Visitor* visitor) = 0; + virtual ~Node() {} +}; + +/** + * Base class that implements the visitor methods for a + * subclass of Node. + */ +template <typename Derived> +struct BaseNode : public Node { + BaseNode(NodeType t); + virtual void accept(Visitor* visitor) override; +}; + +/** + * A Namespace XML node. Can only have one child. + */ +struct Namespace : public BaseNode<Namespace> { + std::u16string namespacePrefix; + std::u16string namespaceUri; + + Namespace(); + virtual std::unique_ptr<Node> clone() const override; +}; + +/** + * An XML attribute. + */ +struct Attribute { + std::u16string namespaceUri; + std::u16string name; + std::u16string value; +}; + +/** + * An Element XML node. + */ +struct Element : public BaseNode<Element> { + std::u16string namespaceUri; + std::u16string name; + std::vector<Attribute> attributes; + + Element(); + virtual std::unique_ptr<Node> clone() const override; + Attribute* findAttribute(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChild(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, + const xml::Attribute* reqAttr); + std::vector<xml::Element*> getChildElements(); +}; + +/** + * A Text (CDATA) XML node. Can not have any children. + */ +struct Text : public BaseNode<Text> { + std::u16string text; + + Text(); + virtual std::unique_ptr<Node> clone() const override; +}; + +/** + * Inflates an XML DOM from a text stream, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger); + +/** + * Inflates an XML DOM from a binary ResXMLTree, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger); + +/** + * A visitor interface for the different XML Node subtypes. + */ +struct Visitor { + virtual void visit(Namespace* node) = 0; + virtual void visit(Element* node) = 0; + virtual void visit(Text* text) = 0; +}; + +// Implementations + +template <typename Derived> +BaseNode<Derived>::BaseNode(NodeType type) : Node(type) { +} + +template <typename Derived> +void BaseNode<Derived>::accept(Visitor* visitor) { + visitor->visit(static_cast<Derived*>(this)); +} + +} // namespace xml +} // namespace aapt + +#endif // AAPT_XML_DOM_H diff --git a/tools/aapt2/XmlDom_test.cpp b/tools/aapt2/XmlDom_test.cpp new file mode 100644 index 0000000..0217144 --- /dev/null +++ b/tools/aapt2/XmlDom_test.cpp @@ -0,0 +1,49 @@ +/* + * 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. + */ + +#include "XmlDom.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + +TEST(XmlDomTest, Inflate) { + std::stringstream in(kXmlPreamble); + in << R"EOF( + <Layout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </Layout> + )EOF"; + + SourceLogger logger(Source{ "/test/path" }); + std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); + ASSERT_NE(root, nullptr); + + EXPECT_EQ(root->type, xml::NodeType::kNamespace); + xml::Namespace* ns = static_cast<xml::Namespace*>(root.get()); + EXPECT_EQ(ns->namespaceUri, u"http://schemas.android.com/apk/res/android"); + EXPECT_EQ(ns->namespacePrefix, u"android"); +} + +} // namespace aapt diff --git a/tools/aapt2/XmlFlattener.cpp b/tools/aapt2/XmlFlattener.cpp index f78e38d..56b5613 100644 --- a/tools/aapt2/XmlFlattener.cpp +++ b/tools/aapt2/XmlFlattener.cpp @@ -34,425 +34,444 @@ #include <vector> namespace aapt { +namespace xml { -constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; - -struct AttributeValueFlattener : ValueVisitor { - AttributeValueFlattener( - std::shared_ptr<IResolver> resolver, SourceLogger* logger, - android::Res_value* outValue, std::shared_ptr<XmlPullParser> parser, bool* outError, - StringPool::Ref rawValue, std::u16string* defaultPackage, - std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>* outStringRefs) : - mResolver(resolver), mLogger(logger), mOutValue(outValue), mParser(parser), - mError(outError), mRawValue(rawValue), mDefaultPackage(defaultPackage), - mStringRefs(outStringRefs) { +constexpr uint32_t kLowPriority = 0xffffffffu; + +// A vector that maps String refs to their final destination in the out buffer. +using FlatStringRefList = std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>; + +struct XmlFlattener : public Visitor { + XmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, + const std::u16string& defaultPackage) : + mOut(outBuffer), mPool(pool), mStringRefs(stringRefs), + mDefaultPackage(defaultPackage) { } - void visit(Reference& reference, ValueVisitorArgs&) override { - // First see if we can convert the package name from a prefix to a real - // package name. - ResourceName aliasedName = reference.name; + // No copying. + XmlFlattener(const XmlFlattener&) = delete; + XmlFlattener& operator=(const XmlFlattener&) = delete; + + void writeNamespace(Namespace* node, uint16_t type) { + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_namespaceExt* flatNs = + mOut->nextBlock<android::ResXMLTree_namespaceExt>(); + mOut->align4(); + + flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; + addString(node->namespacePrefix, kLowPriority, &flatNs->prefix); + addString(node->namespaceUri, kLowPriority, &flatNs->uri); + } - if (!reference.name.package.empty()) { - // Only if we specified a package do we look for its alias. - mParser->applyPackageAlias(&reference.name.package, *mDefaultPackage); - } else { - reference.name.package = *mDefaultPackage; + virtual void visit(Namespace* node) override { + // Extract the package/prefix from this namespace node. + Maybe<std::u16string> package = util::extractPackageFromNamespace(node->namespaceUri); + if (package) { + mPackageAliases.emplace_back( + node->namespacePrefix, + package.value().empty() ? mDefaultPackage : package.value()); } - Maybe<ResourceId> result = mResolver->findId(reference.name); - if (!result || !result.value().isValid()) { - std::ostream& out = mLogger->error(mParser->getLineNumber()) - << "unresolved reference '" - << aliasedName - << "'"; - if (aliasedName != reference.name) { - out << " (aka '" << reference.name << "')"; - } - out << "'." << std::endl; - *mError = true; - } else { - reference.id = result.value(); - reference.flatten(*mOutValue); + writeNamespace(node, android::RES_XML_START_NAMESPACE_TYPE); + for (const auto& child : node->children) { + child->accept(this); } - } + writeNamespace(node, android::RES_XML_END_NAMESPACE_TYPE); - void visit(String& string, ValueVisitorArgs&) override { - mOutValue->dataType = android::Res_value::TYPE_STRING; - mStringRefs->emplace_back( - mRawValue, - reinterpret_cast<android::ResStringPool_ref*>(mOutValue->data)); + if (package) { + mPackageAliases.pop_back(); + } } - void visitItem(Item& item, ValueVisitorArgs&) override { - item.flatten(*mOutValue); + virtual void visit(Text* node) override { + if (util::trimWhitespace(node->text).empty()) { + return; + } + + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_cdataExt* flatText = mOut->nextBlock<android::ResXMLTree_cdataExt>(); + mOut->align4(); + + const uint16_t type = android::RES_XML_CDATA_TYPE; + flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; + addString(node->text, kLowPriority, &flatText->data); } -private: - std::shared_ptr<IResolver> mResolver; - SourceLogger* mLogger; - android::Res_value* mOutValue; - std::shared_ptr<XmlPullParser> mParser; - bool* mError; - StringPool::Ref mRawValue; - std::u16string* mDefaultPackage; - std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>* mStringRefs; -}; + virtual void visit(Element* node) override { + const size_t startIndex = mOut->size(); + android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_attrExt* flatElem = mOut->nextBlock<android::ResXMLTree_attrExt>(); -struct XmlAttribute { - uint32_t resourceId; - const XmlPullParser::Attribute* xmlAttr; - const Attribute* attr; - StringPool::Ref nameRef; -}; + const uint16_t type = android::RES_XML_START_ELEMENT_TYPE; + flatNode->header = { type, sizeof(*flatNode), 0 }; + flatNode->lineNumber = node->lineNumber; + flatNode->comment.index = -1; -static bool lessAttributeId(const XmlAttribute& a, uint32_t id) { - return a.resourceId < id; -} + addString(node->namespaceUri, kLowPriority, &flatElem->ns); + addString(node->name, kLowPriority, &flatElem->name); + flatElem->attributeStart = sizeof(*flatElem); + flatElem->attributeSize = sizeof(android::ResXMLTree_attribute); + flatElem->attributeCount = node->attributes.size(); -XmlFlattener::XmlFlattener(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver) : - mTable(table), mResolver(resolver) { -} + if (!writeAttributes(mOut, node, flatElem)) { + mError = true; + } -/** - * Reads events from the parser and writes to a BigBuffer. The binary XML file - * expects the StringPool to appear first, but we haven't collected the strings yet. We - * write to a temporary BigBuffer while parsing the input, adding strings we encounter - * to the StringPool. At the end, we write the StringPool to the given BigBuffer and - * then move the data from the temporary BigBuffer into the given one. This incurs no - * copies as the given BigBuffer simply takes ownership of the data. - */ -Maybe<size_t> XmlFlattener::flatten(const Source& source, - const std::shared_ptr<XmlPullParser>& parser, - BigBuffer* outBuffer, Options options) { - SourceLogger logger(source); - StringPool pool; - bool error = false; + mOut->align4(); + flatNode->header.size = (uint32_t)(mOut->size() - startIndex); - size_t smallestStrippedAttributeSdk = std::numeric_limits<size_t>::max(); + for (const auto& child : node->children) { + child->accept(this); + } - // Attribute names are stored without packages, but we use - // their StringPool index to lookup their resource IDs. - // This will cause collisions, so we can't dedupe - // attribute names from different packages. We use separate - // pools that we later combine. - std::map<std::u16string, StringPool> packagePools; + const size_t startEndIndex = mOut->size(); + android::ResXMLTree_node* flatEndNode = mOut->nextBlock<android::ResXMLTree_node>(); + android::ResXMLTree_endElementExt* flatEndElem = + mOut->nextBlock<android::ResXMLTree_endElementExt>(); + mOut->align4(); - // Attribute resource IDs are stored in the same order - // as the attribute names appear in the StringPool. - // Since the StringPool contains more than just attribute - // names, to maintain a tight packing of resource IDs, - // we must ensure that attribute names appear first - // in our StringPool. For this, we assign a low priority - // (0xffffffff) to non-attribute strings. Attribute - // names will be stored along with a priority equal - // to their resource ID so that they are ordered. - StringPool::Context lowPriority { 0xffffffffu }; + const uint16_t endType = android::RES_XML_END_ELEMENT_TYPE; + flatEndNode->header = { endType, sizeof(*flatEndNode), + (uint32_t)(mOut->size() - startEndIndex) }; + flatEndNode->lineNumber = node->lineNumber; + flatEndNode->comment.index = -1; - // Once we sort the StringPool, we can assign the updated indices - // to the correct data locations. - std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>> stringRefs; + addString(node->namespaceUri, kLowPriority, &flatEndElem->ns); + addString(node->name, kLowPriority, &flatEndElem->name); + } - // Since we don't know the size of the final StringPool, we write to this - // temporary BigBuffer, which we will append to outBuffer later. - BigBuffer out(1024); - while (XmlPullParser::isGoodEvent(parser->next())) { - XmlPullParser::Event event = parser->getEvent(); - switch (event) { - case XmlPullParser::Event::kStartNamespace: - case XmlPullParser::Event::kEndNamespace: { - const size_t startIndex = out.size(); - android::ResXMLTree_node* node = out.nextBlock<android::ResXMLTree_node>(); - if (event == XmlPullParser::Event::kStartNamespace) { - node->header.type = android::RES_XML_START_NAMESPACE_TYPE; - } else { - node->header.type = android::RES_XML_END_NAMESPACE_TYPE; - } + bool success() const { + return !mError; + } - node->header.headerSize = sizeof(*node); - node->lineNumber = parser->getLineNumber(); - node->comment.index = -1; +protected: + void addString(const StringPiece16& str, uint32_t priority, android::ResStringPool_ref* dest) { + if (!str.empty()) { + mStringRefs->emplace_back(mPool->makeRef(str, StringPool::Context{ priority }), dest); + } else { + // The device doesn't think a string of size 0 is the same as null. + dest->index = -1; + } + } - android::ResXMLTree_namespaceExt* ns = - out.nextBlock<android::ResXMLTree_namespaceExt>(); - stringRefs.emplace_back( - pool.makeRef(parser->getNamespacePrefix(), lowPriority), &ns->prefix); - stringRefs.emplace_back( - pool.makeRef(parser->getNamespaceUri(), lowPriority), &ns->uri); + void addString(const StringPool::Ref& ref, android::ResStringPool_ref* dest) { + mStringRefs->emplace_back(ref, dest); + } - out.align4(); - node->header.size = out.size() - startIndex; - break; + Maybe<std::u16string> getPackageAlias(const std::u16string& prefix) { + const auto endIter = mPackageAliases.rend(); + for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { + if (iter->first == prefix) { + return iter->second; } + } + return {}; + } - case XmlPullParser::Event::kStartElement: { - const size_t startIndex = out.size(); - android::ResXMLTree_node* node = out.nextBlock<android::ResXMLTree_node>(); - node->header.type = android::RES_XML_START_ELEMENT_TYPE; - node->header.headerSize = sizeof(*node); - node->lineNumber = parser->getLineNumber(); - node->comment.index = -1; - - android::ResXMLTree_attrExt* elem = out.nextBlock<android::ResXMLTree_attrExt>(); - if (!parser->getElementNamespace().empty()) { - stringRefs.emplace_back( - pool.makeRef(parser->getElementNamespace(), lowPriority), &elem->ns); - } else { - // The device doesn't think a string of size 0 is the same as null. - elem->ns.index = -1; - } - stringRefs.emplace_back( - pool.makeRef(parser->getElementName(), lowPriority), &elem->name); - elem->attributeStart = sizeof(*elem); - elem->attributeSize = sizeof(android::ResXMLTree_attribute); - - // The resource system expects attributes to be sorted by resource ID. - std::vector<XmlAttribute> sortedAttributes; - uint32_t nextAttributeId = 0; - const auto endAttrIter = parser->endAttributes(); - for (auto attrIter = parser->beginAttributes(); - attrIter != endAttrIter; - ++attrIter) { - uint32_t id; - StringPool::Ref nameRef; - const Attribute* attr = nullptr; - - if (options.maxSdkAttribute && attrIter->namespaceUri == kSchemaAndroid) { - size_t sdkVersion = findAttributeSdkLevel(attrIter->name); - if (sdkVersion > options.maxSdkAttribute.value()) { - // We will silently omit this attribute - smallestStrippedAttributeSdk = - std::min(smallestStrippedAttributeSdk, sdkVersion); - continue; - } - } - - ResourceNameRef genIdName; - bool create = false; - bool privateRef = false; - if (mTable && ResourceParser::tryParseReference(attrIter->value, &genIdName, - &create, &privateRef) && create) { - mTable->addResource(genIdName, {}, source.line(parser->getLineNumber()), - util::make_unique<Id>()); - } + const std::u16string& getDefaultPackage() const { + return mDefaultPackage; + } + /** + * Subclasses override this to deal with attributes. Attributes can be flattened as + * raw values or as resources. + */ + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) = 0; - Maybe<std::u16string> package = util::extractPackageFromNamespace( - attrIter->namespaceUri); - if (!package || !mResolver) { - // Attributes that have no resource ID (because they don't belong to a - // package) should appear after those that do have resource IDs. Assign - // them some integer value that will appear after. - id = 0x80000000u | nextAttributeId++; - nameRef = pool.makeRef(attrIter->name, StringPool::Context{ id }); - - } else { - // Find the Attribute object via our Resolver. - ResourceName attrName = { - package.value(), ResourceType::kAttr, attrIter->name }; - - if (attrName.package.empty()) { - attrName.package = options.defaultPackage; - } - - Maybe<IResolver::Entry> result = mResolver->findAttribute(attrName); - if (!result || !result.value().id.isValid()) { - logger.error(parser->getLineNumber()) - << "unresolved attribute '" - << attrName - << "'." - << std::endl; - error = true; - continue; - } - - if (!result.value().attr) { - logger.error(parser->getLineNumber()) - << "not a valid attribute '" - << attrName - << "'." - << std::endl; - error = true; - continue; - } - - id = result.value().id.id; - attr = result.value().attr; - - // Put the attribute name into a package specific pool, since we don't - // want to collapse names from different packages. - nameRef = packagePools[package.value()].makeRef( - attrIter->name, StringPool::Context{ id }); - } +private: + BigBuffer* mOut; + StringPool* mPool; + FlatStringRefList* mStringRefs; + std::u16string mDefaultPackage; + bool mError = false; + std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; +}; - // Insert the attribute into the sorted vector. - auto iter = std::lower_bound(sortedAttributes.begin(), sortedAttributes.end(), - id, lessAttributeId); - sortedAttributes.insert(iter, XmlAttribute{ id, &*attrIter, attr, nameRef }); - } +/** + * Flattens XML, encoding the attributes as raw strings. This is used in the compile phase. + */ +struct CompileXmlFlattener : public XmlFlattener { + CompileXmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, + const std::u16string& defaultPackage) : + XmlFlattener(outBuffer, pool, stringRefs, defaultPackage) { + } - if (error) { - break; - } + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) override { + flatElem->attributeCount = node->attributes.size(); + if (node->attributes.empty()) { + return true; + } - // Now that we have filtered out some attributes, get the final count. - elem->attributeCount = sortedAttributes.size(); - - // Flatten the sorted attributes. - uint16_t attributeIndex = 1; - for (auto entry : sortedAttributes) { - android::ResXMLTree_attribute* attr = - out.nextBlock<android::ResXMLTree_attribute>(); - if (!entry.xmlAttr->namespaceUri.empty()) { - stringRefs.emplace_back( - pool.makeRef(entry.xmlAttr->namespaceUri, lowPriority), &attr->ns); - } else { - attr->ns.index = -1; - } + android::ResXMLTree_attribute* flatAttrs = out->nextBlock<android::ResXMLTree_attribute>( + node->attributes.size()); + for (const Attribute& attr : node->attributes) { + addString(attr.namespaceUri, kLowPriority, &flatAttrs->ns); + addString(attr.name, kLowPriority, &flatAttrs->name); + addString(attr.value, kLowPriority, &flatAttrs->rawValue); + flatAttrs++; + } + return true; + } +}; - StringPool::Ref rawValueRef = pool.makeRef(entry.xmlAttr->value, lowPriority); +struct AttributeToFlatten { + uint32_t resourceId = 0; + const Attribute* xmlAttr = nullptr; + const ::aapt::Attribute* resourceAttr = nullptr; +}; - stringRefs.emplace_back(entry.nameRef, &attr->name); +static bool lessAttributeId(const AttributeToFlatten& a, uint32_t id) { + return a.resourceId < id; +} - if (options.keepRawValues) { - stringRefs.emplace_back(rawValueRef, &attr->rawValue); - } else { - attr->rawValue.index = -1; - } +/** + * Flattens XML, encoding the attributes as resources. + */ +struct LinkedXmlFlattener : public XmlFlattener { + LinkedXmlFlattener(BigBuffer* outBuffer, StringPool* pool, + std::map<std::u16string, StringPool>* packagePools, + FlatStringRefList* stringRefs, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + SourceLogger* logger, + const FlattenOptions& options) : + XmlFlattener(outBuffer, pool, stringRefs, defaultPackage), mResolver(resolver), + mLogger(logger), mPackagePools(packagePools), mOptions(options) { + } - // Assign the indices for specific attributes. - if (entry.xmlAttr->namespaceUri == kSchemaAndroid && - entry.xmlAttr->name == u"id") { - elem->idIndex = attributeIndex; - } else if (entry.xmlAttr->namespaceUri.empty()) { - if (entry.xmlAttr->name == u"class") { - elem->classIndex = attributeIndex; - } else if (entry.xmlAttr->name == u"style") { - elem->styleIndex = attributeIndex; - } - } - attributeIndex++; - - std::unique_ptr<Item> value; - if (entry.attr) { - value = ResourceParser::parseItemForAttribute(entry.xmlAttr->value, - *entry.attr); - } else { - bool create = false; - value = ResourceParser::tryParseReference(entry.xmlAttr->value, &create); - } + virtual bool writeAttributes(BigBuffer* out, Element* node, + android::ResXMLTree_attrExt* flatElem) override { + bool error = false; + std::vector<AttributeToFlatten> sortedAttributes; + uint32_t nextAttributeId = 0x80000000u; + + // Sort and filter attributes by their resource ID. + for (const Attribute& attr : node->attributes) { + AttributeToFlatten attrToFlatten; + attrToFlatten.xmlAttr = &attr; + + Maybe<std::u16string> package = util::extractPackageFromNamespace(attr.namespaceUri); + if (package) { + // Find the Attribute object via our Resolver. + ResourceName attrName = { package.value(), ResourceType::kAttr, attr.name }; + if (attrName.package.empty()) { + attrName.package = getDefaultPackage(); + } - if (mResolver && value) { - AttributeValueFlattener flattener( - mResolver, - &logger, - &attr->typedValue, - parser, - &error, - rawValueRef, - &options.defaultPackage, - &stringRefs); - value->accept(flattener, {}); - } else if (!value && entry.attr && - !(entry.attr->typeMask & android::ResTable_map::TYPE_STRING)) { - logger.error(parser->getLineNumber()) - << "'" - << *rawValueRef - << "' is not compatible with attribute " - << *entry.attr - << "." - << std::endl; - error = true; - } else { - attr->typedValue.dataType = android::Res_value::TYPE_STRING; - if (!options.keepRawValues) { - // Don't set the string twice. - stringRefs.emplace_back(rawValueRef, &attr->rawValue); - } - stringRefs.emplace_back(rawValueRef, - reinterpret_cast<android::ResStringPool_ref*>( - &attr->typedValue.data)); + Maybe<IResolver::Entry> result = mResolver->findAttribute(attrName); + if (!result || !result.value().id.isValid() || !result.value().attr) { + error = true; + mLogger->error(node->lineNumber) + << "unresolved attribute '" << attrName << "'." + << std::endl; + } else { + attrToFlatten.resourceId = result.value().id.id; + attrToFlatten.resourceAttr = result.value().attr; + + size_t sdk = findAttributeSdkLevel(attrToFlatten.resourceId); + if (mOptions.maxSdkAttribute && sdk > mOptions.maxSdkAttribute.value()) { + // We need to filter this attribute out. + mSmallestFilteredSdk = std::min(mSmallestFilteredSdk, sdk); + continue; } - attr->typedValue.size = sizeof(attr->typedValue); } - - out.align4(); - node->header.size = out.size() - startIndex; - break; } - case XmlPullParser::Event::kEndElement: { - const size_t startIndex = out.size(); - android::ResXMLTree_node* node = out.nextBlock<android::ResXMLTree_node>(); - node->header.type = android::RES_XML_END_ELEMENT_TYPE; - node->header.headerSize = sizeof(*node); - node->lineNumber = parser->getLineNumber(); - node->comment.index = -1; - - android::ResXMLTree_endElementExt* elem = - out.nextBlock<android::ResXMLTree_endElementExt>(); - stringRefs.emplace_back( - pool.makeRef(parser->getElementNamespace(), lowPriority), &elem->ns); - stringRefs.emplace_back( - pool.makeRef(parser->getElementName(), lowPriority), &elem->name); - - out.align4(); - node->header.size = out.size() - startIndex; - break; + if (attrToFlatten.resourceId == 0) { + // Attributes that have no resource ID (because they don't belong to a + // package) should appear after those that do have resource IDs. Assign + // them some integer value that will appear after. + attrToFlatten.resourceId = nextAttributeId++; } - case XmlPullParser::Event::kText: { - StringPiece16 text = util::trimWhitespace(parser->getText()); - if (text.empty()) { - break; + // Insert the attribute into the sorted vector. + auto iter = std::lower_bound(sortedAttributes.begin(), sortedAttributes.end(), + attrToFlatten.resourceId, lessAttributeId); + sortedAttributes.insert(iter, std::move(attrToFlatten)); + } + + flatElem->attributeCount = sortedAttributes.size(); + if (sortedAttributes.empty()) { + return true; + } + + android::ResXMLTree_attribute* flatAttr = out->nextBlock<android::ResXMLTree_attribute>( + sortedAttributes.size()); + + // Now that we have sorted the attributes into their final encoded order, it's time + // to actually write them out. + uint16_t attributeIndex = 1; + for (const AttributeToFlatten& attrToFlatten : sortedAttributes) { + Maybe<std::u16string> package = util::extractPackageFromNamespace( + attrToFlatten.xmlAttr->namespaceUri); + + // Assign the indices for specific attributes. + if (package && package.value() == u"android" && attrToFlatten.xmlAttr->name == u"id") { + flatElem->idIndex = attributeIndex; + } else if (attrToFlatten.xmlAttr->namespaceUri.empty()) { + if (attrToFlatten.xmlAttr->name == u"class") { + flatElem->classIndex = attributeIndex; + } else if (attrToFlatten.xmlAttr->name == u"style") { + flatElem->styleIndex = attributeIndex; + } + } + attributeIndex++; + + // Add the namespaceUri and name to the list of StringRefs to encode. + addString(attrToFlatten.xmlAttr->namespaceUri, kLowPriority, &flatAttr->ns); + flatAttr->rawValue.index = -1; + + if (!attrToFlatten.resourceAttr) { + addString(attrToFlatten.xmlAttr->name, kLowPriority, &flatAttr->name); + } else { + // We've already extracted the package successfully before. + assert(package); + + // Attribute names are stored without packages, but we use + // their StringPool index to lookup their resource IDs. + // This will cause collisions, so we can't dedupe + // attribute names from different packages. We use separate + // pools that we later combine. + // + // Lookup the StringPool for this package and make the reference there. + StringPool::Ref nameRef = (*mPackagePools)[package.value()].makeRef( + attrToFlatten.xmlAttr->name, + StringPool::Context{ attrToFlatten.resourceId }); + + // Add it to the list of strings to flatten. + addString(nameRef, &flatAttr->name); + + if (mOptions.keepRawValues) { + // Keep raw values (this is for static libraries). + // TODO(with a smarter inflater for binary XML, we can do without this). + addString(attrToFlatten.xmlAttr->value, kLowPriority, &flatAttr->rawValue); } + } - const size_t startIndex = out.size(); - android::ResXMLTree_node* node = out.nextBlock<android::ResXMLTree_node>(); - node->header.type = android::RES_XML_CDATA_TYPE; - node->header.headerSize = sizeof(*node); - node->lineNumber = parser->getLineNumber(); - node->comment.index = -1; + error |= !flattenItem(node, attrToFlatten.xmlAttr->value, attrToFlatten.resourceAttr, + flatAttr); + flatAttr->typedValue.size = sizeof(flatAttr->typedValue); + flatAttr++; + } + return !error; + } - android::ResXMLTree_cdataExt* elem = out.nextBlock<android::ResXMLTree_cdataExt>(); - stringRefs.emplace_back(pool.makeRef(text, lowPriority), &elem->data); + Maybe<size_t> getSmallestFilteredSdk() const { + if (mSmallestFilteredSdk == std::numeric_limits<size_t>::max()) { + return {}; + } + return mSmallestFilteredSdk; + } - out.align4(); - node->header.size = out.size() - startIndex; - break; +private: + bool flattenItem(const Node* el, const std::u16string& value, const ::aapt::Attribute* attr, + android::ResXMLTree_attribute* flatAttr) { + std::unique_ptr<Item> item; + if (!attr) { + bool create = false; + item = ResourceParser::tryParseReference(value, &create); + if (!item) { + flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; + addString(value, kLowPriority, &flatAttr->rawValue); + addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( + &flatAttr->typedValue.data)); + return true; } + } else { + item = ResourceParser::parseItemForAttribute(value, *attr); + if (!item) { + if (!(attr->typeMask & android::ResTable_map::TYPE_STRING)) { + mLogger->error(el->lineNumber) + << "'" + << value + << "' is not compatible with attribute '" + << *attr + << "'." + << std::endl; + return false; + } - default: - break; + flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; + addString(value, kLowPriority, &flatAttr->rawValue); + addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( + &flatAttr->typedValue.data)); + return true; + } } - } - out.align4(); + assert(item); - if (error) { - return {}; - } + bool error = false; - if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { - logger.error(parser->getLineNumber()) - << parser->getLastError() - << std::endl; - return {}; - } + // If this is a reference, resolve the name into an ID. + visitFunc<Reference>(*item, [&](Reference& reference) { + // First see if we can convert the package name from a prefix to a real + // package name. + ResourceName realName = reference.name; + if (!realName.package.empty()) { + Maybe<std::u16string> package = getPackageAlias(realName.package); + if (package) { + realName.package = package.value(); + } + } else { + realName.package = getDefaultPackage(); + } - // Merge the package pools into the main pool. - for (auto& packagePoolEntry : packagePools) { - pool.merge(std::move(packagePoolEntry.second)); + Maybe<ResourceId> result = mResolver->findId(realName); + if (!result || !result.value().isValid()) { + std::ostream& out = mLogger->error(el->lineNumber) + << "unresolved reference '" + << reference.name + << "'"; + if (realName != reference.name) { + out << " (aka '" << realName << "')"; + } + out << "'." << std::endl; + error = true; + } else { + reference.id = result.value(); + } + }); + + if (error) { + return false; + } + + item->flatten(flatAttr->typedValue); + return true; } - // Sort so that attribute resource IDs show up first. - pool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + std::shared_ptr<IResolver> mResolver; + SourceLogger* mLogger; + std::map<std::u16string, StringPool>* mPackagePools; + FlattenOptions mOptions; + size_t mSmallestFilteredSdk = std::numeric_limits<size_t>::max(); +}; + +/** + * The binary XML file expects the StringPool to appear first, but we haven't collected the + * strings yet. We write to a temporary BigBuffer while parsing the input, adding strings + * we encounter to the StringPool. At the end, we write the StringPool to the given BigBuffer and + * then move the data from the temporary BigBuffer into the given one. This incurs no + * copies as the given BigBuffer simply takes ownership of the data. + */ +static void flattenXml(StringPool* pool, FlatStringRefList* stringRefs, BigBuffer* outBuffer, + BigBuffer&& xmlTreeBuffer) { + // Sort the string pool so that attribute resource IDs show up first. + pool->sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { return a.context.priority < b.context.priority; }); // Now we flatten the string pool references into the correct places. - for (const auto& refEntry : stringRefs) { + for (const auto& refEntry : *stringRefs) { refEntry.second->index = refEntry.first.getIndex(); } @@ -463,36 +482,93 @@ Maybe<size_t> XmlFlattener::flatten(const Source& source, header->header.headerSize = sizeof(*header); // Flatten the StringPool. - StringPool::flattenUtf16(outBuffer, pool); + StringPool::flattenUtf16(outBuffer, *pool); // Write the array of resource IDs, indexed by StringPool order. const size_t beforeResIdMapIndex = outBuffer->size(); android::ResChunk_header* resIdMapChunk = outBuffer->nextBlock<android::ResChunk_header>(); resIdMapChunk->type = android::RES_XML_RESOURCE_MAP_TYPE; resIdMapChunk->headerSize = sizeof(*resIdMapChunk); - for (const auto& str : pool) { + for (const auto& str : *pool) { ResourceId id { str->context.priority }; - if (!id.isValid()) { + if (id.id == kLowPriority || !id.isValid()) { // When we see the first non-resource ID, // we're done. break; } - uint32_t* flatId = outBuffer->nextBlock<uint32_t>(); - *flatId = id.id; + *outBuffer->nextBlock<uint32_t>() = id.id; } resIdMapChunk->size = outBuffer->size() - beforeResIdMapIndex; // Move the temporary BigBuffer into outBuffer. - outBuffer->appendBuffer(std::move(out)); - + outBuffer->appendBuffer(std::move(xmlTreeBuffer)); header->header.size = outBuffer->size() - beforeXmlTreeIndex; +} + +bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer) { + StringPool pool; + + // This will hold the StringRefs and the location in which to write the index. + // Once we sort the StringPool, we can assign the updated indices + // to the correct data locations. + FlatStringRefList stringRefs; + + // Since we don't know the size of the final StringPool, we write to this + // temporary BigBuffer, which we will append to outBuffer later. + BigBuffer out(1024); + + CompileXmlFlattener flattener(&out, &pool, &stringRefs, defaultPackage); + root->accept(&flattener); + + if (!flattener.success()) { + return false; + } + + flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); + return true; +}; + +Maybe<size_t> flattenAndLink(const Source& source, Node* root, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + const FlattenOptions& options, BigBuffer* outBuffer) { + SourceLogger logger(source); + StringPool pool; + + // Attribute names are stored without packages, but we use + // their StringPool index to lookup their resource IDs. + // This will cause collisions, so we can't dedupe + // attribute names from different packages. We use separate + // pools that we later combine. + std::map<std::u16string, StringPool> packagePools; + + FlatStringRefList stringRefs; + + // Since we don't know the size of the final StringPool, we write to this + // temporary BigBuffer, which we will append to outBuffer later. + BigBuffer out(1024); + + LinkedXmlFlattener flattener(&out, &pool, &packagePools, &stringRefs, defaultPackage, resolver, + &logger, options); + root->accept(&flattener); + + if (!flattener.success()) { + return {}; + } + + // Merge the package pools into the main pool. + for (auto& packagePoolEntry : packagePools) { + pool.merge(std::move(packagePoolEntry.second)); + } + + flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); - if (smallestStrippedAttributeSdk == std::numeric_limits<size_t>::max()) { - // Nothing was stripped - return 0u; + if (flattener.getSmallestFilteredSdk()) { + return flattener.getSmallestFilteredSdk(); } - return smallestStrippedAttributeSdk; + return 0; } +} // namespace xml } // namespace aapt diff --git a/tools/aapt2/XmlFlattener.h b/tools/aapt2/XmlFlattener.h index 2cfcc16..4ece0a3 100644 --- a/tools/aapt2/XmlFlattener.h +++ b/tools/aapt2/XmlFlattener.h @@ -21,64 +21,49 @@ #include "Maybe.h" #include "Resolver.h" #include "Source.h" -#include "XmlPullParser.h" +#include "XmlDom.h" #include <string> namespace aapt { +namespace xml { /** * Flattens an XML file into a binary representation parseable by - * the Android resource system. References to resources are checked - * and string values are transformed to typed data where possible. + * the Android resource system. */ -class XmlFlattener { -public: - struct Options { - /** - * The package to use when a reference has no package specified - * (or a namespace URI equals "http://schemas.android.com/apk/res-auto"). - */ - std::u16string defaultPackage; - - /** - * If set, tells the XmlFlattener to strip out - * attributes that have been introduced after - * max SDK. - */ - Maybe<size_t> maxSdkAttribute; - - /** - * Setting this to true will keep the raw string value of - * an attribute's value when it has been resolved. - */ - bool keepRawValues = false; - }; +bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer); +/** + * Options for flattenAndLink. + */ +struct FlattenOptions { /** - * Creates a flattener with a Resolver to resolve references - * and attributes. + * Keep attribute raw string values along with typed values. */ - XmlFlattener(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver); - - XmlFlattener(const XmlFlattener&) = delete; // Not copyable. + bool keepRawValues = false; /** - * Flatten an XML file, reading from the XML parser and writing to the - * BigBuffer. The source object is mainly for logging errors. If the - * function succeeds, returns the smallest SDK version of an attribute that - * was stripped out. If no attributes were stripped out, the return value - * is 0. + * If set, any attribute introduced in a later SDK will not be encoded. */ - Maybe<size_t> flatten(const Source& source, const std::shared_ptr<XmlPullParser>& parser, - BigBuffer* outBuffer, Options options); - -private: - std::shared_ptr<ResourceTable> mTable; - std::shared_ptr<IResolver> mResolver; + Maybe<size_t> maxSdkAttribute; }; +/** + * Like flatten(Node*,BigBuffer*), but references to resources are checked + * and string values are transformed to typed data where possible. + * + * `defaultPackage` is used when a reference has no package or the namespace URI + * "http://schemas.android.com/apk/res-auto" is used. + * + * `resolver` is used to resolve references to resources. + */ +Maybe<size_t> flattenAndLink(const Source& source, Node* root, + const std::u16string& defaultPackage, + const std::shared_ptr<IResolver>& resolver, + const FlattenOptions& options, BigBuffer* outBuffer); + +} // namespace xml } // namespace aapt #endif // AAPT_XML_FLATTENER_H diff --git a/tools/aapt2/XmlFlattener_test.cpp b/tools/aapt2/XmlFlattener_test.cpp index b45cd9b..8915d24 100644 --- a/tools/aapt2/XmlFlattener_test.cpp +++ b/tools/aapt2/XmlFlattener_test.cpp @@ -17,7 +17,6 @@ #include "MockResolver.h" #include "ResourceTable.h" #include "ResourceValues.h" -#include "SourceXmlPullParser.h" #include "Util.h" #include "XmlFlattener.h" @@ -30,13 +29,14 @@ using namespace android; namespace aapt { +namespace xml { constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; class XmlFlattenerTest : public ::testing::Test { public: virtual void SetUp() override { - std::shared_ptr<IResolver> resolver = std::make_shared<MockResolver>( + mResolver = std::make_shared<MockResolver>( std::make_shared<ResourceTable>(), std::map<ResourceName, ResourceId>({ { ResourceName{ u"android", ResourceType::kAttr, u"attr" }, @@ -47,18 +47,21 @@ public: ResourceId{ 0x01010001u } }, { ResourceName{ u"com.lib", ResourceType::kId, u"id" }, ResourceId{ 0x01020001u } }})); - - mFlattener = std::make_shared<XmlFlattener>(nullptr, resolver); } ::testing::AssertionResult testFlatten(const std::string& in, ResXMLTree* outTree) { std::stringstream input(kXmlPreamble); input << in << std::endl; - std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(input); + + SourceLogger logger(Source{ "test.xml" }); + std::unique_ptr<Node> root = inflate(&input, &logger); + if (!root) { + return ::testing::AssertionFailure(); + } + BigBuffer outBuffer(1024); - XmlFlattener::Options xmlOptions; - xmlOptions.defaultPackage = u"android"; - if (!mFlattener->flatten(Source{ "test" }, xmlParser, &outBuffer, xmlOptions)) { + if (!flattenAndLink(Source{ "test.xml" }, root.get(), std::u16string(u"android"), + mResolver, {}, &outBuffer)) { return ::testing::AssertionFailure(); } @@ -69,16 +72,48 @@ public: return ::testing::AssertionSuccess(); } - std::shared_ptr<XmlFlattener> mFlattener; + std::shared_ptr<IResolver> mResolver; }; TEST_F(XmlFlattenerTest, ParseSimpleView) { - std::string input = "<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - " android:attr=\"@id/id\">\n" - "</View>"; + std::string input = R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:attr="@id/id" + class="str" + style="@id/id"> + </View> + )EOF"; ResXMLTree tree; ASSERT_TRUE(testFlatten(input, &tree)); + while (tree.next() != ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); + ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); + } + + const StringPiece16 androidNs = u"http://schemas.android.com/apk/res/android"; + const StringPiece16 attrName = u"attr"; + ssize_t idx = tree.indexOfAttribute(androidNs.data(), androidNs.size(), attrName.data(), + attrName.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0x01010000u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); + + const StringPiece16 class16 = u"class"; + idx = tree.indexOfAttribute(nullptr, 0, class16.data(), class16.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_STRING); + EXPECT_EQ(tree.getAttributeData(idx), tree.getAttributeValueStringID(idx)); + + const StringPiece16 style16 = u"style"; + idx = tree.indexOfAttribute(nullptr, 0, style16.data(), style16.size()); + ASSERT_GE(idx, 0); + EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); + EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); + EXPECT_EQ((uint32_t) tree.getAttributeData(idx), 0x01020000u); + EXPECT_EQ(tree.getAttributeValueStringID(idx), -1); + while (tree.next() != ResXMLTree::END_DOCUMENT) { ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); } @@ -193,4 +228,5 @@ TEST_F(XmlFlattenerTest, NoNamespaceIsNotTheSameAsEmptyNamespace) { EXPECT_GE(tree.indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()), 0); } +} // namespace xml } // namespace aapt diff --git a/tools/aapt2/data/lib/AndroidManifest.xml b/tools/aapt2/data/lib/AndroidManifest.xml index c1612e5..08b468e 100644 --- a/tools/aapt2/data/lib/AndroidManifest.xml +++ b/tools/aapt2/data/lib/AndroidManifest.xml @@ -1,3 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android.appcompat"/> + package="android.appcompat"> + + <uses-feature android:name="bloooop" /> +</manifest> |