| /* |
| * 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 "BigBuffer.h" |
| #include "Logger.h" |
| #include "Maybe.h" |
| #include "Resolver.h" |
| #include "Resource.h" |
| #include "ResourceParser.h" |
| #include "ResourceValues.h" |
| #include "SdkConstants.h" |
| #include "Source.h" |
| #include "StringPool.h" |
| #include "Util.h" |
| #include "XmlFlattener.h" |
| |
| #include <androidfw/ResourceTypes.h> |
| #include <limits> |
| #include <map> |
| #include <string> |
| #include <vector> |
| |
| namespace aapt { |
| |
| 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) { |
| } |
| |
| 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; |
| |
| 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; |
| } |
| |
| 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); |
| } |
| } |
| |
| void visit(String& string, ValueVisitorArgs&) override { |
| mOutValue->dataType = android::Res_value::TYPE_STRING; |
| mStringRefs->emplace_back( |
| mRawValue, |
| reinterpret_cast<android::ResStringPool_ref*>(mOutValue->data)); |
| } |
| |
| void visitItem(Item& item, ValueVisitorArgs&) override { |
| item.flatten(*mOutValue); |
| } |
| |
| 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; |
| }; |
| |
| struct XmlAttribute { |
| uint32_t resourceId; |
| const XmlPullParser::Attribute* xmlAttr; |
| const Attribute* attr; |
| StringPool::Ref nameRef; |
| }; |
| |
| static bool lessAttributeId(const XmlAttribute& a, uint32_t id) { |
| return a.resourceId < id; |
| } |
| |
| XmlFlattener::XmlFlattener(const std::shared_ptr<ResourceTable>& table, |
| const std::shared_ptr<IResolver>& resolver) : |
| mTable(table), mResolver(resolver) { |
| } |
| |
| /** |
| * 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; |
| |
| size_t smallestStrippedAttributeSdk = std::numeric_limits<size_t>::max(); |
| |
| // 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; |
| |
| // 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 }; |
| |
| // 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; |
| |
| // 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; |
| } |
| |
| node->header.headerSize = sizeof(*node); |
| node->lineNumber = parser->getLineNumber(); |
| node->comment.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); |
| |
| out.align4(); |
| node->header.size = out.size() - startIndex; |
| break; |
| } |
| |
| 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>()); |
| } |
| |
| |
| 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 }); |
| } |
| |
| // 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 }); |
| } |
| |
| if (error) { |
| break; |
| } |
| |
| // 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; |
| } |
| |
| StringPool::Ref rawValueRef = pool.makeRef(entry.xmlAttr->value, lowPriority); |
| |
| stringRefs.emplace_back(entry.nameRef, &attr->name); |
| |
| if (options.keepRawValues) { |
| stringRefs.emplace_back(rawValueRef, &attr->rawValue); |
| } else { |
| attr->rawValue.index = -1; |
| } |
| |
| // 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); |
| } |
| |
| 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)); |
| } |
| 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; |
| } |
| |
| case XmlPullParser::Event::kText: { |
| StringPiece16 text = util::trimWhitespace(parser->getText()); |
| if (text.empty()) { |
| break; |
| } |
| |
| 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; |
| |
| android::ResXMLTree_cdataExt* elem = out.nextBlock<android::ResXMLTree_cdataExt>(); |
| stringRefs.emplace_back(pool.makeRef(text, lowPriority), &elem->data); |
| |
| out.align4(); |
| node->header.size = out.size() - startIndex; |
| break; |
| } |
| |
| default: |
| break; |
| } |
| |
| } |
| out.align4(); |
| |
| if (error) { |
| return {}; |
| } |
| |
| if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { |
| logger.error(parser->getLineNumber()) |
| << parser->getLastError() |
| << std::endl; |
| return {}; |
| } |
| |
| // Merge the package pools into the main pool. |
| for (auto& packagePoolEntry : packagePools) { |
| pool.merge(std::move(packagePoolEntry.second)); |
| } |
| |
| // Sort 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) { |
| refEntry.second->index = refEntry.first.getIndex(); |
| } |
| |
| // Write the XML header. |
| const size_t beforeXmlTreeIndex = outBuffer->size(); |
| android::ResXMLTree_header* header = outBuffer->nextBlock<android::ResXMLTree_header>(); |
| header->header.type = android::RES_XML_TYPE; |
| header->header.headerSize = sizeof(*header); |
| |
| // Flatten the StringPool. |
| 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) { |
| ResourceId id { str->context.priority }; |
| if (!id.isValid()) { |
| // When we see the first non-resource ID, |
| // we're done. |
| break; |
| } |
| |
| uint32_t* flatId = outBuffer->nextBlock<uint32_t>(); |
| *flatId = id.id; |
| } |
| resIdMapChunk->size = outBuffer->size() - beforeResIdMapIndex; |
| |
| // Move the temporary BigBuffer into outBuffer. |
| outBuffer->appendBuffer(std::move(out)); |
| |
| header->header.size = outBuffer->size() - beforeXmlTreeIndex; |
| |
| if (smallestStrippedAttributeSdk == std::numeric_limits<size_t>::max()) { |
| // Nothing was stripped |
| return 0u; |
| } |
| return smallestStrippedAttributeSdk; |
| } |
| |
| } // namespace aapt |