diff options
Diffstat (limited to 'tools/aapt2')
86 files changed, 16143 insertions, 0 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk new file mode 100644 index 0000000..e61fd29 --- /dev/null +++ b/tools/aapt2/Android.mk @@ -0,0 +1,133 @@ +# +# 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. +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)),) + +# ========================================================== +# Setup some common variables for the different build +# targets here. +# ========================================================== +LOCAL_PATH:= $(call my-dir) + +main := Main.cpp +sources := \ + BigBuffer.cpp \ + BinaryResourceParser.cpp \ + ConfigDescription.cpp \ + Files.cpp \ + JavaClassGenerator.cpp \ + Linker.cpp \ + Locale.cpp \ + Logger.cpp \ + ManifestParser.cpp \ + ManifestValidator.cpp \ + ResChunkPullParser.cpp \ + Resolver.cpp \ + Resource.cpp \ + ResourceParser.cpp \ + ResourceTable.cpp \ + ResourceValues.cpp \ + SdkConstants.cpp \ + StringPool.cpp \ + TableFlattener.cpp \ + Util.cpp \ + ScopedXmlPullParser.cpp \ + SourceXmlPullParser.cpp \ + XliffXmlPullParser.cpp \ + XmlFlattener.cpp + +testSources := \ + BigBuffer_test.cpp \ + Compat_test.cpp \ + ConfigDescription_test.cpp \ + JavaClassGenerator_test.cpp \ + Linker_test.cpp \ + Locale_test.cpp \ + ManifestParser_test.cpp \ + Maybe_test.cpp \ + ResourceParser_test.cpp \ + Resource_test.cpp \ + ResourceTable_test.cpp \ + ScopedXmlPullParser_test.cpp \ + StringPiece_test.cpp \ + StringPool_test.cpp \ + Util_test.cpp \ + XliffXmlPullParser_test.cpp \ + XmlFlattener_test.cpp + +cIncludes := + +hostLdLibs := -lz +hostStaticLibs := \ + libandroidfw \ + libutils \ + liblog \ + libcutils \ + libexpat \ + libziparchive-host + +cFlags := -Wall -Werror -Wno-unused-parameter -UNDEBUG +cppFlags := -std=c++11 -Wno-missing-field-initializers + +# ========================================================== +# Build the host static library: libaapt2 +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libaapt2 + +LOCAL_SRC_FILES := $(sources) +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_STATIC_LIBRARY) + + +# ========================================================== +# Build the host tests: libaapt2_tests +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libaapt2_tests +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(testSources) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libaapt2 $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_NATIVE_TEST) + +# ========================================================== +# Build the host executable: aapt2 +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := aapt2 + +LOCAL_SRC_FILES := $(main) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libaapt2 $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) +LOCAL_CPPFLAGS += $(cppFlags) + +include $(BUILD_HOST_EXECUTABLE) + +endif # No TARGET_BUILD_APPS or TARGET_BUILD_PDK diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h new file mode 100644 index 0000000..30047f7 --- /dev/null +++ b/tools/aapt2/AppInfo.h @@ -0,0 +1,37 @@ +/* + * 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_APP_INFO_H +#define AAPT_APP_INFO_H + +#include <string> + +namespace aapt { + +/** + * Holds basic information about the app being built. Most of this information + * will come from the app's AndroidManifest. + */ +struct AppInfo { + /** + * App's package name. + */ + std::u16string package; +}; + +} // namespace aapt + +#endif // AAPT_APP_INFO_H diff --git a/tools/aapt2/BigBuffer.cpp b/tools/aapt2/BigBuffer.cpp new file mode 100644 index 0000000..8f57172 --- /dev/null +++ b/tools/aapt2/BigBuffer.cpp @@ -0,0 +1,52 @@ +/* + * 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 <algorithm> +#include <memory> +#include <vector> + +namespace aapt { + +void* BigBuffer::nextBlockImpl(size_t size) { + if (!mBlocks.empty()) { + Block& block = mBlocks.back(); + if (block.mBlockSize - block.size >= size) { + void* outBuffer = block.buffer.get() + block.size; + block.size += size; + mSize += size; + return outBuffer; + } + } + + const size_t actualSize = std::max(mBlockSize, size); + + Block block = {}; + + // Zero-allocate the block's buffer. + block.buffer = std::unique_ptr<uint8_t[]>(new uint8_t[actualSize]()); + assert(block.buffer); + + block.size = size; + block.mBlockSize = actualSize; + + mBlocks.push_back(std::move(block)); + mSize += size; + return mBlocks.back().buffer.get(); +} + +} // namespace aapt diff --git a/tools/aapt2/BigBuffer.h b/tools/aapt2/BigBuffer.h new file mode 100644 index 0000000..025142b --- /dev/null +++ b/tools/aapt2/BigBuffer.h @@ -0,0 +1,158 @@ +/* + * 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_BIG_BUFFER_H +#define AAPT_BIG_BUFFER_H + +#include <cstring> +#include <memory> +#include <vector> + +namespace aapt { + +/** + * Inspired by protobuf's ZeroCopyOutputStream, offers blocks of memory + * in which to write without knowing the full size of the entire payload. + * This is essentially a list of memory blocks. As one fills up, another + * block is allocated and appended to the end of the list. + */ +class BigBuffer { +public: + /** + * A contiguous block of allocated memory. + */ + struct Block { + /** + * Pointer to the memory. + */ + std::unique_ptr<uint8_t[]> buffer; + + /** + * Size of memory that is currently occupied. The actual + * allocation may be larger. + */ + size_t size; + + private: + friend class BigBuffer; + + /** + * The size of the memory block allocation. + */ + size_t mBlockSize; + }; + + typedef std::vector<Block>::const_iterator const_iterator; + + /** + * Create a BigBuffer with block allocation sizes + * of blockSize. + */ + BigBuffer(size_t blockSize); + + BigBuffer(const BigBuffer&) = delete; // No copying. + + BigBuffer(BigBuffer&& rhs); + + /** + * Number of occupied bytes in all the allocated blocks. + */ + size_t size() const; + + /** + * Returns a pointer to an array of T, where T is + * a POD type. The elements are zero-initialized. + */ + template <typename T> + T* nextBlock(size_t count = 1); + + /** + * Moves the specified BigBuffer into this one. When this method + * returns, buffer is empty. + */ + void appendBuffer(BigBuffer&& buffer); + + /** + * Pads the block with 'bytes' bytes of zero values. + */ + void pad(size_t bytes); + + /** + * Pads the block so that it aligns on a 4 byte boundary. + */ + void align4(); + + const_iterator begin() const; + const_iterator end() const; + +private: + /** + * Returns a pointer to a buffer of the requested size. + * The buffer is zero-initialized. + */ + void* nextBlockImpl(size_t size); + + size_t mBlockSize; + size_t mSize; + std::vector<Block> mBlocks; +}; + +inline BigBuffer::BigBuffer(size_t blockSize) : mBlockSize(blockSize), mSize(0) { +} + +inline BigBuffer::BigBuffer(BigBuffer&& rhs) : + mBlockSize(rhs.mBlockSize), mSize(rhs.mSize), mBlocks(std::move(rhs.mBlocks)) { +} + +inline size_t BigBuffer::size() const { + return mSize; +} + +template <typename T> +inline T* BigBuffer::nextBlock(size_t count) { + assert(count != 0); + return reinterpret_cast<T*>(nextBlockImpl(sizeof(T) * count)); +} + +inline void BigBuffer::appendBuffer(BigBuffer&& buffer) { + std::move(buffer.mBlocks.begin(), buffer.mBlocks.end(), std::back_inserter(mBlocks)); + mSize += buffer.mSize; + buffer.mBlocks.clear(); + buffer.mSize = 0; +} + +inline void BigBuffer::pad(size_t bytes) { + nextBlock<char>(bytes); +} + +inline void BigBuffer::align4() { + const size_t unaligned = mSize % 4; + if (unaligned != 0) { + pad(4 - unaligned); + } +} + +inline BigBuffer::const_iterator BigBuffer::begin() const { + return mBlocks.begin(); +} + +inline BigBuffer::const_iterator BigBuffer::end() const { + return mBlocks.end(); +} + +} // namespace aapt + +#endif // AAPT_BIG_BUFFER_H diff --git a/tools/aapt2/BigBuffer_test.cpp b/tools/aapt2/BigBuffer_test.cpp new file mode 100644 index 0000000..01ee8d7 --- /dev/null +++ b/tools/aapt2/BigBuffer_test.cpp @@ -0,0 +1,98 @@ +/* + * 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 <gtest/gtest.h> + +namespace aapt { + +TEST(BigBufferTest, AllocateSingleBlock) { + BigBuffer buffer(4); + + EXPECT_NE(nullptr, buffer.nextBlock<char>(2)); + EXPECT_EQ(2u, buffer.size()); +} + +TEST(BigBufferTest, ReturnSameBlockIfNextAllocationFits) { + BigBuffer buffer(16); + + char* b1 = buffer.nextBlock<char>(8); + EXPECT_NE(nullptr, b1); + + char* b2 = buffer.nextBlock<char>(4); + EXPECT_NE(nullptr, b2); + + EXPECT_EQ(b1 + 8, b2); +} + +TEST(BigBufferTest, AllocateExactSizeBlockIfLargerThanBlockSize) { + BigBuffer buffer(16); + + EXPECT_NE(nullptr, buffer.nextBlock<char>(32)); + EXPECT_EQ(32u, buffer.size()); +} + +TEST(BigBufferTest, AppendAndMoveBlock) { + BigBuffer buffer(16); + + uint32_t* b1 = buffer.nextBlock<uint32_t>(); + ASSERT_NE(nullptr, b1); + *b1 = 33; + + { + BigBuffer buffer2(16); + b1 = buffer2.nextBlock<uint32_t>(); + ASSERT_NE(nullptr, b1); + *b1 = 44; + + buffer.appendBuffer(std::move(buffer2)); + EXPECT_EQ(0u, buffer2.size()); + EXPECT_EQ(buffer2.begin(), buffer2.end()); + } + + EXPECT_EQ(2 * sizeof(uint32_t), buffer.size()); + + auto b = buffer.begin(); + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(33u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_NE(b, buffer.end()); + ASSERT_EQ(sizeof(uint32_t), b->size); + ASSERT_EQ(44u, *reinterpret_cast<uint32_t*>(b->buffer.get())); + ++b; + + ASSERT_EQ(b, buffer.end()); +} + +TEST(BigBufferTest, PadAndAlignProperly) { + BigBuffer buffer(16); + + ASSERT_NE(buffer.nextBlock<char>(2), nullptr); + ASSERT_EQ(2u, buffer.size()); + buffer.pad(2); + ASSERT_EQ(4u, buffer.size()); + buffer.align4(); + ASSERT_EQ(4u, buffer.size()); + buffer.pad(2); + ASSERT_EQ(6u, buffer.size()); + buffer.align4(); + ASSERT_EQ(8u, buffer.size()); +} + +} // namespace aapt diff --git a/tools/aapt2/BinaryResourceParser.cpp b/tools/aapt2/BinaryResourceParser.cpp new file mode 100644 index 0000000..d58f05a --- /dev/null +++ b/tools/aapt2/BinaryResourceParser.cpp @@ -0,0 +1,794 @@ +/* + * 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 "BinaryResourceParser.h" +#include "Logger.h" +#include "ResChunkPullParser.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <androidfw/TypeWrappers.h> +#include <map> +#include <string> + +namespace aapt { + +using namespace android; + +template <typename T> +inline static const T* convertTo(const ResChunk_header* chunk) { + if (chunk->headerSize < sizeof(T)) { + return nullptr; + } + return reinterpret_cast<const T*>(chunk); +} + +inline static const uint8_t* getChunkData(const ResChunk_header& chunk) { + return reinterpret_cast<const uint8_t*>(&chunk) + chunk.headerSize; +} + +inline static size_t getChunkDataLen(const ResChunk_header& chunk) { + return chunk.size - chunk.headerSize; +} + +/* + * Visitor that converts a reference's resource ID to a resource name, + * given a mapping from resource ID to resource name. + */ +struct ReferenceIdToNameVisitor : ValueVisitor { + ReferenceIdToNameVisitor(const std::map<ResourceId, ResourceName>& cache) : mCache(cache) { + } + + void visit(Reference& reference, ValueVisitorArgs&) override { + idToName(reference); + } + + void visit(Attribute& attr, ValueVisitorArgs&) override { + for (auto& entry : attr.symbols) { + idToName(entry.symbol); + } + } + + void visit(Style& style, ValueVisitorArgs&) override { + if (style.parent.id.isValid()) { + idToName(style.parent); + } + + for (auto& entry : style.entries) { + idToName(entry.key); + entry.value->accept(*this, {}); + } + } + + void visit(Styleable& styleable, ValueVisitorArgs&) override { + for (auto& attr : styleable.entries) { + idToName(attr); + } + } + + void visit(Array& array, ValueVisitorArgs&) override { + for (auto& item : array.items) { + item->accept(*this, {}); + } + } + + void visit(Plural& plural, ValueVisitorArgs&) override { + for (auto& item : plural.values) { + if (item) { + item->accept(*this, {}); + } + } + } + +private: + void idToName(Reference& reference) { + if (!reference.id.isValid()) { + return; + } + + auto cacheIter = mCache.find(reference.id); + if (cacheIter == std::end(mCache)) { + Logger::note() << "failed to find " << reference.id << std::endl; + } else { + reference.name = cacheIter->second; + reference.id = 0; + } + } + + const std::map<ResourceId, ResourceName>& mCache; +}; + + +BinaryResourceParser::BinaryResourceParser(std::shared_ptr<ResourceTable> table, + const Source& source, + const void* data, + size_t len) : + mTable(table), mSource(source), mData(data), mDataLen(len) { +} + +bool BinaryResourceParser::parse() { + ResChunkPullParser parser(mData, mDataLen); + + bool error = false; + while(ResChunkPullParser::isGoodEvent(parser.next())) { + if (parser.getChunk()->type != android::RES_TABLE_TYPE) { + Logger::warn(mSource) + << "unknown chunk of type '" + << parser.getChunk()->type + << "'." + << std::endl; + continue; + } + + error |= !parseTable(parser.getChunk()); + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad document: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + return !error; +} + +bool BinaryResourceParser::getSymbol(const void* data, ResourceNameRef* outSymbol) { + if (!mSymbolEntries || mSymbolEntryCount == 0) { + return false; + } + + // We only support 32 bit offsets right now. + const ptrdiff_t offset = reinterpret_cast<uintptr_t>(data) - + reinterpret_cast<uintptr_t>(mData); + if (offset > std::numeric_limits<uint32_t>::max()) { + return false; + } + + for (size_t i = 0; i < mSymbolEntryCount; i++) { + if (mSymbolEntries[i].offset == offset) { + // This offset is a symbol! + const StringPiece16 str = util::getString(mSymbolPool, + mSymbolEntries[i].stringIndex); + StringPiece16 typeStr; + ResourceParser::extractResourceName(str, &outSymbol->package, &typeStr, + &outSymbol->entry); + const ResourceType* type = parseResourceType(typeStr); + if (!type) { + return false; + } + outSymbol->type = *type; + + // Since we scan the symbol table in order, we can start looking for the + // next symbol from this point. + mSymbolEntryCount -= i + 1; + mSymbolEntries += i + 1; + return true; + } + } + return false; +} + +bool BinaryResourceParser::parseSymbolTable(const ResChunk_header* chunk) { + const SymbolTable_header* symbolTableHeader = convertTo<SymbolTable_header>(chunk); + if (!symbolTableHeader) { + Logger::error(mSource) + << "could not parse chunk as SymbolTable_header." + << std::endl; + return false; + } + + const size_t entrySizeBytes = symbolTableHeader->count * sizeof(SymbolTable_entry); + if (entrySizeBytes > getChunkDataLen(symbolTableHeader->header)) { + Logger::error(mSource) + << "entries extend beyond chunk." + << std::endl; + return false; + } + + mSymbolEntries = reinterpret_cast<const SymbolTable_entry*>( + getChunkData(symbolTableHeader->header)); + mSymbolEntryCount = symbolTableHeader->count; + + ResChunkPullParser parser(getChunkData(symbolTableHeader->header) + entrySizeBytes, + getChunkDataLen(symbolTableHeader->header) - entrySizeBytes); + if (!ResChunkPullParser::isGoodEvent(parser.next())) { + Logger::error(mSource) + << "failed to parse chunk: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + + if (parser.getChunk()->type != android::RES_STRING_POOL_TYPE) { + Logger::error(mSource) + << "expected Symbol string pool." + << std::endl; + return false; + } + + if (mSymbolPool.setTo(parser.getChunk(), parser.getChunk()->size) != android::NO_ERROR) { + Logger::error(mSource) + << "failed to parse symbol string pool with code: " + << mSymbolPool.getError() + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parseTable(const ResChunk_header* chunk) { + const ResTable_header* tableHeader = convertTo<ResTable_header>(chunk); + if (!tableHeader) { + Logger::error(mSource) + << "could not parse chunk as ResTable_header." + << std::endl; + return false; + } + + ResChunkPullParser parser(getChunkData(tableHeader->header), + getChunkDataLen(tableHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (parser.getChunk()->type) { + case android::RES_STRING_POOL_TYPE: + if (mValuePool.getError() == android::NO_INIT) { + if (mValuePool.setTo(parser.getChunk(), parser.getChunk()->size) != + android::NO_ERROR) { + Logger::error(mSource) + << "failed to parse value string pool with code: " + << mValuePool.getError() + << "." + << std::endl; + return false; + } + + // Reserve some space for the strings we are going to add. + mTable->getValueStringPool().hintWillAdd( + mValuePool.size(), mValuePool.styleCount()); + } else { + Logger::warn(mSource) + << "unexpected string pool." + << std::endl; + } + break; + + case RES_TABLE_SYMBOL_TABLE_TYPE: + if (!parseSymbolTable(parser.getChunk())) { + return false; + } + break; + + case RES_TABLE_SOURCE_POOL_TYPE: { + if (mSourcePool.setTo(getChunkData(*parser.getChunk()), + getChunkDataLen(*parser.getChunk())) != android::NO_ERROR) { + Logger::error(mSource) + << "failed to parse source pool with code: " + << mSourcePool.getError() + << "." + << std::endl; + return false; + } + break; + } + + case android::RES_TABLE_PACKAGE_TYPE: + if (!parsePackage(parser.getChunk())) { + return false; + } + break; + + default: + Logger::warn(mSource) + << "unexpected chunk of type " + << parser.getChunk()->type + << "." + << std::endl; + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad resource table: " << parser.getLastError() + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parsePackage(const ResChunk_header* chunk) { + if (mValuePool.getError() != android::NO_ERROR) { + Logger::error(mSource) + << "no value string pool for ResTable." + << std::endl; + return false; + } + + const ResTable_package* packageHeader = convertTo<ResTable_package>(chunk); + if (!packageHeader) { + Logger::error(mSource) + << "could not parse chunk as ResTable_header." + << std::endl; + return false; + } + + if (mTable->getPackageId() == ResourceTable::kUnsetPackageId) { + // This is the first time the table has it's package ID set. + mTable->setPackageId(packageHeader->id); + } else if (mTable->getPackageId() != packageHeader->id) { + Logger::error(mSource) + << "ResTable_package has package ID " + << std::hex << packageHeader->id << std::dec + << " but ResourceTable has package ID " + << std::hex << mTable->getPackageId() << std::dec + << std::endl; + return false; + } + + size_t len = strnlen16(reinterpret_cast<const char16_t*>(packageHeader->name), + sizeof(packageHeader->name) / sizeof(packageHeader->name[0])); + mTable->setPackage(StringPiece16(reinterpret_cast<const char16_t*>(packageHeader->name), len)); + + ResChunkPullParser parser(getChunkData(packageHeader->header), + getChunkDataLen(packageHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (parser.getChunk()->type) { + case android::RES_STRING_POOL_TYPE: + if (mTypePool.getError() == android::NO_INIT) { + if (mTypePool.setTo(parser.getChunk(), parser.getChunk()->size) != + android::NO_ERROR) { + Logger::error(mSource) + << "failed to parse type string pool with code " + << mTypePool.getError() + << "." + << std::endl; + return false; + } + } else if (mKeyPool.getError() == android::NO_INIT) { + if (mKeyPool.setTo(parser.getChunk(), parser.getChunk()->size) != + android::NO_ERROR) { + Logger::error(mSource) + << "failed to parse key string pool with code " + << mKeyPool.getError() + << "." + << std::endl; + return false; + } + } else { + Logger::warn(mSource) + << "unexpected string pool." + << std::endl; + } + break; + + case android::RES_TABLE_TYPE_SPEC_TYPE: + if (!parseTypeSpec(parser.getChunk())) { + return false; + } + break; + + case android::RES_TABLE_TYPE_TYPE: + if (!parseType(parser.getChunk())) { + return false; + } + break; + + default: + Logger::warn(mSource) + << "unexpected chunk of type " + << parser.getChunk()->type + << "." + << std::endl; + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + Logger::error(mSource) + << "bad package: " + << parser.getLastError() + << "." + << std::endl; + return false; + } + + // Now go through the table and change resource ID references to + // symbolic references. + + ReferenceIdToNameVisitor visitor(mIdIndex); + for (auto& type : *mTable) { + for (auto& entry : type->entries) { + for (auto& configValue : entry->values) { + configValue.value->accept(visitor, {}); + } + } + } + return true; +} + +bool BinaryResourceParser::parseTypeSpec(const ResChunk_header* chunk) { + if (mTypePool.getError() != android::NO_ERROR) { + Logger::error(mSource) + << "no type string pool available for ResTable_typeSpec." + << std::endl; + return false; + } + + const ResTable_typeSpec* typeSpec = convertTo<ResTable_typeSpec>(chunk); + if (!typeSpec) { + Logger::error(mSource) + << "could not parse chunk as ResTable_typeSpec." + << std::endl; + return false; + } + + if (typeSpec->id == 0) { + Logger::error(mSource) + << "ResTable_typeSpec has invalid id: " + << typeSpec->id + << "." + << std::endl; + return false; + } + return true; +} + +bool BinaryResourceParser::parseType(const ResChunk_header* chunk) { + if (mTypePool.getError() != android::NO_ERROR) { + Logger::error(mSource) + << "no type string pool available for ResTable_typeSpec." + << std::endl; + return false; + } + + if (mKeyPool.getError() != android::NO_ERROR) { + Logger::error(mSource) + << "no key string pool available for ResTable_type." + << std::endl; + return false; + } + + const ResTable_type* type = convertTo<ResTable_type>(chunk); + if (!type) { + Logger::error(mSource) + << "could not parse chunk as ResTable_type." + << std::endl; + return false; + } + + if (type->id == 0) { + Logger::error(mSource) + << "ResTable_type has invalid id: " + << type->id + << "." + << std::endl; + return false; + } + + const ConfigDescription config(type->config); + const StringPiece16 typeName = util::getString(mTypePool, type->id - 1); + + const ResourceType* parsedType = parseResourceType(typeName); + if (!parsedType) { + Logger::error(mSource) + << "invalid type name '" + << typeName + << "' for type with ID " + << uint32_t(type->id) + << "." << std::endl; + return false; + } + + android::TypeVariant tv(type); + for (auto it = tv.beginEntries(); it != tv.endEntries(); ++it) { + if (!*it) { + continue; + } + + const ResTable_entry* entry = *it; + const ResourceName name = { + mTable->getPackage(), + *parsedType, + util::getString(mKeyPool, entry->key.index).toString() + }; + + const ResourceId resId = { mTable->getPackageId(), type->id, it.index() }; + + std::unique_ptr<Value> resourceValue; + const ResTable_entry_source* sourceBlock = nullptr; + if (entry->flags & ResTable_entry::FLAG_COMPLEX) { + const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry); + if (mapEntry->size - sizeof(*mapEntry) == sizeof(*sourceBlock)) { + const uint8_t* data = reinterpret_cast<const uint8_t*>(mapEntry); + data += mapEntry->size - sizeof(*sourceBlock); + sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); + } + + // TODO(adamlesinski): Check that the entry count is valid. + resourceValue = parseMapEntry(name, config, mapEntry); + } else { + if (entry->size - sizeof(*entry) == sizeof(*sourceBlock)) { + const uint8_t* data = reinterpret_cast<const uint8_t*>(entry); + data += entry->size - sizeof(*sourceBlock); + sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); + } + + const Res_value* value = reinterpret_cast<const Res_value*>( + reinterpret_cast<const uint8_t*>(entry) + entry->size); + resourceValue = parseValue(name, config, value, entry->flags); + } + + if (!resourceValue) { + // TODO(adamlesinski): For now this is ok, but it really shouldn't be. + continue; + } + + SourceLine source = mSource.line(0); + if (sourceBlock) { + size_t len; + const char* str = mSourcePool.string8At(sourceBlock->pathIndex, &len); + if (str) { + source.path.assign(str, len); + } + source.line = sourceBlock->line; + } + + if (!mTable->addResource(name, config, source, std::move(resourceValue))) { + return false; + } + + if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0) { + if (!mTable->markPublic(name, resId, mSource.line(0))) { + return false; + } + } + + // Add this resource name->id mapping to the index so + // that we can resolve all ID references to name references. + auto cacheIter = mIdIndex.find(resId); + if (cacheIter == mIdIndex.end()) { + mIdIndex.insert({ resId, name }); + } + } + return true; +} + +std::unique_ptr<Item> BinaryResourceParser::parseValue(const ResourceNameRef& name, + const ConfigDescription& config, + const Res_value* value, + uint16_t flags) { + if (value->dataType == Res_value::TYPE_STRING) { + StringPiece16 str = util::getString(mValuePool, value->data); + + const ResStringPool_span* spans = mValuePool.styleAt(value->data); + if (spans != nullptr) { + StyleString styleStr = { str.toString() }; + while (spans->name.index != ResStringPool_span::END) { + styleStr.spans.push_back(Span{ + util::getString(mValuePool, spans->name.index).toString(), + spans->firstChar, + spans->lastChar + }); + spans++; + } + return util::make_unique<StyledString>( + mTable->getValueStringPool().makeRef( + styleStr, StringPool::Context{1, config})); + } else { + // There are no styles associated with this string, so treat it as + // a simple string. + return util::make_unique<String>( + mTable->getValueStringPool().makeRef( + str, StringPool::Context{1, config})); + } + } + + if (value->dataType == Res_value::TYPE_REFERENCE || + value->dataType == Res_value::TYPE_ATTRIBUTE) { + const Reference::Type type = (value->dataType == Res_value::TYPE_REFERENCE) ? + Reference::Type::kResource : Reference::Type::kAttribute; + + if (value->data != 0) { + // This is a normal reference. + return util::make_unique<Reference>(value->data, type); + } + + // This reference has an invalid ID. Check if it is an unresolved symbol. + ResourceNameRef symbol; + if (getSymbol(&value->data, &symbol)) { + return util::make_unique<Reference>(symbol, type); + } + + // This is not an unresolved symbol, so it must be the magic @null reference. + Res_value nullType = {}; + nullType.dataType = Res_value::TYPE_NULL; + nullType.data = Res_value::DATA_NULL_UNDEFINED; + return util::make_unique<BinaryPrimitive>(nullType); + } + + if (value->dataType == ExtendedTypes::TYPE_SENTINEL) { + return util::make_unique<Sentinel>(); + } + + if (value->dataType == ExtendedTypes::TYPE_RAW_STRING) { + return util::make_unique<RawString>( + mTable->getValueStringPool().makeRef(util::getString(mValuePool, value->data), + StringPool::Context{ 1, config })); + } + + if (name.type == ResourceType::kId || + (value->dataType == Res_value::TYPE_NULL && + value->data == Res_value::DATA_NULL_UNDEFINED && + (flags & ResTable_entry::FLAG_WEAK) != 0)) { + return util::make_unique<Id>(); + } + + // Treat this as a raw binary primitive. + return util::make_unique<BinaryPrimitive>(*value); +} + +std::unique_ptr<Value> BinaryResourceParser::parseMapEntry(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + switch (name.type) { + case ResourceType::kStyle: + return parseStyle(name, config, map); + case ResourceType::kAttr: + return parseAttr(name, config, map); + case ResourceType::kArray: + return parseArray(name, config, map); + case ResourceType::kStyleable: + return parseStyleable(name, config, map); + case ResourceType::kPlurals: + return parsePlural(name, config, map); + default: + break; + } + return {}; +} + +std::unique_ptr<Style> BinaryResourceParser::parseStyle(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + if (map->parent.ident == 0) { + // The parent is either not set or it is an unresolved symbol. + // Check to see if it is a symbol. + ResourceNameRef symbol; + if (getSymbol(&map->parent.ident, &symbol)) { + style->parent.name = symbol.toResourceName(); + } + } else { + // The parent is a regular reference to a resource. + style->parent.id = map->parent.ident; + } + + for (const ResTable_map& mapEntry : map) { + style->entries.emplace_back(); + Style::Entry& styleEntry = style->entries.back(); + + if (mapEntry.name.ident == 0) { + // The map entry's key (attribute) is not set. This must be + // a symbol reference, so resolve it. + ResourceNameRef symbol; + bool result = getSymbol(&mapEntry.name.ident, &symbol); + assert(result); + styleEntry.key.name = symbol.toResourceName(); + } else { + // The map entry's key (attribute) is a regular reference. + styleEntry.key.id = mapEntry.name.ident; + } + + // Parse the attribute's value. + styleEntry.value = parseValue(name, config, &mapEntry.value, 0); + assert(styleEntry.value); + } + return style; +} + +std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + const bool isWeak = (map->flags & ResTable_entry::FLAG_WEAK) != 0; + std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak); + + // First we must discover what type of attribute this is. Find the type mask. + auto typeMaskIter = std::find_if(begin(map), end(map), [](const ResTable_map& entry) -> bool { + return entry.name.ident == ResTable_map::ATTR_TYPE; + }); + + if (typeMaskIter != end(map)) { + attr->typeMask = typeMaskIter->value.data; + } + + if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) { + for (const ResTable_map& mapEntry : map) { + if (Res_INTERNALID(mapEntry.name.ident)) { + continue; + } + + attr->symbols.push_back(Attribute::Symbol{ + Reference(mapEntry.name.ident), + mapEntry.value.data + }); + } + } + + // TODO(adamlesinski): Find min, max, i80n, etc attributes. + return attr; +} + +std::unique_ptr<Array> BinaryResourceParser::parseArray(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Array> array = util::make_unique<Array>(); + for (const ResTable_map& mapEntry : map) { + array->items.push_back(parseValue(name, config, &mapEntry.value, 0)); + } + return array; +} + +std::unique_ptr<Styleable> BinaryResourceParser::parseStyleable(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + for (const ResTable_map& mapEntry : map) { + styleable->entries.emplace_back(mapEntry.name.ident); + } + return styleable; +} + +std::unique_ptr<Plural> BinaryResourceParser::parsePlural(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + for (const ResTable_map& mapEntry : map) { + std::unique_ptr<Item> item = parseValue(name, config, &mapEntry.value, 0); + + switch (mapEntry.name.ident) { + case android::ResTable_map::ATTR_ZERO: + plural->values[Plural::Zero] = std::move(item); + break; + case android::ResTable_map::ATTR_ONE: + plural->values[Plural::One] = std::move(item); + break; + case android::ResTable_map::ATTR_TWO: + plural->values[Plural::Two] = std::move(item); + break; + case android::ResTable_map::ATTR_FEW: + plural->values[Plural::Few] = std::move(item); + break; + case android::ResTable_map::ATTR_MANY: + plural->values[Plural::Many] = std::move(item); + break; + case android::ResTable_map::ATTR_OTHER: + plural->values[Plural::Other] = std::move(item); + break; + } + } + return plural; +} + +} // namespace aapt diff --git a/tools/aapt2/BinaryResourceParser.h b/tools/aapt2/BinaryResourceParser.h new file mode 100644 index 0000000..9268078 --- /dev/null +++ b/tools/aapt2/BinaryResourceParser.h @@ -0,0 +1,153 @@ +/* + * 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_RESOURCE_PARSER_H +#define AAPT_BINARY_RESOURCE_PARSER_H + +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Source.h" + +#include <androidfw/ResourceTypes.h> +#include <string> + +namespace aapt { + +struct SymbolTable_entry; + +/* + * Parses a binary resource table (resources.arsc) and adds the entries + * to a ResourceTable. This is different than the libandroidfw ResTable + * in that it scans the table from top to bottom and doesn't require + * support for random access. It is also able to parse non-runtime + * chunks and types. + */ +class BinaryResourceParser { +public: + /* + * Creates a parser, which will read `len` bytes from `data`, and + * add any resources parsed to `table`. `source` is for logging purposes. + */ + BinaryResourceParser(std::shared_ptr<ResourceTable> table, const Source& source, + const void* data, size_t len); + + BinaryResourceParser(const BinaryResourceParser&) = delete; // No copy. + + /* + * Parses the binary resource table and returns true if successful. + */ + bool parse(); + +private: + // Helper method to retrieve the symbol name for a given table offset specified + // as a pointer. + bool getSymbol(const void* data, ResourceNameRef* outSymbol); + + bool parseTable(const android::ResChunk_header* chunk); + bool parseSymbolTable(const android::ResChunk_header* chunk); + + // Looks up the resource ID in the reference and converts it to a name if available. + bool idToName(Reference* reference); + + bool parsePackage(const android::ResChunk_header* chunk); + bool parseTypeSpec(const android::ResChunk_header* chunk); + bool parseType(const android::ResChunk_header* chunk); + + std::unique_ptr<Item> parseValue(const ResourceNameRef& name, + const ConfigDescription& config, const android::Res_value* value, uint16_t flags); + + std::unique_ptr<Value> parseMapEntry(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Style> parseStyle(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Attribute> parseAttr(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Array> parseArray(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Plural> parsePlural(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::unique_ptr<Styleable> parseStyleable(const ResourceNameRef& name, + const ConfigDescription& config, const android::ResTable_map_entry* map); + + std::shared_ptr<ResourceTable> mTable; + + const Source mSource; + + const void* mData; + const size_t mDataLen; + + // The package name of the resource table. + std::u16string mPackage; + + // The array of symbol entries. Each element points to an offset + // in the table and an index into the symbol table string pool. + const SymbolTable_entry* mSymbolEntries = nullptr; + + // Number of symbol entries. + size_t mSymbolEntryCount = 0; + + // The symbol table string pool. Holds the names of symbols + // referenced in this table but not defined nor resolved to an + // ID. + android::ResStringPool mSymbolPool; + + // The source string pool. Resource entries may have an extra + // field that points into this string pool, which denotes where + // the resource was parsed from originally. + android::ResStringPool mSourcePool; + + // The standard value string pool for resource values. + android::ResStringPool mValuePool; + + // The string pool that holds the names of the types defined + // in this table. + android::ResStringPool mTypePool; + + // The string pool that holds the names of the entries defined + // in this table. + android::ResStringPool mKeyPool; + + // A mapping of resource ID to resource name. When we finish parsing + // we use this to convert all resource IDs to symbolic references. + std::map<ResourceId, ResourceName> mIdIndex; +}; + +} // namespace aapt + +namespace android { + +/** + * Iterator functionality for ResTable_map_entry. + */ + +inline const ResTable_map* begin(const ResTable_map_entry* map) { + return reinterpret_cast<const ResTable_map*>( + reinterpret_cast<const uint8_t*>(map) + map->size); +} + +inline const ResTable_map* end(const ResTable_map_entry* map) { + return reinterpret_cast<const ResTable_map*>( + reinterpret_cast<const uint8_t*>(map) + map->size) + map->count; +} + +} // namespace android + +#endif // AAPT_BINARY_RESOURCE_PARSER_H diff --git a/tools/aapt2/Compat_test.cpp b/tools/aapt2/Compat_test.cpp new file mode 100644 index 0000000..96aee44 --- /dev/null +++ b/tools/aapt2/Compat_test.cpp @@ -0,0 +1,33 @@ +/* + * 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 <gtest/gtest.h> + +namespace aapt { + +TEST(CompatTest, VersionAttributesInStyle) { +} + +TEST(CompatTest, VersionAttributesInXML) { +} + +TEST(CompatTest, DoNotOverrideExistingVersionedFiles) { +} + +TEST(CompatTest, VersionAttributesInStyleWithCorrectPrecedence) { +} + +} // namespace aapt diff --git a/tools/aapt2/ConfigDescription.cpp b/tools/aapt2/ConfigDescription.cpp new file mode 100644 index 0000000..6ddf94a --- /dev/null +++ b/tools/aapt2/ConfigDescription.cpp @@ -0,0 +1,752 @@ +/* + * 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 "ConfigDescription.h" +#include "Locale.h" +#include "SdkConstants.h" +#include "StringPiece.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <string> +#include <vector> + +namespace aapt { + +using android::ResTable_config; + +static const char* kWildcardName = "any"; + +static bool parseMcc(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val != 3) return false; + + int d = atoi(val); + if (d != 0) { + if (out) out->mcc = d; + return true; + } + + return false; +} + +static bool parseMnc(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'n') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val == 0 || c-val > 3) return false; + + if (out) { + out->mnc = atoi(val); + if (out->mnc == 0) { + out->mnc = ACONFIGURATION_MNC_ZERO; + } + } + + return true; +} + +static bool parseLayoutDirection(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_ANY; + return true; + } else if (strcmp(name, "ldltr") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_LTR; + return true; + } else if (strcmp(name, "ldrtl") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_RTL; + return true; + } + + return false; +} + +static bool parseScreenLayoutSize(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_ANY; + return true; + } else if (strcmp(name, "small") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_SMALL; + return true; + } else if (strcmp(name, "normal") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_NORMAL; + return true; + } else if (strcmp(name, "large") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_LARGE; + return true; + } else if (strcmp(name, "xlarge") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_XLARGE; + return true; + } + + return false; +} + +static bool parseScreenLayoutLong(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_ANY; + return true; + } else if (strcmp(name, "long") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_YES; + return true; + } else if (strcmp(name, "notlong") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_NO; + return true; + } + + return false; +} + +static bool parseOrientation(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->orientation = out->ORIENTATION_ANY; + return true; + } else if (strcmp(name, "port") == 0) { + if (out) out->orientation = out->ORIENTATION_PORT; + return true; + } else if (strcmp(name, "land") == 0) { + if (out) out->orientation = out->ORIENTATION_LAND; + return true; + } else if (strcmp(name, "square") == 0) { + if (out) out->orientation = out->ORIENTATION_SQUARE; + return true; + } + + return false; +} + +static bool parseUiModeType(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_ANY; + return true; + } else if (strcmp(name, "desk") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_DESK; + return true; + } else if (strcmp(name, "car") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_CAR; + return true; + } else if (strcmp(name, "television") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_TELEVISION; + return true; + } else if (strcmp(name, "appliance") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_APPLIANCE; + return true; + } else if (strcmp(name, "watch") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_WATCH; + return true; + } + + return false; +} + +static bool parseUiModeNight(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_ANY; + return true; + } else if (strcmp(name, "night") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_YES; + return true; + } else if (strcmp(name, "notnight") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_NO; + return true; + } + + return false; +} + +static bool parseDensity(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->density = ResTable_config::DENSITY_DEFAULT; + return true; + } + + if (strcmp(name, "anydpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_ANY; + return true; + } + + if (strcmp(name, "nodpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_NONE; + return true; + } + + if (strcmp(name, "ldpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_LOW; + return true; + } + + if (strcmp(name, "mdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_MEDIUM; + return true; + } + + if (strcmp(name, "tvdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_TV; + return true; + } + + if (strcmp(name, "hdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_HIGH; + return true; + } + + if (strcmp(name, "xhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XHIGH; + return true; + } + + if (strcmp(name, "xxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXHIGH; + return true; + } + + if (strcmp(name, "xxxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXXHIGH; + return true; + } + + char* c = (char*)name; + while (*c >= '0' && *c <= '9') { + c++; + } + + // check that we have 'dpi' after the last digit. + if (toupper(c[0]) != 'D' || + toupper(c[1]) != 'P' || + toupper(c[2]) != 'I' || + c[3] != 0) { + return false; + } + + // temporarily replace the first letter with \0 to + // use atoi. + char tmp = c[0]; + c[0] = '\0'; + + int d = atoi(name); + c[0] = tmp; + + if (d != 0) { + if (out) out->density = d; + return true; + } + + return false; +} + +static bool parseTouchscreen(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_ANY; + return true; + } else if (strcmp(name, "notouch") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_NOTOUCH; + return true; + } else if (strcmp(name, "stylus") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_STYLUS; + return true; + } else if (strcmp(name, "finger") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_FINGER; + return true; + } + + return false; +} + +static bool parseKeysHidden(const char* name, ResTable_config* out) { + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_ANY; + } else if (strcmp(name, "keysexposed") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_NO; + } else if (strcmp(name, "keyshidden") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_YES; + } else if (strcmp(name, "keyssoft") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_SOFT; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + return true; + } + + return false; +} + +static bool parseKeyboard(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->keyboard = out->KEYBOARD_ANY; + return true; + } else if (strcmp(name, "nokeys") == 0) { + if (out) out->keyboard = out->KEYBOARD_NOKEYS; + return true; + } else if (strcmp(name, "qwerty") == 0) { + if (out) out->keyboard = out->KEYBOARD_QWERTY; + return true; + } else if (strcmp(name, "12key") == 0) { + if (out) out->keyboard = out->KEYBOARD_12KEY; + return true; + } + + return false; +} + +static bool parseNavHidden(const char* name, ResTable_config* out) { + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_ANY; + } else if (strcmp(name, "navexposed") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_NO; + } else if (strcmp(name, "navhidden") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_YES; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + return true; + } + + return false; +} + +static bool parseNavigation(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->navigation = out->NAVIGATION_ANY; + return true; + } else if (strcmp(name, "nonav") == 0) { + if (out) out->navigation = out->NAVIGATION_NONAV; + return true; + } else if (strcmp(name, "dpad") == 0) { + if (out) out->navigation = out->NAVIGATION_DPAD; + return true; + } else if (strcmp(name, "trackball") == 0) { + if (out) out->navigation = out->NAVIGATION_TRACKBALL; + return true; + } else if (strcmp(name, "wheel") == 0) { + if (out) out->navigation = out->NAVIGATION_WHEEL; + return true; + } + + return false; +} + +static bool parseScreenSize(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidth = out->SCREENWIDTH_ANY; + out->screenHeight = out->SCREENHEIGHT_ANY; + } + return true; + } + + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || *x != 'x') return false; + std::string xName(name, x-name); + x++; + + const char* y = x; + while (*y >= '0' && *y <= '9') y++; + if (y == name || *y != 0) return false; + std::string yName(x, y-x); + + uint16_t w = (uint16_t)atoi(xName.c_str()); + uint16_t h = (uint16_t)atoi(yName.c_str()); + if (w < h) { + return false; + } + + if (out) { + out->screenWidth = w; + out->screenHeight = h; + } + + return true; +} + +static bool parseSmallestScreenWidthDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->smallestScreenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 's') return false; + name++; + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->smallestScreenWidthDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseScreenWidthDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->screenWidthDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseScreenHeightDp(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenHeightDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'h') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + std::string xName(name, x-name); + + if (out) { + out->screenHeightDp = (uint16_t)atoi(xName.c_str()); + } + + return true; +} + +static bool parseVersion(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->sdkVersion = out->SDKVERSION_ANY; + out->minorVersion = out->MINORVERSION_ANY; + } + return true; + } + + if (*name != 'v') { + return false; + } + + name++; + const char* s = name; + while (*s >= '0' && *s <= '9') s++; + if (s == name || *s != 0) return false; + std::string sdkName(name, s-name); + + if (out) { + out->sdkVersion = (uint16_t)atoi(sdkName.c_str()); + out->minorVersion = 0; + } + + return true; +} + +bool ConfigDescription::parse(const StringPiece& str, ConfigDescription* out) { + std::vector<std::string> parts = util::splitAndLowercase(str, '-'); + + ConfigDescription config; + ssize_t partsConsumed = 0; + LocaleValue locale; + + const auto partsEnd = parts.end(); + auto partIter = parts.begin(); + + if (str.size() == 0) { + goto success; + } + + if (parseMcc(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseMnc(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + // Locale spans a few '-' separators, so we let it + // control the index. + partsConsumed = locale.initFromParts(partIter, partsEnd); + if (partsConsumed < 0) { + return false; + } else { + locale.writeTo(&config); + partIter += partsConsumed; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseLayoutDirection(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseSmallestScreenWidthDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenWidthDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenHeightDp(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenLayoutSize(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenLayoutLong(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseOrientation(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseUiModeType(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseUiModeNight(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseDensity(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseTouchscreen(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseKeysHidden(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseKeyboard(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseNavHidden(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseNavigation(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseScreenSize(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + if (parseVersion(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + + // Unrecognized. + return false; + +success: + if (out != NULL) { + applyVersionForCompatibility(&config); + *out = config; + } + return true; +} + +void ConfigDescription::applyVersionForCompatibility(ConfigDescription* config) { + uint16_t minSdk = 0; + if (config->density == ResTable_config::DENSITY_ANY) { + minSdk = SDK_LOLLIPOP; + } else if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY + || config->screenWidthDp != ResTable_config::SCREENWIDTH_ANY + || config->screenHeightDp != ResTable_config::SCREENHEIGHT_ANY) { + minSdk = SDK_HONEYCOMB_MR2; + } else if ((config->uiMode & ResTable_config::MASK_UI_MODE_TYPE) + != ResTable_config::UI_MODE_TYPE_ANY + || (config->uiMode & ResTable_config::MASK_UI_MODE_NIGHT) + != ResTable_config::UI_MODE_NIGHT_ANY) { + minSdk = SDK_FROYO; + } else if ((config->screenLayout & ResTable_config::MASK_SCREENSIZE) + != ResTable_config::SCREENSIZE_ANY + || (config->screenLayout & ResTable_config::MASK_SCREENLONG) + != ResTable_config::SCREENLONG_ANY + || config->density != ResTable_config::DENSITY_DEFAULT) { + minSdk = SDK_DONUT; + } + + if (minSdk > config->sdkVersion) { + config->sdkVersion = minSdk; + } +} + +} // namespace aapt diff --git a/tools/aapt2/ConfigDescription.h b/tools/aapt2/ConfigDescription.h new file mode 100644 index 0000000..67b4b75 --- /dev/null +++ b/tools/aapt2/ConfigDescription.h @@ -0,0 +1,129 @@ +/* + * 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_CONFIG_DESCRIPTION_H +#define AAPT_CONFIG_DESCRIPTION_H + +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> +#include <ostream> + +namespace aapt { + +/* + * Subclass of ResTable_config that adds convenient + * initialization and comparison methods. + */ +struct ConfigDescription : public android::ResTable_config { + /* + * Parse a string of the form 'fr-sw600dp-land' and fill in the + * given ResTable_config with resulting configuration parameters. + * + * The resulting configuration has the appropriate sdkVersion defined + * for backwards compatibility. + */ + static bool parse(const StringPiece& str, ConfigDescription* out = nullptr); + + /** + * If the configuration uses an axis that was added after + * the original Android release, make sure the SDK version + * is set accordingly. + */ + static void applyVersionForCompatibility(ConfigDescription* config); + + ConfigDescription(); + ConfigDescription(const android::ResTable_config& o); + ConfigDescription(const ConfigDescription& o); + ConfigDescription(ConfigDescription&& o); + + ConfigDescription& operator=(const android::ResTable_config& o); + ConfigDescription& operator=(const ConfigDescription& o); + ConfigDescription& operator=(ConfigDescription&& o); + + bool operator<(const ConfigDescription& o) const; + bool operator<=(const ConfigDescription& o) const; + bool operator==(const ConfigDescription& o) const; + bool operator!=(const ConfigDescription& o) const; + bool operator>=(const ConfigDescription& o) const; + bool operator>(const ConfigDescription& o) const; +}; + +inline ConfigDescription::ConfigDescription() { + memset(this, 0, sizeof(*this)); + size = sizeof(android::ResTable_config); +} + +inline ConfigDescription::ConfigDescription(const android::ResTable_config& o) { + *static_cast<android::ResTable_config*>(this) = o; + size = sizeof(android::ResTable_config); +} + +inline ConfigDescription::ConfigDescription(const ConfigDescription& o) { + *static_cast<android::ResTable_config*>(this) = o; +} + +inline ConfigDescription::ConfigDescription(ConfigDescription&& o) { + *this = o; +} + +inline ConfigDescription& ConfigDescription::operator=(const android::ResTable_config& o) { + *static_cast<android::ResTable_config*>(this) = o; + size = sizeof(android::ResTable_config); + return *this; +} + +inline ConfigDescription& ConfigDescription::operator=(const ConfigDescription& o) { + *static_cast<android::ResTable_config*>(this) = o; + return *this; +} + +inline ConfigDescription& ConfigDescription::operator=(ConfigDescription&& o) { + *this = o; + return *this; +} + +inline bool ConfigDescription::operator<(const ConfigDescription& o) const { + return compare(o) < 0; +} + +inline bool ConfigDescription::operator<=(const ConfigDescription& o) const { + return compare(o) <= 0; +} + +inline bool ConfigDescription::operator==(const ConfigDescription& o) const { + return compare(o) == 0; +} + +inline bool ConfigDescription::operator!=(const ConfigDescription& o) const { + return compare(o) != 0; +} + +inline bool ConfigDescription::operator>=(const ConfigDescription& o) const { + return compare(o) >= 0; +} + +inline bool ConfigDescription::operator>(const ConfigDescription& o) const { + return compare(o) > 0; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const ConfigDescription& o) { + return out << o.toString().string(); +} + +} // namespace aapt + +#endif // AAPT_CONFIG_DESCRIPTION_H diff --git a/tools/aapt2/ConfigDescription_test.cpp b/tools/aapt2/ConfigDescription_test.cpp new file mode 100644 index 0000000..c57e351 --- /dev/null +++ b/tools/aapt2/ConfigDescription_test.cpp @@ -0,0 +1,82 @@ +/* + * 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 "ConfigDescription.h" +#include "StringPiece.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +static ::testing::AssertionResult TestParse(const StringPiece& input, + ConfigDescription* config = nullptr) { + if (ConfigDescription::parse(input, config)) { + return ::testing::AssertionSuccess() << input << " was successfully parsed"; + } + return ::testing::AssertionFailure() << input << " could not be parsed"; +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersAreOutOfOrder) { + EXPECT_FALSE(TestParse("en-sw600dp-ldrtl")); + EXPECT_FALSE(TestParse("land-en")); + EXPECT_FALSE(TestParse("hdpi-320dpi")); +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersAreNotMatched) { + EXPECT_FALSE(TestParse("en-sw600dp-ILLEGAL")); +} + +TEST(ConfigDescriptionTest, ParseFailWhenQualifiersHaveTrailingDash) { + EXPECT_FALSE(TestParse("en-sw600dp-land-")); +} + +TEST(ConfigDescriptionTest, ParseBasicQualifiers) { + ConfigDescription config; + EXPECT_TRUE(TestParse("", &config)); + EXPECT_EQ(std::string(""), config.toString().string()); + + EXPECT_TRUE(TestParse("fr-land", &config)); + EXPECT_EQ(std::string("fr-land"), config.toString().string()); + + EXPECT_TRUE(TestParse("mcc310-pl-sw720dp-normal-long-port-night-" + "xhdpi-keyssoft-qwerty-navexposed-nonav", &config)); + EXPECT_EQ(std::string("mcc310-pl-sw720dp-normal-long-port-night-" + "xhdpi-keyssoft-qwerty-navexposed-nonav-v13"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseLocales) { + ConfigDescription config; + EXPECT_TRUE(TestParse("en-rUS", &config)); + EXPECT_EQ(std::string("en-rUS"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseQualifierAddedInApi13) { + ConfigDescription config; + EXPECT_TRUE(TestParse("sw600dp", &config)); + EXPECT_EQ(std::string("sw600dp-v13"), config.toString().string()); + + EXPECT_TRUE(TestParse("sw600dp-v8", &config)); + EXPECT_EQ(std::string("sw600dp-v13"), config.toString().string()); +} + +TEST(ConfigDescriptionTest, ParseCarAttribute) { + ConfigDescription config; + EXPECT_TRUE(TestParse("car", &config)); + EXPECT_EQ(android::ResTable_config::UI_MODE_TYPE_CAR, config.uiMode); +} + +} // namespace aapt diff --git a/tools/aapt2/Files.cpp b/tools/aapt2/Files.cpp new file mode 100644 index 0000000..c910c81 --- /dev/null +++ b/tools/aapt2/Files.cpp @@ -0,0 +1,168 @@ +/* + * 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 "Files.h" +#include "Util.h" + +#include <cerrno> +#include <dirent.h> +#include <string> +#include <sys/stat.h> + +namespace aapt { + +FileType getFileType(const StringPiece& path) { + struct stat sb; + if (stat(path.data(), &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) { + return FileType::kNonexistant; + } + return FileType::kUnknown; + } + + if (S_ISREG(sb.st_mode)) { + return FileType::kRegular; + } else if (S_ISDIR(sb.st_mode)) { + return FileType::kDirectory; + } else if (S_ISCHR(sb.st_mode)) { + return FileType::kCharDev; + } else if (S_ISBLK(sb.st_mode)) { + return FileType::kBlockDev; + } else if (S_ISFIFO(sb.st_mode)) { + return FileType::kFifo; + } else if (S_ISLNK(sb.st_mode)) { + return FileType::kSymlink; + } else if (S_ISSOCK(sb.st_mode)) { + return FileType::kSocket; + } else { + return FileType::kUnknown; + } +} + +std::vector<std::string> listFiles(const StringPiece& root) { + DIR* dir = opendir(root.data()); + if (dir == nullptr) { + Logger::error(Source{ root.toString() }) + << "unable to open file: " + << strerror(errno) + << "." + << std::endl; + return {}; + } + + std::vector<std::string> files; + dirent* entry; + while ((entry = readdir(dir))) { + files.emplace_back(entry->d_name); + } + + closedir(dir); + return files; +} + +inline static int mkdirImpl(const StringPiece& path) { +#ifdef HAVE_MS_C_RUNTIME + return _mkdir(path.toString().c_str()); +#else + return mkdir(path.toString().c_str(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); +#endif +} + +bool mkdirs(const StringPiece& path) { + const char* start = path.begin(); + const char* end = path.end(); + for (const char* current = start; current != end; ++current) { + if (*current == sDirSep) { + StringPiece parentPath(start, current - start); + int result = mkdirImpl(parentPath); + if (result < 0 && errno != EEXIST) { + return false; + } + } + } + return mkdirImpl(path) == 0 || errno == EEXIST; +} + +bool FileFilter::setPattern(const StringPiece& pattern) { + mPatternTokens = util::splitAndLowercase(pattern, ':'); + return true; +} + +bool FileFilter::operator()(const std::string& filename, FileType type) const { + if (filename == "." || filename == "..") { + return false; + } + + const char kDir[] = "dir"; + const char kFile[] = "file"; + const size_t filenameLen = filename.length(); + bool chatty = true; + for (const std::string& token : mPatternTokens) { + const char* tokenStr = token.c_str(); + if (*tokenStr == '!') { + chatty = false; + tokenStr++; + } + + if (strncasecmp(tokenStr, kDir, sizeof(kDir)) == 0) { + if (type != FileType::kDirectory) { + continue; + } + tokenStr += sizeof(kDir); + } + + if (strncasecmp(tokenStr, kFile, sizeof(kFile)) == 0) { + if (type != FileType::kRegular) { + continue; + } + tokenStr += sizeof(kFile); + } + + bool ignore = false; + size_t n = strlen(tokenStr); + if (*tokenStr == '*') { + // Math suffix. + tokenStr++; + n--; + if (n <= filenameLen) { + ignore = strncasecmp(tokenStr, filename.c_str() + filenameLen - n, n) == 0; + } + } else if (n > 1 && tokenStr[n - 1] == '*') { + // Match prefix. + ignore = strncasecmp(tokenStr, filename.c_str(), n - 1) == 0; + } else { + ignore = strcasecmp(tokenStr, filename.c_str()) == 0; + } + + if (ignore) { + if (chatty) { + Logger::warn() + << "skipping " << + (type == FileType::kDirectory ? "dir '" : "file '") + << filename + << "' due to ignore pattern '" + << token + << "'." + << std::endl; + } + return false; + } + } + return true; +} + + +} // namespace aapt diff --git a/tools/aapt2/Files.h b/tools/aapt2/Files.h new file mode 100644 index 0000000..e5e196e --- /dev/null +++ b/tools/aapt2/Files.h @@ -0,0 +1,122 @@ +/* + * 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_FILES_H +#define AAPT_FILES_H + +#include "Logger.h" +#include "Source.h" +#include "StringPiece.h" + +#include <string> +#include <vector> + +namespace aapt { + +#ifdef _WIN32 +constexpr const char sDirSep = '\\'; +#else +constexpr const char sDirSep = '/'; +#endif + +enum class FileType { + kUnknown = 0, + kNonexistant, + kRegular, + kDirectory, + kCharDev, + kBlockDev, + kFifo, + kSymlink, + kSocket, +}; + +FileType getFileType(const StringPiece& path); + +/* + * Lists files under the directory `root`. Files are listed + * with just their leaf (filename) names. + */ +std::vector<std::string> listFiles(const StringPiece& root); + +/* + * Appends a path to `base`, separated by the directory separator. + */ +void appendPath(std::string* base, const StringPiece& part); + +/* + * Appends a series of paths to `base`, separated by the + * system directory separator. + */ +template <typename... Ts > +void appendPath(std::string* base, const StringPiece& part, const Ts&... parts); + +/* + * Makes all the directories in `path`. The last element in the path + * is interpreted as a directory. + */ +bool mkdirs(const StringPiece& path); + +/* + * Filter that determines which resource files/directories are + * processed by AAPT. Takes a pattern string supplied by the user. + * Pattern format is specified in the + * FileFilter::setPattern(const std::string&) method. + */ +class FileFilter { +public: + /* + * Patterns syntax: + * - Delimiter is : + * - Entry can start with the flag ! to avoid printing a warning + * about the file being ignored. + * - Entry can have the flag "<dir>" to match only directories + * or <file> to match only files. Default is to match both. + * - Entry can be a simplified glob "<prefix>*" or "*<suffix>" + * where prefix/suffix must have at least 1 character (so that + * we don't match a '*' catch-all pattern.) + * - The special filenames "." and ".." are always ignored. + * - Otherwise the full string is matched. + * - match is not case-sensitive. + */ + bool setPattern(const StringPiece& pattern); + + /** + * Applies the filter, returning true for pass, false for fail. + */ + bool operator()(const std::string& filename, FileType type) const; + +private: + std::vector<std::string> mPatternTokens; +}; + +inline void appendPath(std::string* base, const StringPiece& part) { + assert(base); + *base += sDirSep; + base->append(part.data(), part.size()); +} + +template <typename... Ts > +void appendPath(std::string* base, const StringPiece& part, const Ts&... parts) { + assert(base); + *base += sDirSep; + base->append(part.data(), part.size()); + appendPath(base, parts...); +} + +} // namespace aapt + +#endif // AAPT_FILES_H diff --git a/tools/aapt2/JavaClassGenerator.cpp b/tools/aapt2/JavaClassGenerator.cpp new file mode 100644 index 0000000..7ec2848 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator.cpp @@ -0,0 +1,189 @@ +/* + * 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 "JavaClassGenerator.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" + +#include <ostream> +#include <set> +#include <sstream> +#include <tuple> + +namespace aapt { + +// The number of attributes to emit per line in a Styleable array. +constexpr size_t kAttribsPerLine = 4; + +JavaClassGenerator::JavaClassGenerator(std::shared_ptr<const ResourceTable> table, + Options options) : + mTable(table), mOptions(options) { +} + +static void generateHeader(std::ostream& out, const StringPiece16& package) { + out << "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" + " *\n" + " * This class was automatically generated by the\n" + " * aapt tool from the resource data it found. It\n" + " * should not be modified by hand.\n" + " */\n\n"; + out << "package " << package << ";" + << std::endl + << std::endl; +} + +static const std::set<StringPiece16> sJavaIdentifiers = { + u"abstract", u"assert", u"boolean", u"break", u"byte", + u"case", u"catch", u"char", u"class", u"const", u"continue", + u"default", u"do", u"double", u"else", u"enum", u"extends", + u"final", u"finally", u"float", u"for", u"goto", u"if", + u"implements", u"import", u"instanceof", u"int", u"interface", + u"long", u"native", u"new", u"package", u"private", u"protected", + u"public", u"return", u"short", u"static", u"strictfp", u"super", + u"switch", u"synchronized", u"this", u"throw", u"throws", + u"transient", u"try", u"void", u"volatile", u"while", u"true", + u"false", u"null" +}; + +static bool isValidSymbol(const StringPiece16& symbol) { + return sJavaIdentifiers.find(symbol) == sJavaIdentifiers.end(); +} + +/* + * Java symbols can not contain . or -, but those are valid in a resource name. + * Replace those with '_'. + */ +static std::u16string transform(const StringPiece16& symbol) { + std::u16string output = symbol.toString(); + for (char16_t& c : output) { + if (c == u'.' || c == u'-') { + c = u'_'; + } + } + return output; +} + +bool JavaClassGenerator::generateType(std::ostream& out, const ResourceTableType& type, + size_t packageId) { + const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; + + for (const auto& entry : type.entries) { + ResourceId id = { packageId, type.typeId, entry->entryId }; + assert(id.isValid()); + + if (!isValidSymbol(entry->name)) { + mError = (std::stringstream() + << "invalid symbol name '" + << StringPiece16(entry->name) + << "'").str(); + return false; + } + + out << " " + << "public static" << finalModifier + << " int " << transform(entry->name) << " = " << id << ";" << std::endl; + } + return true; +} + +struct GenArgs : ValueVisitorArgs { + GenArgs(std::ostream& o, const ResourceEntry& e) : out(o), entry(e) { + } + + std::ostream& out; + const ResourceEntry& entry; +}; + +void JavaClassGenerator::visit(const Styleable& styleable, ValueVisitorArgs& a) { + const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; + std::ostream& out = static_cast<GenArgs&>(a).out; + const ResourceEntry& entry = static_cast<GenArgs&>(a).entry; + + // This must be sorted by resource ID. + std::vector<std::pair<ResourceId, StringPiece16>> sortedAttributes; + sortedAttributes.reserve(styleable.entries.size()); + for (const auto& attr : styleable.entries) { + assert(attr.id.isValid() && "no ID set for Styleable entry"); + assert(attr.name.isValid() && "no name set for Styleable entry"); + sortedAttributes.emplace_back(attr.id, attr.name.entry); + } + std::sort(sortedAttributes.begin(), sortedAttributes.end()); + + // First we emit the array containing the IDs of each attribute. + out << " " + << "public static final int[] " << transform(entry.name) << " = {"; + + const size_t attrCount = sortedAttributes.size(); + for (size_t i = 0; i < attrCount; i++) { + if (i % kAttribsPerLine == 0) { + out << std::endl << " "; + } + + out << sortedAttributes[i].first; + if (i != attrCount - 1) { + out << ", "; + } + } + out << std::endl << " };" << std::endl; + + // Now we emit the indices into the array. + for (size_t i = 0; i < attrCount; i++) { + out << " " + << "public static" << finalModifier + << " int " << transform(entry.name) << "_" << transform(sortedAttributes[i].second) + << " = " << i << ";" << std::endl; + } +} + +bool JavaClassGenerator::generate(std::ostream& out) { + const size_t packageId = mTable->getPackageId(); + + generateHeader(out, mTable->getPackage()); + + out << "public final class R {" << std::endl; + + for (const auto& type : *mTable) { + out << " public static final class " << type->type << " {" << std::endl; + bool result; + if (type->type == ResourceType::kStyleable) { + for (const auto& entry : type->entries) { + assert(!entry->values.empty()); + if (!isValidSymbol(entry->name)) { + mError = (std::stringstream() + << "invalid symbol name '" + << StringPiece16(entry->name) + << "'").str(); + return false; + } + entry->values.front().value->accept(*this, GenArgs{ out, *entry }); + } + } else { + result = generateType(out, *type, packageId); + } + + if (!result) { + return false; + } + out << " }" << std::endl; + } + + out << "}" << std::endl; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/JavaClassGenerator.h b/tools/aapt2/JavaClassGenerator.h new file mode 100644 index 0000000..5b8e500 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator.h @@ -0,0 +1,72 @@ +/* + * 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_JAVA_CLASS_GENERATOR_H +#define AAPT_JAVA_CLASS_GENERATOR_H + +#include "ResourceTable.h" +#include "ResourceValues.h" + +#include <ostream> +#include <string> + +namespace aapt { + +/* + * Generates the R.java file for a resource table. + */ +class JavaClassGenerator : ConstValueVisitor { +public: + /* + * A set of options for this JavaClassGenerator. + */ + struct Options { + /* + * Specifies whether to use the 'final' modifier + * on resource entries. Default is true. + */ + bool useFinal = true; + }; + + JavaClassGenerator(std::shared_ptr<const ResourceTable> table, Options options); + + /* + * Writes the R.java file to `out`. Returns true on success. + */ + bool generate(std::ostream& out); + + /* + * ConstValueVisitor implementation. + */ + void visit(const Styleable& styleable, ValueVisitorArgs& args); + + const std::string& getError() const; + +private: + bool generateType(std::ostream& out, const ResourceTableType& type, size_t packageId); + + std::shared_ptr<const ResourceTable> mTable; + Options mOptions; + std::string mError; +}; + +inline const std::string& JavaClassGenerator::getError() const { + return mError; +} + +} // namespace aapt + +#endif // AAPT_JAVA_CLASS_GENERATOR_H diff --git a/tools/aapt2/JavaClassGenerator_test.cpp b/tools/aapt2/JavaClassGenerator_test.cpp new file mode 100644 index 0000000..32050e3 --- /dev/null +++ b/tools/aapt2/JavaClassGenerator_test.cpp @@ -0,0 +1,85 @@ +/* + * 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 "JavaClassGenerator.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +struct JavaClassGeneratorTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + mTable->setPackageId(0x01); + } + + bool addResource(const ResourceNameRef& name, ResourceId id) { + return mTable->addResource(name, id, {}, SourceLine{ "test.xml", 21 }, + util::make_unique<Id>()); + } + + std::shared_ptr<ResourceTable> mTable; +}; + +TEST_F(JavaClassGeneratorTest, FailWhenEntryIsJavaKeyword) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"class" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_FALSE(generator.generate(out)); +} + +TEST_F(JavaClassGeneratorTest, TransformInvalidJavaIdentifierCharacter) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"hey-man" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kAttr, u"cool.attr" }, + ResourceId{ 0x01, 0x01, 0x0000 })); + + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + Reference ref(ResourceName{ u"android", ResourceType::kAttr, u"cool.attr"}); + ref.id = ResourceId{ 0x01, 0x01, 0x0000 }; + styleable->entries.emplace_back(ref); + + ASSERT_TRUE(mTable->addResource(ResourceName{ {}, ResourceType::kStyleable, u"hey.dude" }, + ResourceId{ 0x01, 0x03, 0x0000 }, {}, + SourceLine{ "test.xml", 21 }, std::move(styleable))); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(out)); + std::string output = out.str(); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_man = 0x01020000;")); + + EXPECT_NE(std::string::npos, + output.find("public static final int[] hey_dude = {")); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_dude_cool_attr = 0;")); +} + +} // namespace aapt diff --git a/tools/aapt2/Linker.cpp b/tools/aapt2/Linker.cpp new file mode 100644 index 0000000..a863197 --- /dev/null +++ b/tools/aapt2/Linker.cpp @@ -0,0 +1,282 @@ +/* + * 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 "Linker.h" +#include "Logger.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <array> +#include <iostream> +#include <map> +#include <ostream> +#include <set> +#include <sstream> +#include <tuple> +#include <vector> + +namespace aapt { + +Linker::Args::Args(const ResourceNameRef& r, const SourceLine& s) : referrer(r), source(s) { +} + +Linker::Linker(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver) : + mTable(table), mResolver(resolver), mError(false) { +} + +bool Linker::linkAndValidate() { + std::bitset<256> usedTypeIds; + std::array<std::set<uint16_t>, 256> usedIds; + usedTypeIds.set(0); + + // First build the graph of references. + for (auto& type : *mTable) { + if (type->typeId != ResourceTableType::kUnsetTypeId) { + // The ID for this type has already been set. We + // mark this ID as taken so we don't re-assign it + // later. + usedTypeIds.set(type->typeId); + } + + for (auto& entry : type->entries) { + if (type->typeId != ResourceTableType::kUnsetTypeId && + entry->entryId != ResourceEntry::kUnsetEntryId) { + // The ID for this entry has already been set. We + // mark this ID as taken so we don't re-assign it + // later. + usedIds[type->typeId].insert(entry->entryId); + } + + for (auto& valueConfig : entry->values) { + // Dispatch to the right method of this linker + // based on the value's type. + valueConfig.value->accept(*this, Args{ + ResourceNameRef{ mTable->getPackage(), type->type, entry->name }, + valueConfig.source + }); + } + } + } + + /* + * Assign resource IDs that are available. + */ + size_t nextTypeIndex = 0; + for (auto& type : *mTable) { + if (type->typeId == ResourceTableType::kUnsetTypeId) { + while (nextTypeIndex < usedTypeIds.size() && usedTypeIds[nextTypeIndex]) { + nextTypeIndex++; + } + type->typeId = nextTypeIndex++; + } + + const auto endEntryIter = std::end(usedIds[type->typeId]); + auto nextEntryIter = std::begin(usedIds[type->typeId]); + size_t nextIndex = 0; + for (auto& entry : type->entries) { + if (entry->entryId == ResourceTableType::kUnsetTypeId) { + while (nextEntryIter != endEntryIter && + nextIndex == *nextEntryIter) { + nextIndex++; + ++nextEntryIter; + } + entry->entryId = nextIndex++; + + // Update callers of this resource with the right ID. + auto callersIter = mGraph.find(ResourceNameRef{ + mTable->getPackage(), + type->type, + entry->name + }); + + if (callersIter != std::end(mGraph)) { + for (Node& caller : callersIter->second) { + caller.reference->id = ResourceId(mTable->getPackageId(), + type->typeId, + entry->entryId); + } + } + } + } + } + + return !mError; +} + +const Linker::ResourceNameToSourceMap& Linker::getUnresolvedReferences() const { + return mUnresolvedSymbols; +} + +void Linker::visit(Reference& reference, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + Maybe<ResourceId> result = mResolver->findId(reference.name); + if (!result) { + addUnresolvedSymbol(reference.name, args.source); + return; + } + + const ResourceId& id = result.value(); + if (id.isValid()) { + reference.id = id; + } else { + // We need to update the ID when it is set, so add it + // to the graph. + mGraph[reference.name].push_back(Node{ + args.referrer, + args.source.path, + args.source.line, + &reference + }); + } + + // TODO(adamlesinski): Verify the referencedType is another reference + // or a compatible primitive. +} + +void Linker::processAttributeValue(const ResourceNameRef& name, const SourceLine& source, + const Attribute& attr, std::unique_ptr<Item>& value) { + std::unique_ptr<Item> convertedValue; + visitFunc<RawString>(*value, [&](RawString& str) { + // This is a raw string, so check if it can be converted to anything. + // We can NOT swap value with the converted value in here, since + // we called through the original value. + + auto onCreateReference = [&](const ResourceName& name) { + mTable->addResource(name, ConfigDescription{}, + source, util::make_unique<Id>()); + }; + + convertedValue = ResourceParser::parseItemForAttribute( + *str.value, attr, mResolver->getDefaultPackage(), + onCreateReference); + if (!convertedValue && attr.typeMask & android::ResTable_map::TYPE_STRING) { + // Last effort is to parse as a string. + util::StringBuilder builder; + builder.append(*str.value); + if (builder) { + convertedValue = util::make_unique<String>( + mTable->getValueStringPool().makeRef(builder.str())); + } + } + }); + + if (convertedValue) { + value = std::move(convertedValue); + } + + // Process this new or old value (it can be a reference!). + value->accept(*this, Args{ name, source }); + + // Flatten the value to see what resource type it is. + android::Res_value resValue; + value->flatten(resValue); + + // Always allow references. + const uint32_t typeMask = attr.typeMask | android::ResTable_map::TYPE_REFERENCE; + if (!(typeMask & ResourceParser::androidTypeToAttributeTypeMask(resValue.dataType))) { + Logger::error(source) + << *value + << " is not compatible with attribute " + << attr + << "." + << std::endl; + mError = true; + } +} + +void Linker::visit(Style& style, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + if (style.parent.name.isValid()) { + visit(style.parent, a); + } + + for (Style::Entry& styleEntry : style.entries) { + Maybe<Resolver::Entry> result = mResolver->findAttribute(styleEntry.key.name); + if (!result || !result.value().attr) { + addUnresolvedSymbol(styleEntry.key.name, args.source); + continue; + } + + const Resolver::Entry& entry = result.value(); + if (entry.id.isValid()) { + styleEntry.key.id = entry.id; + } else { + // Create a dependency for the style on this attribute. + mGraph[styleEntry.key.name].push_back(Node{ + args.referrer, + args.source.path, + args.source.line, + &styleEntry.key + }); + } + processAttributeValue(args.referrer, args.source, *entry.attr, styleEntry.value); + } +} + +void Linker::visit(Attribute& attr, ValueVisitorArgs& a) { + static constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_FLAGS; + if (attr.typeMask & kMask) { + for (auto& symbol : attr.symbols) { + visit(symbol.symbol, a); + } + } +} + +void Linker::visit(Styleable& styleable, ValueVisitorArgs& a) { + for (auto& attrRef : styleable.entries) { + visit(attrRef, a); + } +} + +void Linker::visit(Sentinel& sentinel, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + addUnresolvedSymbol(args.referrer, args.source); +} + +void Linker::visit(Array& array, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + for (auto& item : array.items) { + item->accept(*this, Args{ args.referrer, args.source }); + } +} + +void Linker::visit(Plural& plural, ValueVisitorArgs& a) { + Args& args = static_cast<Args&>(a); + + for (auto& item : plural.values) { + if (item) { + item->accept(*this, Args{ args.referrer, args.source }); + } + } +} + +void Linker::addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source) { + mUnresolvedSymbols[name.toResourceName()].push_back(source); +} + +::std::ostream& operator<<(::std::ostream& out, const Linker::Node& node) { + return out << node.name << "(" << node.source << ":" << node.line << ")"; +} + +} // namespace aapt diff --git a/tools/aapt2/Linker.h b/tools/aapt2/Linker.h new file mode 100644 index 0000000..9b911b7 --- /dev/null +++ b/tools/aapt2/Linker.h @@ -0,0 +1,128 @@ +/* + * 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_LINKER_H +#define AAPT_LINKER_H + +#include "Resolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Source.h" +#include "StringPiece.h" + +#include <androidfw/AssetManager.h> +#include <map> +#include <memory> +#include <ostream> +#include <set> +#include <vector> + +namespace aapt { + +/** + * The Linker has two jobs. It follows resource references + * and verifies that their targert exists and that their + * types are compatible. The Linker will also assign resource + * IDs and fill in all the dependent references with the newly + * assigned resource IDs. + * + * To do this, the Linker builds a graph of references. This + * can be useful to do other analysis, like building a + * dependency graph of source files. The hope is to be able to + * add functionality that operates on the graph without + * overcomplicating the Linker. + * + * TODO(adamlesinski): Build the graph first then run the separate + * steps over the graph. + */ +class Linker : ValueVisitor { +public: + /** + * Create a Linker for the given resource table with the sources available in + * Resolver. Resolver should contain the ResourceTable as a source too. + */ + Linker(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver); + + Linker(const Linker&) = delete; + + /** + * Entry point to the linker. Assigns resource IDs, follows references, + * and validates types. Returns true if all references to defined values + * are type-compatible. Missing resource references are recorded but do + * not cause this method to fail. + */ + bool linkAndValidate(); + + /** + * Returns any references to resources that were not defined in any of the + * sources. + */ + using ResourceNameToSourceMap = std::map<ResourceName, std::vector<SourceLine>>; + const ResourceNameToSourceMap& getUnresolvedReferences() const; + +private: + struct Args : public ValueVisitorArgs { + Args(const ResourceNameRef& r, const SourceLine& s); + + const ResourceNameRef& referrer; + const SourceLine& source; + }; + + // + // Overrides of ValueVisitor + // + void visit(Reference& reference, ValueVisitorArgs& args) override; + void visit(Attribute& attribute, ValueVisitorArgs& args) override; + void visit(Styleable& styleable, ValueVisitorArgs& args) override; + void visit(Style& style, ValueVisitorArgs& args) override; + void visit(Sentinel& sentinel, ValueVisitorArgs& args) override; + void visit(Array& array, ValueVisitorArgs& args) override; + void visit(Plural& plural, ValueVisitorArgs& args) override; + + void processAttributeValue(const ResourceNameRef& name, const SourceLine& source, + const Attribute& attr, std::unique_ptr<Item>& value); + + void addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source); + + /** + * Node of the resource table graph. + */ + struct Node { + // We use ResourceNameRef and StringPiece, which are safe so long as the ResourceTable + // that defines the data isn't modified. + ResourceNameRef name; + StringPiece source; + size_t line; + + // The reference object that points to name. + Reference* reference; + + bool operator<(const Node& rhs) const; + bool operator==(const Node& rhs) const; + bool operator!=(const Node& rhs) const; + }; + friend ::std::ostream& operator<<(::std::ostream&, const Node&); + + std::shared_ptr<ResourceTable> mTable; + std::shared_ptr<Resolver> mResolver; + std::map<ResourceNameRef, std::vector<Node>> mGraph; + std::map<ResourceName, std::vector<SourceLine>> mUnresolvedSymbols; + bool mError; +}; + +} // namespace aapt + +#endif // AAPT_LINKER_H diff --git a/tools/aapt2/Linker_test.cpp b/tools/aapt2/Linker_test.cpp new file mode 100644 index 0000000..b1e201b --- /dev/null +++ b/tools/aapt2/Linker_test.cpp @@ -0,0 +1,143 @@ +/* + * 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 "Linker.h" +#include "Resolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +struct LinkerTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + mLinker = std::make_shared<Linker>(mTable, std::make_shared<Resolver>( + mTable, std::make_shared<android::AssetManager>())); + + // Create a few attributes for use in the tests. + + addResource(ResourceName{ {}, ResourceType::kAttr, u"integer" }, + util::make_unique<Attribute>(false, android::ResTable_map::TYPE_INTEGER)); + + addResource(ResourceName{ {}, ResourceType::kAttr, u"string" }, + util::make_unique<Attribute>(false, android::ResTable_map::TYPE_STRING)); + + addResource(ResourceName{ {}, ResourceType::kId, u"apple" }, util::make_unique<Id>()); + + addResource(ResourceName{ {}, ResourceType::kId, u"banana" }, util::make_unique<Id>()); + + std::unique_ptr<Attribute> flagAttr = util::make_unique<Attribute>( + false, android::ResTable_map::TYPE_FLAGS); + flagAttr->symbols.push_back(Attribute::Symbol{ + ResourceNameRef{ u"android", ResourceType::kId, u"apple" }, 1 }); + flagAttr->symbols.push_back(Attribute::Symbol{ + ResourceNameRef{ u"android", ResourceType::kId, u"banana" }, 2 }); + addResource(ResourceName{ {}, ResourceType::kAttr, u"flags" }, std::move(flagAttr)); + } + + /* + * Convenience method for adding resources with the default configuration and some + * bogus source line. + */ + bool addResource(const ResourceNameRef& name, std::unique_ptr<Value> value) { + return mTable->addResource(name, {}, SourceLine{ "test.xml", 21 }, std::move(value)); + } + + std::shared_ptr<ResourceTable> mTable; + std::shared_ptr<Linker> mLinker; +}; + +TEST_F(LinkerTest, DoNotInterpretEscapedStringAsReference) { + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kString, u"foo" }, + util::make_unique<String>(mTable->getValueStringPool().makeRef(u"?123")))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); +} + +TEST_F(LinkerTest, EscapeAndConvertRawString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u" 123")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + EXPECT_NE(nullptr, dynamic_cast<BinaryPrimitive*>(result->entries.front().value.get())); +} + +TEST_F(LinkerTest, FailToConvertRawString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"yo what is up?")) + }); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_FALSE(mLinker->linkAndValidate()); +} + +TEST_F(LinkerTest, ConvertRawStringToString) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"string" }, + util::make_unique<RawString>( + mTable->getValueStringPool().makeRef(u" \"this is \\u00fa\".")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + const String* str = dynamic_cast<const String*>(result->entries.front().value.get()); + ASSERT_NE(nullptr, str); + EXPECT_EQ(*str->value, u"this is \u00fa."); +} + +TEST_F(LinkerTest, ConvertRawStringToFlags) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + style->entries.push_back(Style::Entry{ + ResourceNameRef{ u"android", ResourceType::kAttr, u"flags" }, + util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"banana | apple")) + }); + const Style* result = style.get(); + ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, + std::move(style))); + + ASSERT_TRUE(mLinker->linkAndValidate()); + EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); + + const BinaryPrimitive* bin = dynamic_cast<const BinaryPrimitive*>( + result->entries.front().value.get()); + ASSERT_NE(nullptr, bin); + EXPECT_EQ(bin->value.data, 1u | 2u); +} + +} // namespace aapt diff --git a/tools/aapt2/Locale.cpp b/tools/aapt2/Locale.cpp new file mode 100644 index 0000000..eed0ea7 --- /dev/null +++ b/tools/aapt2/Locale.cpp @@ -0,0 +1,274 @@ +/* + * 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 "Locale.h" +#include "Util.h" + +#include <algorithm> +#include <ctype.h> +#include <string> +#include <vector> + +namespace aapt { + +using android::ResTable_config; + +void LocaleValue::setLanguage(const char* languageChars) { + size_t i = 0; + while ((*languageChars) != '\0') { + language[i++] = ::tolower(*languageChars); + languageChars++; + } +} + +void LocaleValue::setRegion(const char* regionChars) { + size_t i = 0; + while ((*regionChars) != '\0') { + region[i++] = ::toupper(*regionChars); + regionChars++; + } +} + +void LocaleValue::setScript(const char* scriptChars) { + size_t i = 0; + while ((*scriptChars) != '\0') { + if (i == 0) { + script[i++] = ::toupper(*scriptChars); + } else { + script[i++] = ::tolower(*scriptChars); + } + scriptChars++; + } +} + +void LocaleValue::setVariant(const char* variantChars) { + size_t i = 0; + while ((*variantChars) != '\0') { + variant[i++] = *variantChars; + variantChars++; + } +} + +static inline bool isAlpha(const std::string& str) { + return std::all_of(std::begin(str), std::end(str), ::isalpha); +} + +static inline bool isNumber(const std::string& str) { + return std::all_of(std::begin(str), std::end(str), ::isdigit); +} + +bool LocaleValue::initFromFilterString(const std::string& str) { + // A locale (as specified in the filter) is an underscore separated name such + // as "en_US", "en_Latn_US", or "en_US_POSIX". + std::vector<std::string> parts = util::splitAndLowercase(str, '_'); + + const int numTags = parts.size(); + bool valid = false; + if (numTags >= 1) { + const std::string& lang = parts[0]; + if (isAlpha(lang) && (lang.length() == 2 || lang.length() == 3)) { + setLanguage(lang.c_str()); + valid = true; + } + } + + if (!valid || numTags == 1) { + return valid; + } + + // At this point, valid == true && numTags > 1. + const std::string& part2 = parts[1]; + if ((part2.length() == 2 && isAlpha(part2)) || + (part2.length() == 3 && isNumber(part2))) { + setRegion(part2.c_str()); + } else if (part2.length() == 4 && isAlpha(part2)) { + setScript(part2.c_str()); + } else if (part2.length() >= 5 && part2.length() <= 8) { + setVariant(part2.c_str()); + } else { + valid = false; + } + + if (!valid || numTags == 2) { + return valid; + } + + // At this point, valid == true && numTags > 1. + const std::string& part3 = parts[2]; + if (((part3.length() == 2 && isAlpha(part3)) || + (part3.length() == 3 && isNumber(part3))) && script[0]) { + setRegion(part3.c_str()); + } else if (part3.length() >= 5 && part3.length() <= 8) { + setVariant(part3.c_str()); + } else { + valid = false; + } + + if (!valid || numTags == 3) { + return valid; + } + + const std::string& part4 = parts[3]; + if (part4.length() >= 5 && part4.length() <= 8) { + setVariant(part4.c_str()); + } else { + valid = false; + } + + if (!valid || numTags > 4) { + return false; + } + + return true; +} + +ssize_t LocaleValue::initFromParts(std::vector<std::string>::iterator iter, + std::vector<std::string>::iterator end) { + const std::vector<std::string>::iterator startIter = iter; + + std::string& part = *iter; + if (part[0] == 'b' && part[1] == '+') { + // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags, + // except that the separator is "+" and not "-". + std::vector<std::string> subtags = util::splitAndLowercase(part, '+'); + subtags.erase(subtags.begin()); + if (subtags.size() == 1) { + setLanguage(subtags[0].c_str()); + } else if (subtags.size() == 2) { + setLanguage(subtags[0].c_str()); + + // The second tag can either be a region, a variant or a script. + switch (subtags[1].size()) { + case 2: + case 3: + setRegion(subtags[1].c_str()); + break; + case 4: + setScript(subtags[1].c_str()); + break; + case 5: + case 6: + case 7: + case 8: + setVariant(subtags[1].c_str()); + break; + default: + return -1; + } + } else if (subtags.size() == 3) { + // The language is always the first subtag. + setLanguage(subtags[0].c_str()); + + // The second subtag can either be a script or a region code. + // If its size is 4, it's a script code, else it's a region code. + if (subtags[1].size() == 4) { + setScript(subtags[1].c_str()); + } else if (subtags[1].size() == 2 || subtags[1].size() == 3) { + setRegion(subtags[1].c_str()); + } else { + return -1; + } + + // The third tag can either be a region code (if the second tag was + // a script), else a variant code. + if (subtags[2].size() > 4) { + setVariant(subtags[2].c_str()); + } else { + setRegion(subtags[2].c_str()); + } + } else if (subtags.size() == 4) { + setLanguage(subtags[0].c_str()); + setScript(subtags[1].c_str()); + setRegion(subtags[2].c_str()); + setVariant(subtags[3].c_str()); + } else { + return -1; + } + + ++iter; + + } else { + if ((part.length() == 2 || part.length() == 3) + && isAlpha(part) && part != "car") { + setLanguage(part.c_str()); + ++iter; + + if (iter != end) { + const std::string& regionPart = *iter; + if (regionPart.c_str()[0] == 'r' && regionPart.length() == 3) { + setRegion(regionPart.c_str() + 1); + ++iter; + } + } + } + } + + return static_cast<ssize_t>(iter - startIter); +} + + +std::string LocaleValue::toDirName() const { + std::string dirName; + if (language[0]) { + dirName += language; + } else { + return dirName; + } + + if (script[0]) { + dirName += "-s"; + dirName += script; + } + + if (region[0]) { + dirName += "-r"; + dirName += region; + } + + if (variant[0]) { + dirName += "-v"; + dirName += variant; + } + + return dirName; +} + +void LocaleValue::initFromResTable(const ResTable_config& config) { + config.unpackLanguage(language); + config.unpackRegion(region); + if (config.localeScript[0]) { + memcpy(script, config.localeScript, sizeof(config.localeScript)); + } + + if (config.localeVariant[0]) { + memcpy(variant, config.localeVariant, sizeof(config.localeVariant)); + } +} + +void LocaleValue::writeTo(ResTable_config* out) const { + out->packLanguage(language); + out->packRegion(region); + + if (script[0]) { + memcpy(out->localeScript, script, sizeof(out->localeScript)); + } + + if (variant[0]) { + memcpy(out->localeVariant, variant, sizeof(out->localeVariant)); + } +} + +} // namespace aapt diff --git a/tools/aapt2/Locale.h b/tools/aapt2/Locale.h new file mode 100644 index 0000000..ceec764 --- /dev/null +++ b/tools/aapt2/Locale.h @@ -0,0 +1,114 @@ +/* + * 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_LOCALE_VALUE_H +#define AAPT_LOCALE_VALUE_H + +#include <androidfw/ResourceTypes.h> +#include <string> +#include <vector> + +namespace aapt { + +/** + * A convenience class to build and parse locales. + */ +struct LocaleValue { + char language[4]; + char region[4]; + char script[4]; + char variant[8]; + + inline LocaleValue(); + + /** + * Initialize this LocaleValue from a config string. + */ + bool initFromFilterString(const std::string& config); + + /** + * Initialize this LocaleValue from parts of a vector. + */ + ssize_t initFromParts(std::vector<std::string>::iterator iter, + std::vector<std::string>::iterator end); + + /** + * Initialize this LocaleValue from a ResTable_config. + */ + void initFromResTable(const android::ResTable_config& config); + + /** + * Set the locale in a ResTable_config from this LocaleValue. + */ + void writeTo(android::ResTable_config* out) const; + + std::string toDirName() const; + + inline int compare(const LocaleValue& other) const; + + inline bool operator<(const LocaleValue& o) const; + inline bool operator<=(const LocaleValue& o) const; + inline bool operator==(const LocaleValue& o) const; + inline bool operator!=(const LocaleValue& o) const; + inline bool operator>=(const LocaleValue& o) const; + inline bool operator>(const LocaleValue& o) const; + +private: + void setLanguage(const char* language); + void setRegion(const char* language); + void setScript(const char* script); + void setVariant(const char* variant); +}; + +// +// Implementation +// + +LocaleValue::LocaleValue() { + memset(this, 0, sizeof(LocaleValue)); +} + +int LocaleValue::compare(const LocaleValue& other) const { + return memcmp(this, &other, sizeof(LocaleValue)); +} + +bool LocaleValue::operator<(const LocaleValue& o) const { + return compare(o) < 0; +} + +bool LocaleValue::operator<=(const LocaleValue& o) const { + return compare(o) <= 0; +} + +bool LocaleValue::operator==(const LocaleValue& o) const { + return compare(o) == 0; +} + +bool LocaleValue::operator!=(const LocaleValue& o) const { + return compare(o) != 0; +} + +bool LocaleValue::operator>=(const LocaleValue& o) const { + return compare(o) >= 0; +} + +bool LocaleValue::operator>(const LocaleValue& o) const { + return compare(o) > 0; +} + +} // namespace aapt + +#endif // AAPT_LOCALE_VALUE_H diff --git a/tools/aapt2/Locale_test.cpp b/tools/aapt2/Locale_test.cpp new file mode 100644 index 0000000..4e154d6 --- /dev/null +++ b/tools/aapt2/Locale_test.cpp @@ -0,0 +1,82 @@ +/* + * 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 "Locale.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +static ::testing::AssertionResult TestLanguage(const char* input, const char* lang) { + std::vector<std::string> parts = util::splitAndLowercase(std::string(input), '-'); + LocaleValue lv; + ssize_t count = lv.initFromParts(std::begin(parts), std::end(parts)); + if (count < 0) { + return ::testing::AssertionFailure() << " failed to parse '" << input << "'."; + } + + if (count != 1) { + return ::testing::AssertionFailure() << count + << " parts were consumed parsing '" << input << "' but expected 1."; + } + + if (memcmp(lv.language, lang, std::min(strlen(lang), sizeof(lv.language))) != 0) { + return ::testing::AssertionFailure() << "expected " << lang << " but got " + << std::string(lv.language, sizeof(lv.language)) << "."; + } + + return ::testing::AssertionSuccess(); +} + +static ::testing::AssertionResult TestLanguageRegion(const char* input, const char* lang, + const char* region) { + std::vector<std::string> parts = util::splitAndLowercase(std::string(input), '-'); + LocaleValue lv; + ssize_t count = lv.initFromParts(std::begin(parts), std::end(parts)); + if (count < 0) { + return ::testing::AssertionFailure() << " failed to parse '" << input << "'."; + } + + if (count != 2) { + return ::testing::AssertionFailure() << count + << " parts were consumed parsing '" << input << "' but expected 2."; + } + + if (memcmp(lv.language, lang, std::min(strlen(lang), sizeof(lv.language))) != 0) { + return ::testing::AssertionFailure() << "expected " << input << " but got " + << std::string(lv.language, sizeof(lv.language)) << "."; + } + + if (memcmp(lv.region, region, std::min(strlen(region), sizeof(lv.region))) != 0) { + return ::testing::AssertionFailure() << "expected " << region << " but got " + << std::string(lv.region, sizeof(lv.region)) << "."; + } + + return ::testing::AssertionSuccess(); +} + +TEST(ConfigDescriptionTest, ParseLanguage) { + EXPECT_TRUE(TestLanguage("en", "en")); + EXPECT_TRUE(TestLanguage("fr", "fr")); + EXPECT_FALSE(TestLanguage("land", "")); + EXPECT_TRUE(TestLanguage("fr-land", "fr")); + + EXPECT_TRUE(TestLanguageRegion("fr-rCA", "fr", "CA")); +} + +} // namespace aapt diff --git a/tools/aapt2/Logger.cpp b/tools/aapt2/Logger.cpp new file mode 100644 index 0000000..3847185 --- /dev/null +++ b/tools/aapt2/Logger.cpp @@ -0,0 +1,97 @@ +/* + * 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 "Source.h" + +#include <memory> +#include <iostream> + +namespace aapt { + +Log::Log(std::ostream& _out, std::ostream& _err) : out(_out), err(_err) { +} + +std::shared_ptr<Log> Logger::sLog(std::make_shared<Log>(std::cerr, std::cerr)); + +void Logger::setLog(const std::shared_ptr<Log>& log) { + sLog = log; +} + +std::ostream& Logger::error() { + return sLog->err << "error: "; +} + +std::ostream& Logger::error(const Source& source) { + return sLog->err << source << ": error: "; +} + +std::ostream& Logger::error(const SourceLine& source) { + return sLog->err << source << ": error: "; +} + +std::ostream& Logger::warn() { + return sLog->err << "warning: "; +} + +std::ostream& Logger::warn(const Source& source) { + return sLog->err << source << ": warning: "; +} + +std::ostream& Logger::warn(const SourceLine& source) { + return sLog->err << source << ": warning: "; +} + +std::ostream& Logger::note() { + return sLog->out << "note: "; +} + +std::ostream& Logger::note(const Source& source) { + return sLog->err << source << ": note: "; +} + +std::ostream& Logger::note(const SourceLine& source) { + return sLog->err << source << ": note: "; +} + +SourceLogger::SourceLogger(const Source& source) +: mSource(source) { +} + +std::ostream& SourceLogger::error() { + return Logger::error(mSource); +} + +std::ostream& SourceLogger::error(size_t line) { + return Logger::error(SourceLine{ mSource.path, line }); +} + +std::ostream& SourceLogger::warn() { + return Logger::warn(mSource); +} + +std::ostream& SourceLogger::warn(size_t line) { + return Logger::warn(SourceLine{ mSource.path, line }); +} + +std::ostream& SourceLogger::note() { + return Logger::note(mSource); +} + +std::ostream& SourceLogger::note(size_t line) { + return Logger::note(SourceLine{ mSource.path, line }); +} + +} // namespace aapt diff --git a/tools/aapt2/Logger.h b/tools/aapt2/Logger.h new file mode 100644 index 0000000..1d437eb --- /dev/null +++ b/tools/aapt2/Logger.h @@ -0,0 +1,81 @@ +/* + * 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_LOGGER_H +#define AAPT_LOGGER_H + +#include "Source.h" + +#include <memory> +#include <ostream> +#include <string> +#include <utils/String8.h> + +namespace aapt { + +struct Log { + Log(std::ostream& out, std::ostream& err); + Log(const Log& rhs) = delete; + + std::ostream& out; + std::ostream& err; +}; + +class Logger { +public: + static void setLog(const std::shared_ptr<Log>& log); + + static std::ostream& error(); + static std::ostream& error(const Source& source); + static std::ostream& error(const SourceLine& sourceLine); + + static std::ostream& warn(); + static std::ostream& warn(const Source& source); + static std::ostream& warn(const SourceLine& sourceLine); + + static std::ostream& note(); + static std::ostream& note(const Source& source); + static std::ostream& note(const SourceLine& sourceLine); + +private: + static std::shared_ptr<Log> sLog; +}; + +class SourceLogger { +public: + SourceLogger(const Source& source); + + std::ostream& error(); + std::ostream& error(size_t line); + + std::ostream& warn(); + std::ostream& warn(size_t line); + + std::ostream& note(); + std::ostream& note(size_t line); + +private: + Source mSource; +}; + +inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) { + android::String8 utf8(str.data(), str.size()); + return out.write(utf8.string(), utf8.size()); +} + +} // namespace aapt + +#endif // AAPT_LOGGER_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp new file mode 100644 index 0000000..f4e80c5 --- /dev/null +++ b/tools/aapt2/Main.cpp @@ -0,0 +1,1421 @@ +/* + * 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 "AppInfo.h" +#include "BigBuffer.h" +#include "BinaryResourceParser.h" +#include "Files.h" +#include "JavaClassGenerator.h" +#include "Linker.h" +#include "ManifestParser.h" +#include "ManifestValidator.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "SdkConstants.h" +#include "SourceXmlPullParser.h" +#include "StringPiece.h" +#include "TableFlattener.h" +#include "Util.h" +#include "XmlFlattener.h" + +#include <algorithm> +#include <androidfw/AssetManager.h> +#include <cstdlib> +#include <dirent.h> +#include <errno.h> +#include <fstream> +#include <iostream> +#include <sstream> +#include <sys/stat.h> + +using namespace aapt; + +void printTable(const ResourceTable& table) { + std::cout << "ResourceTable package=" << table.getPackage(); + if (table.getPackageId() != ResourceTable::kUnsetPackageId) { + std::cout << " id=" << std::hex << table.getPackageId() << std::dec; + } + std::cout << std::endl + << "---------------------------------------------------------" << std::endl; + + for (const auto& type : table) { + std::cout << "Type " << type->type; + if (type->typeId != ResourceTableType::kUnsetTypeId) { + std::cout << " [" << type->typeId << "]"; + } + std::cout << " (" << type->entries.size() << " entries)" << std::endl; + for (const auto& entry : type->entries) { + std::cout << " " << entry->name; + if (entry->entryId != ResourceEntry::kUnsetEntryId) { + std::cout << " [" << entry->entryId << "]"; + } + std::cout << " (" << entry->values.size() << " configurations)"; + if (entry->publicStatus.isPublic) { + std::cout << " PUBLIC"; + } + std::cout << std::endl; + for (const auto& value : entry->values) { + std::cout << " " << value.config << " (" << value.source << ") : "; + value.value->print(std::cout); + std::cout << std::endl; + } + } + } +} + +void printStringPool(const StringPool& pool) { + std::cout << "String pool of length " << pool.size() << std::endl + << "---------------------------------------------------------" << std::endl; + + size_t i = 0; + for (const auto& entry : pool) { + std::cout << "[" << i << "]: " + << entry->value + << " (Priority " << entry->context.priority + << ", Config '" << entry->context.config << "')" + << std::endl; + i++; + } +} + +std::unique_ptr<FileReference> makeFileReference(StringPool& pool, const StringPiece& filename, + ResourceType type, const ConfigDescription& config) { + std::stringstream path; + path << "res/" << type; + if (config != ConfigDescription{}) { + path << "-" << config; + } + path << "/" << filename; + return util::make_unique<FileReference>(pool.makeRef(util::utf8ToUtf16(path.str()))); +} + +/** + * Collect files from 'root', filtering out any files that do not + * match the FileFilter 'filter'. + */ +bool walkTree(const StringPiece& root, const FileFilter& filter, + std::vector<Source>& outEntries) { + bool error = false; + + for (const std::string& dirName : listFiles(root)) { + std::string dir(root.toString()); + appendPath(&dir, dirName); + + FileType ft = getFileType(dir); + if (!filter(dirName, ft)) { + continue; + } + + if (ft != FileType::kDirectory) { + continue; + } + + for (const std::string& fileName : listFiles(dir)) { + std::string file(dir); + appendPath(&file, fileName); + + FileType ft = getFileType(file); + if (!filter(fileName, ft)) { + continue; + } + + if (ft != FileType::kRegular) { + Logger::error(Source{ file }) + << "not a regular file." + << std::endl; + error = true; + continue; + } + outEntries.emplace_back(Source{ file }); + } + } + return !error; +} + +bool loadBinaryResourceTable(std::shared_ptr<ResourceTable> table, const Source& source) { + std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); + if (!ifs) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::streampos fsize = ifs.tellg(); + ifs.seekg(0, std::ios::end); + fsize = ifs.tellg() - fsize; + ifs.seekg(0, std::ios::beg); + + assert(fsize >= 0); + size_t dataSize = static_cast<size_t>(fsize); + char* buf = new char[dataSize]; + ifs.read(buf, dataSize); + + BinaryResourceParser parser(table, source, buf, dataSize); + bool result = parser.parse(); + + delete [] buf; + return result; +} + +bool loadResTable(android::ResTable* table, const Source& source) { + std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); + if (!ifs) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::streampos fsize = ifs.tellg(); + ifs.seekg(0, std::ios::end); + fsize = ifs.tellg() - fsize; + ifs.seekg(0, std::ios::beg); + + assert(fsize >= 0); + size_t dataSize = static_cast<size_t>(fsize); + char* buf = new char[dataSize]; + ifs.read(buf, dataSize); + + bool result = table->add(buf, dataSize, -1, true) == android::NO_ERROR; + + delete [] buf; + return result; +} + +void versionStylesForCompat(std::shared_ptr<ResourceTable> table) { + for (auto& type : *table) { + if (type->type != ResourceType::kStyle) { + continue; + } + + for (auto& entry : type->entries) { + // Add the versioned styles we want to create + // here. They are added to the table after + // iterating over the original set of styles. + // + // A stack is used since auto-generated styles + // from later versions should override + // auto-generated styles from earlier versions. + // Iterating over the styles is done in order, + // so we will always visit sdkVersions from smallest + // to largest. + std::stack<ResourceConfigValue> addStack; + + for (ResourceConfigValue& configValue : entry->values) { + visitFunc<Style>(*configValue.value, [&](Style& style) { + // Collect which entries we've stripped and the smallest + // SDK level which was stripped. + size_t minSdkStripped = std::numeric_limits<size_t>::max(); + std::vector<Style::Entry> stripped; + + // Iterate over the style's entries and erase/record the + // attributes whose SDK level exceeds the config's sdkVersion. + 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); + if (sdkLevel > 1 && sdkLevel > configValue.config.sdkVersion) { + // Record that we are about to strip this. + stripped.emplace_back(std::move(*iter)); + minSdkStripped = std::min(minSdkStripped, sdkLevel); + + // Erase this from this style. + iter = style.entries.erase(iter); + continue; + } + } + ++iter; + } + + if (!stripped.empty()) { + // We have stripped attributes, so let's create a new style to hold them. + ConfigDescription versionConfig(configValue.config); + versionConfig.sdkVersion = minSdkStripped; + + ResourceConfigValue value = { + versionConfig, + configValue.source, + {}, + + // Create a copy of the original style. + std::unique_ptr<Value>(configValue.value->clone()) + }; + + Style& newStyle = static_cast<Style&>(*value.value); + + // Move the recorded stripped attributes into this new style. + std::move(stripped.begin(), stripped.end(), + std::back_inserter(newStyle.entries)); + + // We will add this style to the table later. If we do it now, we will + // mess up iteration. + addStack.push(std::move(value)); + } + }); + } + + auto comparator = + [](const ResourceConfigValue& lhs, const ConfigDescription& rhs) -> bool { + return lhs.config < rhs; + }; + + while (!addStack.empty()) { + ResourceConfigValue& value = addStack.top(); + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), + value.config, comparator); + if (iter == entry->values.end() || iter->config != value.config) { + entry->values.insert(iter, std::move(value)); + } + addStack.pop(); + } + } + } +} + +bool collectXml(std::shared_ptr<ResourceTable> table, const Source& source, + const ResourceName& name, + const ConfigDescription& config) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::set<size_t> sdkLevels; + + SourceXmlPullParser pullParser(in); + while (XmlPullParser::isGoodEvent(pullParser.next())) { + if (pullParser.getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + const auto endIter = pullParser.endAttributes(); + for (auto iter = pullParser.beginAttributes(); iter != endIter; ++iter) { + if (iter->namespaceUri == u"http://schemas.android.com/apk/res/android") { + size_t sdkLevel = findAttributeSdkLevel(iter->name); + if (sdkLevel > 1) { + sdkLevels.insert(sdkLevel); + } + } + + ResourceNameRef refName; + bool create = false; + bool privateRef = false; + if (ResourceParser::tryParseReference(iter->value, &refName, &create, &privateRef) && + create) { + table->addResource(refName, {}, source.line(pullParser.getLineNumber()), + util::make_unique<Id>()); + } + } + } + + std::unique_ptr<FileReference> fileResource = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(name.entry) + ".xml", + name.type, + config); + table->addResource(name, config, source.line(0), std::move(fileResource)); + + for (size_t level : sdkLevels) { + Logger::note(source) + << "creating v" << level << " versioned file." + << std::endl; + ConfigDescription newConfig = config; + newConfig.sdkVersion = level; + + std::unique_ptr<FileReference> fileResource = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(name.entry) + ".xml", + name.type, + newConfig); + table->addResource(name, newConfig, source.line(0), std::move(fileResource)); + } + return true; +} + +struct CompileXml { + Source source; + ResourceName name; + ConfigDescription config; +}; + +bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, + const Source& outputSource, std::queue<CompileXml>* queue) { + std::ifstream in(item.source.path, std::ifstream::binary); + if (!in) { + Logger::error(item.source) << strerror(errno) << std::endl; + return false; + } + + BigBuffer outBuffer(1024); + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); + XmlFlattener flattener(resolver); + + // We strip attributes that do not belong in this version of the resource. + // Non-version qualified resources have an implicit version 1 requirement. + XmlFlattener::Options options = { item.config.sdkVersion ? item.config.sdkVersion : 1 }; + Maybe<size_t> minStrippedSdk = flattener.flatten(item.source, xmlParser, &outBuffer, options); + if (!minStrippedSdk) { + return false; + } + + if (minStrippedSdk.value() > 0) { + // Something was stripped, so let's generate a new file + // with the version of the smallest SDK version stripped. + CompileXml newWork = item; + newWork.config.sdkVersion = minStrippedSdk.value(); + queue->push(newWork); + } + + std::ofstream out(outputSource.path, std::ofstream::binary); + if (!out) { + Logger::error(outputSource) << strerror(errno) << std::endl; + return false; + } + + if (!util::writeAll(out, outBuffer)) { + Logger::error(outputSource) << strerror(errno) << std::endl; + return false; + } + return true; +} + +struct AaptOptions { + enum class Phase { + LegacyFull, + Collect, + Link, + Compile, + }; + + // The phase to process. + Phase phase; + + // Details about the app. + AppInfo appInfo; + + // The location of the manifest file. + Source manifest; + + // The files to process. + std::vector<Source> sources; + + // The libraries these files may reference. + std::vector<Source> libraries; + + // Output directory. + Source output; + + // Whether to generate a Java Class. + Maybe<Source> generateJavaClass; + + // Whether to output verbose details about + // compilation. + bool verbose = false; +}; + +bool compileAndroidManifest(std::shared_ptr<Resolver> resolver, const AaptOptions& options) { + Source outSource = options.output; + appendPath(&outSource.path, "AndroidManifest.xml"); + + if (options.verbose) { + Logger::note(outSource) << "compiling AndroidManifest.xml." << std::endl; + } + + std::ifstream in(options.manifest.path, std::ifstream::binary); + if (!in) { + Logger::error(options.manifest) << strerror(errno) << std::endl; + return false; + } + + BigBuffer outBuffer(1024); + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); + XmlFlattener flattener(resolver); + + Maybe<size_t> result = flattener.flatten(options.manifest, xmlParser, &outBuffer, + XmlFlattener::Options{}); + if (!result) { + return false; + } + + std::unique_ptr<uint8_t[]> data = std::unique_ptr<uint8_t[]>(new uint8_t[outBuffer.size()]); + uint8_t* p = data.get(); + for (const auto& b : outBuffer) { + memcpy(p, b.buffer.get(), b.size); + p += b.size; + } + + android::ResXMLTree tree; + if (tree.setTo(data.get(), outBuffer.size()) != android::NO_ERROR) { + return false; + } + + ManifestValidator validator(resolver->getResTable()); + if (!validator.validate(options.manifest, &tree)) { + return false; + } + + std::ofstream out(outSource.path, std::ofstream::binary); + if (!out) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; + } + + if (!util::writeAll(out, outBuffer)) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; + } + return true; +} + +bool loadAppInfo(const Source& source, AppInfo* outInfo) { + std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); + if (!ifs) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + ManifestParser parser; + std::shared_ptr<XmlPullParser> pullParser = std::make_shared<SourceXmlPullParser>(ifs); + return parser.parse(source, pullParser, outInfo); +} + +/** + * Parses legacy options and walks the source directories collecting + * files to process. + */ +bool prepareLegacy(std::vector<StringPiece>::const_iterator argsIter, + const std::vector<StringPiece>::const_iterator argsEndIter, + AaptOptions &options) { + options.phase = AaptOptions::Phase::LegacyFull; + + std::vector<StringPiece> sourceDirs; + while (argsIter != argsEndIter) { + if (*argsIter == "-S") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-S missing argument." << std::endl; + return false; + } + sourceDirs.push_back(*argsIter); + } else if (*argsIter == "-I") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-I missing argument." << std::endl; + return false; + } + options.libraries.push_back(Source{ argsIter->toString() }); + } else if (*argsIter == "-M") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-M missing argument." << std::endl; + return false; + } + + if (!options.manifest.path.empty()) { + Logger::error() << "multiple -M flags are not allowed." << std::endl; + return false; + } + options.manifest.path = argsIter->toString(); + } else if (*argsIter == "-o") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-o missing argument." << std::endl; + return false; + } + options.output = Source{ argsIter->toString() }; + } else if (*argsIter == "-J") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-J missing argument." << std::endl; + return false; + } + options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); + } else if (*argsIter == "-v") { + options.verbose = true; + } else { + Logger::error() << "unrecognized option '" << *argsIter << "'." << std::endl; + return false; + } + + ++argsIter; + } + + if (options.manifest.path.empty()) { + Logger::error() << "must specify manifest file with -M." << std::endl; + return false; + } + + // Load the App's package name, etc. + if (!loadAppInfo(options.manifest, &options.appInfo)) { + return false; + } + + /** + * Set up the file filter to ignore certain files. + */ + const char* customIgnore = getenv("ANDROID_AAPT_IGNORE"); + FileFilter fileFilter; + if (customIgnore && customIgnore[0]) { + fileFilter.setPattern(customIgnore); + } else { + fileFilter.setPattern( + "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"); + } + + /* + * Enumerate the files in each source directory. + */ + for (const StringPiece& source : sourceDirs) { + if (!walkTree(source, fileFilter, options.sources)) { + return false; + } + } + return true; +} + +bool prepareCollect(std::vector<StringPiece>::const_iterator argsIter, + const std::vector<StringPiece>::const_iterator argsEndIter, + AaptOptions& options) { + options.phase = AaptOptions::Phase::Collect; + + while (argsIter != argsEndIter) { + if (*argsIter == "--package") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "--package missing argument." << std::endl; + return false; + } + options.appInfo.package = util::utf8ToUtf16(*argsIter); + } else if (*argsIter == "-o") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-o missing argument." << std::endl; + return false; + } + options.output = Source{ argsIter->toString() }; + } else if (*argsIter == "-v") { + options.verbose = true; + } else if (argsIter->data()[0] != '-') { + options.sources.push_back(Source{ argsIter->toString() }); + } else { + Logger::error() + << "unknown option '" + << *argsIter + << "'." + << std::endl; + return false; + } + ++argsIter; + } + return true; +} + +bool prepareLink(std::vector<StringPiece>::const_iterator argsIter, + const std::vector<StringPiece>::const_iterator argsEndIter, + AaptOptions& options) { + options.phase = AaptOptions::Phase::Link; + + while (argsIter != argsEndIter) { + if (*argsIter == "--package") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "--package missing argument." << std::endl; + return false; + } + options.appInfo.package = util::utf8ToUtf16(*argsIter); + } else if (*argsIter == "-o") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-o missing argument." << std::endl; + return false; + } + options.output = Source{ argsIter->toString() }; + } else if (*argsIter == "-I") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-I missing argument." << std::endl; + return false; + } + options.libraries.push_back(Source{ argsIter->toString() }); + } else if (*argsIter == "--java") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "--java missing argument." << std::endl; + return false; + } + options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); + } else if (*argsIter == "-v") { + options.verbose = true; + } else if (argsIter->data()[0] != '-') { + options.sources.push_back(Source{ argsIter->toString() }); + } else { + Logger::error() + << "unknown option '" + << *argsIter + << "'." + << std::endl; + return false; + } + ++argsIter; + } + return true; +} + +bool prepareCompile(std::vector<StringPiece>::const_iterator argsIter, + const std::vector<StringPiece>::const_iterator argsEndIter, + AaptOptions& options) { + options.phase = AaptOptions::Phase::Compile; + + while (argsIter != argsEndIter) { + if (*argsIter == "--package") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "--package missing argument." << std::endl; + return false; + } + options.appInfo.package = util::utf8ToUtf16(*argsIter); + } else if (*argsIter == "-o") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-o missing argument." << std::endl; + return false; + } + options.output = Source{ argsIter->toString() }; + } else if (*argsIter == "-I") { + ++argsIter; + if (argsIter == argsEndIter) { + Logger::error() << "-I missing argument." << std::endl; + return false; + } + options.libraries.push_back(Source{ argsIter->toString() }); + } else if (*argsIter == "-v") { + options.verbose = true; + } else if (argsIter->data()[0] != '-') { + options.sources.push_back(Source{ argsIter->toString() }); + } else { + Logger::error() + << "unknown option '" + << *argsIter + << "'." + << std::endl; + return false; + } + ++argsIter; + } + return true; +} + +struct CollectValuesItem { + Source source; + ConfigDescription config; +}; + +bool collectValues(std::shared_ptr<ResourceTable> table, const CollectValuesItem& item) { + std::ifstream in(item.source.path, std::ifstream::binary); + if (!in) { + Logger::error(item.source) << strerror(errno) << std::endl; + return false; + } + + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); + ResourceParser parser(table, item.source, item.config, xmlParser); + return parser.parse(); +} + +struct ResourcePathData { + std::u16string resourceDir; + std::u16string name; + std::string extension; + ConfigDescription config; +}; + +/** + * Resource file paths are expected to look like: + * [--/res/]type[-config]/name + */ +Maybe<ResourcePathData> extractResourcePathData(const Source& source) { + std::vector<std::string> parts = util::splitAndLowercase(source.path, '/'); + if (parts.size() < 2) { + Logger::error(source) << "bad resource path." << std::endl; + return {}; + } + + std::string& dir = parts[parts.size() - 2]; + StringPiece dirStr = dir; + + ConfigDescription config; + size_t dashPos = dir.find('-'); + if (dashPos != std::string::npos) { + StringPiece configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1)); + if (!ConfigDescription::parse(configStr, &config)) { + Logger::error(source) + << "invalid configuration '" + << configStr + << "'." + << std::endl; + return {}; + } + dirStr = dirStr.substr(0, dashPos); + } + + std::string& filename = parts[parts.size() - 1]; + StringPiece name = filename; + StringPiece extension; + size_t dotPos = filename.find('.'); + if (dotPos != std::string::npos) { + extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1)); + name = name.substr(0, dotPos); + } + + return ResourcePathData{ + util::utf8ToUtf16(dirStr), + util::utf8ToUtf16(name), + extension.toString(), + config + }; +} + +static bool doLegacy(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, + const AaptOptions& options) { + bool error = false; + std::queue<CompileXml> xmlCompileQueue; + + // + // Read values XML files and XML/PNG files. + // Need to parse the resource type/config/filename. + // + for (const Source& source : options.sources) { + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } + + const ResourcePathData& pathData = maybePathData.value(); + if (pathData.resourceDir == u"values") { + if (options.verbose) { + Logger::note(source) << "collecting values..." << std::endl; + } + + error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); + continue; + } + + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) + << "invalid resource type '" + << pathData.resourceDir + << "'." + << std::endl; + return false; + } + + ResourceName resourceName = { table->getPackage(), *type, pathData.name }; + if (pathData.extension == "xml") { + if (options.verbose) { + Logger::note(source) << "collecting XML..." << std::endl; + } + + error |= !collectXml(table, source, resourceName, pathData.config); + xmlCompileQueue.push(CompileXml{ + source, + resourceName, + pathData.config + }); + } else { + std::unique_ptr<FileReference> fileReference = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(pathData.name) + "." + pathData.extension, + *type, pathData.config); + + error |= !table->addResource(resourceName, pathData.config, source.line(0), + std::move(fileReference)); + } + } + + if (error) { + return false; + } + + versionStylesForCompat(table); + + // + // Verify all references and data types. + // + Linker linker(table, resolver); + if (!linker.linkAndValidate()) { + Logger::error() + << "linking failed." + << std::endl; + return false; + } + + const auto& unresolvedRefs = linker.getUnresolvedReferences(); + if (!unresolvedRefs.empty()) { + for (const auto& entry : unresolvedRefs) { + for (const auto& source : entry.second) { + Logger::error(source) + << "unresolved symbol '" + << entry.first + << "'." + << std::endl; + } + } + return false; + } + + // + // Compile the XML files. + // + while (!xmlCompileQueue.empty()) { + const CompileXml& item = xmlCompileQueue.front(); + + // Create the output path from the resource name. + std::stringstream outputPath; + outputPath << item.name.type; + if (item.config != ConfigDescription{}) { + outputPath << "-" << item.config.toString(); + } + + Source outSource = options.output; + appendPath(&outSource.path, "res"); + appendPath(&outSource.path, outputPath.str()); + + if (!mkdirs(outSource.path)) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; + } + + appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); + + if (options.verbose) { + Logger::note(outSource) << "compiling XML file." << std::endl; + } + + error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); + xmlCompileQueue.pop(); + } + + if (error) { + return false; + } + + // + // Compile the AndroidManifest.xml file. + // + if (!compileAndroidManifest(resolver, options)) { + return false; + } + + // + // Generate the Java R class. + // + if (options.generateJavaClass) { + Source outPath = options.generateJavaClass.value(); + if (options.verbose) { + Logger::note() + << "writing symbols to " + << outPath + << "." + << std::endl; + } + + for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { + appendPath(&outPath.path, part); + } + + if (!mkdirs(outPath.path)) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + appendPath(&outPath.path, "R.java"); + + std::ofstream fout(outPath.path); + if (!fout) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + JavaClassGenerator generator(table, JavaClassGenerator::Options{}); + if (!generator.generate(fout)) { + Logger::error(outPath) + << generator.getError() + << "." + << std::endl; + return false; + } + } + + // + // Flatten resource table. + // + if (table->begin() != table->end()) { + BigBuffer buffer(1024); + TableFlattener::Options tableOptions; + tableOptions.useExtendedChunks = false; + TableFlattener flattener(tableOptions); + if (!flattener.flatten(&buffer, *table)) { + Logger::error() + << "failed to flatten resource table->" + << std::endl; + return false; + } + + if (options.verbose) { + Logger::note() + << "Final resource table size=" + << util::formatSize(buffer.size()) + << std::endl; + } + + std::string outTable(options.output.path); + appendPath(&outTable, "resources.arsc"); + + std::ofstream fout(outTable, std::ofstream::binary); + if (!fout) { + Logger::error(Source{outTable}) + << strerror(errno) + << "." + << std::endl; + return false; + } + + if (!util::writeAll(fout, buffer)) { + Logger::error(Source{outTable}) + << strerror(errno) + << "." + << std::endl; + return false; + } + fout.flush(); + } + return true; +} + +static bool doCollect(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, + const AaptOptions& options) { + bool error = false; + + // + // Read values XML files and XML/PNG files. + // Need to parse the resource type/config/filename. + // + for (const Source& source : options.sources) { + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } + + const ResourcePathData& pathData = maybePathData.value(); + if (pathData.resourceDir == u"values") { + if (options.verbose) { + Logger::note(source) << "collecting values..." << std::endl; + } + + error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); + continue; + } + + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) + << "invalid resource type '" + << pathData.resourceDir + << "'." + << std::endl; + return false; + } + + ResourceName resourceName = { table->getPackage(), *type, pathData.name }; + if (pathData.extension == "xml") { + if (options.verbose) { + Logger::note(source) << "collecting XML..." << std::endl; + } + + error |= !collectXml(table, source, resourceName, pathData.config); + } else { + std::unique_ptr<FileReference> fileReference = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(pathData.name) + "." + pathData.extension, + *type, + pathData.config); + error |= !table->addResource(resourceName, pathData.config, source.line(0), + std::move(fileReference)); + } + } + + if (error) { + return false; + } + + Linker linker(table, resolver); + if (!linker.linkAndValidate()) { + return false; + } + + // + // Flatten resource table-> + // + if (table->begin() != table->end()) { + BigBuffer buffer(1024); + TableFlattener::Options tableOptions; + tableOptions.useExtendedChunks = true; + TableFlattener flattener(tableOptions); + if (!flattener.flatten(&buffer, *table)) { + Logger::error() + << "failed to flatten resource table->" + << std::endl; + return false; + } + + std::ofstream fout(options.output.path, std::ofstream::binary); + if (!fout) { + Logger::error(options.output) + << strerror(errno) + << "." + << std::endl; + return false; + } + + if (!util::writeAll(fout, buffer)) { + Logger::error(options.output) + << strerror(errno) + << "." + << std::endl; + return false; + } + fout.flush(); + } + return true; +} + +static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, + const AaptOptions& options) { + bool error = false; + + for (const Source& source : options.sources) { + error |= !loadBinaryResourceTable(table, source); + } + + if (error) { + return false; + } + + versionStylesForCompat(table); + + Linker linker(table, resolver); + if (!linker.linkAndValidate()) { + return false; + } + + const auto& unresolvedRefs = linker.getUnresolvedReferences(); + if (!unresolvedRefs.empty()) { + for (const auto& entry : unresolvedRefs) { + for (const auto& source : entry.second) { + Logger::error(source) + << "unresolved symbol '" + << entry.first + << "'." + << std::endl; + } + } + return false; + } + + // + // Generate the Java R class. + // + if (options.generateJavaClass) { + Source outPath = options.generateJavaClass.value(); + if (options.verbose) { + Logger::note() + << "writing symbols to " + << outPath + << "." + << std::endl; + } + + for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { + appendPath(&outPath.path, part); + } + + if (!mkdirs(outPath.path)) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + appendPath(&outPath.path, "R.java"); + + std::ofstream fout(outPath.path); + if (!fout) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + JavaClassGenerator generator(table, JavaClassGenerator::Options{}); + if (!generator.generate(fout)) { + Logger::error(outPath) + << generator.getError() + << "." + << std::endl; + return false; + } + } + + // + // Flatten resource table. + // + if (table->begin() != table->end()) { + BigBuffer buffer(1024); + TableFlattener::Options tableOptions; + tableOptions.useExtendedChunks = false; + TableFlattener flattener(tableOptions); + if (!flattener.flatten(&buffer, *table)) { + Logger::error() + << "failed to flatten resource table->" + << std::endl; + return false; + } + + if (options.verbose) { + Logger::note() + << "Final resource table size=" + << util::formatSize(buffer.size()) + << std::endl; + } + + std::ofstream fout(options.output.path, std::ofstream::binary); + if (!fout) { + Logger::error(options.output) + << strerror(errno) + << "." + << std::endl; + return false; + } + + if (!util::writeAll(fout, buffer)) { + Logger::error(options.output) + << strerror(errno) + << "." + << std::endl; + return false; + } + fout.flush(); + } + return true; +} + +static bool doCompile(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, + const AaptOptions& options) { + std::queue<CompileXml> xmlCompileQueue; + + for (const Source& source : options.sources) { + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } + + ResourcePathData& pathData = maybePathData.value(); + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) + << "invalid resource type '" + << pathData.resourceDir + << "'." + << std::endl; + return false; + } + + ResourceName resourceName = { table->getPackage(), *type, pathData.name }; + if (pathData.extension == "xml") { + xmlCompileQueue.push(CompileXml{ + source, + resourceName, + pathData.config + }); + } else { + // TODO(adamlesinski): Handle images here. + } + } + + bool error = false; + while (!xmlCompileQueue.empty()) { + const CompileXml& item = xmlCompileQueue.front(); + + // Create the output path from the resource name. + std::stringstream outputPath; + outputPath << item.name.type; + if (item.config != ConfigDescription{}) { + outputPath << "-" << item.config.toString(); + } + + Source outSource = options.output; + appendPath(&outSource.path, "res"); + appendPath(&outSource.path, outputPath.str()); + + if (!mkdirs(outSource.path)) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; + } + + appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); + + if (options.verbose) { + Logger::note(outSource) << "compiling XML file." << std::endl; + } + + error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); + xmlCompileQueue.pop(); + } + return !error; +} + +int main(int argc, char** argv) { + Logger::setLog(std::make_shared<Log>(std::cerr, std::cerr)); + + std::vector<StringPiece> args; + args.reserve(argc - 1); + for (int i = 1; i < argc; i++) { + args.emplace_back(argv[i], strlen(argv[i])); + } + + if (args.empty()) { + Logger::error() << "no command specified." << std::endl; + return 1; + } + + AaptOptions options; + + // Check the command we're running. + const StringPiece& command = args.front(); + if (command == "package") { + if (!prepareLegacy(std::begin(args) + 1, std::end(args), options)) { + return 1; + } + } else if (command == "collect") { + if (!prepareCollect(std::begin(args) + 1, std::end(args), options)) { + return 1; + } + } else if (command == "link") { + if (!prepareLink(std::begin(args) + 1, std::end(args), options)) { + return 1; + } + } else if (command == "compile") { + if (!prepareCompile(std::begin(args) + 1, std::end(args), options)) { + return 1; + } + } else { + Logger::error() << "unknown command '" << command << "'." << std::endl; + return 1; + } + + // + // Verify we have some common options set. + // + + if (options.sources.empty()) { + Logger::error() << "no sources specified." << std::endl; + return false; + } + + if (options.output.path.empty()) { + Logger::error() << "no output directory specified." << std::endl; + return false; + } + + if (options.appInfo.package.empty()) { + Logger::error() << "no package name specified." << std::endl; + return false; + } + + + // + // Every phase needs a resource table and a resolver/linker. + // + + std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); + table->setPackage(options.appInfo.package); + if (options.appInfo.package == u"android") { + table->setPackageId(0x01); + } else { + table->setPackageId(0x7f); + } + + // + // Load the included libraries. + // + std::shared_ptr<android::AssetManager> libraries = std::make_shared<android::AssetManager>(); + for (const Source& source : options.libraries) { + if (util::stringEndsWith(source.path, ".arsc")) { + // We'll process these last so as to avoid a cookie issue. + continue; + } + + int32_t cookie; + if (!libraries->addAssetPath(android::String8(source.path.data()), &cookie)) { + Logger::error(source) << "failed to load library." << std::endl; + return false; + } + } + + for (const Source& source : options.libraries) { + if (!util::stringEndsWith(source.path, ".arsc")) { + // We've already processed this. + continue; + } + + // Dirty hack but there is no other way to get a + // writeable ResTable. + if (!loadResTable(const_cast<android::ResTable*>(&libraries->getResources(false)), + source)) { + return false; + } + } + + // Make the resolver that will cache IDs for us. + std::shared_ptr<Resolver> resolver = std::make_shared<Resolver>(table, libraries); + + // + // Dispatch to the real phase here. + // + + bool result = true; + switch (options.phase) { + case AaptOptions::Phase::LegacyFull: + result = doLegacy(table, resolver, options); + break; + + case AaptOptions::Phase::Collect: + result = doCollect(table, resolver, options); + break; + + case AaptOptions::Phase::Link: + result = doLink(table, resolver, options); + break; + + case AaptOptions::Phase::Compile: + result = doCompile(table, resolver, options); + break; + } + + if (!result) { + Logger::error() + << "aapt exiting with failures." + << std::endl; + return 1; + } + return 0; +} diff --git a/tools/aapt2/ManifestParser.cpp b/tools/aapt2/ManifestParser.cpp new file mode 100644 index 0000000..b8f0a43 --- /dev/null +++ b/tools/aapt2/ManifestParser.cpp @@ -0,0 +1,84 @@ +/* + * 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 "AppInfo.h" +#include "Logger.h" +#include "ManifestParser.h" +#include "Source.h" +#include "XmlPullParser.h" + +#include <string> + +namespace aapt { + +bool ManifestParser::parse(const Source& source, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo) { + SourceLogger logger = { source }; + + int depth = 0; + while (XmlPullParser::isGoodEvent(parser->next())) { + XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kEndElement) { + depth--; + continue; + } else if (event != XmlPullParser::Event::kStartElement) { + continue; + } + + depth++; + + const std::u16string& element = parser->getElementName(); + if (depth == 1) { + if (element == u"manifest") { + if (!parseManifest(logger, parser, outInfo)) { + return false; + } + } else { + logger.error() + << "unexpected top-level element '" + << element + << "'." + << std::endl; + return false; + } + } else { + XmlPullParser::skipCurrentElement(parser.get()); + } + } + + if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { + logger.error(parser->getLineNumber()) + << "failed to parse manifest: " + << parser->getLastError() + << "." + << std::endl; + return false; + } + return true; +} + +bool ManifestParser::parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo) { + auto attrIter = parser->findAttribute(u"", u"package"); + if (attrIter == parser->endAttributes() || attrIter->value.empty()) { + logger.error() << "no 'package' attribute found for element <manifest>." << std::endl; + return false; + } + outInfo->package = attrIter->value; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestParser.h b/tools/aapt2/ManifestParser.h new file mode 100644 index 0000000..f2e43d4 --- /dev/null +++ b/tools/aapt2/ManifestParser.h @@ -0,0 +1,45 @@ +/* + * 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_MANIFEST_PARSER_H +#define AAPT_MANIFEST_PARSER_H + +#include "AppInfo.h" +#include "Logger.h" +#include "Source.h" +#include "XmlPullParser.h" + +namespace aapt { + +/* + * Parses an AndroidManifest.xml file and fills in an AppInfo structure with + * app data. + */ +class ManifestParser { +public: + ManifestParser() = default; + ManifestParser(const ManifestParser&) = delete; + + bool parse(const Source& source, std::shared_ptr<XmlPullParser> parser, AppInfo* outInfo); + +private: + bool parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, + AppInfo* outInfo); +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_PARSER_H diff --git a/tools/aapt2/ManifestParser_test.cpp b/tools/aapt2/ManifestParser_test.cpp new file mode 100644 index 0000000..be3a6fb --- /dev/null +++ b/tools/aapt2/ManifestParser_test.cpp @@ -0,0 +1,42 @@ +/* + * 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 "AppInfo.h" +#include "ManifestParser.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(ManifestParserTest, FindPackage) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + "package=\"android\">\n" + "</manifest>\n"; + + ManifestParser parser; + AppInfo info; + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(input); + ASSERT_TRUE(parser.parse(Source{ "AndroidManifest.xml" }, xmlParser, &info)); + + EXPECT_EQ(std::u16string(u"android"), info.package); +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.cpp b/tools/aapt2/ManifestValidator.cpp new file mode 100644 index 0000000..596c758 --- /dev/null +++ b/tools/aapt2/ManifestValidator.cpp @@ -0,0 +1,209 @@ +/* + * 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 "ManifestValidator.h" +#include "Maybe.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +ManifestValidator::ManifestValidator(const android::ResTable& table) +: mTable(table) { +} + +bool ManifestValidator::validate(const Source& source, android::ResXMLParser* parser) { + SourceLogger logger(source); + + android::ResXMLParser::event_code_t code; + while ((code = parser->next()) != android::ResXMLParser::END_DOCUMENT && + code != android::ResXMLParser::BAD_DOCUMENT) { + if (code != android::ResXMLParser::START_TAG) { + continue; + } + + size_t len = 0; + const StringPiece16 namespaceUri(parser->getElementNamespace(&len), len); + if (!namespaceUri.empty()) { + continue; + } + + const StringPiece16 name(parser->getElementName(&len), len); + if (name.empty()) { + logger.error(parser->getLineNumber()) + << "failed to get the element name." + << std::endl; + return false; + } + + if (name == u"manifest") { + if (!validateManifest(source, parser)) { + return false; + } + } + } + return true; +} + +Maybe<StringPiece16> ManifestValidator::getAttributeValue(android::ResXMLParser* parser, + size_t idx) { + android::Res_value value; + if (parser->getAttributeValue(idx, &value) < 0) { + return StringPiece16(); + } + + const android::ResStringPool* pool = &parser->getStrings(); + if (value.dataType == android::Res_value::TYPE_REFERENCE) { + ssize_t strIdx = mTable.resolveReference(&value, 0x10000000u); + if (strIdx < 0) { + return {}; + } + pool = mTable.getTableStringBlock(strIdx); + } + + if (value.dataType != android::Res_value::TYPE_STRING || !pool) { + return {}; + } + return util::getString(*pool, value.data); +} + +Maybe<StringPiece16> ManifestValidator::getAttributeInlineValue(android::ResXMLParser* parser, + size_t idx) { + android::Res_value value; + if (parser->getAttributeValue(idx, &value) < 0) { + return StringPiece16(); + } + + if (value.dataType != android::Res_value::TYPE_STRING) { + return {}; + } + return util::getString(parser->getStrings(), value.data); +} + +bool ManifestValidator::validateInlineAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, + const StringPiece16& charSet) { + size_t len = 0; + StringPiece16 element(parser->getElementName(&len), len); + StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); + Maybe<StringPiece16> result = getAttributeInlineValue(parser, idx); + if (!result) { + logger.error(parser->getLineNumber()) + << "<" + << element + << "> must have a '" + << attributeName + << "' attribute with a string literal value." + << std::endl; + return false; + } + return validateAttributeImpl(element, attributeName, result.value(), charSet, + parser->getLineNumber(), logger); +} + +bool ManifestValidator::validateAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, const StringPiece16& charSet) { + size_t len = 0; + StringPiece16 element(parser->getElementName(&len), len); + StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); + Maybe<StringPiece16> result = getAttributeValue(parser, idx); + if (!result) { + logger.error(parser->getLineNumber()) + << "<" + << element + << "> must have a '" + << attributeName + << "' attribute that points to a string." + << std::endl; + return false; + } + return validateAttributeImpl(element, attributeName, result.value(), charSet, + parser->getLineNumber(), logger); +} + +bool ManifestValidator::validateAttributeImpl(const StringPiece16& element, + const StringPiece16& attributeName, + const StringPiece16& attributeValue, + const StringPiece16& charSet, size_t lineNumber, + SourceLogger& logger) { + StringPiece16::const_iterator badIter = + util::findNonAlphaNumericAndNotInSet(attributeValue, charSet); + if (badIter != attributeValue.end()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' has invalid character '" + << *badIter + << "'." + << std::endl; + return false; + } + + if (!attributeValue.empty()) { + StringPiece16 trimmed = util::trimWhitespace(attributeValue); + if (attributeValue.begin() != trimmed.begin()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' can not start with whitespace." + << std::endl; + return false; + } + + if (attributeValue.end() != trimmed.end()) { + logger.error(lineNumber) + << "tag <" + << element + << "> attribute '" + << attributeName + << "' can not end with whitespace." + << std::endl; + return false; + } + } + return true; +} + +constexpr const char16_t* kPackageIdentSet = u"._"; + +bool ManifestValidator::validateManifest(const Source& source, android::ResXMLParser* parser) { + bool error = false; + SourceLogger logger(source); + + const size_t attrCount = parser->getAttributeCount(); + for (size_t i = 0; i < attrCount; i++) { + size_t len = 0; + StringPiece16 attrNamespace(parser->getAttributeNamespace(i, &len), len); + StringPiece16 attrName(parser->getAttributeName(i, &len), len); + if (attrNamespace.empty() && attrName == u"package") { + error |= !validateInlineAttribute(parser, i, logger, kPackageIdentSet); + } else if (attrNamespace == u"android") { + if (attrName == u"sharedUserId") { + error |= !validateInlineAttribute(parser, i, logger, kPackageIdentSet); + } + } + } + return !error; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.h b/tools/aapt2/ManifestValidator.h new file mode 100644 index 0000000..3188784 --- /dev/null +++ b/tools/aapt2/ManifestValidator.h @@ -0,0 +1,55 @@ +/* + * 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_MANIFEST_VALIDATOR_H +#define AAPT_MANIFEST_VALIDATOR_H + +#include "Logger.h" +#include "Maybe.h" +#include "Source.h" +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +class ManifestValidator { +public: + ManifestValidator(const android::ResTable& table); + ManifestValidator(const ManifestValidator&) = delete; + + bool validate(const Source& source, android::ResXMLParser* parser); + +private: + bool validateManifest(const Source& source, android::ResXMLParser* parser); + + Maybe<StringPiece16> getAttributeInlineValue(android::ResXMLParser* parser, size_t idx); + Maybe<StringPiece16> getAttributeValue(android::ResXMLParser* parser, size_t idx); + + bool validateInlineAttribute(android::ResXMLParser* parser, size_t idx, + SourceLogger& logger, const StringPiece16& charSet); + bool validateAttribute(android::ResXMLParser* parser, size_t idx, SourceLogger& logger, + const StringPiece16& charSet); + bool validateAttributeImpl(const StringPiece16& element, const StringPiece16& attributeName, + const StringPiece16& attributeValue, const StringPiece16& charSet, + size_t lineNumber, SourceLogger& logger); + + const android::ResTable& mTable; +}; + +} // namespace aapt + +#endif // AAPT_MANIFEST_VALIDATOR_H diff --git a/tools/aapt2/Maybe.h b/tools/aapt2/Maybe.h new file mode 100644 index 0000000..f6a396d --- /dev/null +++ b/tools/aapt2/Maybe.h @@ -0,0 +1,224 @@ +/* + * 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_MAYBE_H +#define AAPT_MAYBE_H + +#include <cassert> +#include <type_traits> +#include <utility> + +namespace aapt { + +/** + * Either holds a valid value of type T, or holds Nothing. + * The value is stored inline in this structure, so no + * heap memory is used when creating a Maybe<T> object. + */ +template <typename T> +class Maybe { +public: + /** + * Construct Nothing. + */ + inline Maybe(); + + inline ~Maybe(); + + template <typename U> + inline Maybe(const Maybe<U>& rhs); + + template <typename U> + inline Maybe(Maybe<U>&& rhs); + + template <typename U> + inline Maybe& operator=(const Maybe<U>& rhs); + + template <typename U> + inline Maybe& operator=(Maybe<U>&& rhs); + + /** + * Construct a Maybe holding a value. + */ + inline Maybe(const T& value); + + /** + * Construct a Maybe holding a value. + */ + inline Maybe(T&& value); + + /** + * True if this holds a value, false if + * it holds Nothing. + */ + inline operator bool() const; + + /** + * Gets the value if one exists, or else + * panics. + */ + inline T& value(); + + /** + * Gets the value if one exists, or else + * panics. + */ + inline const T& value() const; + +private: + template <typename U> + friend class Maybe; + + void destroy(); + + bool mNothing; + + typename std::aligned_storage<sizeof(T), alignof(T)>::type mStorage; +}; + +template <typename T> +Maybe<T>::Maybe() +: mNothing(true) { +} + +template <typename T> +Maybe<T>::~Maybe() { + if (!mNothing) { + destroy(); + } +} + +template <typename T> +template <typename U> +Maybe<T>::Maybe(const Maybe<U>& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + new (&mStorage) T(reinterpret_cast<const U&>(rhs.mStorage)); + } +} + +template <typename T> +template <typename U> +Maybe<T>::Maybe(Maybe<U>&& rhs) +: mNothing(rhs.mNothing) { + if (!rhs.mNothing) { + rhs.mNothing = true; + + // Move the value from rhs. + new (&mStorage) T(std::move(reinterpret_cast<U&>(rhs.mStorage))); + + // Since the value in rhs is now Nothing, + // run the destructor for the value. + rhs.destroy(); + } +} + +template <typename T> +template <typename U> +Maybe<T>& Maybe<T>::operator=(const Maybe<U>& rhs) { + if (mNothing && rhs.mNothing) { + // Both are nothing, nothing to do. + return *this; + } else if (!mNothing && !rhs.mNothing) { + // We both are something, so assign rhs to us. + reinterpret_cast<T&>(mStorage) = reinterpret_cast<const U&>(rhs.mStorage); + } else if (mNothing) { + // We are nothing but rhs is something. + mNothing = rhs.mNothing; + + // Copy the value from rhs. + new (&mStorage) T(reinterpret_cast<const U&>(rhs.mStorage)); + } else { + // We are something but rhs is nothing, so destroy our value. + mNothing = rhs.mNothing; + destroy(); + } + return *this; +} + +template <typename T> +template <typename U> +Maybe<T>& Maybe<T>::operator=(Maybe<U>&& rhs) { + if (mNothing && rhs.mNothing) { + // Both are nothing, nothing to do. + return *this; + } else if (!mNothing && !rhs.mNothing) { + // We both are something, so move assign rhs to us. + rhs.mNothing = true; + reinterpret_cast<T&>(mStorage) = std::move(reinterpret_cast<U&>(rhs.mStorage)); + rhs.destroy(); + } else if (mNothing) { + // We are nothing but rhs is something. + mNothing = rhs.mNothing; + + // Move the value from rhs. + new (&mStorage) T(std::move(reinterpret_cast<U&>(rhs.mStorage))); + rhs.destroy(); + } else { + // We are something but rhs is nothing, so destroy our value. + mNothing = rhs.mNothing; + destroy(); + } + return *this; +} + +template <typename T> +Maybe<T>::Maybe(const T& value) +: mNothing(false) { + new (&mStorage) T(value); +} + +template <typename T> +Maybe<T>::Maybe(T&& value) +: mNothing(false) { + new (&mStorage) T(std::forward<T>(value)); +} + +template <typename T> +Maybe<T>::operator bool() const { + return !mNothing; +} + +template <typename T> +T& Maybe<T>::value() { + assert(!mNothing && "Maybe<T>::value() called on Nothing"); + return reinterpret_cast<T&>(mStorage); +} + +template <typename T> +const T& Maybe<T>::value() const { + assert(!mNothing && "Maybe<T>::value() called on Nothing"); + return reinterpret_cast<const T&>(mStorage); +} + +template <typename T> +void Maybe<T>::destroy() { + reinterpret_cast<T&>(mStorage).~T(); +} + +template <typename T> +inline Maybe<typename std::remove_reference<T>::type> make_value(T&& value) { + return Maybe<typename std::remove_reference<T>::type>(std::forward<T>(value)); +} + +template <typename T> +inline Maybe<T> make_nothing() { + return Maybe<T>(); +} + +} // namespace aapt + +#endif // AAPT_MAYBE_H diff --git a/tools/aapt2/Maybe_test.cpp b/tools/aapt2/Maybe_test.cpp new file mode 100644 index 0000000..348d7dd --- /dev/null +++ b/tools/aapt2/Maybe_test.cpp @@ -0,0 +1,69 @@ +/* + * 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 <gtest/gtest.h> +#include <string> + +#include "Maybe.h" + +namespace aapt { + +struct Dummy { + Dummy() { + std::cerr << "Constructing Dummy " << (void *) this << std::endl; + } + + Dummy(const Dummy& rhs) { + std::cerr << "Copying Dummy " << (void *) this << " from " << (const void*) &rhs << std::endl; + } + + Dummy(Dummy&& rhs) { + std::cerr << "Moving Dummy " << (void *) this << " from " << (void*) &rhs << std::endl; + } + + ~Dummy() { + std::cerr << "Destroying Dummy " << (void *) this << std::endl; + } +}; + +TEST(MaybeTest, MakeNothing) { + Maybe<int> val = make_nothing<int>(); + EXPECT_FALSE(val); + + Maybe<std::string> val2 = make_nothing<std::string>(); + EXPECT_FALSE(val2); + + val2 = make_nothing<std::string>(); + EXPECT_FALSE(val2); +} + +TEST(MaybeTest, MakeSomething) { + Maybe<int> val = make_value(23); + ASSERT_TRUE(val); + EXPECT_EQ(23, val.value()); + + Maybe<std::string> val2 = make_value(std::string("hey")); + ASSERT_TRUE(val2); + EXPECT_EQ(std::string("hey"), val2.value()); +} + +TEST(MaybeTest, Lifecycle) { + Maybe<Dummy> val = make_nothing<Dummy>(); + + Maybe<Dummy> val2 = make_value(Dummy()); +} + +} // namespace aapt diff --git a/tools/aapt2/ResChunkPullParser.cpp b/tools/aapt2/ResChunkPullParser.cpp new file mode 100644 index 0000000..78ea60e --- /dev/null +++ b/tools/aapt2/ResChunkPullParser.cpp @@ -0,0 +1,68 @@ +/* + * 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 "ResChunkPullParser.h" + +#include <androidfw/ResourceTypes.h> +#include <cstddef> + +namespace aapt { + +using android::ResChunk_header; + +ResChunkPullParser::Event ResChunkPullParser::next() { + if (!isGoodEvent(mEvent)) { + return mEvent; + } + + if (mEvent == Event::StartDocument) { + mCurrentChunk = mData; + } else { + mCurrentChunk = reinterpret_cast<const ResChunk_header*>( + reinterpret_cast<const char*>(mCurrentChunk) + mCurrentChunk->size); + } + + const std::ptrdiff_t diff = reinterpret_cast<const char*>(mCurrentChunk) + - reinterpret_cast<const char*>(mData); + assert(diff >= 0 && "diff is negative"); + const size_t offset = static_cast<const size_t>(diff); + + if (offset == mLen) { + mCurrentChunk = nullptr; + return (mEvent = Event::EndDocument); + } else if (offset + sizeof(ResChunk_header) > mLen) { + mLastError = "chunk is past the end of the document"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } + + if (mCurrentChunk->headerSize < sizeof(ResChunk_header)) { + mLastError = "chunk has too small header"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } else if (mCurrentChunk->size < mCurrentChunk->headerSize) { + mLastError = "chunk's total size is smaller than header"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } else if (offset + mCurrentChunk->size > mLen) { + mLastError = "chunk's data extends past the end of the document"; + mCurrentChunk = nullptr; + return (mEvent = Event::BadDocument); + } + return (mEvent = Event::Chunk); +} + +} // namespace aapt diff --git a/tools/aapt2/ResChunkPullParser.h b/tools/aapt2/ResChunkPullParser.h new file mode 100644 index 0000000..7366c89 --- /dev/null +++ b/tools/aapt2/ResChunkPullParser.h @@ -0,0 +1,106 @@ +/* + * 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_RES_CHUNK_PULL_PARSER_H +#define AAPT_RES_CHUNK_PULL_PARSER_H + +#include <androidfw/ResourceTypes.h> +#include <string> + +namespace aapt { + +/** + * A pull parser, modeled after XmlPullParser, that reads + * android::ResChunk_header structs from a block of data. + * + * An android::ResChunk_header specifies a type, headerSize, + * and size. The pull parser will verify that the chunk's size + * doesn't extend beyond the available data, and will iterate + * over each chunk in the given block of data. + * + * Processing nested chunks is done by creating a new ResChunkPullParser + * pointing to the data portion of a chunk. + */ +class ResChunkPullParser { +public: + enum class Event { + StartDocument, + EndDocument, + BadDocument, + + Chunk, + }; + + /** + * Returns false if the event is EndDocument or BadDocument. + */ + static bool isGoodEvent(Event event); + + /** + * Create a ResChunkPullParser to read android::ResChunk_headers + * from the memory pointed to by data, of len bytes. + */ + ResChunkPullParser(const void* data, size_t len); + + ResChunkPullParser(const ResChunkPullParser&) = delete; + + Event getEvent() const; + const std::string& getLastError() const; + const android::ResChunk_header* getChunk() const; + + /** + * Move to the next android::ResChunk_header. + */ + Event next(); + +private: + Event mEvent; + const android::ResChunk_header* mData; + size_t mLen; + const android::ResChunk_header* mCurrentChunk; + std::string mLastError; +}; + +// +// Implementation +// + +inline bool ResChunkPullParser::isGoodEvent(ResChunkPullParser::Event event) { + return event != Event::EndDocument && event != Event::BadDocument; +} + +inline ResChunkPullParser::ResChunkPullParser(const void* data, size_t len) : + mEvent(Event::StartDocument), + mData(reinterpret_cast<const android::ResChunk_header*>(data)), + mLen(len), + mCurrentChunk(nullptr) { +} + +inline ResChunkPullParser::Event ResChunkPullParser::getEvent() const { + return mEvent; +} + +inline const std::string& ResChunkPullParser::getLastError() const { + return mLastError; +} + +inline const android::ResChunk_header* ResChunkPullParser::getChunk() const { + return mCurrentChunk; +} + +} // namespace aapt + +#endif // AAPT_RES_CHUNK_PULL_PARSER_H diff --git a/tools/aapt2/Resolver.cpp b/tools/aapt2/Resolver.cpp new file mode 100644 index 0000000..93b5e98 --- /dev/null +++ b/tools/aapt2/Resolver.cpp @@ -0,0 +1,151 @@ +/* + * 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 "Maybe.h" +#include "Resolver.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <vector> + +namespace aapt { + +Resolver::Resolver(std::shared_ptr<const ResourceTable> table, + std::shared_ptr<const android::AssetManager> sources) : + mTable(table), mSources(sources) { +} + +Maybe<ResourceId> Resolver::findId(const ResourceName& name) { + Maybe<Entry> result = findAttribute(name); + if (result) { + return result.value().id; + } + return {}; +} + +Maybe<Resolver::Entry> Resolver::findAttribute(const ResourceName& name) { + auto cacheIter = mCache.find(name); + if (cacheIter != std::end(mCache)) { + return Entry{ cacheIter->second.id, cacheIter->second.attr.get() }; + } + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(name); + if (type && entry) { + Entry result = {}; + if (mTable->getPackageId() != ResourceTable::kUnsetPackageId && + type->typeId != ResourceTableType::kUnsetTypeId && + entry->entryId != ResourceEntry::kUnsetEntryId) { + result.id = ResourceId(mTable->getPackageId(), type->typeId, entry->entryId); + } + + if (!entry->values.empty()) { + visitFunc<Attribute>(*entry->values.front().value, [&result](Attribute& attr) { + result.attr = &attr; + }); + } + return result; + } + + const CacheEntry* cacheEntry = buildCacheEntry(name); + if (cacheEntry) { + return Entry{ cacheEntry->id, cacheEntry->attr.get() }; + } + return {}; +} + +/** + * This is called when we need to lookup a resource name in the AssetManager. + * Since the values in the AssetManager are not parsed like in a ResourceTable, + * we must create Attribute objects here if we find them. + */ +const Resolver::CacheEntry* Resolver::buildCacheEntry(const ResourceName& name) { + const android::ResTable& table = mSources->getResources(false); + + const StringPiece16 type16 = toString(name.type); + ResourceId resId { + table.identifierForName( + name.entry.data(), name.entry.size(), + type16.data(), type16.size(), + name.package.data(), name.package.size()) + }; + + if (!resId.isValid()) { + return nullptr; + } + + CacheEntry& entry = mCache[name]; + entry.id = resId; + + // + // Now check to see if this resource is an Attribute. + // + + const android::ResTable::bag_entry* bagBegin; + ssize_t bags = table.lockBag(resId.id, &bagBegin); + if (bags < 1) { + table.unlockBag(bagBegin); + return &entry; + } + + // Look for the ATTR_TYPE key in the bag and check the types it supports. + uint32_t attrTypeMask = 0; + for (ssize_t i = 0; i < bags; i++) { + if (bagBegin[i].map.name.ident == android::ResTable_map::ATTR_TYPE) { + attrTypeMask = bagBegin[i].map.value.data; + } + } + + entry.attr = util::make_unique<Attribute>(false); + + if (attrTypeMask & android::ResTable_map::TYPE_ENUM || + attrTypeMask & android::ResTable_map::TYPE_FLAGS) { + for (ssize_t i = 0; i < bags; i++) { + if (Res_INTERNALID(bagBegin[i].map.name.ident)) { + // Internal IDs are special keys, which are not enum/flag symbols, so skip. + continue; + } + + android::ResTable::resource_name symbolName; + bool result = table.getResourceName(bagBegin[i].map.name.ident, false, + &symbolName); + assert(result); + const ResourceType* type = parseResourceType( + StringPiece16(symbolName.type, symbolName.typeLen)); + assert(type); + + entry.attr->symbols.push_back(Attribute::Symbol{ + Reference(ResourceNameRef( + StringPiece16(symbolName.package, symbolName.packageLen), + *type, + StringPiece16(symbolName.name, symbolName.nameLen))), + bagBegin[i].map.value.data + }); + } + } + + entry.attr->typeMask |= attrTypeMask; + table.unlockBag(bagBegin); + return &entry; +} + +} // namespace aapt diff --git a/tools/aapt2/Resolver.h b/tools/aapt2/Resolver.h new file mode 100644 index 0000000..90a8cd9 --- /dev/null +++ b/tools/aapt2/Resolver.h @@ -0,0 +1,109 @@ +/* + * 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_RESOLVER_H +#define AAPT_RESOLVER_H + +#include "Maybe.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <vector> + +namespace aapt { + +/** + * Resolves symbolic references (package:type/entry) into resource IDs/objects. + * Encapsulates the search of library sources as well as the local ResourceTable. + */ +class Resolver { +public: + /** + * Creates a resolver with a local ResourceTable and an AssetManager + * loaded with library packages. + */ + Resolver(std::shared_ptr<const ResourceTable> table, + std::shared_ptr<const android::AssetManager> sources); + + Resolver(const Resolver&) = delete; // Not copyable. + + /** + * Holds the result of a resource name lookup. + */ + struct Entry { + /** + * The ID of the resource. ResourceId::isValid() may + * return false if the resource has not been assigned + * an ID. + */ + ResourceId id; + + /** + * If the resource is an attribute, this will point + * to a valid Attribute object, or else it will be + * nullptr. + */ + const Attribute* attr; + }; + + /** + * Return the package to use when none is specified. This + * is the package name of the app being built. + */ + const std::u16string& getDefaultPackage() const; + + /** + * Returns a ResourceID if the name is found. The ResourceID + * may not be valid if the resource was not assigned an ID. + */ + Maybe<ResourceId> findId(const ResourceName& name); + + /** + * Returns an Entry if the name is found. Entry::attr + * may be nullptr if the resource is not an attribute. + */ + Maybe<Entry> findAttribute(const ResourceName& name); + + const android::ResTable& getResTable() const; + +private: + struct CacheEntry { + ResourceId id; + std::unique_ptr<Attribute> attr; + }; + + const CacheEntry* buildCacheEntry(const ResourceName& name); + + std::shared_ptr<const ResourceTable> mTable; + std::shared_ptr<const android::AssetManager> mSources; + std::map<ResourceName, CacheEntry> mCache; +}; + +inline const std::u16string& Resolver::getDefaultPackage() const { + return mTable->getPackage(); +} + +inline const android::ResTable& Resolver::getResTable() const { + return mSources->getResources(false); +} + +} // namespace aapt + +#endif // AAPT_RESOLVER_H diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp new file mode 100644 index 0000000..287d8de --- /dev/null +++ b/tools/aapt2/Resource.cpp @@ -0,0 +1,90 @@ +/* + * 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 "Resource.h" +#include "StringPiece.h" + +#include <map> +#include <string> + +namespace aapt { + +StringPiece16 toString(ResourceType type) { + switch (type) { + case ResourceType::kAnim: return u"anim"; + case ResourceType::kAnimator: return u"animator"; + case ResourceType::kArray: return u"array"; + case ResourceType::kAttr: return u"attr"; + case ResourceType::kAttrPrivate: return u"attr"; + case ResourceType::kBool: return u"bool"; + case ResourceType::kColor: return u"color"; + case ResourceType::kDimen: return u"dimen"; + case ResourceType::kDrawable: return u"drawable"; + case ResourceType::kFraction: return u"fraction"; + case ResourceType::kId: return u"id"; + case ResourceType::kInteger: return u"integer"; + case ResourceType::kIntegerArray: return u"integer-array"; + case ResourceType::kInterpolator: return u"interpolator"; + case ResourceType::kLayout: return u"layout"; + case ResourceType::kMenu: return u"menu"; + case ResourceType::kMipmap: return u"mipmap"; + case ResourceType::kPlurals: return u"plurals"; + case ResourceType::kRaw: return u"raw"; + case ResourceType::kString: return u"string"; + case ResourceType::kStyle: return u"style"; + case ResourceType::kStyleable: return u"styleable"; + case ResourceType::kTransition: return u"transition"; + case ResourceType::kXml: return u"xml"; + } + return {}; +} + +static const std::map<StringPiece16, ResourceType> sResourceTypeMap { + { u"anim", ResourceType::kAnim }, + { u"animator", ResourceType::kAnimator }, + { u"array", ResourceType::kArray }, + { u"attr", ResourceType::kAttr }, + { u"^attr-private", ResourceType::kAttrPrivate }, + { u"bool", ResourceType::kBool }, + { u"color", ResourceType::kColor }, + { u"dimen", ResourceType::kDimen }, + { u"drawable", ResourceType::kDrawable }, + { u"fraction", ResourceType::kFraction }, + { u"id", ResourceType::kId }, + { u"integer", ResourceType::kInteger }, + { u"integer-array", ResourceType::kIntegerArray }, + { u"interpolator", ResourceType::kInterpolator }, + { u"layout", ResourceType::kLayout }, + { u"menu", ResourceType::kMenu }, + { u"mipmap", ResourceType::kMipmap }, + { u"plurals", ResourceType::kPlurals }, + { u"raw", ResourceType::kRaw }, + { u"string", ResourceType::kString }, + { u"style", ResourceType::kStyle }, + { u"styleable", ResourceType::kStyleable }, + { u"transition", ResourceType::kTransition }, + { u"xml", ResourceType::kXml }, +}; + +const ResourceType* parseResourceType(const StringPiece16& str) { + auto iter = sResourceTypeMap.find(str); + if (iter == std::end(sResourceTypeMap)) { + return nullptr; + } + return &iter->second; +} + +} // namespace aapt diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h new file mode 100644 index 0000000..3fd678e --- /dev/null +++ b/tools/aapt2/Resource.h @@ -0,0 +1,276 @@ +/* + * 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_RESOURCE_H +#define AAPT_RESOURCE_H + +#include "StringPiece.h" + +#include <iomanip> +#include <string> +#include <tuple> + +namespace aapt { + +/** + * The various types of resource types available. Corresponds + * to the 'type' in package:type/entry. + */ +enum class ResourceType { + kAnim, + kAnimator, + kArray, + kAttr, + kAttrPrivate, + kBool, + kColor, + kDimen, + kDrawable, + kFraction, + kId, + kInteger, + kIntegerArray, + kInterpolator, + kLayout, + kMenu, + kMipmap, + kPlurals, + kRaw, + kString, + kStyle, + kStyleable, + kTransition, + kXml, +}; + +StringPiece16 toString(ResourceType type); + +/** + * Returns a pointer to a valid ResourceType, or nullptr if + * the string was invalid. + */ +const ResourceType* parseResourceType(const StringPiece16& str); + +/** + * A resource's name. This can uniquely identify + * a resource in the ResourceTable. + */ +struct ResourceName { + std::u16string package; + ResourceType type; + std::u16string entry; + + bool isValid() const; + bool operator<(const ResourceName& rhs) const; + bool operator==(const ResourceName& rhs) const; + bool operator!=(const ResourceName& rhs) const; +}; + +/** + * Same as ResourceName, but uses StringPieces instead. + * Use this if you need to avoid copying and know that + * the lifetime of this object is shorter than that + * of the original string. + */ +struct ResourceNameRef { + StringPiece16 package; + ResourceType type; + StringPiece16 entry; + + ResourceNameRef() = default; + ResourceNameRef(const ResourceNameRef&) = default; + ResourceNameRef(ResourceNameRef&&) = default; + ResourceNameRef(const ResourceName& rhs); + ResourceNameRef(const StringPiece16& p, ResourceType t, const StringPiece16& e); + ResourceNameRef& operator=(const ResourceName& rhs); + + ResourceName toResourceName() const; + bool isValid() const; + + bool operator<(const ResourceNameRef& rhs) const; + bool operator==(const ResourceNameRef& rhs) const; + bool operator!=(const ResourceNameRef& rhs) const; +}; + +/** + * A binary identifier representing a resource. Internally it + * is a 32bit integer split as follows: + * + * 0xPPTTEEEE + * + * PP: 8 bit package identifier. 0x01 is reserved for system + * and 0x7f is reserved for the running app. + * TT: 8 bit type identifier. 0x00 is invalid. + * EEEE: 16 bit entry identifier. + */ +struct ResourceId { + uint32_t id; + + ResourceId(); + ResourceId(const ResourceId& rhs); + ResourceId(uint32_t resId); + ResourceId(size_t p, size_t t, size_t e); + + bool isValid() const; + uint8_t packageId() const; + uint8_t typeId() const; + uint16_t entryId() const; + bool operator<(const ResourceId& rhs) const; +}; + +// +// ResourceId implementation. +// + +inline ResourceId::ResourceId() : id(0) { +} + +inline ResourceId::ResourceId(const ResourceId& rhs) : id(rhs.id) { +} + +inline ResourceId::ResourceId(uint32_t resId) : id(resId) { +} + +inline ResourceId::ResourceId(size_t p, size_t t, size_t e) : id(0) { + if (p > std::numeric_limits<uint8_t>::max() || + t > std::numeric_limits<uint8_t>::max() || + e > std::numeric_limits<uint16_t>::max()) { + // This will leave the ResourceId in an invalid state. + return; + } + + id = (static_cast<uint8_t>(p) << 24) | + (static_cast<uint8_t>(t) << 16) | + static_cast<uint16_t>(e); +} + +inline bool ResourceId::isValid() const { + return (id & 0xff000000u) != 0 && (id & 0x00ff0000u) != 0; +} + +inline uint8_t ResourceId::packageId() const { + return static_cast<uint8_t>(id >> 24); +} + +inline uint8_t ResourceId::typeId() const { + return static_cast<uint8_t>(id >> 16); +} + +inline uint16_t ResourceId::entryId() const { + return static_cast<uint16_t>(id); +} + +inline bool ResourceId::operator<(const ResourceId& rhs) const { + return id < rhs.id; +} + +inline ::std::ostream& operator<<(::std::ostream& out, + const ResourceId& resId) { + std::ios_base::fmtflags oldFlags = out.flags(); + char oldFill = out.fill(); + out << "0x" << std::internal << std::setfill('0') << std::setw(8) + << std::hex << resId.id; + out.flags(oldFlags); + out.fill(oldFill); + return out; +} + +// +// ResourceType implementation. +// + +inline ::std::ostream& operator<<(::std::ostream& out, + const ResourceType& val) { + return out << toString(val); +} + +// +// ResourceName implementation. +// + +inline bool ResourceName::isValid() const { + return !package.empty() && !entry.empty(); +} + +inline bool ResourceName::operator<(const ResourceName& rhs) const { + return std::tie(package, type, entry) + < std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceName::operator==(const ResourceName& rhs) const { + return std::tie(package, type, entry) + == std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceName::operator!=(const ResourceName& rhs) const { + return std::tie(package, type, entry) + != std::tie(rhs.package, rhs.type, rhs.entry); +} + +// +// ResourceNameRef implementation. +// + +inline ResourceNameRef::ResourceNameRef(const ResourceName& rhs) : + package(rhs.package), type(rhs.type), entry(rhs.entry) { +} + +inline ResourceNameRef::ResourceNameRef(const StringPiece16& p, ResourceType t, + const StringPiece16& e) : + package(p), type(t), entry(e) { +} + +inline ResourceNameRef& ResourceNameRef::operator=(const ResourceName& rhs) { + package = rhs.package; + type = rhs.type; + entry = rhs.entry; + return *this; +} + +inline ResourceName ResourceNameRef::toResourceName() const { + return { package.toString(), type, entry.toString() }; +} + +inline bool ResourceNameRef::isValid() const { + return !package.empty() && !entry.empty(); +} + +inline bool ResourceNameRef::operator<(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + < std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceNameRef::operator==(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + == std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline bool ResourceNameRef::operator!=(const ResourceNameRef& rhs) const { + return std::tie(package, type, entry) + != std::tie(rhs.package, rhs.type, rhs.entry); +} + +inline ::std::ostream& operator<<(::std::ostream& out, + const ResourceNameRef& name) { + if (!name.package.empty()) { + out << name.package << ":"; + } + return out << name.type << "/" << name.entry; +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_H diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp new file mode 100644 index 0000000..d3720c4 --- /dev/null +++ b/tools/aapt2/ResourceParser.cpp @@ -0,0 +1,1317 @@ +/* + * 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 "ResourceParser.h" +#include "ResourceValues.h" +#include "ScopedXmlPullParser.h" +#include "SourceXmlPullParser.h" +#include "Util.h" +#include "XliffXmlPullParser.h" + +namespace aapt { + +void ResourceParser::extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry) { + const char16_t* start = str.data(); + const char16_t* end = start + str.size(); + const char16_t* current = start; + while (current != end) { + if (outType->size() == 0 && *current == u'/') { + outType->assign(start, current - start); + start = current + 1; + } else if (outPackage->size() == 0 && *current == u':') { + outPackage->assign(start, current - start); + start = current + 1; + } + current++; + } + outEntry->assign(start, end - start); +} + +bool ResourceParser::tryParseReference(const StringPiece16& str, ResourceNameRef* outRef, + bool* outCreate, bool* outPrivate) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + if (trimmedStr.data()[0] == u'@') { + size_t offset = 1; + *outCreate = false; + if (trimmedStr.data()[1] == u'+') { + *outCreate = true; + offset += 1; + } else if (trimmedStr.data()[1] == u'*') { + *outPrivate = true; + offset += 1; + } + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + extractResourceName(trimmedStr.substr(offset, trimmedStr.size() - offset), + &package, &type, &entry); + + const ResourceType* parsedType = parseResourceType(type); + if (!parsedType) { + return false; + } + + if (*outCreate && *parsedType != ResourceType::kId) { + return false; + } + + outRef->package = package; + outRef->type = *parsedType; + outRef->entry = entry; + return true; + } + return false; +} + +bool ResourceParser::tryParseAttributeReference(const StringPiece16& str, + ResourceNameRef* outRef) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + if (*trimmedStr.data() == u'?') { + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + extractResourceName(trimmedStr.substr(1, trimmedStr.size() - 1), &package, &type, &entry); + + if (!type.empty() && type != u"attr") { + return false; + } + + outRef->package = package; + outRef->type = ResourceType::kAttr; + outRef->entry = entry; + return true; + } + return false; +} + +std::unique_ptr<Reference> ResourceParser::tryParseReference(const StringPiece16& str, + const StringPiece16& defaultPackage, + bool* outCreate) { + ResourceNameRef ref; + bool privateRef = false; + if (tryParseReference(str, &ref, outCreate, &privateRef)) { + if (ref.package.empty()) { + ref.package = defaultPackage; + } + std::unique_ptr<Reference> value = util::make_unique<Reference>(ref); + value->privateReference = privateRef; + return value; + } + + if (tryParseAttributeReference(str, &ref)) { + if (ref.package.empty()) { + ref.package = defaultPackage; + } + *outCreate = false; + return util::make_unique<Reference>(ref, Reference::Type::kAttribute); + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseNullOrEmpty(const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + uint32_t data = 0; + if (trimmedStr == u"@null") { + data = android::Res_value::DATA_NULL_UNDEFINED; + } else if (trimmedStr == u"@empty") { + data = android::Res_value::DATA_NULL_EMPTY; + } else { + return {}; + } + + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_NULL; + value.data = data; + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseEnumSymbol(const Attribute& enumAttr, + const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + for (const auto& entry : enumAttr.symbols) { + // Enum symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& enumSymbolResourceName = entry.symbol.name; + if (trimmedStr == enumSymbolResourceName.entry) { + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_INT_DEC; + value.data = entry.value; + return util::make_unique<BinaryPrimitive>(value); + } + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFlagSymbol(const Attribute& flagAttr, + const StringPiece16& str) { + android::Res_value flags = {}; + flags.dataType = android::Res_value::TYPE_INT_DEC; + + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + + bool flagSet = false; + for (const auto& entry : flagAttr.symbols) { + // Flag symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& flagSymbolResourceName = entry.symbol.name; + if (trimmedPart == flagSymbolResourceName.entry) { + flags.data |= entry.value; + flagSet = true; + break; + } + } + + if (!flagSet) { + return {}; + } + } + return util::make_unique<BinaryPrimitive>(flags); +} + +static uint32_t parseHex(char16_t c, bool* outError) { + if (c >= u'0' && c <= u'9') { + return c - u'0'; + } else if (c >= u'a' && c <= u'f') { + return c - u'a' + 0xa; + } else if (c >= u'A' && c <= u'F') { + return c - u'A' + 0xa; + } else { + *outError = true; + return 0xffffffffu; + } +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseColor(const StringPiece16& str) { + StringPiece16 colorStr(util::trimWhitespace(str)); + const char16_t* start = colorStr.data(); + const size_t len = colorStr.size(); + if (len == 0 || start[0] != u'#') { + return {}; + } + + android::Res_value value = {}; + bool error = false; + if (len == 4) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB4; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[1], &error) << 16; + value.data |= parseHex(start[2], &error) << 12; + value.data |= parseHex(start[2], &error) << 8; + value.data |= parseHex(start[3], &error) << 4; + value.data |= parseHex(start[3], &error); + } else if (len == 5) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB4; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[1], &error) << 24; + value.data |= parseHex(start[2], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[3], &error) << 8; + value.data |= parseHex(start[4], &error) << 4; + value.data |= parseHex(start[4], &error); + } else if (len == 7) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB8; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[4], &error) << 8; + value.data |= parseHex(start[5], &error) << 4; + value.data |= parseHex(start[6], &error); + } else if (len == 9) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB8; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[2], &error) << 24; + value.data |= parseHex(start[3], &error) << 20; + value.data |= parseHex(start[4], &error) << 16; + value.data |= parseHex(start[5], &error) << 12; + value.data |= parseHex(start[6], &error) << 8; + value.data |= parseHex(start[7], &error) << 4; + value.data |= parseHex(start[8], &error); + } else { + return {}; + } + return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseBool(const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + uint32_t data = 0; + if (trimmedStr == u"true" || trimmedStr == u"TRUE") { + data = 1; + } else if (trimmedStr != u"false" && trimmedStr != u"FALSE") { + return {}; + } + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_INT_BOOLEAN; + value.data = data; + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseInt(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToInt(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFloat(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToFloat(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +uint32_t ResourceParser::androidTypeToAttributeTypeMask(uint16_t type) { + switch (type) { + case android::Res_value::TYPE_NULL: + case android::Res_value::TYPE_REFERENCE: + case android::Res_value::TYPE_ATTRIBUTE: + case android::Res_value::TYPE_DYNAMIC_REFERENCE: + return android::ResTable_map::TYPE_REFERENCE; + + case android::Res_value::TYPE_STRING: + return android::ResTable_map::TYPE_STRING; + + case android::Res_value::TYPE_FLOAT: + return android::ResTable_map::TYPE_FLOAT; + + case android::Res_value::TYPE_DIMENSION: + return android::ResTable_map::TYPE_DIMENSION; + + case android::Res_value::TYPE_FRACTION: + return android::ResTable_map::TYPE_FRACTION; + + case android::Res_value::TYPE_INT_DEC: + case android::Res_value::TYPE_INT_HEX: + return android::ResTable_map::TYPE_INTEGER | + android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_FLAGS; + + case android::Res_value::TYPE_INT_BOOLEAN: + return android::ResTable_map::TYPE_BOOLEAN; + + case android::Res_value::TYPE_INT_COLOR_ARGB8: + case android::Res_value::TYPE_INT_COLOR_RGB8: + case android::Res_value::TYPE_INT_COLOR_ARGB4: + case android::Res_value::TYPE_INT_COLOR_RGB4: + return android::ResTable_map::TYPE_COLOR; + + default: + return 0; + }; +} + +std::unique_ptr<Item> ResourceParser::parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, const StringPiece16& defaultPackage, + std::function<void(const ResourceName&)> onCreateReference) { + std::unique_ptr<BinaryPrimitive> nullOrEmpty = tryParseNullOrEmpty(value); + if (nullOrEmpty) { + return std::move(nullOrEmpty); + } + + bool create = false; + std::unique_ptr<Reference> reference = tryParseReference(value, defaultPackage, &create); + if (reference) { + if (create && onCreateReference) { + onCreateReference(reference->name); + } + return std::move(reference); + } + + if (typeMask & android::ResTable_map::TYPE_COLOR) { + // Try parsing this as a color. + std::unique_ptr<BinaryPrimitive> color = tryParseColor(value); + if (color) { + return std::move(color); + } + } + + if (typeMask & android::ResTable_map::TYPE_BOOLEAN) { + // Try parsing this as a boolean. + std::unique_ptr<BinaryPrimitive> boolean = tryParseBool(value); + if (boolean) { + return std::move(boolean); + } + } + + if (typeMask & android::ResTable_map::TYPE_INTEGER) { + // Try parsing this as an integer. + std::unique_ptr<BinaryPrimitive> integer = tryParseInt(value); + if (integer) { + return std::move(integer); + } + } + + const uint32_t floatMask = android::ResTable_map::TYPE_FLOAT | + android::ResTable_map::TYPE_DIMENSION | + android::ResTable_map::TYPE_FRACTION; + if (typeMask & floatMask) { + // Try parsing this as a float. + std::unique_ptr<BinaryPrimitive> floatingPoint = tryParseFloat(value); + if (floatingPoint) { + if (typeMask & androidTypeToAttributeTypeMask(floatingPoint->value.dataType)) { + return std::move(floatingPoint); + } + } + } + return {}; +} + +/** + * We successively try to parse the string as a resource type that the Attribute + * allows. + */ +std::unique_ptr<Item> ResourceParser::parseItemForAttribute( + const StringPiece16& str, const Attribute& attr, const StringPiece16& defaultPackage, + std::function<void(const ResourceName&)> onCreateReference) { + const uint32_t typeMask = attr.typeMask; + std::unique_ptr<Item> value = parseItemForAttribute(str, typeMask, defaultPackage, + onCreateReference); + if (value) { + return value; + } + + if (typeMask & android::ResTable_map::TYPE_ENUM) { + // Try parsing this as an enum. + std::unique_ptr<BinaryPrimitive> enumValue = tryParseEnumSymbol(attr, str); + if (enumValue) { + return std::move(enumValue); + } + } + + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + // Try parsing this as a flag. + std::unique_ptr<BinaryPrimitive> flagValue = tryParseFlagSymbol(attr, str); + if (flagValue) { + return std::move(flagValue); + } + } + return {}; +} + +ResourceParser::ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config, + const std::shared_ptr<XmlPullParser>& parser) : + mTable(table), mSource(source), mConfig(config), mLogger(source), + mParser(std::make_shared<XliffXmlPullParser>(parser)) { +} + +/** + * Build a string from XML that converts nested elements into Span objects. + */ +bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString, + StyleString* outStyleString) { + std::vector<Span> spanStack; + + outRawString->clear(); + outStyleString->spans.clear(); + util::StringBuilder builder; + size_t depth = 1; + while (XmlPullParser::isGoodEvent(parser->next())) { + const XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kEndElement) { + depth--; + if (depth == 0) { + break; + } + + spanStack.back().lastChar = builder.str().size(); + outStyleString->spans.push_back(spanStack.back()); + spanStack.pop_back(); + + } else if (event == XmlPullParser::Event::kText) { + // TODO(adamlesinski): Verify format strings. + outRawString->append(parser->getText()); + builder.append(parser->getText()); + + } else if (event == XmlPullParser::Event::kStartElement) { + if (parser->getElementNamespace().size() > 0) { + mLogger.warn(parser->getLineNumber()) + << "skipping element '" + << parser->getElementName() + << "' with unknown namespace '" + << parser->getElementNamespace() + << "'." + << std::endl; + XmlPullParser::skipCurrentElement(parser); + continue; + } + depth++; + + // Build a span object out of the nested element. + std::u16string spanName = parser->getElementName(); + const auto endAttrIter = parser->endAttributes(); + for (auto attrIter = parser->beginAttributes(); attrIter != endAttrIter; ++attrIter) { + spanName += u";"; + spanName += attrIter->name; + spanName += u"="; + spanName += attrIter->value; + } + + if (builder.str().size() > std::numeric_limits<uint32_t>::max()) { + mLogger.error(parser->getLineNumber()) + << "style string '" + << builder.str() + << "' is too long." + << std::endl; + return false; + } + spanStack.push_back(Span{ spanName, static_cast<uint32_t>(builder.str().size()) }); + + } else if (event == XmlPullParser::Event::kComment) { + // Skip + } else { + mLogger.warn(parser->getLineNumber()) + << "unknown event " + << event + << "." + << std::endl; + } + } + assert(spanStack.empty() && "spans haven't been fully processed"); + + outStyleString->str = builder.str(); + return true; +} + +bool ResourceParser::parse() { + while (XmlPullParser::isGoodEvent(mParser->next())) { + if (mParser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser parser(mParser.get()); + if (!parser.getElementNamespace().empty() || + parser.getElementName() != u"resources") { + mLogger.error(parser.getLineNumber()) + << "root element must be <resources> in the global namespace." + << std::endl; + return false; + } + + if (!parseResources(&parser)) { + return false; + } + } + + if (mParser->getEvent() == XmlPullParser::Event::kBadDocument) { + mLogger.error(mParser->getLineNumber()) + << mParser->getLastError() + << std::endl; + return false; + } + return true; +} + +bool ResourceParser::parseResources(XmlPullParser* parser) { + bool success = true; + + std::u16string comment; + while (XmlPullParser::isGoodEvent(parser->next())) { + const XmlPullParser::Event event = parser->getEvent(); + if (event == XmlPullParser::Event::kComment) { + comment = parser->getComment(); + continue; + } + + if (event == XmlPullParser::Event::kText) { + if (!util::trimWhitespace(parser->getText()).empty()) { + comment = u""; + } + continue; + } + + if (event != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (!childParser.getElementNamespace().empty()) { + // Skip unknown namespace. + continue; + } + + StringPiece16 name = childParser.getElementName(); + if (name == u"skip" || name == u"eat-comment") { + continue; + } + + if (name == u"private-symbols") { + // Handle differently. + mLogger.note(childParser.getLineNumber()) + << "got a <private-symbols> tag." + << std::endl; + continue; + } + + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"name"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<" << name << "> tag must have a 'name' attribute." + << std::endl; + success = false; + continue; + } + + // Copy because our iterator will go out of scope when + // we parse more XML. + std::u16string attributeName = attrIter->value; + + if (name == u"item") { + // Items simply have their type encoded in the type attribute. + auto typeIter = childParser.findAttribute(u"", u"type"); + if (typeIter == endAttrIter || typeIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<item> must have a 'type' attribute." + << std::endl; + success = false; + continue; + } + name = typeIter->value; + } + + if (name == u"id") { + success &= mTable->addResource(ResourceNameRef{ {}, ResourceType::kId, attributeName }, + {}, mSource.line(childParser.getLineNumber()), + util::make_unique<Id>()); + } else if (name == u"string") { + success &= parseString(&childParser, + ResourceNameRef{ {}, ResourceType::kString, attributeName }); + } else if (name == u"color") { + success &= parseColor(&childParser, + ResourceNameRef{ {}, ResourceType::kColor, attributeName }); + } else if (name == u"drawable") { + success &= parseColor(&childParser, + ResourceNameRef{ {}, ResourceType::kDrawable, attributeName }); + } else if (name == u"bool") { + success &= parsePrimitive(&childParser, + ResourceNameRef{ {}, ResourceType::kBool, attributeName }); + } else if (name == u"integer") { + success &= parsePrimitive( + &childParser, + ResourceNameRef{ {}, ResourceType::kInteger, attributeName }); + } else if (name == u"dimen") { + success &= parsePrimitive(&childParser, + ResourceNameRef{ {}, ResourceType::kDimen, attributeName }); + } else if (name == u"fraction") { +// success &= parsePrimitive( +// &childParser, +// ResourceNameRef{ {}, ResourceType::kFraction, attributeName }); + } else if (name == u"style") { + success &= parseStyle(&childParser, + ResourceNameRef{ {}, ResourceType::kStyle, attributeName }); + } else if (name == u"plurals") { + success &= parsePlural(&childParser, + ResourceNameRef{ {}, ResourceType::kPlurals, attributeName }); + } else if (name == u"array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_ANY); + } else if (name == u"string-array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_STRING); + } else if (name == u"integer-array") { + success &= parseArray(&childParser, + ResourceNameRef{ {}, ResourceType::kArray, attributeName }, + android::ResTable_map::TYPE_INTEGER); + } else if (name == u"public") { + success &= parsePublic(&childParser, attributeName); + } else if (name == u"declare-styleable") { + success &= parseDeclareStyleable( + &childParser, + ResourceNameRef{ {}, ResourceType::kStyleable, attributeName }); + } else if (name == u"attr") { + success &= parseAttr(&childParser, + ResourceNameRef{ {}, ResourceType::kAttr, attributeName }); + } else if (name == u"bag") { + } else if (name == u"public-padding") { + } else if (name == u"java-symbol") { + } else if (name == u"add-resource") { + } + } + + if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { + mLogger.error(parser->getLineNumber()) + << parser->getLastError() + << std::endl; + return false; + } + return success; +} + + + +enum { + kAllowRawString = true, + kNoRawString = false +}; + +/** + * Reads the entire XML subtree and attempts to parse it as some Item, + * with typeMask denoting which items it can be. If allowRawValue is + * true, a RawString is returned if the XML couldn't be parsed as + * an Item. If allowRawValue is false, nullptr is returned in this + * case. + */ +std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, uint32_t typeMask, + bool allowRawValue) { + const size_t beginXmlLine = parser->getLineNumber(); + + std::u16string rawValue; + StyleString styleString; + if (!flattenXmlSubtree(parser, &rawValue, &styleString)) { + return {}; + } + + StringPool& pool = mTable->getValueStringPool(); + + if (!styleString.spans.empty()) { + // This can only be a StyledString. + return util::make_unique<StyledString>( + pool.makeRef(styleString, StringPool::Context{ 1, mConfig })); + } + + auto onCreateReference = [&](const ResourceName& name) { + mTable->addResource(name, {}, mSource.line(beginXmlLine), util::make_unique<Id>()); + }; + + // Process the raw value. + std::unique_ptr<Item> processedItem = parseItemForAttribute(rawValue, typeMask, + mTable->getPackage(), + onCreateReference); + if (processedItem) { + return processedItem; + } + + // Try making a regular string. + if (typeMask & android::ResTable_map::TYPE_STRING) { + // Use the trimmed, escaped string. + return util::make_unique<String>( + pool.makeRef(styleString.str, StringPool::Context{ 1, mConfig })); + } + + // We can't parse this so return a RawString if we are allowed. + if (allowRawValue) { + return util::make_unique<RawString>( + pool.makeRef(rawValue, StringPool::Context{ 1, mConfig })); + } + return {}; +} + +bool ResourceParser::parseString(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + // Mark the string as untranslateable if needed. + const auto endAttrIter = parser->endAttributes(); + auto attrIter = parser->findAttribute(u"", u"untranslateable"); + // bool untranslateable = attrIter != endAttrIter; + // TODO(adamlesinski): Do something with this (mark the string). + + // Deal with the product. + attrIter = parser->findAttribute(u"", u"product"); + if (attrIter != endAttrIter) { + if (attrIter->value != u"default" && attrIter->value != u"phone") { + // TODO(adamlesinski): Match products. + return true; + } + } + + std::unique_ptr<Item> processedItem = parseXml(parser, android::ResTable_map::TYPE_STRING, + kNoRawString); + if (!processedItem) { + mLogger.error(source.line) + << "not a valid string." + << std::endl; + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(processedItem)); +} + +bool ResourceParser::parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + std::unique_ptr<Item> item = parseXml(parser, android::ResTable_map::TYPE_COLOR, kNoRawString); + if (!item) { + mLogger.error(source.line) << "invalid color." << std::endl; + return false; + } + return mTable->addResource(resourceName, mConfig, source, std::move(item)); +} + +bool ResourceParser::parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + uint32_t typeMask = 0; + switch (resourceName.type) { + case ResourceType::kInteger: + typeMask |= android::ResTable_map::TYPE_INTEGER; + break; + + case ResourceType::kDimen: + typeMask |= android::ResTable_map::TYPE_DIMENSION + | android::ResTable_map::TYPE_FLOAT + | android::ResTable_map::TYPE_FRACTION; + break; + + case ResourceType::kBool: + typeMask |= android::ResTable_map::TYPE_BOOLEAN; + break; + + default: + assert(false); + break; + } + + std::unique_ptr<Item> item = parseXml(parser, typeMask, kNoRawString); + if (!item) { + mLogger.error(source.line) + << "invalid " + << resourceName.type + << "." + << std::endl; + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(item)); +} + +bool ResourceParser::parsePublic(XmlPullParser* parser, const StringPiece16& name) { + const SourceLine source = mSource.line(parser->getLineNumber()); + + const auto endAttrIter = parser->endAttributes(); + const auto typeAttrIter = parser->findAttribute(u"", u"type"); + if (typeAttrIter == endAttrIter || typeAttrIter->value.empty()) { + mLogger.error(source.line) + << "<public> must have a 'type' attribute." + << std::endl; + return false; + } + + const ResourceType* parsedType = parseResourceType(typeAttrIter->value); + if (!parsedType) { + mLogger.error(source.line) + << "invalid resource type '" + << typeAttrIter->value + << "' in <public>." + << std::endl; + return false; + } + + ResourceNameRef resourceName { {}, *parsedType, name }; + ResourceId resourceId; + + const auto idAttrIter = parser->findAttribute(u"", u"id"); + if (idAttrIter != endAttrIter && !idAttrIter->value.empty()) { + android::Res_value val; + bool result = android::ResTable::stringToInt(idAttrIter->value.data(), + idAttrIter->value.size(), &val); + resourceId.id = val.data; + if (!result || !resourceId.isValid()) { + mLogger.error(source.line) + << "invalid resource ID '" + << idAttrIter->value + << "' in <public>." + << std::endl; + return false; + } + } + + if (*parsedType == ResourceType::kId) { + // An ID marked as public is also the definition of an ID. + mTable->addResource(resourceName, {}, source, util::make_unique<Id>()); + } + + return mTable->markPublic(resourceName, resourceId, source); +} + +static uint32_t parseFormatType(const StringPiece16& piece) { + if (piece == u"reference") return android::ResTable_map::TYPE_REFERENCE; + else if (piece == u"string") return android::ResTable_map::TYPE_STRING; + else if (piece == u"integer") return android::ResTable_map::TYPE_INTEGER; + else if (piece == u"boolean") return android::ResTable_map::TYPE_BOOLEAN; + else if (piece == u"color") return android::ResTable_map::TYPE_COLOR; + else if (piece == u"float") return android::ResTable_map::TYPE_FLOAT; + else if (piece == u"dimension") return android::ResTable_map::TYPE_DIMENSION; + else if (piece == u"fraction") return android::ResTable_map::TYPE_FRACTION; + else if (piece == u"enum") return android::ResTable_map::TYPE_ENUM; + else if (piece == u"flags") return android::ResTable_map::TYPE_FLAGS; + return 0; +} + +static uint32_t parseFormatAttribute(const StringPiece16& str) { + uint32_t mask = 0; + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + uint32_t type = parseFormatType(trimmedPart); + if (type == 0) { + return 0; + } + mask |= type; + } + return mask; +} + +bool ResourceParser::parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Attribute> attr = parseAttrImpl(parser, resourceName, false); + if (!attr) { + return false; + } + return mTable->addResource(resourceName, mConfig, source, std::move(attr)); +} + +std::unique_ptr<Attribute> ResourceParser::parseAttrImpl(XmlPullParser* parser, + const ResourceNameRef& resourceName, + bool weak) { + uint32_t typeMask = 0; + + const auto endAttrIter = parser->endAttributes(); + const auto formatAttrIter = parser->findAttribute(u"", u"format"); + if (formatAttrIter != endAttrIter) { + typeMask = parseFormatAttribute(formatAttrIter->value); + if (typeMask == 0) { + mLogger.error(parser->getLineNumber()) + << "invalid attribute format '" + << formatAttrIter->value + << "'." + << std::endl; + return {}; + } + } + + std::vector<Attribute::Symbol> items; + + bool error = false; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + const std::u16string& name = childParser.getElementName(); + if (!childParser.getElementNamespace().empty() + || (name != u"flag" && name != u"enum")) { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << name + << "> in <attr>." + << std::endl; + error = true; + continue; + } + + if (name == u"enum") { + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + mLogger.error(childParser.getLineNumber()) + << "can not define an <enum>; already defined a <flag>." + << std::endl; + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_ENUM; + } else if (name == u"flag") { + if (typeMask & android::ResTable_map::TYPE_ENUM) { + mLogger.error(childParser.getLineNumber()) + << "can not define a <flag>; already defined an <enum>." + << std::endl; + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_FLAGS; + } + + Attribute::Symbol item; + if (parseEnumOrFlagItem(&childParser, name, &item)) { + if (!mTable->addResource(item.symbol.name, mConfig, + mSource.line(childParser.getLineNumber()), + util::make_unique<Id>())) { + error = true; + } else { + items.push_back(std::move(item)); + } + } else { + error = true; + } + } + + if (error) { + return {}; + } + + std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(weak); + attr->symbols.swap(items); + attr->typeMask = typeMask ? typeMask : android::ResTable_map::TYPE_ANY; + return attr; +} + +bool ResourceParser::parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, + Attribute::Symbol* outSymbol) { + const auto attrIterEnd = parser->endAttributes(); + const auto nameAttrIter = parser->findAttribute(u"", u"name"); + if (nameAttrIter == attrIterEnd || nameAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "no attribute 'name' found for tag <" << tag << ">." + << std::endl; + return false; + } + + const auto valueAttrIter = parser->findAttribute(u"", u"value"); + if (valueAttrIter == attrIterEnd || valueAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "no attribute 'value' found for tag <" << tag << ">." + << std::endl; + return false; + } + + android::Res_value val; + if (!android::ResTable::stringToInt(valueAttrIter->value.data(), + valueAttrIter->value.size(), &val)) { + mLogger.error(parser->getLineNumber()) + << "invalid value '" + << valueAttrIter->value + << "' for <" << tag << ">; must be an integer." + << std::endl; + return false; + } + + outSymbol->symbol.name = ResourceName { + mTable->getPackage(), ResourceType::kId, nameAttrIter->value }; + outSymbol->value = val.data; + return true; +} + +static bool parseXmlAttributeName(StringPiece16 str, ResourceNameRef* outRef) { + str = util::trimWhitespace(str); + const char16_t* const start = str.data(); + const char16_t* const end = start + str.size(); + const char16_t* p = start; + + StringPiece16 package; + StringPiece16 name; + while (p != end) { + if (*p == u':') { + package = StringPiece16(start, p - start); + name = StringPiece16(p + 1, end - (p + 1)); + break; + } + p++; + } + + outRef->package = package; + outRef->type = ResourceType::kAttr; + if (name.size() == 0) { + outRef->entry = str; + } else { + outRef->entry = name; + } + return true; +} + +bool ResourceParser::parseUntypedItem(XmlPullParser* parser, Style& style) { + const auto endAttrIter = parser->endAttributes(); + const auto nameAttrIter = parser->findAttribute(u"", u"name"); + if (nameAttrIter == endAttrIter || nameAttrIter->value.empty()) { + mLogger.error(parser->getLineNumber()) + << "<item> must have a 'name' attribute." + << std::endl; + return false; + } + + ResourceNameRef keyRef; + if (!parseXmlAttributeName(nameAttrIter->value, &keyRef)) { + mLogger.error(parser->getLineNumber()) + << "invalid attribute name '" + << nameAttrIter->value + << "'." + << std::endl; + return false; + } + + if (keyRef.package.empty()) { + keyRef.package = mTable->getPackage(); + } + + // Create a copy instead of a reference because we + // are about to invalidate keyRef when advancing the parser. + ResourceName key = keyRef.toResourceName(); + + std::unique_ptr<Item> value = parseXml(parser, 0, kAllowRawString); + if (!value) { + return false; + } + + style.entries.push_back(Style::Entry{ Reference(key), std::move(value) }); + return true; +} + +bool ResourceParser::parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Style> style = util::make_unique<Style>(); + + const auto endAttrIter = parser->endAttributes(); + const auto parentAttrIter = parser->findAttribute(u"", u"parent"); + if (parentAttrIter != endAttrIter) { + ResourceNameRef ref; + bool create = false; + bool privateRef = false; + if (tryParseReference(parentAttrIter->value, &ref, &create, &privateRef)) { + if (create) { + mLogger.error(source.line) + << "parent of style can not be an ID." + << std::endl; + return false; + } + style->parent.name = ref.toResourceName(); + style->parent.privateReference = privateRef; + } else if (tryParseAttributeReference(parentAttrIter->value, &ref)) { + style->parent.name = ref.toResourceName(); + } else { + // TODO(adamlesinski): Try parsing without the '@' or '?'. + // Also, make sure to check the entry name for weird symbols. + style->parent.name = ResourceName { + {}, ResourceType::kStyle, parentAttrIter->value + }; + } + + if (style->parent.name.package.empty()) { + style->parent.name.package = mTable->getPackage(); + } + } + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + const std::u16string& name = childParser.getElementName(); + if (name == u"item") { + success &= parseUntypedItem(&childParser, *style); + } else { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << name + << "> in <style> resource." + << std::endl; + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(style)); +} + +bool ResourceParser::parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, + uint32_t typeMask) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Array> array = util::make_unique<Array>(); + + bool error = false; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (childParser.getElementName() != u"item") { + mLogger.error(childParser.getLineNumber()) + << "unexpected tag <" + << childParser.getElementName() + << "> in <array> resource." + << std::endl; + error = true; + continue; + } + + std::unique_ptr<Item> item = parseXml(&childParser, typeMask, kNoRawString); + if (!item) { + error = true; + continue; + } + array->items.emplace_back(std::move(item)); + } + + if (error) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(array)); +} + +bool ResourceParser::parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + if (!childParser.getElementNamespace().empty() || + childParser.getElementName() != u"item") { + success = false; + continue; + } + + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"quantity"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<item> in <plurals> requires attribute 'quantity'." + << std::endl; + success = false; + continue; + } + + StringPiece16 trimmedQuantity = util::trimWhitespace(attrIter->value); + size_t index = 0; + if (trimmedQuantity == u"zero") { + index = Plural::Zero; + } else if (trimmedQuantity == u"one") { + index = Plural::One; + } else if (trimmedQuantity == u"two") { + index = Plural::Two; + } else if (trimmedQuantity == u"few") { + index = Plural::Few; + } else if (trimmedQuantity == u"many") { + index = Plural::Many; + } else if (trimmedQuantity == u"other") { + index = Plural::Other; + } else { + mLogger.error(childParser.getLineNumber()) + << "<item> in <plural> has invalid value '" + << trimmedQuantity + << "' for attribute 'quantity'." + << std::endl; + success = false; + continue; + } + + if (plural->values[index]) { + mLogger.error(childParser.getLineNumber()) + << "duplicate quantity '" + << trimmedQuantity + << "'." + << std::endl; + success = false; + continue; + } + + if (!(plural->values[index] = parseXml(&childParser, android::ResTable_map::TYPE_STRING, + kNoRawString))) { + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(plural)); +} + +bool ResourceParser::parseDeclareStyleable(XmlPullParser* parser, + const ResourceNameRef& resourceName) { + const SourceLine source = mSource.line(parser->getLineNumber()); + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + + bool success = true; + while (XmlPullParser::isGoodEvent(parser->next())) { + if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser childParser(parser); + + const std::u16string& elementName = childParser.getElementName(); + if (elementName == u"attr") { + const auto endAttrIter = childParser.endAttributes(); + auto attrIter = childParser.findAttribute(u"", u"name"); + if (attrIter == endAttrIter || attrIter->value.empty()) { + mLogger.error(childParser.getLineNumber()) + << "<attr> tag must have a 'name' attribute." + << std::endl; + success = false; + continue; + } + + // Copy because our iterator will be invalidated. + std::u16string attrName = attrIter->value; + + ResourceNameRef attrResourceName = { + mTable->getPackage(), + ResourceType::kAttr, + attrName + }; + + std::unique_ptr<Attribute> attr = parseAttrImpl(&childParser, attrResourceName, true); + if (!attr) { + success = false; + continue; + } + + styleable->entries.emplace_back(attrResourceName); + + success &= mTable->addResource(attrResourceName, mConfig, + mSource.line(childParser.getLineNumber()), + std::move(attr)); + + } else if (elementName != u"eat-comment" && elementName != u"skip") { + mLogger.error(childParser.getLineNumber()) + << "<" + << elementName + << "> is not allowed inside <declare-styleable>." + << std::endl; + success = false; + } + } + + if (!success) { + return false; + } + + return mTable->addResource(resourceName, mConfig, source, std::move(styleable)); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h new file mode 100644 index 0000000..96bba4f --- /dev/null +++ b/tools/aapt2/ResourceParser.h @@ -0,0 +1,188 @@ +/* + * 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_RESOURCE_PARSER_H +#define AAPT_RESOURCE_PARSER_H + +#include "ConfigDescription.h" +#include "Logger.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "StringPiece.h" +#include "StringPool.h" +#include "XmlPullParser.h" + +#include <istream> +#include <memory> + +namespace aapt { + +/* + * Parses an XML file for resources and adds them to a ResourceTable. + */ +class ResourceParser { +public: + /* + * Extracts the package, type, and name from a string of the format: + * + * [package:]type/name + * + * where the package can be empty. Validation must be performed on each + * individual extracted piece to verify that the pieces are valid. + */ + static void extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry); + + /* + * Returns true if the string was parsed as a reference (@[+][package:]type/name), with + * `outReference` set to the parsed reference. + * + * If '+' was present in the reference, `outCreate` is set to true. + * If '*' was present in the reference, `outPrivate` is set to true. + */ + static bool tryParseReference(const StringPiece16& str, ResourceNameRef* outReference, + bool* outCreate, bool* outPrivate); + + /* + * Returns true if the string was parsed as an attribute reference (?[package:]type/name), + * with `outReference` set to the parsed reference. + */ + static bool tryParseAttributeReference(const StringPiece16& str, + ResourceNameRef* outReference); + + /* + * Returns a Reference object if the string was parsed as a resource or attribute reference, + * ( @[+][package:]type/name | ?[package:]type/name ) + * assigning defaultPackage if the package was not present in the string, and setting + * outCreate to true if the '+' was present in the string. + */ + static std::unique_ptr<Reference> tryParseReference(const StringPiece16& str, + const StringPiece16& defaultPackage, + bool* outCreate); + + /* + * Returns a BinaryPrimitve object representing @null or @empty if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseNullOrEmpty(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a color if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a boolean if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing an integer if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a floating point number + * (float, dimension, etc) if the string was parsed as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseFloat(const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing an enum symbol if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseEnumSymbol(const Attribute& enumAttr, + const StringPiece16& str); + + /* + * Returns a BinaryPrimitve object representing a flag symbol if the string was parsed + * as one. + */ + static std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute& enumAttr, + const StringPiece16& str); + + /* + * Try to convert a string to an Item for the given attribute. The attribute will + * restrict what values the string can be converted to. + * The defaultPackage is used when the string is a reference with no defined package. + * The callback function onCreateReference is called when the parsed item is a + * reference to an ID that must be created (@+id/foo). + */ + static std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, const Attribute& attr, const StringPiece16& defaultPackage, + std::function<void(const ResourceName&)> onCreateReference = {}); + + static std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, const StringPiece16& defaultPackage, + std::function<void(const ResourceName&)> onCreateReference = {}); + + static uint32_t androidTypeToAttributeTypeMask(uint16_t type); + + ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config, const std::shared_ptr<XmlPullParser>& parser); + + ResourceParser(const ResourceParser&) = delete; // No copy. + + bool parse(); + +private: + /* + * Parses the XML subtree as a StyleString (flattened XML representation for strings + * with formatting). If successful, `outStyleString` + * contains the escaped and whitespace trimmed text, while `outRawString` + * contains the unescaped text. Returns true on success. + */ + bool flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString,\ + StyleString* outStyleString); + + /* + * Parses the XML subtree and converts it to an Item. The type of Item that can be + * parsed is denoted by the `typeMask`. If `allowRawValue` is true and the subtree + * can not be parsed as a regular Item, then a RawString is returned. Otherwise + * this returns nullptr. + */ + std::unique_ptr<Item> parseXml(XmlPullParser* parser, uint32_t typeMask, bool allowRawValue); + + bool parseResources(XmlPullParser* parser); + bool parseString(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parsePublic(XmlPullParser* parser, const StringPiece16& name); + bool parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName); + std::unique_ptr<Attribute> parseAttrImpl(XmlPullParser* parser, + const ResourceNameRef& resourceName, + bool weak); + bool parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, + Attribute::Symbol* outSymbol); + bool parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseUntypedItem(XmlPullParser* parser, Style& style); + bool parseDeclareStyleable(XmlPullParser* parser, const ResourceNameRef& resourceName); + bool parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, uint32_t typeMask); + bool parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName); + + std::shared_ptr<ResourceTable> mTable; + Source mSource; + ConfigDescription mConfig; + SourceLogger mLogger; + std::shared_ptr<XmlPullParser> mParser; +}; + +} // namespace aapt + +#endif // AAPT_RESOURCE_PARSER_H diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp new file mode 100644 index 0000000..5afbaf4 --- /dev/null +++ b/tools/aapt2/ResourceParser_test.cpp @@ -0,0 +1,399 @@ +/* + * 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 "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + +TEST(ResourceParserReferenceTest, ParseReferenceWithNoPackage) { + ResourceNameRef expected = { {}, ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@color/foo", &actual, &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseReferenceWithPackage) { + ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@android:color/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseReferenceWithSurroundingWhitespace) { + ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"\t @android:color/foo\n \n\t", &actual, + &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParseAutoCreateIdReference) { + ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@+android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_TRUE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceParserReferenceTest, ParsePrivateReference) { + ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceParser::tryParseReference(u"@*android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_TRUE(privateRef); +} + +TEST(ResourceParserReferenceTest, FailToParseAutoCreateNonIdReference) { + bool create = false; + bool privateRef = false; + ResourceNameRef actual; + EXPECT_FALSE(ResourceParser::tryParseReference(u"@+android:color/foo", &actual, &create, + &privateRef)); +} + +struct ResourceParserTest : public ::testing::Test { + virtual void SetUp() override { + mTable = std::make_shared<ResourceTable>(); + mTable->setPackage(u"android"); + } + + ::testing::AssertionResult testParse(std::istream& in) { + std::stringstream input(kXmlPreamble); + input << "<resources>" << std::endl + << in.rdbuf() << std::endl + << "</resources>" << std::endl; + ResourceParser parser(mTable, Source{ "test" }, {}, + std::make_shared<SourceXmlPullParser>(input)); + if (parser.parse()) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure(); + } + + template <typename T> + const T* findResource(const ResourceNameRef& name, const ConfigDescription& config) { + using std::begin; + using std::end; + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(name); + if (!type || !entry) { + return nullptr; + } + + for (const auto& configValue : entry->values) { + if (configValue.config == config) { + return dynamic_cast<const T*>(configValue.value.get()); + } + } + return nullptr; + } + + template <typename T> + const T* findResource(const ResourceNameRef& name) { + return findResource<T>(name, {}); + } + + std::shared_ptr<ResourceTable> mTable; +}; + +TEST_F(ResourceParserTest, FailToParseWithNoRootResourcesElement) { + std::stringstream input(kXmlPreamble); + input << "<attr name=\"foo\"/>" << std::endl; + ResourceParser parser(mTable, {}, {}, std::make_shared<SourceXmlPullParser>(input)); + ASSERT_FALSE(parser.parse()); +} + +TEST_F(ResourceParserTest, ParseQuotedString) { + std::stringstream input("<string name=\"foo\"> \" hey there \" </string>"); + ASSERT_TRUE(testParse(input)); + + const String* str = findResource<String>(ResourceName{ + u"android", ResourceType::kString, u"foo"}); + ASSERT_NE(nullptr, str); + EXPECT_EQ(std::u16string(u" hey there "), *str->value); +} + +TEST_F(ResourceParserTest, ParseEscapedString) { + std::stringstream input("<string name=\"foo\">\\?123</string>"); + ASSERT_TRUE(testParse(input)); + + const String* str = findResource<String>(ResourceName{ + u"android", ResourceType::kString, u"foo" }); + ASSERT_NE(nullptr, str); + EXPECT_EQ(std::u16string(u"?123"), *str->value); +} + +TEST_F(ResourceParserTest, ParseAttr) { + std::stringstream input; + input << "<attr name=\"foo\" format=\"string\"/>" << std::endl + << "<attr name=\"bar\"/>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + EXPECT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); + + attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"bar"}); + EXPECT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_ANY), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseUseAndDeclOfAttr) { + std::stringstream input; + input << "<declare-styleable name=\"Styleable\">" << std::endl + << " <attr name=\"foo\" />" << std::endl + << "</declare-styleable>" << std::endl + << "<attr name=\"foo\" format=\"string\"/>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseDoubleUseOfAttr) { + std::stringstream input; + input << "<declare-styleable name=\"Theme\">" << std::endl + << " <attr name=\"foo\" />" << std::endl + << "</declare-styleable>" << std::endl + << "<declare-styleable name=\"Window\">" << std::endl + << " <attr name=\"foo\" format=\"boolean\"/>" << std::endl + << "</declare-styleable>" << std::endl; + + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_BOOLEAN), attr->typeMask); +} + +TEST_F(ResourceParserTest, ParseEnumAttr) { + std::stringstream input; + input << "<attr name=\"foo\">" << std::endl + << " <enum name=\"bar\" value=\"0\"/>" << std::endl + << " <enum name=\"bat\" value=\"1\"/>" << std::endl + << " <enum name=\"baz\" value=\"2\"/>" << std::endl + << "</attr>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Attribute* enumAttr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(enumAttr, nullptr); + EXPECT_EQ(enumAttr->typeMask, android::ResTable_map::TYPE_ENUM); + ASSERT_EQ(enumAttr->symbols.size(), 3u); + + EXPECT_EQ(enumAttr->symbols[0].symbol.name.entry, u"bar"); + EXPECT_EQ(enumAttr->symbols[0].value, 0u); + + EXPECT_EQ(enumAttr->symbols[1].symbol.name.entry, u"bat"); + EXPECT_EQ(enumAttr->symbols[1].value, 1u); + + EXPECT_EQ(enumAttr->symbols[2].symbol.name.entry, u"baz"); + EXPECT_EQ(enumAttr->symbols[2].value, 2u); +} + +TEST_F(ResourceParserTest, ParseFlagAttr) { + std::stringstream input; + input << "<attr name=\"foo\">" << std::endl + << " <flag name=\"bar\" value=\"0\"/>" << std::endl + << " <flag name=\"bat\" value=\"1\"/>" << std::endl + << " <flag name=\"baz\" value=\"2\"/>" << std::endl + << "</attr>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Attribute* flagAttr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"foo"}); + ASSERT_NE(flagAttr, nullptr); + EXPECT_EQ(flagAttr->typeMask, android::ResTable_map::TYPE_FLAGS); + ASSERT_EQ(flagAttr->symbols.size(), 3u); + + EXPECT_EQ(flagAttr->symbols[0].symbol.name.entry, u"bar"); + EXPECT_EQ(flagAttr->symbols[0].value, 0u); + + EXPECT_EQ(flagAttr->symbols[1].symbol.name.entry, u"bat"); + EXPECT_EQ(flagAttr->symbols[1].value, 1u); + + EXPECT_EQ(flagAttr->symbols[2].symbol.name.entry, u"baz"); + EXPECT_EQ(flagAttr->symbols[2].value, 2u); + + std::unique_ptr<BinaryPrimitive> flagValue = + ResourceParser::tryParseFlagSymbol(*flagAttr, u"baz|bat"); + ASSERT_NE(flagValue, nullptr); + EXPECT_EQ(flagValue->value.data, 1u | 2u); +} + +TEST_F(ResourceParserTest, FailToParseEnumAttrWithNonUniqueKeys) { + std::stringstream input; + input << "<attr name=\"foo\">" << std::endl + << " <enum name=\"bar\" value=\"0\"/>" << std::endl + << " <enum name=\"bat\" value=\"1\"/>" << std::endl + << " <enum name=\"bat\" value=\"2\"/>" << std::endl + << "</attr>" << std::endl; + ASSERT_FALSE(testParse(input)); +} + +TEST_F(ResourceParserTest, ParseStyle) { + std::stringstream input; + input << "<style name=\"foo\" parent=\"fu\">" << std::endl + << " <item name=\"bar\">#ffffffff</item>" << std::endl + << " <item name=\"bat\">@string/hey</item>" << std::endl + << " <item name=\"baz\"><b>hey</b></item>" << std::endl + << "</style>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Style* style = findResource<Style>(ResourceName{ + u"android", ResourceType::kStyle, u"foo"}); + ASSERT_NE(style, nullptr); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kStyle, u"fu"), style->parent.name); + ASSERT_EQ(style->entries.size(), 3u); + + EXPECT_EQ(style->entries[0].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"bar" })); + EXPECT_EQ(style->entries[1].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"bat" })); + EXPECT_EQ(style->entries[2].key.name, + (ResourceName{ u"android", ResourceType::kAttr, u"baz" })); +} + +TEST_F(ResourceParserTest, ParseAutoGeneratedIdReference) { + std::stringstream input; + input << "<string name=\"foo\">@+id/bar</string>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"bar"}); + ASSERT_NE(id, nullptr); +} + +TEST_F(ResourceParserTest, ParseAttributesDeclareStyleable) { + std::stringstream input; + input << "<declare-styleable name=\"foo\">" << std::endl + << " <attr name=\"bar\" />" << std::endl + << " <attr name=\"bat\" format=\"string|reference\"/>" << std::endl + << "</declare-styleable>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Attribute* attr = findResource<Attribute>(ResourceName{ + u"android", ResourceType::kAttr, u"bar"}); + ASSERT_NE(attr, nullptr); + EXPECT_TRUE(attr->isWeak()); + + attr = findResource<Attribute>(ResourceName{ u"android", ResourceType::kAttr, u"bat"}); + ASSERT_NE(attr, nullptr); + EXPECT_TRUE(attr->isWeak()); + + const Styleable* styleable = findResource<Styleable>(ResourceName{ + u"android", ResourceType::kStyleable, u"foo" }); + ASSERT_NE(styleable, nullptr); + ASSERT_EQ(2u, styleable->entries.size()); + + EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bar"}), styleable->entries[0].name); + EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bat"}), styleable->entries[1].name); +} + +TEST_F(ResourceParserTest, ParseArray) { + std::stringstream input; + input << "<array name=\"foo\">" << std::endl + << " <item>@string/ref</item>" << std::endl + << " <item>hey</item>" << std::endl + << " <item>23</item>" << std::endl + << "</array>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const Array* array = findResource<Array>(ResourceName{ + u"android", ResourceType::kArray, u"foo" }); + ASSERT_NE(array, nullptr); + ASSERT_EQ(3u, array->items.size()); + + EXPECT_NE(nullptr, dynamic_cast<const Reference*>(array->items[0].get())); + EXPECT_NE(nullptr, dynamic_cast<const String*>(array->items[1].get())); + EXPECT_NE(nullptr, dynamic_cast<const BinaryPrimitive*>(array->items[2].get())); +} + +TEST_F(ResourceParserTest, ParsePlural) { + std::stringstream input; + input << "<plurals name=\"foo\">" << std::endl + << " <item quantity=\"other\">apples</item>" << std::endl + << " <item quantity=\"one\">apple</item>" << std::endl + << "</plurals>" << std::endl + << std::endl; + ASSERT_TRUE(testParse(input)); +} + +TEST_F(ResourceParserTest, ParseCommentsWithResource) { + std::stringstream input; + input << "<!-- This is a comment -->" << std::endl + << "<string name=\"foo\">Hi</string>" << std::endl; + ASSERT_TRUE(testParse(input)); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = mTable->findResource(ResourceName{ + u"android", ResourceType::kString, u"foo"}); + ASSERT_NE(type, nullptr); + ASSERT_NE(entry, nullptr); + ASSERT_FALSE(entry->values.empty()); + EXPECT_EQ(entry->values.front().comment, u"This is a comment"); +} + +/* + * Declaring an ID as public should not require a separate definition + * (as an ID has no value). + */ +TEST_F(ResourceParserTest, ParsePublicIdAsDefinition) { + std::stringstream input("<public type=\"id\" name=\"foo\"/>"); + ASSERT_TRUE(testParse(input)); + + const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"foo" }); + ASSERT_NE(nullptr, id); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp new file mode 100644 index 0000000..0b3dd78 --- /dev/null +++ b/tools/aapt2/ResourceTable.cpp @@ -0,0 +1,334 @@ +/* + * 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 "ConfigDescription.h" +#include "Logger.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <algorithm> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <string> +#include <tuple> + +namespace aapt { + +static bool compareConfigs(const ResourceConfigValue& lhs, const ConfigDescription& rhs) { + return lhs.config < rhs; +} + +static bool lessThanType(const std::unique_ptr<ResourceTableType>& lhs, ResourceType rhs) { + return lhs->type < rhs; +} + +static bool lessThanEntry(const std::unique_ptr<ResourceEntry>& lhs, const StringPiece16& rhs) { + return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0; +} + +ResourceTable::ResourceTable() : mPackageId(kUnsetPackageId) { +} + +std::unique_ptr<ResourceTableType>& ResourceTable::findOrCreateType(ResourceType type) { + auto last = mTypes.end(); + auto iter = std::lower_bound(mTypes.begin(), last, type, lessThanType); + if (iter != last) { + if ((*iter)->type == type) { + return *iter; + } + } + return *mTypes.emplace(iter, new ResourceTableType{ type }); +} + +std::unique_ptr<ResourceEntry>& ResourceTable::findOrCreateEntry( + std::unique_ptr<ResourceTableType>& type, const StringPiece16& name) { + auto last = type->entries.end(); + auto iter = std::lower_bound(type->entries.begin(), last, name, lessThanEntry); + if (iter != last) { + if (name == (*iter)->name) { + return *iter; + } + } + return *type->entries.emplace(iter, new ResourceEntry{ name }); +} + +struct IsAttributeVisitor : ConstValueVisitor { + bool isAttribute = false; + + void visit(const Attribute&, ValueVisitorArgs&) override { + isAttribute = true; + } + + operator bool() { + return isAttribute; + } +}; + +/** + * The default handler for collisions. A return value of -1 means keep the + * existing value, 0 means fail, and +1 means take the incoming value. + */ +static int defaultCollisionHandler(const Value& existing, const Value& incoming) { + IsAttributeVisitor existingIsAttr, incomingIsAttr; + existing.accept(existingIsAttr, {}); + incoming.accept(incomingIsAttr, {}); + + if (!incomingIsAttr) { + if (incoming.isWeak()) { + // We're trying to add a weak resource but a resource + // already exists. Keep the existing. + return -1; + } else if (existing.isWeak()) { + // Override the weak resource with the new strong resource. + return 1; + } + // The existing and incoming values are strong, this is an error + // if the values are not both attributes. + return 0; + } + + if (!existingIsAttr) { + if (existing.isWeak()) { + // The existing value is not an attribute and it is weak, + // so take the incoming attribute value. + return 1; + } + // The existing value is not an attribute and it is strong, + // so the incoming attribute value is an error. + return 0; + } + + // + // Attribute specific handling. At this point we know both + // values are attributes. Since we can declare and define + // attributes all-over, we do special handling to see + // which definition sticks. + // + const Attribute& existingAttr = static_cast<const Attribute&>(existing); + const Attribute& incomingAttr = static_cast<const Attribute&>(incoming); + if (existingAttr.typeMask == incomingAttr.typeMask) { + // The two attributes are both DECLs, but they are plain attributes + // with the same formats. + // Keep the strongest one. + return existingAttr.isWeak() ? 1 : -1; + } + + if (existingAttr.isWeak() && existingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + // Any incoming attribute is better than this. + return 1; + } + + if (incomingAttr.isWeak() && incomingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + // The incoming attribute may be a USE instead of a DECL. + // Keep the existing attribute. + return -1; + } + return 0; +} + +static constexpr const char16_t* kValidNameChars = u"._-"; + +bool ResourceTable::addResource(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value) { + if (!name.package.empty() && name.package != mPackage) { + Logger::error(source) + << "resource '" + << name + << "' has incompatible package. Must be '" + << mPackage + << "'." + << std::endl; + return false; + } + + auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, kValidNameChars); + if (badCharIter != name.entry.end()) { + Logger::error(source) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << *badCharIter + << "'." + << std::endl; + return false; + } + + std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); + if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && + type->typeId != resId.typeId()) { + Logger::error(source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << type->typeId << std::dec + << "." + << std::endl; + return false; + } + + std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); + if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && + entry->entryId != resId.entryId()) { + Logger::error(source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but resource already has ID " + << ResourceId(mPackageId, type->typeId, entry->entryId) + << "." + << std::endl; + return false; + } + + const auto endIter = std::end(entry->values); + auto iter = std::lower_bound(std::begin(entry->values), endIter, config, compareConfigs); + if (iter == endIter || iter->config != config) { + // This resource did not exist before, add it. + entry->values.insert(iter, ResourceConfigValue{ config, source, {}, std::move(value) }); + } else { + int collisionResult = defaultCollisionHandler(*iter->value, *value); + if (collisionResult > 0) { + // Take the incoming value. + *iter = ResourceConfigValue{ config, source, {}, std::move(value) }; + } else if (collisionResult == 0) { + Logger::error(source) + << "duplicate value for resource '" << name << "' " + << "with config '" << iter->config << "'." + << std::endl; + + Logger::error(iter->source) + << "resource previously defined here." + << std::endl; + return false; + } + } + + if (resId.isValid()) { + type->typeId = resId.typeId(); + entry->entryId = resId.entryId(); + } + return true; +} + +bool ResourceTable::addResource(const ResourceNameRef& name, const ConfigDescription& config, + const SourceLine& source, std::unique_ptr<Value> value) { + return addResource(name, ResourceId{}, config, source, std::move(value)); +} + +bool ResourceTable::markPublic(const ResourceNameRef& name, const ResourceId resId, + const SourceLine& source) { + if (!name.package.empty() && name.package != mPackage) { + Logger::error(source) + << "resource '" + << name + << "' has incompatible package. Must be '" + << mPackage + << "'." + << std::endl; + return false; + } + + auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, kValidNameChars); + if (badCharIter != name.entry.end()) { + Logger::error(source) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << *badCharIter + << "'." + << std::endl; + return false; + } + + std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); + if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && + type->typeId != resId.typeId()) { + Logger::error(source) + << "trying to make resource '" + << name + << "' public with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << type->typeId << std::dec + << "." + << std::endl; + return false; + } + + std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); + if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && + entry->entryId != resId.entryId()) { + Logger::error(source) + << "trying to make resource '" + << name + << "' public with ID " + << resId + << " but resource already has ID " + << ResourceId(mPackageId, type->typeId, entry->entryId) + << "." + << std::endl; + return false; + } + + type->publicStatus.isPublic = true; + entry->publicStatus.isPublic = true; + + if (resId.isValid()) { + type->typeId = resId.typeId(); + entry->entryId = resId.entryId(); + } + + if (entry->values.empty()) { + entry->values.push_back(ResourceConfigValue{ {}, source, {}, + util::make_unique<Sentinel>() }); + } + return true; +} + +std::tuple<const ResourceTableType*, const ResourceEntry*> +ResourceTable::findResource(const ResourceNameRef& name) const { + if (name.package != mPackage) { + return {nullptr, nullptr}; + } + + auto iter = std::lower_bound(mTypes.begin(), mTypes.end(), name.type, lessThanType); + if (iter == mTypes.end() || (*iter)->type != name.type) { + return {nullptr, nullptr}; + } + + const std::unique_ptr<ResourceTableType>& type = *iter; + auto iter2 = std::lower_bound(type->entries.begin(), type->entries.end(), name.entry, + lessThanEntry); + if (iter2 == type->entries.end() || name.entry != (*iter2)->name) { + return {nullptr, nullptr}; + } + return {iter->get(), iter2->get()}; +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h new file mode 100644 index 0000000..57b5213 --- /dev/null +++ b/tools/aapt2/ResourceTable.h @@ -0,0 +1,254 @@ +/* + * 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_RESOURCE_TABLE_H +#define AAPT_RESOURCE_TABLE_H + +#include "ConfigDescription.h" +#include "Resource.h" +#include "ResourceValues.h" +#include "Source.h" +#include "StringPool.h" + +#include <memory> +#include <string> +#include <tuple> +#include <vector> + +namespace aapt { + +/** + * The Public status of a resource. + */ +struct Public { + bool isPublic = false; + std::u16string comment; +}; + +/** + * The resource value for a specific configuration. + */ +struct ResourceConfigValue { + ConfigDescription config; + SourceLine source; + std::u16string comment; + std::unique_ptr<Value> value; +}; + +/** + * Represents a resource entry, which may have + * varying values for each defined configuration. + */ +struct ResourceEntry { + enum { + kUnsetEntryId = 0xffffffffu + }; + + /** + * The name of the resource. Immutable, as + * this determines the order of this resource + * when doing lookups. + */ + const std::u16string name; + + /** + * The entry ID for this resource. + */ + size_t entryId; + + /** + * Whether this resource is public (and must maintain the same + * entry ID across builds). + */ + Public publicStatus; + + /** + * The resource's values for each configuration. + */ + std::vector<ResourceConfigValue> values; + + inline ResourceEntry(const StringPiece16& _name); + inline ResourceEntry(const ResourceEntry* rhs); +}; + +/** + * Represents a resource type, which holds entries defined + * for this type. + */ +struct ResourceTableType { + enum { + kUnsetTypeId = 0xffffffffu + }; + + /** + * The logical type of resource (string, drawable, layout, etc.). + */ + const ResourceType type; + + /** + * The type ID for this resource. + */ + size_t typeId; + + /** + * Whether this type is public (and must maintain the same + * type ID across builds). + */ + Public publicStatus; + + /** + * List of resources for this type. + */ + std::vector<std::unique_ptr<ResourceEntry>> entries; + + ResourceTableType(const ResourceType _type); + ResourceTableType(const ResourceTableType* rhs); +}; + +/** + * The container and index for all resources defined for an app. This gets + * flattened into a binary resource table (resources.arsc). + */ +class ResourceTable { +public: + using iterator = std::vector<std::unique_ptr<ResourceTableType>>::iterator; + using const_iterator = std::vector<std::unique_ptr<ResourceTableType>>::const_iterator; + + enum { + kUnsetPackageId = 0xffffffff + }; + + ResourceTable(); + + size_t getPackageId() const; + void setPackageId(size_t packageId); + + const std::u16string& getPackage() const; + void setPackage(const StringPiece16& package); + + bool addResource(const ResourceNameRef& name, const ConfigDescription& config, + const SourceLine& source, std::unique_ptr<Value> value); + + bool addResource(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, const SourceLine& source, + std::unique_ptr<Value> value); + + bool markPublic(const ResourceNameRef& name, const ResourceId resId, const SourceLine& source); + + /** + * Returns the string pool used by this ResourceTable. + * Values that reference strings should use this pool to create + * their strings. + */ + StringPool& getValueStringPool(); + const StringPool& getValueStringPool() const; + + std::tuple<const ResourceTableType*, const ResourceEntry*> + findResource(const ResourceNameRef& name) const; + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + +private: + std::unique_ptr<ResourceTableType>& findOrCreateType(ResourceType type); + std::unique_ptr<ResourceEntry>& findOrCreateEntry(std::unique_ptr<ResourceTableType>& type, + const StringPiece16& name); + + std::u16string mPackage; + size_t mPackageId; + + // StringPool must come before mTypes so that it is destroyed after. + // When StringPool references are destroyed (as they will be when mTypes + // is destroyed), they decrement a refCount, which would cause invalid + // memory access if the pool was already destroyed. + StringPool mValuePool; + + std::vector<std::unique_ptr<ResourceTableType>> mTypes; +}; + +// +// ResourceEntry implementation. +// + +inline ResourceEntry::ResourceEntry(const StringPiece16& _name) : + name(_name.toString()), entryId(kUnsetEntryId) { +} + +inline ResourceEntry::ResourceEntry(const ResourceEntry* rhs) : + name(rhs->name), entryId(rhs->entryId), publicStatus(rhs->publicStatus) { +} + +// +// ResourceTableType implementation. +// + +inline ResourceTableType::ResourceTableType(const ResourceType _type) : + type(_type), typeId(kUnsetTypeId) { +} + +inline ResourceTableType::ResourceTableType(const ResourceTableType* rhs) : + type(rhs->type), typeId(rhs->typeId), publicStatus(rhs->publicStatus) { +} + +// +// ResourceTable implementation. +// + +inline StringPool& ResourceTable::getValueStringPool() { + return mValuePool; +} + +inline const StringPool& ResourceTable::getValueStringPool() const { + return mValuePool; +} + +inline ResourceTable::iterator ResourceTable::begin() { + return mTypes.begin(); +} + +inline ResourceTable::iterator ResourceTable::end() { + return mTypes.end(); +} + +inline ResourceTable::const_iterator ResourceTable::begin() const { + return mTypes.begin(); +} + +inline ResourceTable::const_iterator ResourceTable::end() const { + return mTypes.end(); +} + +inline const std::u16string& ResourceTable::getPackage() const { + return mPackage; +} + +inline size_t ResourceTable::getPackageId() const { + return mPackageId; +} + +inline void ResourceTable::setPackage(const StringPiece16& package) { + mPackage = package.toString(); +} + +inline void ResourceTable::setPackageId(size_t packageId) { + mPackageId = packageId; +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_TABLE_H diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp new file mode 100644 index 0000000..785ea15 --- /dev/null +++ b/tools/aapt2/ResourceTable_test.cpp @@ -0,0 +1,228 @@ +/* + * 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 "ResourceTable.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <algorithm> +#include <gtest/gtest.h> +#include <ostream> +#include <string> + +namespace aapt { + +struct TestValue : public Value { + std::u16string value; + + TestValue(StringPiece16 str) : value(str.toString()) { + } + + TestValue* clone() const override { + return new TestValue(value); + } + + void print(std::ostream& out) const override { + out << "(test) " << value; + } + + virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} + virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} +}; + +struct TestWeakValue : public Value { + bool isWeak() const override { + return true; + } + + TestWeakValue* clone() const override { + return new TestWeakValue(); + } + + void print(std::ostream& out) const override { + out << "(test) [weak]"; + } + + virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} + virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} +}; + +TEST(ResourceTableTest, FailToAddResourceWithBadName) { + ResourceTable table; + table.setPackage(u"android"); + + EXPECT_FALSE(table.addResource( + ResourceNameRef{ u"android", ResourceType::kId, u"hey,there" }, + {}, SourceLine{ "test.xml", 21 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_FALSE(table.addResource( + ResourceNameRef{ u"android", ResourceType::kId, u"hey:there" }, + {}, SourceLine{ "test.xml", 21 }, + util::make_unique<TestValue>(u"rawValue"))); +} + +TEST(ResourceTableTest, AddOneResource) { + const std::u16string kAndroidPackage = u"android"; + + ResourceTable table; + table.setPackage(kAndroidPackage); + + const ResourceName name = { kAndroidPackage, ResourceType::kAttr, u"id" }; + + EXPECT_TRUE(table.addResource(name, {}, SourceLine{ "test/path/file.xml", 23 }, + util::make_unique<TestValue>(u"rawValue"))); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table.findResource(name); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + EXPECT_EQ(name.entry, entry->name); + + ASSERT_NE(std::end(entry->values), + std::find_if(std::begin(entry->values), std::end(entry->values), + [](const ResourceConfigValue& val) -> bool { + return val.config == ConfigDescription{}; + })); +} + +TEST(ResourceTableTest, AddMultipleResources) { + const std::u16string kAndroidPackage = u"android"; + ResourceTable table; + table.setPackage(kAndroidPackage); + + ConfigDescription config; + ConfigDescription languageConfig; + memcpy(languageConfig.language, "pl", sizeof(languageConfig.language)); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kAttr, u"layout_width" }, + config, SourceLine{ "test/path/file.xml", 10 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kAttr, u"id" }, + config, SourceLine{ "test/path/file.xml", 12 }, + util::make_unique<TestValue>(u"rawValue"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, + config, SourceLine{ "test/path/file.xml", 14 }, + util::make_unique<TestValue>(u"Ok"))); + + EXPECT_TRUE(table.addResource( + ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, + languageConfig, SourceLine{ "test/path/file.xml", 20 }, + util::make_unique<TestValue>(u"Tak"))); + + const auto endTypeIter = std::end(table); + auto typeIter = std::begin(table); + + ASSERT_NE(endTypeIter, typeIter); + EXPECT_EQ(ResourceType::kAttr, (*typeIter)->type); + + { + const std::unique_ptr<ResourceTableType>& type = *typeIter; + const auto endEntryIter = std::end(type->entries); + auto entryIter = std::begin(type->entries); + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"id"), (*entryIter)->name); + + ++entryIter; + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"layout_width"), (*entryIter)->name); + + ++entryIter; + ASSERT_EQ(endEntryIter, entryIter); + } + + ++typeIter; + ASSERT_NE(endTypeIter, typeIter); + EXPECT_EQ(ResourceType::kString, (*typeIter)->type); + + { + const std::unique_ptr<ResourceTableType>& type = *typeIter; + const auto endEntryIter = std::end(type->entries); + auto entryIter = std::begin(type->entries); + ASSERT_NE(endEntryIter, entryIter); + EXPECT_EQ(std::u16string(u"ok"), (*entryIter)->name); + + { + const std::unique_ptr<ResourceEntry>& entry = *entryIter; + const auto endConfigIter = std::end(entry->values); + auto configIter = std::begin(entry->values); + + ASSERT_NE(endConfigIter, configIter); + EXPECT_EQ(config, configIter->config); + const TestValue* value = + dynamic_cast<const TestValue*>(configIter->value.get()); + ASSERT_NE(nullptr, value); + EXPECT_EQ(std::u16string(u"Ok"), value->value); + + ++configIter; + ASSERT_NE(endConfigIter, configIter); + EXPECT_EQ(languageConfig, configIter->config); + EXPECT_NE(nullptr, configIter->value); + + value = dynamic_cast<const TestValue*>(configIter->value.get()); + ASSERT_NE(nullptr, value); + EXPECT_EQ(std::u16string(u"Tak"), value->value); + + ++configIter; + EXPECT_EQ(endConfigIter, configIter); + } + + ++entryIter; + ASSERT_EQ(endEntryIter, entryIter); + } + + ++typeIter; + EXPECT_EQ(endTypeIter, typeIter); +} + +TEST(ResourceTableTest, OverrideWeakResourceValue) { + const std::u16string kAndroid = u"android"; + + ResourceTable table; + table.setPackage(kAndroid); + table.setPackageId(0x01); + + ASSERT_TRUE(table.addResource( + ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, + {}, {}, util::make_unique<TestWeakValue>())); + + const ResourceTableType* type; + const ResourceEntry* entry; + std::tie(type, entry) = table.findResource( + ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + ASSERT_EQ(entry->values.size(), 1u); + EXPECT_TRUE(entry->values.front().value->isWeak()); + + ASSERT_TRUE(table.addResource(ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, {}, {}, + util::make_unique<TestValue>(u"bar"))); + + std::tie(type, entry) = table.findResource( + ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); + ASSERT_NE(nullptr, type); + ASSERT_NE(nullptr, entry); + ASSERT_EQ(entry->values.size(), 1u); + EXPECT_FALSE(entry->values.front().value->isWeak()); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTypeExtensions.h b/tools/aapt2/ResourceTypeExtensions.h new file mode 100644 index 0000000..60e225e --- /dev/null +++ b/tools/aapt2/ResourceTypeExtensions.h @@ -0,0 +1,120 @@ +/* + * 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_RESOURCE_TYPE_EXTENSIONS_H +#define AAPT_RESOURCE_TYPE_EXTENSIONS_H + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +/** + * New android::ResChunk_header types defined + * for AAPT to use. + * + * TODO(adamlesinski): Consider reserving these + * enums in androidfw/ResourceTypes.h to avoid + * future collisions. + */ +enum { + /** + * A chunk that holds the string pool + * for source entries (path/to/source:line). + */ + RES_TABLE_SOURCE_POOL_TYPE = 0x000e, + + /** + * A chunk holding names of externally + * defined symbols and offsets to where + * they are referenced in the table. + */ + RES_TABLE_SYMBOL_TABLE_TYPE = 0x000f, +}; + +/** + * New resource types that are meant to only be used + * by AAPT and will not end up on the device. + */ +struct ExtendedTypes { + enum { + /** + * A sentinel value used when a resource is defined as + * public but it has no defined value yet. If we don't + * flatten it with some value, we will lose its name. + */ + TYPE_SENTINEL = 0xff, + + /** + * A raw string value that hasn't had its escape sequences + * processed nor whitespace removed. + */ + TYPE_RAW_STRING = 0xfe + }; +}; + +/** + * A chunk with type RES_TABLE_SYMBOL_TABLE_TYPE. + * Following the header are count number of SymbolTable_entry + * structures, followed by an android::ResStringPool_header. + */ +struct SymbolTable_header { + android::ResChunk_header header; + + /** + * Number of SymbolTable_entry structures following + * this header. + */ + uint32_t count; +}; + +struct SymbolTable_entry { + /** + * Offset from the beginning of the resource table + * where the symbol entry is referenced. + */ + uint32_t offset; + + /** + * The index into the string pool where the name of this + * symbol exists. + */ + uint32_t stringIndex; +}; + +/** + * A structure representing the source of a resourc entry. + * Appears after an android::ResTable_entry or android::ResTable_map_entry. + * + * TODO(adamlesinski): This causes some issues when runtime code checks + * the size of an android::ResTable_entry. It assumes it is an + * android::ResTable_map_entry if the size is bigger than an android::ResTable_entry + * which may not be true if this structure is present. + */ +struct ResTable_entry_source { + /** + * Index into the source string pool. + */ + uint32_t pathIndex; + + /** + * Line number this resource was defined on. + */ + uint32_t line; +}; + +} // namespace aapt + +#endif // AAPT_RESOURCE_TYPE_EXTENSIONS_H diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp new file mode 100644 index 0000000..60ef1a8 --- /dev/null +++ b/tools/aapt2/ResourceValues.cpp @@ -0,0 +1,447 @@ +/* + * 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 "Resource.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <limits> + +namespace aapt { + +bool Value::isItem() const { + return false; +} + +bool Value::isWeak() const { + return false; +} + +bool Item::isItem() const { + return true; +} + +RawString::RawString(const StringPool::Ref& ref) : value(ref) { +} + +RawString* RawString::clone() const { + return new RawString(value); +} + +bool RawString::flatten(android::Res_value& outValue) const { + outValue.dataType = ExtendedTypes::TYPE_RAW_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +void RawString::print(std::ostream& out) const { + out << "(raw string) " << *value; +} + +Reference::Reference() : referenceType(Reference::Type::kResource) { +} + +Reference::Reference(const ResourceNameRef& n, Type t) : + name(n.toResourceName()), referenceType(t) { +} + +Reference::Reference(const ResourceId& i, Type type) : id(i), referenceType(type) { +} + +bool Reference::flatten(android::Res_value& outValue) const { + outValue.dataType = (referenceType == Reference::Type::kResource) + ? android::Res_value::TYPE_REFERENCE + : android::Res_value::TYPE_ATTRIBUTE; + outValue.data = id.id; + return true; +} + +Reference* Reference::clone() const { + Reference* ref = new Reference(); + ref->referenceType = referenceType; + ref->name = name; + ref->id = id; + return ref; +} + +void Reference::print(std::ostream& out) const { + out << "(reference) "; + if (referenceType == Reference::Type::kResource) { + out << "@"; + } else { + out << "?"; + } + + if (name.isValid()) { + out << name; + } + + if (id.isValid() || Res_INTERNALID(id.id)) { + out << " " << id; + } +} + +bool Id::isWeak() const { + return true; +} + +bool Id::flatten(android::Res_value& out) const { + out.dataType = android::Res_value::TYPE_NULL; + out.data = android::Res_value::DATA_NULL_UNDEFINED; + return true; +} + +Id* Id::clone() const { + return new Id(); +} + +void Id::print(std::ostream& out) const { + out << "(id)"; +} + +String::String(const StringPool::Ref& ref) : value(ref) { +} + +bool String::flatten(android::Res_value& outValue) const { + // Verify that our StringPool index is within encodeable limits. + if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +String* String::clone() const { + return new String(value); +} + +void String::print(std::ostream& out) const { + out << "(string) \"" << *value << "\""; +} + +StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) { +} + +bool StyledString::flatten(android::Res_value& outValue) const { + if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(value.getIndex()); + return true; +} + +StyledString* StyledString::clone() const { + return new StyledString(value); +} + +void StyledString::print(std::ostream& out) const { + out << "(styled string) \"" << *value->str << "\""; +} + +FileReference::FileReference(const StringPool::Ref& _path) : path(_path) { +} + +bool FileReference::flatten(android::Res_value& outValue) const { + if (path.getIndex() > std::numeric_limits<uint32_t>::max()) { + return false; + } + + outValue.dataType = android::Res_value::TYPE_STRING; + outValue.data = static_cast<uint32_t>(path.getIndex()); + return true; +} + +FileReference* FileReference::clone() const { + return new FileReference(path); +} + +void FileReference::print(std::ostream& out) const { + out << "(file) " << *path; +} + +BinaryPrimitive::BinaryPrimitive(const android::Res_value& val) : value(val) { +} + +bool BinaryPrimitive::flatten(android::Res_value& outValue) const { + outValue = value; + return true; +} + +BinaryPrimitive* BinaryPrimitive::clone() const { + return new BinaryPrimitive(value); +} + +void BinaryPrimitive::print(std::ostream& out) const { + switch (value.dataType) { + case android::Res_value::TYPE_NULL: + out << "(null)"; + break; + case android::Res_value::TYPE_INT_DEC: + out << "(integer) " << value.data; + break; + case android::Res_value::TYPE_INT_HEX: + out << "(integer) " << std::hex << value.data << std::dec; + break; + case android::Res_value::TYPE_INT_BOOLEAN: + out << "(boolean) " << (value.data != 0 ? "true" : "false"); + break; + case android::Res_value::TYPE_INT_COLOR_ARGB8: + case android::Res_value::TYPE_INT_COLOR_RGB8: + case android::Res_value::TYPE_INT_COLOR_ARGB4: + case android::Res_value::TYPE_INT_COLOR_RGB4: + out << "(color) #" << std::hex << value.data << std::dec; + break; + default: + out << "(unknown 0x" << std::hex << (int) value.dataType << ") 0x" + << std::hex << value.data << std::dec; + break; + } +} + +bool Sentinel::isWeak() const { + return true; +} + +bool Sentinel::flatten(android::Res_value& outValue) const { + outValue.dataType = ExtendedTypes::TYPE_SENTINEL; + outValue.data = 0; + return true; +} + +Sentinel* Sentinel::clone() const { + return new Sentinel(); +} + +void Sentinel::print(std::ostream& out) const { + out << "(sentinel)"; + return; +} + +Attribute::Attribute(bool w, uint32_t t) : weak(w), typeMask(t) { +} + +bool Attribute::isWeak() const { + return weak; +} + +Attribute* Attribute::clone() const { + Attribute* attr = new Attribute(weak); + attr->typeMask = typeMask; + std::copy(symbols.begin(), symbols.end(), std::back_inserter(attr->symbols)); + return attr; +} + +void Attribute::print(std::ostream& out) const { + out << "(attr)"; + if (typeMask == android::ResTable_map::TYPE_ANY) { + out << " any"; + return; + } + + bool set = false; + if ((typeMask & android::ResTable_map::TYPE_REFERENCE) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "reference"; + } + + if ((typeMask & android::ResTable_map::TYPE_STRING) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "string"; + } + + if ((typeMask & android::ResTable_map::TYPE_INTEGER) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "integer"; + } + + if ((typeMask & android::ResTable_map::TYPE_BOOLEAN) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "boolean"; + } + + if ((typeMask & android::ResTable_map::TYPE_COLOR) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "color"; + } + + if ((typeMask & android::ResTable_map::TYPE_FLOAT) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "float"; + } + + if ((typeMask & android::ResTable_map::TYPE_DIMENSION) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "dimension"; + } + + if ((typeMask & android::ResTable_map::TYPE_FRACTION) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "fraction"; + } + + if ((typeMask & android::ResTable_map::TYPE_ENUM) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "enum"; + } + + if ((typeMask & android::ResTable_map::TYPE_FLAGS) != 0) { + if (!set) { + out << " "; + set = true; + } else { + out << "|"; + } + out << "flags"; + } + + out << " [" + << util::joiner(symbols.begin(), symbols.end(), ", ") + << "]"; + + if (weak) { + out << " [weak]"; + } +} + +static ::std::ostream& operator<<(::std::ostream& out, const Attribute::Symbol& s) { + return out << s.symbol.name.entry << "=" << s.value; +} + +Style* Style::clone() const { + Style* style = new Style(); + style->parent = parent; + for (auto& entry : entries) { + style->entries.push_back(Entry{ + entry.key, + std::unique_ptr<Item>(entry.value->clone()) + }); + } + return style; +} + +void Style::print(std::ostream& out) const { + out << "(style) "; + if (!parent.name.entry.empty()) { + out << parent.name; + } + out << " [" + << util::joiner(entries.begin(), entries.end(), ", ") + << "]"; +} + +static ::std::ostream& operator<<(::std::ostream& out, const Style::Entry& value) { + out << value.key.name << " = "; + value.value->print(out); + return out; +} + +Array* Array::clone() const { + Array* array = new Array(); + for (auto& item : items) { + array->items.emplace_back(std::unique_ptr<Item>(item->clone())); + } + return array; +} + +void Array::print(std::ostream& out) const { + out << "(array) [" + << util::joiner(items.begin(), items.end(), ", ") + << "]"; +} + +Plural* Plural::clone() const { + Plural* p = new Plural(); + const size_t count = values.size(); + for (size_t i = 0; i < count; i++) { + if (values[i]) { + p->values[i] = std::unique_ptr<Item>(values[i]->clone()); + } + } + return p; +} + +void Plural::print(std::ostream& out) const { + out << "(plural)"; +} + +static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Item>& item) { + return out << *item; +} + +Styleable* Styleable::clone() const { + Styleable* styleable = new Styleable(); + std::copy(entries.begin(), entries.end(), std::back_inserter(styleable->entries)); + return styleable; +} + +void Styleable::print(std::ostream& out) const { + out << "(styleable) " << " [" + << util::joiner(entries.begin(), entries.end(), ", ") + << "]"; +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h new file mode 100644 index 0000000..f25bcf0 --- /dev/null +++ b/tools/aapt2/ResourceValues.h @@ -0,0 +1,456 @@ +/* + * 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_RESOURCE_VALUES_H +#define AAPT_RESOURCE_VALUES_H + +#include "Resource.h" +#include "StringPool.h" + +#include <array> +#include <androidfw/ResourceTypes.h> +#include <ostream> +#include <vector> + +namespace aapt { + +struct ValueVisitor; +struct ConstValueVisitor; +struct ValueVisitorArgs; + +/** + * A resource value. This is an all-encompassing representation + * of Item and Map and their subclasses. The way to do + * type specific operations is to check the Value's type() and + * cast it to the appropriate subclass. This isn't super clean, + * but it is the simplest strategy. + */ +struct Value { + /** + * Whether or not this is an Item. + */ + virtual bool isItem() const; + + /** + * Whether this value is weak and can be overriden without + * warning or error. Default for base class is false. + */ + virtual bool isWeak() const; + + /** + * Calls the appropriate overload of ValueVisitor. + */ + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) = 0; + + /** + * Const version of accept(). + */ + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const = 0; + + /** + * Clone the value. + */ + virtual Value* clone() const = 0; + + /** + * Human readable printout of this value. + */ + virtual void print(std::ostream& out) const = 0; +}; + +/** + * Inherit from this to get visitor accepting implementations for free. + */ +template <typename Derived> +struct BaseValue : public Value { + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; +}; + +/** + * A resource item with a single value. This maps to android::ResTable_entry. + */ +struct Item : public Value { + /** + * An Item is, of course, an Item. + */ + virtual bool isItem() const override; + + /** + * Clone the Item. + */ + virtual Item* clone() const override = 0; + + /** + * Fills in an android::Res_value structure with this Item's binary representation. + * Returns false if an error ocurred. + */ + virtual bool flatten(android::Res_value& outValue) const = 0; +}; + +/** + * Inherit from this to get visitor accepting implementations for free. + */ +template <typename Derived> +struct BaseItem : public Item { + virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; + virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; +}; + +/** + * A reference to another resource. This maps to android::Res_value::TYPE_REFERENCE. + * + * A reference can be symbolic (with the name set to a valid resource name) or be + * numeric (the id is set to a valid resource ID). + */ +struct Reference : public BaseItem<Reference> { + enum class Type { + kResource, + kAttribute, + }; + + ResourceName name; + ResourceId id; + Reference::Type referenceType; + bool privateReference = false; + + Reference(); + Reference(const ResourceNameRef& n, Type type = Type::kResource); + Reference(const ResourceId& i, Type type = Type::kResource); + + bool flatten(android::Res_value& outValue) const override; + Reference* clone() const override; + void print(std::ostream& out) const override; +}; + +/** + * An ID resource. Has no real value, just a place holder. + */ +struct Id : public BaseItem<Id> { + bool isWeak() const override; + bool flatten(android::Res_value& out) const override; + Id* clone() const override; + void print(std::ostream& out) const override; +}; + +/** + * A raw, unprocessed string. This may contain quotations, + * escape sequences, and whitespace. This shall *NOT* + * end up in the final resource table. + */ +struct RawString : public BaseItem<RawString> { + StringPool::Ref value; + + RawString(const StringPool::Ref& ref); + + bool flatten(android::Res_value& outValue) const override; + RawString* clone() const override; + void print(std::ostream& out) const override; +}; + +struct String : public BaseItem<String> { + StringPool::Ref value; + + String(const StringPool::Ref& ref); + + bool flatten(android::Res_value& outValue) const override; + String* clone() const override; + void print(std::ostream& out) const override; +}; + +struct StyledString : public BaseItem<StyledString> { + StringPool::StyleRef value; + + StyledString(const StringPool::StyleRef& ref); + + bool flatten(android::Res_value& outValue) const override; + StyledString* clone() const override; + void print(std::ostream& out) const override; +}; + +struct FileReference : public BaseItem<FileReference> { + StringPool::Ref path; + + FileReference() = default; + FileReference(const StringPool::Ref& path); + + bool flatten(android::Res_value& outValue) const override; + FileReference* clone() const override; + void print(std::ostream& out) const override; +}; + +/** + * Represents any other android::Res_value. + */ +struct BinaryPrimitive : public BaseItem<BinaryPrimitive> { + android::Res_value value; + + BinaryPrimitive() = default; + BinaryPrimitive(const android::Res_value& val); + + bool flatten(android::Res_value& outValue) const override; + BinaryPrimitive* clone() const override; + void print(::std::ostream& out) const override; +}; + +/** + * Sentinel value that should be ignored in the final output. + * Mainly used as a placeholder for public entries with no + * values defined yet. + */ +struct Sentinel : public BaseItem<Sentinel> { + bool isWeak() const override; + bool flatten(android::Res_value& outValue) const override; + Sentinel* clone() const override; + void print(::std::ostream& out) const override; +}; + +struct Attribute : public BaseValue<Attribute> { + struct Symbol { + Reference symbol; + uint32_t value; + }; + + bool weak; + uint32_t typeMask; + uint32_t minInt; + uint32_t maxInt; + std::vector<Symbol> symbols; + + Attribute(bool w, uint32_t t = 0u); + + bool isWeak() const override; + virtual Attribute* clone() const override; + virtual void print(std::ostream& out) const override; +}; + +struct Style : public BaseValue<Style> { + struct Entry { + Reference key; + std::unique_ptr<Item> value; + }; + + Reference parent; + std::vector<Entry> entries; + + Style* clone() const override; + void print(std::ostream& out) const override; +}; + +struct Array : public BaseValue<Array> { + std::vector<std::unique_ptr<Item>> items; + + Array* clone() const override; + void print(std::ostream& out) const override; +}; + +struct Plural : public BaseValue<Plural> { + enum { + Zero = 0, + One, + Two, + Few, + Many, + Other, + Count + }; + + std::array<std::unique_ptr<Item>, Count> values; + + Plural* clone() const override; + void print(std::ostream& out) const override; +}; + +struct Styleable : public BaseValue<Styleable> { + std::vector<Reference> entries; + + Styleable* clone() const override; + void print(std::ostream& out) const override; +}; + +/** + * Stream operator for printing Value objects. + */ +inline ::std::ostream& operator<<(::std::ostream& out, const Value& value) { + value.print(out); + return out; +} + +/** + * The argument object that gets passed through the value + * back to the ValueVisitor. Subclasses of ValueVisitor should + * subclass ValueVisitorArgs to contain the data they need + * to operate. + */ +struct ValueVisitorArgs {}; + +/** + * Visits a value and runs the appropriate method based on its type. + */ +struct ValueVisitor { + virtual void visit(Reference& reference, ValueVisitorArgs& args) { + visitItem(reference, args); + } + + virtual void visit(RawString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(String& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(StyledString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(FileReference& file, ValueVisitorArgs& args) { + visitItem(file, args); + } + + virtual void visit(Id& id, ValueVisitorArgs& args) { + visitItem(id, args); + } + + virtual void visit(BinaryPrimitive& primitive, ValueVisitorArgs& args) { + visitItem(primitive, args); + } + + virtual void visit(Sentinel& sentinel, ValueVisitorArgs& args) { + visitItem(sentinel, args); + } + + virtual void visit(Attribute& attr, ValueVisitorArgs& args) {} + virtual void visit(Style& style, ValueVisitorArgs& args) {} + virtual void visit(Array& array, ValueVisitorArgs& args) {} + virtual void visit(Plural& array, ValueVisitorArgs& args) {} + virtual void visit(Styleable& styleable, ValueVisitorArgs& args) {} + + virtual void visitItem(Item& item, ValueVisitorArgs& args) {} +}; + +/** + * Const version of ValueVisitor. + */ +struct ConstValueVisitor { + virtual void visit(const Reference& reference, ValueVisitorArgs& args) { + visitItem(reference, args); + } + + virtual void visit(const RawString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const String& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const StyledString& string, ValueVisitorArgs& args) { + visitItem(string, args); + } + + virtual void visit(const FileReference& file, ValueVisitorArgs& args) { + visitItem(file, args); + } + + virtual void visit(const Id& id, ValueVisitorArgs& args) { + visitItem(id, args); + } + + virtual void visit(const BinaryPrimitive& primitive, ValueVisitorArgs& args) { + visitItem(primitive, args); + } + + virtual void visit(const Sentinel& sentinel, ValueVisitorArgs& args) { + visitItem(sentinel, args); + } + + virtual void visit(const Attribute& attr, ValueVisitorArgs& args) {} + virtual void visit(const Style& style, ValueVisitorArgs& args) {} + virtual void visit(const Array& array, ValueVisitorArgs& args) {} + virtual void visit(const Plural& array, ValueVisitorArgs& args) {} + virtual void visit(const Styleable& styleable, ValueVisitorArgs& args) {} + + virtual void visitItem(const Item& item, ValueVisitorArgs& args) {} +}; + +/** + * Convenience Visitor that forwards a specific type to a function. + * Args are not used as the function can bind variables. Do not use + * directly, use the wrapper visitFunc() method. + */ +template <typename T, typename TFunc> +struct ValueVisitorFunc : ValueVisitor { + TFunc func; + + ValueVisitorFunc(TFunc f) : func(f) { + } + + void visit(T& value, ValueVisitorArgs&) override { + func(value); + } +}; + +/** + * Const version of ValueVisitorFunc. + */ +template <typename T, typename TFunc> +struct ConstValueVisitorFunc : ConstValueVisitor { + TFunc func; + + ConstValueVisitorFunc(TFunc f) : func(f) { + } + + void visit(const T& value, ValueVisitorArgs&) override { + func(value); + } +}; + +template <typename T, typename TFunc> +void visitFunc(Value& value, TFunc f) { + ValueVisitorFunc<T, TFunc> visitor(f); + value.accept(visitor, ValueVisitorArgs{}); +} + +template <typename T, typename TFunc> +void visitFunc(const Value& value, TFunc f) { + ConstValueVisitorFunc<T, TFunc> visitor(f); + value.accept(visitor, ValueVisitorArgs{}); +} + +template <typename Derived> +void BaseValue<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { + visitor.visit(static_cast<Derived&>(*this), args); +} + +template <typename Derived> +void BaseValue<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { + visitor.visit(static_cast<const Derived&>(*this), args); +} + +template <typename Derived> +void BaseItem<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { + visitor.visit(static_cast<Derived&>(*this), args); +} + +template <typename Derived> +void BaseItem<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { + visitor.visit(static_cast<const Derived&>(*this), args); +} + +} // namespace aapt + +#endif // AAPT_RESOURCE_VALUES_H diff --git a/tools/aapt2/Resource_test.cpp b/tools/aapt2/Resource_test.cpp new file mode 100644 index 0000000..d957999 --- /dev/null +++ b/tools/aapt2/Resource_test.cpp @@ -0,0 +1,120 @@ +/* + * 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 <gtest/gtest.h> + +#include "Resource.h" + +namespace aapt { + +TEST(ResourceTypeTest, ParseResourceTypes) { + const ResourceType* type = parseResourceType(u"anim"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAnim); + + type = parseResourceType(u"animator"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAnimator); + + type = parseResourceType(u"array"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kArray); + + type = parseResourceType(u"attr"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAttr); + + type = parseResourceType(u"^attr-private"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kAttrPrivate); + + type = parseResourceType(u"bool"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kBool); + + type = parseResourceType(u"color"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kColor); + + type = parseResourceType(u"dimen"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kDimen); + + type = parseResourceType(u"drawable"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kDrawable); + + type = parseResourceType(u"fraction"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kFraction); + + type = parseResourceType(u"id"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kId); + + type = parseResourceType(u"integer"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kInteger); + + type = parseResourceType(u"integer-array"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kIntegerArray); + + type = parseResourceType(u"interpolator"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kInterpolator); + + type = parseResourceType(u"layout"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kLayout); + + type = parseResourceType(u"menu"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kMenu); + + type = parseResourceType(u"mipmap"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kMipmap); + + type = parseResourceType(u"plurals"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kPlurals); + + type = parseResourceType(u"raw"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kRaw); + + type = parseResourceType(u"string"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kString); + + type = parseResourceType(u"style"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kStyle); + + type = parseResourceType(u"transition"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kTransition); + + type = parseResourceType(u"xml"); + ASSERT_NE(type, nullptr); + EXPECT_EQ(*type, ResourceType::kXml); + + type = parseResourceType(u"blahaha"); + EXPECT_EQ(type, nullptr); +} + +} // namespace aapt diff --git a/tools/aapt2/ScopedXmlPullParser.cpp b/tools/aapt2/ScopedXmlPullParser.cpp new file mode 100644 index 0000000..d9ae72c --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser.cpp @@ -0,0 +1,99 @@ +/* + * 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 "ScopedXmlPullParser.h" + +#include <string> + +namespace aapt { + +ScopedXmlPullParser::ScopedXmlPullParser(XmlPullParser* parser) : + mParser(parser), mDepth(parser->getDepth()), mDone(false) { +} + +ScopedXmlPullParser::~ScopedXmlPullParser() { + while (isGoodEvent(next())); +} + +XmlPullParser::Event ScopedXmlPullParser::next() { + if (mDone) { + return Event::kEndDocument; + } + + const Event event = mParser->next(); + if (mParser->getDepth() <= mDepth) { + mDone = true; + } + return event; +} + +XmlPullParser::Event ScopedXmlPullParser::getEvent() const { + return mParser->getEvent(); +} + +const std::string& ScopedXmlPullParser::getLastError() const { + return mParser->getLastError(); +} + +const std::u16string& ScopedXmlPullParser::getComment() const { + return mParser->getComment(); +} + +size_t ScopedXmlPullParser::getLineNumber() const { + return mParser->getLineNumber(); +} + +size_t ScopedXmlPullParser::getDepth() const { + const size_t depth = mParser->getDepth(); + if (depth < mDepth) { + return 0; + } + return depth - mDepth; +} + +const std::u16string& ScopedXmlPullParser::getText() const { + return mParser->getText(); +} + +const std::u16string& ScopedXmlPullParser::getNamespacePrefix() const { + return mParser->getNamespacePrefix(); +} + +const std::u16string& ScopedXmlPullParser::getNamespaceUri() const { + return mParser->getNamespaceUri(); +} + +const std::u16string& ScopedXmlPullParser::getElementNamespace() const { + return mParser->getElementNamespace(); +} + +const std::u16string& ScopedXmlPullParser::getElementName() const { + return mParser->getElementName(); +} + +size_t ScopedXmlPullParser::getAttributeCount() const { + return mParser->getAttributeCount(); +} + +XmlPullParser::const_iterator ScopedXmlPullParser::beginAttributes() const { + return mParser->beginAttributes(); +} + +XmlPullParser::const_iterator ScopedXmlPullParser::endAttributes() const { + return mParser->endAttributes(); +} + +} // namespace aapt diff --git a/tools/aapt2/ScopedXmlPullParser.h b/tools/aapt2/ScopedXmlPullParser.h new file mode 100644 index 0000000..e660499 --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser.h @@ -0,0 +1,83 @@ +/* + * 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_SCOPED_XML_PULL_PARSER_H +#define AAPT_SCOPED_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <string> + +namespace aapt { + +/** + * An XmlPullParser that will not read past the depth + * of the underlying parser. When this parser is destroyed, + * it moves the underlying parser to the same depth it + * started with. + * + * You can write code like this: + * + * while (XmlPullParser::isGoodEvent(parser.next())) { + * if (parser.getEvent() != XmlPullParser::Event::StartElement) { + * continue; + * } + * + * ScopedXmlPullParser scoped(parser); + * if (parser.getElementName() == u"id") { + * // do work. + * } else { + * // do nothing, as all the sub elements will be skipped + * // when scoped goes out of scope. + * } + * } + */ +class ScopedXmlPullParser : public XmlPullParser { +public: + ScopedXmlPullParser(XmlPullParser* parser); + ScopedXmlPullParser(const ScopedXmlPullParser&) = delete; + ScopedXmlPullParser& operator=(const ScopedXmlPullParser&) = delete; + ~ScopedXmlPullParser(); + + Event getEvent() const; + const std::string& getLastError() const; + Event next(); + + const std::u16string& getComment() const; + size_t getLineNumber() const; + size_t getDepth() const; + + const std::u16string& getText() const; + + const std::u16string& getNamespacePrefix() const; + const std::u16string& getNamespaceUri() const; + + const std::u16string& getElementNamespace() const; + const std::u16string& getElementName() const; + + const_iterator beginAttributes() const; + const_iterator endAttributes() const; + size_t getAttributeCount() const; + +private: + XmlPullParser* mParser; + size_t mDepth; + bool mDone; +}; + +} // namespace aapt + +#endif // AAPT_SCOPED_XML_PULL_PARSER_H diff --git a/tools/aapt2/ScopedXmlPullParser_test.cpp b/tools/aapt2/ScopedXmlPullParser_test.cpp new file mode 100644 index 0000000..342f305 --- /dev/null +++ b/tools/aapt2/ScopedXmlPullParser_test.cpp @@ -0,0 +1,106 @@ +/* + * 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 "ScopedXmlPullParser.h" +#include "SourceXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(ScopedXmlPullParserTest, StopIteratingAtNoNZeroDepth) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(XmlPullParser::Event::kEndElement, scopedParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, scopedParser.next()); + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +TEST(ScopedXmlPullParserTest, FinishCurrentElementOnDestruction) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +TEST(ScopedXmlPullParserTest, NestedParsersOperateCorrectly) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources><string><foo></foo></string></resources>" << std::endl; + + SourceXmlPullParser sourceParser(input); + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); + + { + ScopedXmlPullParser scopedParser(&sourceParser); + EXPECT_EQ(std::u16string(u"string"), scopedParser.getElementName()); + while (XmlPullParser::isGoodEvent(scopedParser.next())) { + if (scopedParser.getEvent() != XmlPullParser::Event::kStartElement) { + continue; + } + + ScopedXmlPullParser subScopedParser(&scopedParser); + EXPECT_EQ(std::u16string(u"foo"), subScopedParser.getElementName()); + } + } + + EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); + EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); +} + +} // namespace aapt diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp new file mode 100644 index 0000000..3f156a6 --- /dev/null +++ b/tools/aapt2/SdkConstants.cpp @@ -0,0 +1,693 @@ +/* + * 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 <string> +#include <unordered_map> + +namespace aapt { + +static const std::unordered_map<std::u16string, size_t> sAttrMap = { + { u"marqueeRepeatLimit", 2 }, + { u"windowNoDisplay", 3 }, + { u"backgroundDimEnabled", 3 }, + { u"inputType", 3 }, + { u"isDefault", 3 }, + { u"windowDisablePreview", 3 }, + { u"privateImeOptions", 3 }, + { u"editorExtras", 3 }, + { u"settingsActivity", 3 }, + { u"fastScrollEnabled", 3 }, + { u"reqTouchScreen", 3 }, + { u"reqKeyboardType", 3 }, + { u"reqHardKeyboard", 3 }, + { u"reqNavigation", 3 }, + { u"windowSoftInputMode", 3 }, + { u"imeFullscreenBackground", 3 }, + { u"noHistory", 3 }, + { u"headerDividersEnabled", 3 }, + { u"footerDividersEnabled", 3 }, + { u"candidatesTextStyleSpans", 3 }, + { u"smoothScrollbar", 3 }, + { u"reqFiveWayNav", 3 }, + { u"keyBackground", 3 }, + { u"keyTextSize", 3 }, + { u"labelTextSize", 3 }, + { u"keyTextColor", 3 }, + { u"keyPreviewLayout", 3 }, + { u"keyPreviewOffset", 3 }, + { u"keyPreviewHeight", 3 }, + { u"verticalCorrection", 3 }, + { u"popupLayout", 3 }, + { u"state_long_pressable", 3 }, + { u"keyWidth", 3 }, + { u"keyHeight", 3 }, + { u"horizontalGap", 3 }, + { u"verticalGap", 3 }, + { u"rowEdgeFlags", 3 }, + { u"codes", 3 }, + { u"popupKeyboard", 3 }, + { u"popupCharacters", 3 }, + { u"keyEdgeFlags", 3 }, + { u"isModifier", 3 }, + { u"isSticky", 3 }, + { u"isRepeatable", 3 }, + { u"iconPreview", 3 }, + { u"keyOutputText", 3 }, + { u"keyLabel", 3 }, + { u"keyIcon", 3 }, + { u"keyboardMode", 3 }, + { u"isScrollContainer", 3 }, + { u"fillEnabled", 3 }, + { u"updatePeriodMillis", 3 }, + { u"initialLayout", 3 }, + { u"voiceSearchMode", 3 }, + { u"voiceLanguageModel", 3 }, + { u"voicePromptText", 3 }, + { u"voiceLanguage", 3 }, + { u"voiceMaxResults", 3 }, + { u"bottomOffset", 3 }, + { u"topOffset", 3 }, + { u"allowSingleTap", 3 }, + { u"handle", 3 }, + { u"content", 3 }, + { u"animateOnClick", 3 }, + { u"configure", 3 }, + { u"hapticFeedbackEnabled", 3 }, + { u"innerRadius", 3 }, + { u"thickness", 3 }, + { u"sharedUserLabel", 3 }, + { u"dropDownWidth", 3 }, + { u"dropDownAnchor", 3 }, + { u"imeOptions", 3 }, + { u"imeActionLabel", 3 }, + { u"imeActionId", 3 }, + { u"imeExtractEnterAnimation", 3 }, + { u"imeExtractExitAnimation", 3 }, + { u"tension", 4 }, + { u"extraTension", 4 }, + { u"anyDensity", 4 }, + { u"searchSuggestThreshold", 4 }, + { u"includeInGlobalSearch", 4 }, + { u"onClick", 4 }, + { u"targetSdkVersion", 4 }, + { u"maxSdkVersion", 4 }, + { u"testOnly", 4 }, + { u"contentDescription", 4 }, + { u"gestureStrokeWidth", 4 }, + { u"gestureColor", 4 }, + { u"uncertainGestureColor", 4 }, + { u"fadeOffset", 4 }, + { u"fadeDuration", 4 }, + { u"gestureStrokeType", 4 }, + { u"gestureStrokeLengthThreshold", 4 }, + { u"gestureStrokeSquarenessThreshold", 4 }, + { u"gestureStrokeAngleThreshold", 4 }, + { u"eventsInterceptionEnabled", 4 }, + { u"fadeEnabled", 4 }, + { u"backupAgent", 4 }, + { u"allowBackup", 4 }, + { u"glEsVersion", 4 }, + { u"queryAfterZeroResults", 4 }, + { u"dropDownHeight", 4 }, + { u"smallScreens", 4 }, + { u"normalScreens", 4 }, + { u"largeScreens", 4 }, + { u"progressBarStyleInverse", 4 }, + { u"progressBarStyleSmallInverse", 4 }, + { u"progressBarStyleLargeInverse", 4 }, + { u"searchSettingsDescription", 4 }, + { u"textColorPrimaryInverseDisableOnly", 4 }, + { u"autoUrlDetect", 4 }, + { u"resizeable", 4 }, + { u"required", 5 }, + { u"accountType", 5 }, + { u"contentAuthority", 5 }, + { u"userVisible", 5 }, + { u"windowShowWallpaper", 5 }, + { u"wallpaperOpenEnterAnimation", 5 }, + { u"wallpaperOpenExitAnimation", 5 }, + { u"wallpaperCloseEnterAnimation", 5 }, + { u"wallpaperCloseExitAnimation", 5 }, + { u"wallpaperIntraOpenEnterAnimation", 5 }, + { u"wallpaperIntraOpenExitAnimation", 5 }, + { u"wallpaperIntraCloseEnterAnimation", 5 }, + { u"wallpaperIntraCloseExitAnimation", 5 }, + { u"supportsUploading", 5 }, + { u"killAfterRestore", 5 }, + { u"restoreNeedsApplication", 5 }, + { u"smallIcon", 5 }, + { u"accountPreferences", 5 }, + { u"textAppearanceSearchResultSubtitle", 5 }, + { u"textAppearanceSearchResultTitle", 5 }, + { u"summaryColumn", 5 }, + { u"detailColumn", 5 }, + { u"detailSocialSummary", 5 }, + { u"thumbnail", 5 }, + { u"detachWallpaper", 5 }, + { u"finishOnCloseSystemDialogs", 5 }, + { u"scrollbarFadeDuration", 5 }, + { u"scrollbarDefaultDelayBeforeFade", 5 }, + { u"fadeScrollbars", 5 }, + { u"colorBackgroundCacheHint", 5 }, + { u"dropDownHorizontalOffset", 5 }, + { u"dropDownVerticalOffset", 5 }, + { u"quickContactBadgeStyleWindowSmall", 6 }, + { u"quickContactBadgeStyleWindowMedium", 6 }, + { u"quickContactBadgeStyleWindowLarge", 6 }, + { u"quickContactBadgeStyleSmallWindowSmall", 6 }, + { u"quickContactBadgeStyleSmallWindowMedium", 6 }, + { u"quickContactBadgeStyleSmallWindowLarge", 6 }, + { u"author", 7 }, + { u"autoStart", 7 }, + { u"expandableListViewWhiteStyle", 8 }, + { u"installLocation", 8 }, + { u"vmSafeMode", 8 }, + { u"webTextViewStyle", 8 }, + { u"restoreAnyVersion", 8 }, + { u"tabStripLeft", 8 }, + { u"tabStripRight", 8 }, + { u"tabStripEnabled", 8 }, + { u"logo", 9 }, + { u"xlargeScreens", 9 }, + { u"immersive", 9 }, + { u"overScrollMode", 9 }, + { u"overScrollHeader", 9 }, + { u"overScrollFooter", 9 }, + { u"filterTouchesWhenObscured", 9 }, + { u"textSelectHandleLeft", 9 }, + { u"textSelectHandleRight", 9 }, + { u"textSelectHandle", 9 }, + { u"textSelectHandleWindowStyle", 9 }, + { u"popupAnimationStyle", 9 }, + { u"screenSize", 9 }, + { u"screenDensity", 9 }, + { u"allContactsName", 11 }, + { u"windowActionBar", 11 }, + { u"actionBarStyle", 11 }, + { u"navigationMode", 11 }, + { u"displayOptions", 11 }, + { u"subtitle", 11 }, + { u"customNavigationLayout", 11 }, + { u"hardwareAccelerated", 11 }, + { u"measureWithLargestChild", 11 }, + { u"animateFirstView", 11 }, + { u"dropDownSpinnerStyle", 11 }, + { u"actionDropDownStyle", 11 }, + { u"actionButtonStyle", 11 }, + { u"showAsAction", 11 }, + { u"previewImage", 11 }, + { u"actionModeBackground", 11 }, + { u"actionModeCloseDrawable", 11 }, + { u"windowActionModeOverlay", 11 }, + { u"valueFrom", 11 }, + { u"valueTo", 11 }, + { u"valueType", 11 }, + { u"propertyName", 11 }, + { u"ordering", 11 }, + { u"fragment", 11 }, + { u"windowActionBarOverlay", 11 }, + { u"fragmentOpenEnterAnimation", 11 }, + { u"fragmentOpenExitAnimation", 11 }, + { u"fragmentCloseEnterAnimation", 11 }, + { u"fragmentCloseExitAnimation", 11 }, + { u"fragmentFadeEnterAnimation", 11 }, + { u"fragmentFadeExitAnimation", 11 }, + { u"actionBarSize", 11 }, + { u"imeSubtypeLocale", 11 }, + { u"imeSubtypeMode", 11 }, + { u"imeSubtypeExtraValue", 11 }, + { u"splitMotionEvents", 11 }, + { u"listChoiceBackgroundIndicator", 11 }, + { u"spinnerMode", 11 }, + { u"animateLayoutChanges", 11 }, + { u"actionBarTabStyle", 11 }, + { u"actionBarTabBarStyle", 11 }, + { u"actionBarTabTextStyle", 11 }, + { u"actionOverflowButtonStyle", 11 }, + { u"actionModeCloseButtonStyle", 11 }, + { u"titleTextStyle", 11 }, + { u"subtitleTextStyle", 11 }, + { u"iconifiedByDefault", 11 }, + { u"actionLayout", 11 }, + { u"actionViewClass", 11 }, + { u"activatedBackgroundIndicator", 11 }, + { u"state_activated", 11 }, + { u"listPopupWindowStyle", 11 }, + { u"popupMenuStyle", 11 }, + { u"textAppearanceLargePopupMenu", 11 }, + { u"textAppearanceSmallPopupMenu", 11 }, + { u"breadCrumbTitle", 11 }, + { u"breadCrumbShortTitle", 11 }, + { u"listDividerAlertDialog", 11 }, + { u"textColorAlertDialogListItem", 11 }, + { u"loopViews", 11 }, + { u"dialogTheme", 11 }, + { u"alertDialogTheme", 11 }, + { u"dividerVertical", 11 }, + { u"homeAsUpIndicator", 11 }, + { u"enterFadeDuration", 11 }, + { u"exitFadeDuration", 11 }, + { u"selectableItemBackground", 11 }, + { u"autoAdvanceViewId", 11 }, + { u"useIntrinsicSizeAsMinimum", 11 }, + { u"actionModeCutDrawable", 11 }, + { u"actionModeCopyDrawable", 11 }, + { u"actionModePasteDrawable", 11 }, + { u"textEditPasteWindowLayout", 11 }, + { u"textEditNoPasteWindowLayout", 11 }, + { u"textIsSelectable", 11 }, + { u"windowEnableSplitTouch", 11 }, + { u"indeterminateProgressStyle", 11 }, + { u"progressBarPadding", 11 }, + { u"animationResolution", 11 }, + { u"state_accelerated", 11 }, + { u"baseline", 11 }, + { u"homeLayout", 11 }, + { u"opacity", 11 }, + { u"alpha", 11 }, + { u"transformPivotX", 11 }, + { u"transformPivotY", 11 }, + { u"translationX", 11 }, + { u"translationY", 11 }, + { u"scaleX", 11 }, + { u"scaleY", 11 }, + { u"rotation", 11 }, + { u"rotationX", 11 }, + { u"rotationY", 11 }, + { u"showDividers", 11 }, + { u"dividerPadding", 11 }, + { u"borderlessButtonStyle", 11 }, + { u"dividerHorizontal", 11 }, + { u"itemPadding", 11 }, + { u"buttonBarStyle", 11 }, + { u"buttonBarButtonStyle", 11 }, + { u"segmentedButtonStyle", 11 }, + { u"staticWallpaperPreview", 11 }, + { u"allowParallelSyncs", 11 }, + { u"isAlwaysSyncable", 11 }, + { u"verticalScrollbarPosition", 11 }, + { u"fastScrollAlwaysVisible", 11 }, + { u"fastScrollThumbDrawable", 11 }, + { u"fastScrollPreviewBackgroundLeft", 11 }, + { u"fastScrollPreviewBackgroundRight", 11 }, + { u"fastScrollTrackDrawable", 11 }, + { u"fastScrollOverlayPosition", 11 }, + { u"customTokens", 11 }, + { u"nextFocusForward", 11 }, + { u"firstDayOfWeek", 11 }, + { u"showWeekNumber", 11 }, + { u"minDate", 11 }, + { u"maxDate", 11 }, + { u"shownWeekCount", 11 }, + { u"selectedWeekBackgroundColor", 11 }, + { u"focusedMonthDateColor", 11 }, + { u"unfocusedMonthDateColor", 11 }, + { u"weekNumberColor", 11 }, + { u"weekSeparatorLineColor", 11 }, + { u"selectedDateVerticalBar", 11 }, + { u"weekDayTextAppearance", 11 }, + { u"dateTextAppearance", 11 }, + { u"solidColor", 11 }, + { u"spinnersShown", 11 }, + { u"calendarViewShown", 11 }, + { u"state_multiline", 11 }, + { u"detailsElementBackground", 11 }, + { u"textColorHighlightInverse", 11 }, + { u"textColorLinkInverse", 11 }, + { u"editTextColor", 11 }, + { u"editTextBackground", 11 }, + { u"horizontalScrollViewStyle", 11 }, + { u"layerType", 11 }, + { u"alertDialogIcon", 11 }, + { u"windowMinWidthMajor", 11 }, + { u"windowMinWidthMinor", 11 }, + { u"queryHint", 11 }, + { u"fastScrollTextColor", 11 }, + { u"largeHeap", 11 }, + { u"windowCloseOnTouchOutside", 11 }, + { u"datePickerStyle", 11 }, + { u"calendarViewStyle", 11 }, + { u"textEditSidePasteWindowLayout", 11 }, + { u"textEditSideNoPasteWindowLayout", 11 }, + { u"actionMenuTextAppearance", 11 }, + { u"actionMenuTextColor", 11 }, + { u"textCursorDrawable", 12 }, + { u"resizeMode", 12 }, + { u"requiresSmallestWidthDp", 12 }, + { u"compatibleWidthLimitDp", 12 }, + { u"largestWidthLimitDp", 12 }, + { u"state_hovered", 13 }, + { u"state_drag_can_accept", 13 }, + { u"state_drag_hovered", 13 }, + { u"stopWithTask", 13 }, + { u"switchTextOn", 13 }, + { u"switchTextOff", 13 }, + { u"switchPreferenceStyle", 13 }, + { u"switchTextAppearance", 13 }, + { u"track", 13 }, + { u"switchMinWidth", 13 }, + { u"switchPadding", 13 }, + { u"thumbTextPadding", 13 }, + { u"textSuggestionsWindowStyle", 13 }, + { u"textEditSuggestionItemLayout", 13 }, + { u"rowCount", 13 }, + { u"rowOrderPreserved", 13 }, + { u"columnCount", 13 }, + { u"columnOrderPreserved", 13 }, + { u"useDefaultMargins", 13 }, + { u"alignmentMode", 13 }, + { u"layout_row", 13 }, + { u"layout_rowSpan", 13 }, + { u"layout_columnSpan", 13 }, + { u"actionModeSelectAllDrawable", 13 }, + { u"isAuxiliary", 13 }, + { u"accessibilityEventTypes", 13 }, + { u"packageNames", 13 }, + { u"accessibilityFeedbackType", 13 }, + { u"notificationTimeout", 13 }, + { u"accessibilityFlags", 13 }, + { u"canRetrieveWindowContent", 13 }, + { u"listPreferredItemHeightLarge", 13 }, + { u"listPreferredItemHeightSmall", 13 }, + { u"actionBarSplitStyle", 13 }, + { u"actionProviderClass", 13 }, + { u"backgroundStacked", 13 }, + { u"backgroundSplit", 13 }, + { u"textAllCaps", 13 }, + { u"colorPressedHighlight", 13 }, + { u"colorLongPressedHighlight", 13 }, + { u"colorFocusedHighlight", 13 }, + { u"colorActivatedHighlight", 13 }, + { u"colorMultiSelectHighlight", 13 }, + { u"drawableStart", 13 }, + { u"drawableEnd", 13 }, + { u"actionModeStyle", 13 }, + { u"minResizeWidth", 13 }, + { u"minResizeHeight", 13 }, + { u"actionBarWidgetTheme", 13 }, + { u"uiOptions", 13 }, + { u"subtypeLocale", 13 }, + { u"subtypeExtraValue", 13 }, + { u"actionBarDivider", 13 }, + { u"actionBarItemBackground", 13 }, + { u"actionModeSplitBackground", 13 }, + { u"textAppearanceListItem", 13 }, + { u"textAppearanceListItemSmall", 13 }, + { u"targetDescriptions", 13 }, + { u"directionDescriptions", 13 }, + { u"overridesImplicitlyEnabledSubtype", 13 }, + { u"listPreferredItemPaddingLeft", 13 }, + { u"listPreferredItemPaddingRight", 13 }, + { u"requiresFadingEdge", 13 }, + { u"publicKey", 13 }, + { u"parentActivityName", 16 }, + { u"isolatedProcess", 16 }, + { u"importantForAccessibility", 16 }, + { u"keyboardLayout", 16 }, + { u"fontFamily", 16 }, + { u"mediaRouteButtonStyle", 16 }, + { u"mediaRouteTypes", 16 }, + { u"supportsRtl", 17 }, + { u"textDirection", 17 }, + { u"textAlignment", 17 }, + { u"layoutDirection", 17 }, + { u"paddingStart", 17 }, + { u"paddingEnd", 17 }, + { u"layout_marginStart", 17 }, + { u"layout_marginEnd", 17 }, + { u"layout_toStartOf", 17 }, + { u"layout_toEndOf", 17 }, + { u"layout_alignStart", 17 }, + { u"layout_alignEnd", 17 }, + { u"layout_alignParentStart", 17 }, + { u"layout_alignParentEnd", 17 }, + { u"listPreferredItemPaddingStart", 17 }, + { u"listPreferredItemPaddingEnd", 17 }, + { u"singleUser", 17 }, + { u"presentationTheme", 17 }, + { u"subtypeId", 17 }, + { u"initialKeyguardLayout", 17 }, + { u"widgetCategory", 17 }, + { u"permissionGroupFlags", 17 }, + { u"labelFor", 17 }, + { u"permissionFlags", 17 }, + { u"checkedTextViewStyle", 17 }, + { u"showOnLockScreen", 17 }, + { u"format12Hour", 17 }, + { u"format24Hour", 17 }, + { u"timeZone", 17 }, + { u"mipMap", 18 }, + { u"mirrorForRtl", 18 }, + { u"windowOverscan", 18 }, + { u"requiredForAllUsers", 18 }, + { u"indicatorStart", 18 }, + { u"indicatorEnd", 18 }, + { u"childIndicatorStart", 18 }, + { u"childIndicatorEnd", 18 }, + { u"restrictedAccountType", 18 }, + { u"requiredAccountType", 18 }, + { u"canRequestTouchExplorationMode", 18 }, + { u"canRequestEnhancedWebAccessibility", 18 }, + { u"canRequestFilterKeyEvents", 18 }, + { u"layoutMode", 18 }, + { u"keySet", 19 }, + { u"targetId", 19 }, + { u"fromScene", 19 }, + { u"toScene", 19 }, + { u"transition", 19 }, + { u"transitionOrdering", 19 }, + { u"fadingMode", 19 }, + { u"startDelay", 19 }, + { u"ssp", 19 }, + { u"sspPrefix", 19 }, + { u"sspPattern", 19 }, + { u"addPrintersActivity", 19 }, + { u"vendor", 19 }, + { u"category", 19 }, + { u"isAsciiCapable", 19 }, + { u"autoMirrored", 19 }, + { u"supportsSwitchingToNextInputMethod", 19 }, + { u"requireDeviceUnlock", 19 }, + { u"apduServiceBanner", 19 }, + { u"accessibilityLiveRegion", 19 }, + { u"windowTranslucentStatus", 19 }, + { u"windowTranslucentNavigation", 19 }, + { u"advancedPrintOptionsActivity", 19 }, + { u"banner", 20 }, + { u"windowSwipeToDismiss", 20 }, + { u"isGame", 20 }, + { u"allowEmbedded", 20 }, + { u"setupActivity", 20 }, + { u"fastScrollStyle", 21 }, + { u"windowContentTransitions", 21 }, + { u"windowContentTransitionManager", 21 }, + { u"translationZ", 21 }, + { u"tintMode", 21 }, + { u"controlX1", 21 }, + { u"controlY1", 21 }, + { u"controlX2", 21 }, + { u"controlY2", 21 }, + { u"transitionName", 21 }, + { u"transitionGroup", 21 }, + { u"viewportWidth", 21 }, + { u"viewportHeight", 21 }, + { u"fillColor", 21 }, + { u"pathData", 21 }, + { u"strokeColor", 21 }, + { u"strokeWidth", 21 }, + { u"trimPathStart", 21 }, + { u"trimPathEnd", 21 }, + { u"trimPathOffset", 21 }, + { u"strokeLineCap", 21 }, + { u"strokeLineJoin", 21 }, + { u"strokeMiterLimit", 21 }, + { u"colorControlNormal", 21 }, + { u"colorControlActivated", 21 }, + { u"colorButtonNormal", 21 }, + { u"colorControlHighlight", 21 }, + { u"persistableMode", 21 }, + { u"titleTextAppearance", 21 }, + { u"subtitleTextAppearance", 21 }, + { u"slideEdge", 21 }, + { u"actionBarTheme", 21 }, + { u"textAppearanceListItemSecondary", 21 }, + { u"colorPrimary", 21 }, + { u"colorPrimaryDark", 21 }, + { u"colorAccent", 21 }, + { u"nestedScrollingEnabled", 21 }, + { u"windowEnterTransition", 21 }, + { u"windowExitTransition", 21 }, + { u"windowSharedElementEnterTransition", 21 }, + { u"windowSharedElementExitTransition", 21 }, + { u"windowAllowReturnTransitionOverlap", 21 }, + { u"windowAllowEnterTransitionOverlap", 21 }, + { u"sessionService", 21 }, + { u"stackViewStyle", 21 }, + { u"switchStyle", 21 }, + { u"elevation", 21 }, + { u"excludeId", 21 }, + { u"excludeClass", 21 }, + { u"hideOnContentScroll", 21 }, + { u"actionOverflowMenuStyle", 21 }, + { u"documentLaunchMode", 21 }, + { u"maxRecents", 21 }, + { u"autoRemoveFromRecents", 21 }, + { u"stateListAnimator", 21 }, + { u"toId", 21 }, + { u"fromId", 21 }, + { u"reversible", 21 }, + { u"splitTrack", 21 }, + { u"targetName", 21 }, + { u"excludeName", 21 }, + { u"matchOrder", 21 }, + { u"windowDrawsSystemBarBackgrounds", 21 }, + { u"statusBarColor", 21 }, + { u"navigationBarColor", 21 }, + { u"contentInsetStart", 21 }, + { u"contentInsetEnd", 21 }, + { u"contentInsetLeft", 21 }, + { u"contentInsetRight", 21 }, + { u"paddingMode", 21 }, + { u"layout_rowWeight", 21 }, + { u"layout_columnWeight", 21 }, + { u"translateX", 21 }, + { u"translateY", 21 }, + { u"selectableItemBackgroundBorderless", 21 }, + { u"elegantTextHeight", 21 }, + { u"searchKeyphraseId", 21 }, + { u"searchKeyphrase", 21 }, + { u"searchKeyphraseSupportedLocales", 21 }, + { u"windowTransitionBackgroundFadeDuration", 21 }, + { u"overlapAnchor", 21 }, + { u"progressTint", 21 }, + { u"progressTintMode", 21 }, + { u"progressBackgroundTint", 21 }, + { u"progressBackgroundTintMode", 21 }, + { u"secondaryProgressTint", 21 }, + { u"secondaryProgressTintMode", 21 }, + { u"indeterminateTint", 21 }, + { u"indeterminateTintMode", 21 }, + { u"backgroundTint", 21 }, + { u"backgroundTintMode", 21 }, + { u"foregroundTint", 21 }, + { u"foregroundTintMode", 21 }, + { u"buttonTint", 21 }, + { u"buttonTintMode", 21 }, + { u"thumbTint", 21 }, + { u"thumbTintMode", 21 }, + { u"fullBackupOnly", 21 }, + { u"propertyXName", 21 }, + { u"propertyYName", 21 }, + { u"relinquishTaskIdentity", 21 }, + { u"tileModeX", 21 }, + { u"tileModeY", 21 }, + { u"actionModeShareDrawable", 21 }, + { u"actionModeFindDrawable", 21 }, + { u"actionModeWebSearchDrawable", 21 }, + { u"transitionVisibilityMode", 21 }, + { u"minimumHorizontalAngle", 21 }, + { u"minimumVerticalAngle", 21 }, + { u"maximumAngle", 21 }, + { u"searchViewStyle", 21 }, + { u"closeIcon", 21 }, + { u"goIcon", 21 }, + { u"searchIcon", 21 }, + { u"voiceIcon", 21 }, + { u"commitIcon", 21 }, + { u"suggestionRowLayout", 21 }, + { u"queryBackground", 21 }, + { u"submitBackground", 21 }, + { u"buttonBarPositiveButtonStyle", 21 }, + { u"buttonBarNeutralButtonStyle", 21 }, + { u"buttonBarNegativeButtonStyle", 21 }, + { u"popupElevation", 21 }, + { u"actionBarPopupTheme", 21 }, + { u"multiArch", 21 }, + { u"touchscreenBlocksFocus", 21 }, + { u"windowElevation", 21 }, + { u"launchTaskBehindTargetAnimation", 21 }, + { u"launchTaskBehindSourceAnimation", 21 }, + { u"restrictionType", 21 }, + { u"dayOfWeekBackground", 21 }, + { u"dayOfWeekTextAppearance", 21 }, + { u"headerMonthTextAppearance", 21 }, + { u"headerDayOfMonthTextAppearance", 21 }, + { u"headerYearTextAppearance", 21 }, + { u"yearListItemTextAppearance", 21 }, + { u"yearListSelectorColor", 21 }, + { u"calendarTextColor", 21 }, + { u"recognitionService", 21 }, + { u"timePickerStyle", 21 }, + { u"timePickerDialogTheme", 21 }, + { u"headerTimeTextAppearance", 21 }, + { u"headerAmPmTextAppearance", 21 }, + { u"numbersTextColor", 21 }, + { u"numbersBackgroundColor", 21 }, + { u"numbersSelectorColor", 21 }, + { u"amPmTextColor", 21 }, + { u"amPmBackgroundColor", 21 }, + { u"searchKeyphraseRecognitionFlags", 21 }, + { u"checkMarkTint", 21 }, + { u"checkMarkTintMode", 21 }, + { u"popupTheme", 21 }, + { u"toolbarStyle", 21 }, + { u"windowClipToOutline", 21 }, + { u"datePickerDialogTheme", 21 }, + { u"showText", 21 }, + { u"windowReturnTransition", 21 }, + { u"windowReenterTransition", 21 }, + { u"windowSharedElementReturnTransition", 21 }, + { u"windowSharedElementReenterTransition", 21 }, + { u"resumeWhilePausing", 21 }, + { u"datePickerMode", 21 }, + { u"timePickerMode", 21 }, + { u"inset", 21 }, + { u"letterSpacing", 21 }, + { u"fontFeatureSettings", 21 }, + { u"outlineProvider", 21 }, + { u"contentAgeHint", 21 }, + { u"country", 21 }, + { u"windowSharedElementsUseOverlay", 21 }, + { u"reparent", 21 }, + { u"reparentWithOverlay", 21 }, + { u"ambientShadowAlpha", 21 }, + { u"spotShadowAlpha", 21 }, + { u"navigationIcon", 21 }, + { u"navigationContentDescription", 21 }, + { u"fragmentExitTransition", 21 }, + { u"fragmentEnterTransition", 21 }, + { u"fragmentSharedElementEnterTransition", 21 }, + { u"fragmentReturnTransition", 21 }, + { u"fragmentSharedElementReturnTransition", 21 }, + { u"fragmentReenterTransition", 21 }, + { u"fragmentAllowEnterTransitionOverlap", 21 }, + { u"fragmentAllowReturnTransitionOverlap", 21 }, + { u"patternPathData", 21 }, + { u"strokeAlpha", 21 }, + { u"fillAlpha", 21 }, + { u"windowActivityTransitions", 21 }, + { u"colorEdgeEffect", 21 } +}; + +size_t findAttributeSdkLevel(const std::u16string& name) { + auto iter = sAttrMap.find(name); + if (iter != sAttrMap.end()) { + return iter->second; + } + return 0; +} + +} // namespace aapt diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h new file mode 100644 index 0000000..469c355 --- /dev/null +++ b/tools/aapt2/SdkConstants.h @@ -0,0 +1,53 @@ +/* + * 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_SDK_CONSTANTS_H +#define AAPT_SDK_CONSTANTS_H + +#include "Resource.h" + +#include <string> + +namespace aapt { + +enum { + SDK_CUPCAKE = 3, + SDK_DONUT = 4, + SDK_ECLAIR = 5, + SDK_ECLAIR_0_1 = 6, + SDK_ECLAIR_MR1 = 7, + SDK_FROYO = 8, + SDK_GINGERBREAD = 9, + SDK_GINGERBREAD_MR1 = 10, + SDK_HONEYCOMB = 11, + SDK_HONEYCOMB_MR1 = 12, + SDK_HONEYCOMB_MR2 = 13, + SDK_ICE_CREAM_SANDWICH = 14, + SDK_ICE_CREAM_SANDWICH_MR1 = 15, + SDK_JELLY_BEAN = 16, + SDK_JELLY_BEAN_MR1 = 17, + SDK_JELLY_BEAN_MR2 = 18, + SDK_KITKAT = 19, + SDK_KITKAT_WATCH = 20, + SDK_LOLLIPOP = 21, + SDK_LOLLIPOP_MR1 = 22, +}; + +size_t findAttributeSdkLevel(const std::u16string& name); + +} // namespace aapt + +#endif // AAPT_SDK_CONSTANTS_H diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h new file mode 100644 index 0000000..10c75aa --- /dev/null +++ b/tools/aapt2/Source.h @@ -0,0 +1,85 @@ +/* + * 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_SOURCE_H +#define AAPT_SOURCE_H + +#include <ostream> +#include <string> + +namespace aapt { + +struct SourceLineColumn; +struct SourceLine; + +/** + * Represents a file on disk. Used for logging and + * showing errors. + */ +struct Source { + std::string path; + + inline SourceLine line(size_t line) const; +}; + +/** + * Represents a file on disk and a line number in that file. + * Used for logging and showing errors. + */ +struct SourceLine { + std::string path; + size_t line; + + inline SourceLineColumn column(size_t column) const; +}; + +/** + * Represents a file on disk and a line:column number in that file. + * Used for logging and showing errors. + */ +struct SourceLineColumn { + std::string path; + size_t line; + size_t column; +}; + +// +// Implementations +// + +SourceLine Source::line(size_t line) const { + return SourceLine{ path, line }; +} + +SourceLineColumn SourceLine::column(size_t column) const { + return SourceLineColumn{ path, line, column }; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) { + return out << source.path; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const SourceLine& source) { + return out << source.path << ":" << source.line; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const SourceLineColumn& source) { + return out << source.path << ":" << source.line << ":" << source.column; +} + +} // namespace aapt + +#endif // AAPT_SOURCE_H diff --git a/tools/aapt2/SourceXmlPullParser.cpp b/tools/aapt2/SourceXmlPullParser.cpp new file mode 100644 index 0000000..cb6a3c0 --- /dev/null +++ b/tools/aapt2/SourceXmlPullParser.cpp @@ -0,0 +1,248 @@ +/* + * 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 <iostream> +#include <string> + +#include "SourceXmlPullParser.h" +#include "Util.h" + +namespace aapt { + +constexpr char kXmlNamespaceSep = 1; + +SourceXmlPullParser::SourceXmlPullParser(std::istream& in) : mIn(in), mEmpty(), mDepth(0) { + mParser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); + XML_SetUserData(mParser, this); + XML_SetElementHandler(mParser, startElementHandler, endElementHandler); + XML_SetNamespaceDeclHandler(mParser, startNamespaceHandler, endNamespaceHandler); + XML_SetCharacterDataHandler(mParser, characterDataHandler); + XML_SetCommentHandler(mParser, commentDataHandler); + mEventQueue.push(EventData{ Event::kStartDocument, 0, mDepth++ }); +} + +SourceXmlPullParser::~SourceXmlPullParser() { + XML_ParserFree(mParser); +} + +SourceXmlPullParser::Event SourceXmlPullParser::next() { + const Event currentEvent = getEvent(); + if (currentEvent == Event::kBadDocument || currentEvent == Event::kEndDocument) { + return currentEvent; + } + + mEventQueue.pop(); + while (mEventQueue.empty()) { + mIn.read(mBuffer, sizeof(mBuffer) / sizeof(*mBuffer)); + + const bool done = mIn.eof(); + if (mIn.bad() && !done) { + mLastError = strerror(errno); + mEventQueue.push(EventData{ Event::kBadDocument }); + continue; + } + + if (XML_Parse(mParser, mBuffer, mIn.gcount(), done) == XML_STATUS_ERROR) { + mLastError = XML_ErrorString(XML_GetErrorCode(mParser)); + mEventQueue.push(EventData{ Event::kBadDocument }); + continue; + } + + if (done) { + mEventQueue.push(EventData{ Event::kEndDocument, 0, 0 }); + } + } + + return getEvent(); +} + +SourceXmlPullParser::Event SourceXmlPullParser::getEvent() const { + return mEventQueue.front().event; +} + +const std::string& SourceXmlPullParser::getLastError() const { + return mLastError; +} + +const std::u16string& SourceXmlPullParser::getComment() const { + return mEventQueue.front().comment; +} + +size_t SourceXmlPullParser::getLineNumber() const { + return mEventQueue.front().lineNumber; +} + +size_t SourceXmlPullParser::getDepth() const { + return mEventQueue.front().depth; +} + +const std::u16string& SourceXmlPullParser::getText() const { + if (getEvent() != Event::kText) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getNamespacePrefix() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getNamespaceUri() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { + return mEmpty; + } + return mEventQueue.front().data2; +} + +const std::u16string& SourceXmlPullParser::getElementNamespace() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { + return mEmpty; + } + return mEventQueue.front().data1; +} + +const std::u16string& SourceXmlPullParser::getElementName() const { + const Event currentEvent = getEvent(); + if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { + return mEmpty; + } + return mEventQueue.front().data2; +} + +XmlPullParser::const_iterator SourceXmlPullParser::beginAttributes() const { + return mEventQueue.front().attributes.begin(); +} + +XmlPullParser::const_iterator SourceXmlPullParser::endAttributes() const { + return mEventQueue.front().attributes.end(); +} + +size_t SourceXmlPullParser::getAttributeCount() const { + if (getEvent() != Event::kStartElement) { + return 0; + } + return mEventQueue.front().attributes.size(); +} + +/** + * 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 = std::u16string(); + outName = util::utf8ToUtf16(name); + } else { + outNs = util::utf8ToUtf16(StringPiece(name, (p - name))); + outName = util::utf8ToUtf16(p + 1); + } +} + +void XMLCALL SourceXmlPullParser::startNamespaceHandler(void* userData, const char* prefix, + const char* uri) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + std::u16string namespaceUri = uri != nullptr ? util::utf8ToUtf16(uri) : std::u16string(); + parser->mNamespaceUris.push(namespaceUri); + parser->mEventQueue.push(EventData{ + Event::kStartNamespace, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth++, + prefix != nullptr ? util::utf8ToUtf16(prefix) : std::u16string(), + namespaceUri + }); +} + +void XMLCALL SourceXmlPullParser::startElementHandler(void* userData, const char* name, + const char** attrs) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + EventData data = { + Event::kStartElement, XML_GetCurrentLineNumber(parser->mParser), parser->mDepth++ + }; + splitName(name, data.data1, data.data2); + + while (*attrs) { + Attribute attribute; + splitName(*attrs++, attribute.namespaceUri, attribute.name); + attribute.value = util::utf8ToUtf16(*attrs++); + + // Insert in sorted order. + auto iter = std::lower_bound(data.attributes.begin(), data.attributes.end(), attribute); + data.attributes.insert(iter, std::move(attribute)); + } + + // Move the structure into the queue (no copy). + parser->mEventQueue.push(std::move(data)); +} + +void XMLCALL SourceXmlPullParser::characterDataHandler(void* userData, const char* s, int len) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kText, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth, + util::utf8ToUtf16(StringPiece(s, len)) + }); +} + +void XMLCALL SourceXmlPullParser::endElementHandler(void* userData, const char* name) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + EventData data = { + Event::kEndElement, XML_GetCurrentLineNumber(parser->mParser), --(parser->mDepth) + }; + splitName(name, data.data1, data.data2); + + // Move the data into the queue (no copy). + parser->mEventQueue.push(std::move(data)); +} + +void XMLCALL SourceXmlPullParser::endNamespaceHandler(void* userData, const char* prefix) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kEndNamespace, + XML_GetCurrentLineNumber(parser->mParser), + --(parser->mDepth), + prefix != nullptr ? util::utf8ToUtf16(prefix) : std::u16string(), + parser->mNamespaceUris.top() + }); + parser->mNamespaceUris.pop(); +} + +void XMLCALL SourceXmlPullParser::commentDataHandler(void* userData, const char* comment) { + SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + + parser->mEventQueue.push(EventData{ + Event::kComment, + XML_GetCurrentLineNumber(parser->mParser), + parser->mDepth, + util::utf8ToUtf16(comment) + }); +} + +} // namespace aapt diff --git a/tools/aapt2/SourceXmlPullParser.h b/tools/aapt2/SourceXmlPullParser.h new file mode 100644 index 0000000..ba904ba --- /dev/null +++ b/tools/aapt2/SourceXmlPullParser.h @@ -0,0 +1,87 @@ +/* + * 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_SOURCE_XML_PULL_PARSER_H +#define AAPT_SOURCE_XML_PULL_PARSER_H + +#include <istream> +#include <libexpat/expat.h> +#include <queue> +#include <stack> +#include <string> +#include <vector> + +#include "XmlPullParser.h" + +namespace aapt { + +class SourceXmlPullParser : public XmlPullParser { +public: + SourceXmlPullParser(std::istream& in); + SourceXmlPullParser(const SourceXmlPullParser& rhs) = delete; + ~SourceXmlPullParser(); + + Event getEvent() const; + const std::string& getLastError() const; + Event next(); + + const std::u16string& getComment() const; + size_t getLineNumber() const; + size_t getDepth() const; + + const std::u16string& getText() const; + + const std::u16string& getNamespacePrefix() const; + const std::u16string& getNamespaceUri() const; + + const std::u16string& getElementNamespace() const; + const std::u16string& getElementName() const; + + const_iterator beginAttributes() const; + const_iterator endAttributes() const; + size_t getAttributeCount() const; + +private: + static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri); + static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs); + static void XMLCALL characterDataHandler(void* userData, const char* s, int len); + static void XMLCALL endElementHandler(void* userData, const char* name); + static void XMLCALL endNamespaceHandler(void* userData, const char* prefix); + static void XMLCALL commentDataHandler(void* userData, const char* comment); + + struct EventData { + Event event; + size_t lineNumber; + size_t depth; + std::u16string data1; + std::u16string data2; + std::u16string comment; + std::vector<Attribute> attributes; + }; + + std::istream& mIn; + XML_Parser mParser; + char mBuffer[16384]; + std::queue<EventData> mEventQueue; + std::string mLastError; + const std::u16string mEmpty; + size_t mDepth; + std::stack<std::u16string> mNamespaceUris; +}; + +} // namespace aapt + +#endif // AAPT_SOURCE_XML_PULL_PARSER_H diff --git a/tools/aapt2/StringPiece.h b/tools/aapt2/StringPiece.h new file mode 100644 index 0000000..e2a1597 --- /dev/null +++ b/tools/aapt2/StringPiece.h @@ -0,0 +1,232 @@ +/* + * 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_STRING_PIECE_H +#define AAPT_STRING_PIECE_H + +#include <ostream> +#include <string> +#include <utils/String8.h> +#include <utils/Unicode.h> + +namespace aapt { + +/** + * Read only wrapper around basic C strings. + * Prevents excessive copying. + * + * WARNING: When creating from std::basic_string<>, moving the original + * std::basic_string<> will invalidate the data held in a BasicStringPiece<>. + * BasicStringPiece<> should only be used transitively. + */ +template <typename TChar> +class BasicStringPiece { +public: + using const_iterator = const TChar*; + + BasicStringPiece(); + BasicStringPiece(const BasicStringPiece<TChar>& str); + BasicStringPiece(const std::basic_string<TChar>& str); + BasicStringPiece(const TChar* str); + BasicStringPiece(const TChar* str, size_t len); + + BasicStringPiece<TChar>& operator=(const BasicStringPiece<TChar>& rhs); + BasicStringPiece<TChar>& assign(const TChar* str, size_t len); + + BasicStringPiece<TChar> substr(size_t start, size_t len) const; + BasicStringPiece<TChar> substr(BasicStringPiece<TChar>::const_iterator begin, + BasicStringPiece<TChar>::const_iterator end) const; + + const TChar* data() const; + size_t length() const; + size_t size() const; + bool empty() const; + std::basic_string<TChar> toString() const; + + int compare(const BasicStringPiece<TChar>& rhs) const; + bool operator<(const BasicStringPiece<TChar>& rhs) const; + bool operator>(const BasicStringPiece<TChar>& rhs) const; + bool operator==(const BasicStringPiece<TChar>& rhs) const; + bool operator!=(const BasicStringPiece<TChar>& rhs) const; + + const_iterator begin() const; + const_iterator end() const; + +private: + const TChar* mData; + size_t mLength; +}; + +using StringPiece = BasicStringPiece<char>; +using StringPiece16 = BasicStringPiece<char16_t>; + +// +// BasicStringPiece implementation. +// + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece() : mData(nullptr) , mLength(0) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const BasicStringPiece<TChar>& str) : + mData(str.mData), mLength(str.mLength) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const std::basic_string<TChar>& str) : + mData(str.data()), mLength(str.length()) { +} + +template <> +inline BasicStringPiece<char>::BasicStringPiece(const char* str) : + mData(str), mLength(str != nullptr ? strlen(str) : 0) { +} + +template <> +inline BasicStringPiece<char16_t>::BasicStringPiece(const char16_t* str) : + mData(str), mLength(str != nullptr ? strlen16(str) : 0) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>::BasicStringPiece(const TChar* str, size_t len) : + mData(str), mLength(len) { +} + +template <typename TChar> +inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::operator=( + const BasicStringPiece<TChar>& rhs) { + mData = rhs.mData; + mLength = rhs.mLength; + return *this; +} + +template <typename TChar> +inline BasicStringPiece<TChar>& BasicStringPiece<TChar>::assign(const TChar* str, size_t len) { + mData = str; + mLength = len; + return *this; +} + + +template <typename TChar> +inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr(size_t start, size_t len) const { + if (start + len > mLength) { + return BasicStringPiece<TChar>(); + } + return BasicStringPiece<TChar>(mData + start, len); +} + +template <typename TChar> +inline BasicStringPiece<TChar> BasicStringPiece<TChar>::substr( + BasicStringPiece<TChar>::const_iterator begin, + BasicStringPiece<TChar>::const_iterator end) const { + return BasicStringPiece<TChar>(begin, end - begin); +} + +template <typename TChar> +inline const TChar* BasicStringPiece<TChar>::data() const { + return mData; +} + +template <typename TChar> +inline size_t BasicStringPiece<TChar>::length() const { + return mLength; +} + +template <typename TChar> +inline size_t BasicStringPiece<TChar>::size() const { + return mLength; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::empty() const { + return mLength == 0; +} + +template <typename TChar> +inline std::basic_string<TChar> BasicStringPiece<TChar>::toString() const { + return std::basic_string<TChar>(mData, mLength); +} + +template <> +inline int BasicStringPiece<char>::compare(const BasicStringPiece<char>& rhs) const { + const char nullStr = '\0'; + const char* b1 = mData != nullptr ? mData : &nullStr; + const char* e1 = b1 + mLength; + const char* b2 = rhs.mData != nullptr ? rhs.mData : &nullStr; + const char* e2 = b2 + rhs.mLength; + + while (b1 < e1 && b2 < e2) { + const int d = static_cast<int>(*b1++) - static_cast<int>(*b2++); + if (d) { + return d; + } + } + return static_cast<int>(mLength - rhs.mLength); +} + +inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char16_t>& str) { + android::String8 utf8(str.data(), str.size()); + return out.write(utf8.string(), utf8.size()); +} + + +template <> +inline int BasicStringPiece<char16_t>::compare(const BasicStringPiece<char16_t>& rhs) const { + const char16_t nullStr = u'\0'; + const char16_t* b1 = mData != nullptr ? mData : &nullStr; + const char16_t* b2 = rhs.mData != nullptr ? rhs.mData : &nullStr; + return strzcmp16(b1, mLength, b2, rhs.mLength); +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator<(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) < 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator>(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) > 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator==(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) == 0; +} + +template <typename TChar> +inline bool BasicStringPiece<TChar>::operator!=(const BasicStringPiece<TChar>& rhs) const { + return compare(rhs) != 0; +} + +template <typename TChar> +inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::begin() const { + return mData; +} + +template <typename TChar> +inline typename BasicStringPiece<TChar>::const_iterator BasicStringPiece<TChar>::end() const { + return mData + mLength; +} + +inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<char>& str) { + return out.write(str.data(), str.size()); +} + +} // namespace aapt + +#endif // AAPT_STRING_PIECE_H diff --git a/tools/aapt2/StringPiece_test.cpp b/tools/aapt2/StringPiece_test.cpp new file mode 100644 index 0000000..43f7a37 --- /dev/null +++ b/tools/aapt2/StringPiece_test.cpp @@ -0,0 +1,62 @@ +/* + * 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 <algorithm> +#include <gtest/gtest.h> +#include <string> +#include <vector> + +#include "StringPiece.h" + +namespace aapt { + +TEST(StringPieceTest, CompareNonNullTerminatedPiece) { + StringPiece a("hello world", 5); + StringPiece b("hello moon", 5); + EXPECT_EQ(a, b); + + StringPiece16 a16(u"hello world", 5); + StringPiece16 b16(u"hello moon", 5); + EXPECT_EQ(a16, b16); +} + +TEST(StringPieceTest, PiecesHaveCorrectSortOrder) { + std::u16string testing(u"testing"); + std::u16string banana(u"banana"); + std::u16string car(u"car"); + + EXPECT_TRUE(StringPiece16(testing) > banana); + EXPECT_TRUE(StringPiece16(testing) > car); + EXPECT_TRUE(StringPiece16(banana) < testing); + EXPECT_TRUE(StringPiece16(banana) < car); + EXPECT_TRUE(StringPiece16(car) < testing); + EXPECT_TRUE(StringPiece16(car) > banana); +} + +TEST(StringPieceTest, PiecesHaveCorrectSortOrderUtf8) { + std::string testing("testing"); + std::string banana("banana"); + std::string car("car"); + + EXPECT_TRUE(StringPiece(testing) > banana); + EXPECT_TRUE(StringPiece(testing) > car); + EXPECT_TRUE(StringPiece(banana) < testing); + EXPECT_TRUE(StringPiece(banana) < car); + EXPECT_TRUE(StringPiece(car) < testing); + EXPECT_TRUE(StringPiece(car) > banana); +} + +} // namespace aapt diff --git a/tools/aapt2/StringPool.cpp b/tools/aapt2/StringPool.cpp new file mode 100644 index 0000000..b159a00 --- /dev/null +++ b/tools/aapt2/StringPool.cpp @@ -0,0 +1,348 @@ +/* + * 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 "StringPiece.h" +#include "StringPool.h" +#include "Util.h" + +#include <algorithm> +#include <androidfw/ResourceTypes.h> +#include <memory> +#include <string> + +namespace aapt { + +StringPool::Ref::Ref() : mEntry(nullptr) { +} + +StringPool::Ref::Ref(const StringPool::Ref& rhs) : mEntry(rhs.mEntry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::Ref::Ref(StringPool::Entry* entry) : mEntry(entry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::Ref::~Ref() { + if (mEntry != nullptr) { + mEntry->ref--; + } +} + +StringPool::Ref& StringPool::Ref::operator=(const StringPool::Ref& rhs) { + if (rhs.mEntry != nullptr) { + rhs.mEntry->ref++; + } + + if (mEntry != nullptr) { + mEntry->ref--; + } + mEntry = rhs.mEntry; + return *this; +} + +const std::u16string* StringPool::Ref::operator->() const { + return &mEntry->value; +} + +const std::u16string& StringPool::Ref::operator*() const { + return mEntry->value; +} + +size_t StringPool::Ref::getIndex() const { + return mEntry->index; +} + +const StringPool::Context& StringPool::Ref::getContext() const { + return mEntry->context; +} + +StringPool::StyleRef::StyleRef() : mEntry(nullptr) { +} + +StringPool::StyleRef::StyleRef(const StringPool::StyleRef& rhs) : mEntry(rhs.mEntry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::StyleRef::StyleRef(StringPool::StyleEntry* entry) : mEntry(entry) { + if (mEntry != nullptr) { + mEntry->ref++; + } +} + +StringPool::StyleRef::~StyleRef() { + if (mEntry != nullptr) { + mEntry->ref--; + } +} + +StringPool::StyleRef& StringPool::StyleRef::operator=(const StringPool::StyleRef& rhs) { + if (rhs.mEntry != nullptr) { + rhs.mEntry->ref++; + } + + if (mEntry != nullptr) { + mEntry->ref--; + } + mEntry = rhs.mEntry; + return *this; +} + +const StringPool::StyleEntry* StringPool::StyleRef::operator->() const { + return mEntry; +} + +const StringPool::StyleEntry& StringPool::StyleRef::operator*() const { + return *mEntry; +} + +size_t StringPool::StyleRef::getIndex() const { + return mEntry->str.getIndex(); +} + +const StringPool::Context& StringPool::StyleRef::getContext() const { + return mEntry->str.getContext(); +} + +StringPool::Ref StringPool::makeRef(const StringPiece16& str) { + return makeRefImpl(str, Context{}, true); +} + +StringPool::Ref StringPool::makeRef(const StringPiece16& str, const Context& context) { + return makeRefImpl(str, context, true); +} + +StringPool::Ref StringPool::makeRefImpl(const StringPiece16& str, const Context& context, + bool unique) { + if (unique) { + auto iter = mIndexedStrings.find(str); + if (iter != std::end(mIndexedStrings)) { + return Ref(iter->second); + } + } + + Entry* entry = new Entry(); + entry->value = str.toString(); + entry->context = context; + entry->index = mStrings.size(); + entry->ref = 0; + mStrings.emplace_back(entry); + mIndexedStrings.insert(std::make_pair(StringPiece16(entry->value), entry)); + return Ref(entry); +} + +StringPool::StyleRef StringPool::makeRef(const StyleString& str) { + return makeRef(str, Context{}); +} + +StringPool::StyleRef StringPool::makeRef(const StyleString& str, const Context& context) { + Entry* entry = new Entry(); + entry->value = str.str; + entry->context = context; + entry->index = mStrings.size(); + entry->ref = 0; + mStrings.emplace_back(entry); + mIndexedStrings.insert(std::make_pair(StringPiece16(entry->value), entry)); + + StyleEntry* styleEntry = new StyleEntry(); + styleEntry->str = Ref(entry); + for (const aapt::Span& span : str.spans) { + styleEntry->spans.emplace_back(Span{makeRef(span.name), + span.firstChar, span.lastChar}); + } + styleEntry->ref = 0; + mStyles.emplace_back(styleEntry); + return StyleRef(styleEntry); +} + +void StringPool::merge(StringPool&& pool) { + mIndexedStrings.insert(pool.mIndexedStrings.begin(), pool.mIndexedStrings.end()); + pool.mIndexedStrings.clear(); + std::move(pool.mStrings.begin(), pool.mStrings.end(), std::back_inserter(mStrings)); + pool.mStrings.clear(); + std::move(pool.mStyles.begin(), pool.mStyles.end(), std::back_inserter(mStyles)); + pool.mStyles.clear(); + + // Assign the indices. + const size_t len = mStrings.size(); + for (size_t index = 0; index < len; index++) { + mStrings[index]->index = index; + } +} + +void StringPool::hintWillAdd(size_t stringCount, size_t styleCount) { + mStrings.reserve(mStrings.size() + stringCount); + mStyles.reserve(mStyles.size() + styleCount); +} + +void StringPool::prune() { + const auto iterEnd = std::end(mIndexedStrings); + auto indexIter = std::begin(mIndexedStrings); + while (indexIter != iterEnd) { + if (indexIter->second->ref <= 0) { + mIndexedStrings.erase(indexIter++); + } else { + ++indexIter; + } + } + + auto endIter2 = std::remove_if(std::begin(mStrings), std::end(mStrings), + [](const std::unique_ptr<Entry>& entry) -> bool { + return entry->ref <= 0; + } + ); + + auto endIter3 = std::remove_if(std::begin(mStyles), std::end(mStyles), + [](const std::unique_ptr<StyleEntry>& entry) -> bool { + return entry->ref <= 0; + } + ); + + // Remove the entries at the end or else we'll be accessing + // a deleted string from the StyleEntry. + mStrings.erase(endIter2, std::end(mStrings)); + mStyles.erase(endIter3, std::end(mStyles)); +} + +void StringPool::sort(const std::function<bool(const Entry&, const Entry&)>& cmp) { + std::sort(std::begin(mStrings), std::end(mStrings), + [&cmp](const std::unique_ptr<Entry>& a, const std::unique_ptr<Entry>& b) -> bool { + return cmp(*a, *b); + } + ); + + // Assign the indices. + const size_t len = mStrings.size(); + for (size_t index = 0; index < len; index++) { + mStrings[index]->index = index; + } + + // Reorder the styles. + std::sort(std::begin(mStyles), std::end(mStyles), + [](const std::unique_ptr<StyleEntry>& lhs, + const std::unique_ptr<StyleEntry>& rhs) -> bool { + return lhs->str.getIndex() < rhs->str.getIndex(); + } + ); +} + +static uint8_t* encodeLength(uint8_t* data, size_t length) { + if (length > 0x7fu) { + *data++ = 0x80u | (0x000000ffu & (length >> 8)); + } + *data++ = 0x000000ffu & length; + return data; +} + +static size_t encodedLengthByteCount(size_t length) { + return length > 0x7fu ? 2 : 1; +} + +bool StringPool::flattenUtf8(BigBuffer* out, const StringPool& pool) { + const size_t startIndex = out->size(); + android::ResStringPool_header* header = out->nextBlock<android::ResStringPool_header>(); + header->header.type = android::RES_STRING_POOL_TYPE; + header->header.headerSize = sizeof(*header); + header->stringCount = pool.size(); + header->flags |= android::ResStringPool_header::UTF8_FLAG; + + uint32_t* indices = out->nextBlock<uint32_t>(pool.size()); + + uint32_t* styleIndices = nullptr; + if (!pool.mStyles.empty()) { + header->styleCount = pool.mStyles.back()->str.getIndex() + 1; + styleIndices = out->nextBlock<uint32_t>(header->styleCount); + } + + const size_t beforeStringsIndex = out->size(); + header->stringsStart = beforeStringsIndex - startIndex; + + for (const auto& entry : pool) { + *indices = out->size() - beforeStringsIndex; + indices++; + + std::string encoded = util::utf16ToUtf8(entry->value); + + const size_t stringByteLength = sizeof(char) * encoded.length(); + const size_t totalSize = encodedLengthByteCount(entry->value.size()) + + encodedLengthByteCount(encoded.length()) + + stringByteLength + + sizeof(char); + + uint8_t* data = out->nextBlock<uint8_t>(totalSize); + + // First encode the actual UTF16 string length. + data = encodeLength(data, entry->value.size()); + + // Now encode the size of the converted UTF8 string. + data = encodeLength(data, encoded.length()); + + memcpy(data, encoded.data(), stringByteLength); + data += stringByteLength; + *data = 0; + } + + out->align4(); + + if (!pool.mStyles.empty()) { + const size_t beforeStylesIndex = out->size(); + header->stylesStart = beforeStylesIndex - startIndex; + + size_t currentIndex = 0; + for (const auto& entry : pool.mStyles) { + while (entry->str.getIndex() > currentIndex) { + styleIndices[currentIndex++] = out->size() - beforeStylesIndex; + + uint32_t* spanOffset = out->nextBlock<uint32_t>(); + *spanOffset = android::ResStringPool_span::END; + } + styleIndices[currentIndex++] = out->size() - beforeStylesIndex; + + android::ResStringPool_span* span = + out->nextBlock<android::ResStringPool_span>(entry->spans.size()); + for (const auto& s : entry->spans) { + span->name.index = s.name.getIndex(); + span->firstChar = s.firstChar; + span->lastChar = s.lastChar; + span++; + } + + uint32_t* spanEnd = out->nextBlock<uint32_t>(); + *spanEnd = android::ResStringPool_span::END; + } + + // The error checking code in the platform looks for an entire + // ResStringPool_span structure worth of 0xFFFFFFFF at the end + // of the style block, so fill in the remaining 2 32bit words + // with 0xFFFFFFFF. + const size_t paddingLength = sizeof(android::ResStringPool_span) + - sizeof(android::ResStringPool_span::name); + uint8_t* padding = out->nextBlock<uint8_t>(paddingLength); + memset(padding, 0xff, paddingLength); + out->align4(); + } + header->header.size = out->size() - startIndex; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/StringPool.h b/tools/aapt2/StringPool.h new file mode 100644 index 0000000..2aa5b65 --- /dev/null +++ b/tools/aapt2/StringPool.h @@ -0,0 +1,215 @@ +/* + * 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_STRING_POOL_H +#define AAPT_STRING_POOL_H + +#include "BigBuffer.h" +#include "ConfigDescription.h" +#include "StringPiece.h" + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { + +struct Span { + std::u16string name; + uint32_t firstChar; + uint32_t lastChar; +}; + +struct StyleString { + std::u16string str; + std::vector<Span> spans; +}; + +class StringPool { +public: + struct Context { + uint32_t priority; + ConfigDescription config; + }; + + class Entry; + + class Ref { + public: + Ref(); + Ref(const Ref&); + ~Ref(); + + Ref& operator=(const Ref& rhs); + const std::u16string* operator->() const; + const std::u16string& operator*() const; + + size_t getIndex() const; + const Context& getContext() const; + + private: + friend class StringPool; + + Ref(Entry* entry); + + Entry* mEntry; + }; + + class StyleEntry; + + class StyleRef { + public: + StyleRef(); + StyleRef(const StyleRef&); + ~StyleRef(); + + StyleRef& operator=(const StyleRef& rhs); + const StyleEntry* operator->() const; + const StyleEntry& operator*() const; + + size_t getIndex() const; + const Context& getContext() const; + + private: + friend class StringPool; + + StyleRef(StyleEntry* entry); + + StyleEntry* mEntry; + }; + + class Entry { + public: + std::u16string value; + Context context; + size_t index; + + private: + friend class StringPool; + friend class Ref; + + int ref; + }; + + struct Span { + Ref name; + uint32_t firstChar; + uint32_t lastChar; + }; + + class StyleEntry { + public: + Ref str; + std::vector<Span> spans; + + private: + friend class StringPool; + friend class StyleRef; + + int ref; + }; + + using const_iterator = std::vector<std::unique_ptr<Entry>>::const_iterator; + + static bool flattenUtf8(BigBuffer* out, const StringPool& pool); + static bool flatten(BigBuffer* out, const StringPool& pool); + + StringPool() = default; + StringPool(const StringPool&) = delete; + + /** + * Adds a string to the pool, unless it already exists. Returns + * a reference to the string in the pool. + */ + Ref makeRef(const StringPiece16& str); + + /** + * Adds a string to the pool, unless it already exists, with a context + * object that can be used when sorting the string pool. Returns + * a reference to the string in the pool. + */ + Ref makeRef(const StringPiece16& str, const Context& context); + + /** + * Adds a style to the string pool and returns a reference to it. + */ + StyleRef makeRef(const StyleString& str); + + /** + * Adds a style to the string pool with a context object that + * can be used when sorting the string pool. Returns a reference + * to the style in the string pool. + */ + StyleRef makeRef(const StyleString& str, const Context& context); + + /** + * Moves pool into this one without coalescing strings. When this + * function returns, pool will be empty. + */ + void merge(StringPool&& pool); + + /** + * Retuns the number of strings in the table. + */ + inline size_t size() const; + + /** + * Reserves space for strings and styles as an optimization. + */ + void hintWillAdd(size_t stringCount, size_t styleCount); + + /** + * Sorts the strings according to some comparison function. + */ + void sort(const std::function<bool(const Entry&, const Entry&)>& cmp); + + /** + * Removes any strings that have no references. + */ + void prune(); + +private: + friend const_iterator begin(const StringPool& pool); + friend const_iterator end(const StringPool& pool); + + Ref makeRefImpl(const StringPiece16& str, const Context& context, bool unique); + + std::vector<std::unique_ptr<Entry>> mStrings; + std::vector<std::unique_ptr<StyleEntry>> mStyles; + std::multimap<StringPiece16, Entry*> mIndexedStrings; +}; + +// +// Inline implementation +// + +inline size_t StringPool::size() const { + return mStrings.size(); +} + +inline StringPool::const_iterator begin(const StringPool& pool) { + return pool.mStrings.begin(); +} + +inline StringPool::const_iterator end(const StringPool& pool) { + return pool.mStrings.end(); +} + +} // namespace aapt + +#endif // AAPT_STRING_POOL_H diff --git a/tools/aapt2/StringPool_test.cpp b/tools/aapt2/StringPool_test.cpp new file mode 100644 index 0000000..5ee1a2d --- /dev/null +++ b/tools/aapt2/StringPool_test.cpp @@ -0,0 +1,218 @@ +/* + * 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 "StringPool.h" +#include "Util.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +TEST(StringPoolTest, InsertOneString) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + EXPECT_EQ(*ref, u"wut"); +} + +TEST(StringPoolTest, InsertTwoUniqueStrings) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + StringPool::Ref ref2 = pool.makeRef(u"hey"); + + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(*ref2, u"hey"); +} + +TEST(StringPoolTest, DoNotInsertNewDuplicateString) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"wut"); + StringPool::Ref ref2 = pool.makeRef(u"wut"); + + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(*ref2, u"wut"); + EXPECT_EQ(1u, pool.size()); +} + +TEST(StringPoolTest, MaintainInsertionOrderIndex) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::Ref ref2 = pool.makeRef(u"a"); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + EXPECT_EQ(0u, ref.getIndex()); + EXPECT_EQ(1u, ref2.getIndex()); + EXPECT_EQ(2u, ref3.getIndex()); +} + +TEST(StringPoolTest, PruneStringsWithNoReferences) { + StringPool pool; + + { + StringPool::Ref ref = pool.makeRef(u"wut"); + EXPECT_EQ(*ref, u"wut"); + EXPECT_EQ(1u, pool.size()); + } + + EXPECT_EQ(1u, pool.size()); + pool.prune(); + EXPECT_EQ(0u, pool.size()); +} + +TEST(StringPoolTest, SortAndMaintainIndexesInReferences) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::StyleRef ref2 = pool.makeRef(StyleString{ {u"a"} }); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + EXPECT_EQ(*ref, u"z"); + EXPECT_EQ(0u, ref.getIndex()); + + EXPECT_EQ(*(ref2->str), u"a"); + EXPECT_EQ(1u, ref2.getIndex()); + + EXPECT_EQ(*ref3, u"m"); + EXPECT_EQ(2u, ref3.getIndex()); + + pool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.value < b.value; + }); + + + EXPECT_EQ(*ref, u"z"); + EXPECT_EQ(2u, ref.getIndex()); + + EXPECT_EQ(*(ref2->str), u"a"); + EXPECT_EQ(0u, ref2.getIndex()); + + EXPECT_EQ(*ref3, u"m"); + EXPECT_EQ(1u, ref3.getIndex()); +} + +TEST(StringPoolTest, SortAndStillDedupe) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"z"); + StringPool::Ref ref2 = pool.makeRef(u"a"); + StringPool::Ref ref3 = pool.makeRef(u"m"); + + pool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.value < b.value; + }); + + StringPool::Ref ref4 = pool.makeRef(u"z"); + StringPool::Ref ref5 = pool.makeRef(u"a"); + StringPool::Ref ref6 = pool.makeRef(u"m"); + + EXPECT_EQ(ref4.getIndex(), ref.getIndex()); + EXPECT_EQ(ref5.getIndex(), ref2.getIndex()); + EXPECT_EQ(ref6.getIndex(), ref3.getIndex()); +} + +TEST(StringPoolTest, AddStyles) { + StringPool pool; + + StyleString str { + { u"android" }, + { + Span{ { u"b" }, 2, 6 } + } + }; + + StringPool::StyleRef ref = pool.makeRef(str); + + EXPECT_EQ(0u, ref.getIndex()); + EXPECT_EQ(std::u16string(u"android"), *(ref->str)); + ASSERT_EQ(1u, ref->spans.size()); + + const StringPool::Span& span = ref->spans.front(); + EXPECT_EQ(*(span.name), u"b"); + EXPECT_EQ(2u, span.firstChar); + EXPECT_EQ(6u, span.lastChar); +} + +TEST(StringPoolTest, DoNotDedupeStyleWithSameStringAsNonStyle) { + StringPool pool; + + StringPool::Ref ref = pool.makeRef(u"android"); + + StyleString str { { u"android" } }; + StringPool::StyleRef styleRef = pool.makeRef(str); + + EXPECT_NE(ref.getIndex(), styleRef.getIndex()); +} + +constexpr const char16_t* sLongString = u"バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限します。メール、SMSや、同期を使 用するその他のアプリは、起動しても更新されないことがあります。バッテリーセーバーは端末の充電中は自動的にOFFになります。"; + +TEST(StringPoolTest, FlattenUtf8) { + StringPool pool; + + StringPool::Ref ref1 = pool.makeRef(u"hello"); + StringPool::Ref ref2 = pool.makeRef(u"goodbye"); + StringPool::Ref ref3 = pool.makeRef(sLongString); + StringPool::StyleRef ref4 = pool.makeRef(StyleString{ + { u"style" }, + { Span{ { u"b" }, 0, 1 }, Span{ { u"i" }, 2, 3 } } + }); + + EXPECT_EQ(0u, ref1.getIndex()); + EXPECT_EQ(1u, ref2.getIndex()); + EXPECT_EQ(2u, ref3.getIndex()); + EXPECT_EQ(3u, ref4.getIndex()); + + BigBuffer buffer(1024); + StringPool::flattenUtf8(&buffer, pool); + + uint8_t* data = new uint8_t[buffer.size()]; + uint8_t* p = data; + for (const auto& b : buffer) { + memcpy(p, b.buffer.get(), b.size); + p += b.size; + } + + { + android::ResStringPool test; + ASSERT_EQ(android::NO_ERROR, test.setTo(data, buffer.size())); + + EXPECT_EQ(util::getString(test, 0), u"hello"); + EXPECT_EQ(util::getString(test, 1), u"goodbye"); + EXPECT_EQ(util::getString(test, 2), sLongString); + EXPECT_EQ(util::getString(test, 3), u"style"); + + const android::ResStringPool_span* span = test.styleAt(3); + ASSERT_NE(nullptr, span); + EXPECT_EQ(util::getString(test, span->name.index), u"b"); + EXPECT_EQ(0u, span->firstChar); + EXPECT_EQ(1u, span->lastChar); + span++; + + ASSERT_NE(android::ResStringPool_span::END, span->name.index); + EXPECT_EQ(util::getString(test, span->name.index), u"i"); + EXPECT_EQ(2u, span->firstChar); + EXPECT_EQ(3u, span->lastChar); + span++; + + EXPECT_EQ(android::ResStringPool_span::END, span->name.index); + } + delete[] data; +} + +} // namespace aapt diff --git a/tools/aapt2/TableFlattener.cpp b/tools/aapt2/TableFlattener.cpp new file mode 100644 index 0000000..c306185 --- /dev/null +++ b/tools/aapt2/TableFlattener.cpp @@ -0,0 +1,511 @@ +/* + * 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 "ConfigDescription.h" +#include "Logger.h" +#include "ResourceTable.h" +#include "ResourceTypeExtensions.h" +#include "ResourceValues.h" +#include "StringPool.h" +#include "TableFlattener.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <sstream> + +namespace aapt { + +struct FlatEntry { + const ResourceEntry& entry; + const Value& value; + uint32_t entryKey; + uint32_t sourcePathKey; + uint32_t sourceLine; +}; + +/** + * Visitor that knows how to encode Map values. + */ +class MapFlattener : public ConstValueVisitor { +public: + MapFlattener(BigBuffer* out, const FlatEntry& flatEntry, + std::vector<std::pair<ResourceNameRef, uint32_t>>& symbols) : + mOut(out), mSymbols(symbols) { + mMap = mOut->nextBlock<android::ResTable_map_entry>(); + mMap->key.index = flatEntry.entryKey; + mMap->flags = android::ResTable_entry::FLAG_COMPLEX; + if (flatEntry.entry.publicStatus.isPublic) { + mMap->flags |= android::ResTable_entry::FLAG_PUBLIC; + } + if (flatEntry.value.isWeak()) { + mMap->flags |= android::ResTable_entry::FLAG_WEAK; + } + + ResTable_entry_source* sourceBlock = mOut->nextBlock<ResTable_entry_source>(); + sourceBlock->pathIndex = flatEntry.sourcePathKey; + sourceBlock->line = flatEntry.sourceLine; + + mMap->size = sizeof(*mMap) + sizeof(*sourceBlock); + } + + void flattenParent(const Reference& ref) { + if (!ref.id.isValid()) { + mSymbols.push_back({ + ResourceNameRef(ref.name), + (mOut->size() - mMap->size) + sizeof(*mMap) - sizeof(android::ResTable_entry) + }); + } + mMap->parent.ident = ref.id.id; + } + + void flattenEntry(const Reference& key, const Item& value) { + mMap->count++; + + android::ResTable_map* outMapEntry = mOut->nextBlock<android::ResTable_map>(); + + // Write the key. + if (!Res_INTERNALID(key.id.id) && !key.id.isValid()) { + mSymbols.push_back(std::make_pair(ResourceNameRef(key.name), + mOut->size() - sizeof(*outMapEntry))); + } + outMapEntry->name.ident = key.id.id; + + // Write the value. + value.flatten(outMapEntry->value); + + if (outMapEntry->value.data == 0x0) { + visitFunc<Reference>(value, [&](const Reference& reference) { + mSymbols.push_back(std::make_pair(ResourceNameRef(reference.name), + mOut->size() - sizeof(outMapEntry->value.data))); + }); + } + outMapEntry->value.size = sizeof(outMapEntry->value); + } + + static bool compareStyleEntries(const Style::Entry* lhs, const Style::Entry* rhs) { + return lhs->key.id < rhs->key.id; + } + + void visit(const Style& style, ValueVisitorArgs&) override { + if (style.parent.name.isValid()) { + flattenParent(style.parent); + } + + // First sort the entries by ID. + std::vector<const Style::Entry*> sortedEntries; + for (const auto& styleEntry : style.entries) { + auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), + &styleEntry, compareStyleEntries); + sortedEntries.insert(iter, &styleEntry); + } + + for (const Style::Entry* styleEntry : sortedEntries) { + flattenEntry(styleEntry->key, *styleEntry->value); + } + } + + void visit(const Attribute& attr, ValueVisitorArgs&) override { + android::Res_value tempVal; + tempVal.dataType = android::Res_value::TYPE_INT_DEC; + tempVal.data = attr.typeMask; + flattenEntry(Reference(ResourceId{android::ResTable_map::ATTR_TYPE}), + BinaryPrimitive(tempVal)); + + for (const auto& symbol : attr.symbols) { + tempVal.data = symbol.value; + flattenEntry(symbol.symbol, BinaryPrimitive(tempVal)); + } + } + + void visit(const Styleable& styleable, ValueVisitorArgs&) override { + for (const auto& attr : styleable.entries) { + flattenEntry(attr, BinaryPrimitive(android::Res_value{})); + } + } + + void visit(const Array& array, ValueVisitorArgs&) override { + for (const auto& item : array.items) { + flattenEntry({}, *item); + } + } + + void visit(const Plural& plural, ValueVisitorArgs&) override { + const size_t count = plural.values.size(); + for (size_t i = 0; i < count; i++) { + if (!plural.values[i]) { + continue; + } + + ResourceId q; + switch (i) { + case Plural::Zero: + q.id = android::ResTable_map::ATTR_ZERO; + break; + + case Plural::One: + q.id = android::ResTable_map::ATTR_ONE; + break; + + case Plural::Two: + q.id = android::ResTable_map::ATTR_TWO; + break; + + case Plural::Few: + q.id = android::ResTable_map::ATTR_FEW; + break; + + case Plural::Many: + q.id = android::ResTable_map::ATTR_MANY; + break; + + case Plural::Other: + q.id = android::ResTable_map::ATTR_OTHER; + break; + + default: + assert(false); + break; + } + + flattenEntry(Reference(q), *plural.values[i]); + } + } + +private: + BigBuffer* mOut; + std::vector<std::pair<ResourceNameRef, uint32_t>>& mSymbols; + android::ResTable_map_entry* mMap; +}; + +TableFlattener::TableFlattener(Options options) +: mOptions(options) { +} + +bool TableFlattener::flattenValue(BigBuffer* out, const FlatEntry& flatEntry, + std::vector<std::pair<ResourceNameRef, uint32_t>>& symbolEntries) { + if (flatEntry.value.isItem()) { + android::ResTable_entry* entry = out->nextBlock<android::ResTable_entry>(); + + if (flatEntry.entry.publicStatus.isPublic) { + entry->flags |= android::ResTable_entry::FLAG_PUBLIC; + } + + if (flatEntry.value.isWeak()) { + entry->flags |= android::ResTable_entry::FLAG_WEAK; + } + + entry->key.index = flatEntry.entryKey; + entry->size = sizeof(*entry); + + if (mOptions.useExtendedChunks) { + // Write the extra source block. This will be ignored by + // the Android runtime. + ResTable_entry_source* sourceBlock = out->nextBlock<ResTable_entry_source>(); + sourceBlock->pathIndex = flatEntry.sourcePathKey; + sourceBlock->line = flatEntry.sourceLine; + + entry->size += sizeof(*sourceBlock); + } + + android::Res_value* outValue = out->nextBlock<android::Res_value>(); + + const Item& item = static_cast<const Item&>(flatEntry.value); + if (!item.flatten(*outValue)) { + return false; + } + + if (outValue->data == 0x0) { + visitFunc<Reference>(item, [&](const Reference& reference) { + symbolEntries.push_back({ + ResourceNameRef(reference.name), + out->size() - sizeof(outValue->data) + }); + }); + } + outValue->size = sizeof(*outValue); + return true; + } + + MapFlattener flattener(out, flatEntry, symbolEntries); + flatEntry.value.accept(flattener, {}); + return true; +} + +bool TableFlattener::flatten(BigBuffer* out, const ResourceTable& table) { + const size_t beginning = out->size(); + + if (table.getPackage().size() == 0) { + Logger::error() + << "ResourceTable has no package name." + << std::endl; + return false; + } + + if (table.getPackageId() == ResourceTable::kUnsetPackageId) { + Logger::error() + << "ResourceTable has no package ID set." + << std::endl; + return false; + } + + std::vector<std::pair<ResourceNameRef, uint32_t>> symbolEntries; + + StringPool typePool; + StringPool keyPool; + StringPool sourcePool; + + // Sort the types by their IDs. They will be inserted into the StringPool + // in this order. + std::vector<ResourceTableType*> sortedTypes; + for (const auto& type : table) { + if (type->type == ResourceType::kStyleable && !mOptions.useExtendedChunks) { + continue; + } + + auto iter = std::lower_bound(std::begin(sortedTypes), std::end(sortedTypes), type.get(), + [](const ResourceTableType* lhs, const ResourceTableType* rhs) -> bool { + return lhs->typeId < rhs->typeId; + }); + sortedTypes.insert(iter, type.get()); + } + + BigBuffer typeBlock(1024); + size_t expectedTypeId = 1; + for (const ResourceTableType* type : sortedTypes) { + if (type->typeId == ResourceTableType::kUnsetTypeId + || type->typeId == 0) { + Logger::error() + << "resource type '" + << type->type + << "' from package '" + << table.getPackage() + << "' has no ID." + << std::endl; + return false; + } + + // If there is a gap in the type IDs, fill in the StringPool + // with empty values until we reach the ID we expect. + while (type->typeId > expectedTypeId) { + std::u16string typeName(u"?"); + typeName += expectedTypeId; + typePool.makeRef(typeName); + expectedTypeId++; + } + expectedTypeId++; + typePool.makeRef(toString(type->type)); + + android::ResTable_typeSpec* spec = typeBlock.nextBlock<android::ResTable_typeSpec>(); + spec->header.type = android::RES_TABLE_TYPE_SPEC_TYPE; + spec->header.headerSize = sizeof(*spec); + spec->header.size = spec->header.headerSize + (type->entries.size() * sizeof(uint32_t)); + spec->id = type->typeId; + spec->entryCount = type->entries.size(); + + // Reserve space for the masks of each resource in this type. These + // show for which configuration axis the resource changes. + uint32_t* configMasks = typeBlock.nextBlock<uint32_t>(type->entries.size()); + + // Sort the entries by entry ID and write their configuration masks. + std::vector<ResourceEntry*> entries; + const size_t entryCount = type->entries.size(); + for (size_t entryIndex = 0; entryIndex < entryCount; entryIndex++) { + const auto& entry = type->entries[entryIndex]; + + if (entry->entryId == ResourceEntry::kUnsetEntryId) { + Logger::error() + << "resource '" + << ResourceName{ table.getPackage(), type->type, entry->name } + << "' has no ID." + << std::endl; + return false; + } + + auto iter = std::lower_bound(std::begin(entries), std::end(entries), entry.get(), + [](const ResourceEntry* lhs, const ResourceEntry* rhs) -> bool { + return lhs->entryId < rhs->entryId; + }); + entries.insert(iter, entry.get()); + + // Populate the config masks for this entry. + if (entry->publicStatus.isPublic) { + configMasks[entry->entryId] |= android::ResTable_typeSpec::SPEC_PUBLIC; + } + + const size_t configCount = entry->values.size(); + for (size_t i = 0; i < configCount; i++) { + const ConfigDescription& config = entry->values[i].config; + for (size_t j = i + 1; j < configCount; j++) { + configMasks[entry->entryId] |= config.diff(entry->values[j].config); + } + } + } + + // The binary resource table lists resource entries for each configuration. + // We store them inverted, where a resource entry lists the values for each + // configuration available. Here we reverse this to match the binary table. + std::map<ConfigDescription, std::vector<FlatEntry>> data; + for (const ResourceEntry* entry : entries) { + size_t keyIndex = keyPool.makeRef(entry->name).getIndex(); + + if (keyIndex > std::numeric_limits<uint32_t>::max()) { + Logger::error() + << "resource key string pool exceeded max size." + << std::endl; + return false; + } + + for (const auto& configValue : entry->values) { + data[configValue.config].push_back(FlatEntry{ + *entry, + *configValue.value, + static_cast<uint32_t>(keyIndex), + static_cast<uint32_t>(sourcePool.makeRef(util::utf8ToUtf16( + configValue.source.path)).getIndex()), + static_cast<uint32_t>(configValue.source.line) + }); + } + } + + // Begin flattening a configuration for the current type. + for (const auto& entry : data) { + const size_t typeHeaderStart = typeBlock.size(); + android::ResTable_type* typeHeader = typeBlock.nextBlock<android::ResTable_type>(); + typeHeader->header.type = android::RES_TABLE_TYPE_TYPE; + typeHeader->header.headerSize = sizeof(*typeHeader); + typeHeader->id = type->typeId; + typeHeader->entryCount = type->entries.size(); + typeHeader->entriesStart = typeHeader->header.headerSize + + (sizeof(uint32_t) * type->entries.size()); + typeHeader->config = entry.first; + + uint32_t* indices = typeBlock.nextBlock<uint32_t>(type->entries.size()); + memset(indices, 0xff, type->entries.size() * sizeof(uint32_t)); + + const size_t entryStart = typeBlock.size(); + for (const FlatEntry& flatEntry : entry.second) { + assert(flatEntry.entry.entryId < type->entries.size()); + indices[flatEntry.entry.entryId] = typeBlock.size() - entryStart; + if (!flattenValue(&typeBlock, flatEntry, symbolEntries)) { + Logger::error() + << "failed to flatten resource '" + << ResourceNameRef { + table.getPackage(), type->type, flatEntry.entry.name } + << "' for configuration '" + << entry.first + << "'." + << std::endl; + return false; + } + } + + typeBlock.align4(); + typeHeader->header.size = typeBlock.size() - typeHeaderStart; + } + } + + const size_t beforeTable = out->size(); + android::ResTable_header* header = out->nextBlock<android::ResTable_header>(); + header->header.type = android::RES_TABLE_TYPE; + header->header.headerSize = sizeof(*header); + header->packageCount = 1; + + SymbolTable_entry* symbolEntryData = nullptr; + if (!symbolEntries.empty() && mOptions.useExtendedChunks) { + const size_t beforeSymbolTable = out->size(); + StringPool symbolPool; + SymbolTable_header* symbolHeader = out->nextBlock<SymbolTable_header>(); + symbolHeader->header.type = RES_TABLE_SYMBOL_TABLE_TYPE; + symbolHeader->header.headerSize = sizeof(*symbolHeader); + symbolHeader->count = symbolEntries.size(); + + symbolEntryData = out->nextBlock<SymbolTable_entry>(symbolHeader->count); + + size_t i = 0; + for (const auto& entry : symbolEntries) { + symbolEntryData[i].offset = entry.second; + StringPool::Ref ref = symbolPool.makeRef( + entry.first.package.toString() + u":" + + toString(entry.first.type).toString() + u"/" + + entry.first.entry.toString()); + symbolEntryData[i].stringIndex = ref.getIndex(); + i++; + } + + StringPool::flattenUtf8(out, symbolPool); + out->align4(); + symbolHeader->header.size = out->size() - beforeSymbolTable; + } + + if (sourcePool.size() > 0 && mOptions.useExtendedChunks) { + const size_t beforeSourcePool = out->size(); + android::ResChunk_header* sourceHeader = out->nextBlock<android::ResChunk_header>(); + sourceHeader->type = RES_TABLE_SOURCE_POOL_TYPE; + sourceHeader->headerSize = sizeof(*sourceHeader); + StringPool::flattenUtf8(out, sourcePool); + out->align4(); + sourceHeader->size = out->size() - beforeSourcePool; + } + + StringPool::flattenUtf8(out, table.getValueStringPool()); + + const size_t beforePackageIndex = out->size(); + android::ResTable_package* package = out->nextBlock<android::ResTable_package>(); + package->header.type = android::RES_TABLE_PACKAGE_TYPE; + package->header.headerSize = sizeof(*package); + + if (table.getPackageId() > std::numeric_limits<uint8_t>::max()) { + Logger::error() + << "package ID 0x'" + << std::hex << table.getPackageId() << std::dec + << "' is invalid." + << std::endl; + return false; + } + package->id = table.getPackageId(); + + if (table.getPackage().size() >= sizeof(package->name) / sizeof(package->name[0])) { + Logger::error() + << "package name '" + << table.getPackage() + << "' is too long." + << std::endl; + return false; + } + memcpy(package->name, reinterpret_cast<const uint16_t*>(table.getPackage().data()), + table.getPackage().length() * sizeof(char16_t)); + package->name[table.getPackage().length()] = 0; + + package->typeStrings = package->header.headerSize; + StringPool::flattenUtf8(out, typePool); + package->keyStrings = out->size() - beforePackageIndex; + StringPool::flattenUtf8(out, keyPool); + + if (symbolEntryData != nullptr) { + for (size_t i = 0; i < symbolEntries.size(); i++) { + symbolEntryData[i].offset += out->size() - beginning; + } + } + + out->appendBuffer(std::move(typeBlock)); + + package->header.size = out->size() - beforePackageIndex; + header->header.size = out->size() - beforeTable; + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/TableFlattener.h b/tools/aapt2/TableFlattener.h new file mode 100644 index 0000000..0ae798c --- /dev/null +++ b/tools/aapt2/TableFlattener.h @@ -0,0 +1,60 @@ +/* + * 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_TABLE_FLATTENER_H +#define AAPT_TABLE_FLATTENER_H + +#include "BigBuffer.h" +#include "ResourceTable.h" + +namespace aapt { + +struct FlatEntry; + +/** + * Flattens a ResourceTable into a binary format suitable + * for loading into a ResTable on the host or device. + */ +struct TableFlattener { + /** + * A set of options for this TableFlattener. + */ + struct Options { + /** + * Specifies whether to output extended chunks, like + * source information and mising symbol entries. Default + * is true. + * + * Set this to false when emitting the final table to be used + * on device. + */ + bool useExtendedChunks = true; + }; + + TableFlattener(Options options); + + bool flatten(BigBuffer* out, const ResourceTable& table); + +private: + bool flattenValue(BigBuffer* out, const FlatEntry& flatEntry, + std::vector<std::pair<ResourceNameRef, uint32_t>>& symbolEntries); + + Options mOptions; +}; + +} // namespace aapt + +#endif // AAPT_TABLE_FLATTENER_H diff --git a/tools/aapt2/Util.cpp b/tools/aapt2/Util.cpp new file mode 100644 index 0000000..8a4c88f --- /dev/null +++ b/tools/aapt2/Util.cpp @@ -0,0 +1,290 @@ +/* + * 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 "Maybe.h" +#include "StringPiece.h" +#include "Util.h" + +#include <algorithm> +#include <ostream> +#include <string> +#include <utils/Unicode.h> +#include <vector> + +namespace aapt { +namespace util { + +static std::vector<std::string> splitAndTransform(const StringPiece& str, char sep, + const std::function<char(char)>& f) { + std::vector<std::string> parts; + const StringPiece::const_iterator end = std::end(str); + StringPiece::const_iterator start = std::begin(str); + StringPiece::const_iterator current; + do { + current = std::find(start, end, sep); + parts.emplace_back(str.substr(start, current).toString()); + if (f) { + std::string& part = parts.back(); + std::transform(part.begin(), part.end(), part.begin(), f); + } + start = current + 1; + } while (current != end); + return parts; +} + +std::vector<std::string> split(const StringPiece& str, char sep) { + return splitAndTransform(str, sep, nullptr); +} + +std::vector<std::string> splitAndLowercase(const StringPiece& str, char sep) { + return splitAndTransform(str, sep, ::tolower); +} + +bool stringEndsWith(const StringPiece& str, const StringPiece& suffix) { + if (str.size() < suffix.size()) { + return false; + } + return str.substr(str.size() - suffix.size(), suffix.size()) == suffix; +} + +StringPiece16 trimWhitespace(const StringPiece16& str) { + if (str.size() == 0 || str.data() == nullptr) { + return str; + } + + const char16_t* start = str.data(); + const char16_t* end = str.data() + str.length(); + + while (start != end && util::isspace16(*start)) { + start++; + } + + while (end != start && util::isspace16(*(end - 1))) { + end--; + } + + return StringPiece16(start, end - start); +} + +StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16& str, + const StringPiece16& allowedChars) { + const auto endIter = str.end(); + for (auto iter = str.begin(); iter != endIter; ++iter) { + char16_t c = *iter; + if ((c >= u'a' && c <= u'z') || + (c >= u'A' && c <= u'Z') || + (c >= u'0' && c <= u'9')) { + continue; + } + + bool match = false; + for (char16_t i : allowedChars) { + if (c == i) { + match = true; + break; + } + } + + if (!match) { + return iter; + } + } + return endIter; +} + +static Maybe<char16_t> parseUnicodeCodepoint(const char16_t** start, const char16_t* end) { + char16_t code = 0; + for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) { + char16_t c = **start; + int a; + if (c >= '0' && c <= '9') { + a = c - '0'; + } else if (c >= 'a' && c <= 'f') { + a = c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + a = c - 'A' + 10; + } else { + return make_nothing<char16_t>(); + } + code = (code << 4) | a; + } + return make_value(code); +} + +StringBuilder& StringBuilder::append(const StringPiece16& str) { + if (!mError.empty()) { + return *this; + } + + const char16_t* const end = str.end(); + const char16_t* start = str.begin(); + const char16_t* current = start; + while (current != end) { + if (*current == u'"') { + if (!mQuote && mTrailingSpace) { + // We found an opening quote, and we have + // trailing space, so we should append that + // space now. + if (mTrailingSpace) { + // We had trailing whitespace, so + // replace with a single space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + } + mQuote = !mQuote; + mStr.append(start, current - start); + start = current + 1; + } else if (*current == u'\'' && !mQuote) { + // This should be escaped. + mError = "unescaped apostrophe"; + return *this; + } else if (*current == u'\\') { + // This is an escape sequence, convert to the real value. + if (!mQuote && mTrailingSpace) { + // We had trailing whitespace, so + // replace with a single space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + mStr.append(start, current - start); + start = current + 1; + + current++; + if (current != end) { + switch (*current) { + case u't': + mStr += u'\t'; + break; + case u'n': + mStr += u'\n'; + break; + case u'#': + mStr += u'#'; + break; + case u'@': + mStr += u'@'; + break; + case u'?': + mStr += u'?'; + break; + case u'"': + mStr += u'"'; + break; + case u'\'': + mStr += u'\''; + break; + case u'\\': + mStr += u'\\'; + break; + case u'u': { + current++; + Maybe<char16_t> c = parseUnicodeCodepoint(¤t, end); + if (!c) { + mError = "invalid unicode escape sequence"; + return *this; + } + mStr += c.value(); + current -= 1; + break; + } + + default: + // Ignore. + break; + } + start = current + 1; + } + } else if (!mQuote) { + // This is not quoted text, so look for whitespace. + if (isspace16(*current)) { + // We found whitespace, see if we have seen some + // before. + if (!mTrailingSpace) { + // We didn't see a previous adjacent space, + // so mark that we did. + mTrailingSpace = true; + mStr.append(start, current - start); + } + + // Keep skipping whitespace. + start = current + 1; + } else if (mTrailingSpace) { + // We saw trailing space before, so replace all + // that trailing space with one space. + if (!mStr.empty()) { + mStr += u' '; + } + mTrailingSpace = false; + } + } + current++; + } + mStr.append(start, end - start); + return *this; +} + +std::u16string utf8ToUtf16(const StringPiece& utf8) { + ssize_t utf16Length = utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(utf8.data()), + utf8.length()); + if (utf16Length <= 0) { + return {}; + } + + std::u16string utf16; + utf16.resize(utf16Length); + utf8_to_utf16(reinterpret_cast<const uint8_t*>(utf8.data()), utf8.length(), &*utf16.begin()); + return utf16; +} + +std::string utf16ToUtf8(const StringPiece16& utf16) { + ssize_t utf8Length = utf16_to_utf8_length(utf16.data(), utf16.length()); + if (utf8Length <= 0) { + return {}; + } + + std::string utf8; + utf8.resize(utf8Length); + utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin()); + return utf8; +} + +bool writeAll(std::ostream& out, const BigBuffer& buffer) { + for (const auto& b : buffer) { + if (!out.write(reinterpret_cast<const char*>(b.buffer.get()), b.size)) { + return false; + } + } + return true; +} + +std::unique_ptr<uint8_t[]> copy(const BigBuffer& buffer) { + std::unique_ptr<uint8_t[]> data = std::unique_ptr<uint8_t[]>(new uint8_t[buffer.size()]); + uint8_t* p = data.get(); + for (const auto& block : buffer) { + memcpy(p, block.buffer.get(), block.size); + p += block.size; + } + return data; +} + +} // namespace util +} // namespace aapt diff --git a/tools/aapt2/Util.h b/tools/aapt2/Util.h new file mode 100644 index 0000000..4c5249b --- /dev/null +++ b/tools/aapt2/Util.h @@ -0,0 +1,276 @@ +/* + * 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_UTIL_H +#define AAPT_UTIL_H + +#include "BigBuffer.h" +#include "StringPiece.h" + +#include <androidfw/ResourceTypes.h> +#include <functional> +#include <memory> +#include <ostream> +#include <string> +#include <vector> + +namespace aapt { +namespace util { + +std::vector<std::string> split(const StringPiece& str, char sep); +std::vector<std::string> splitAndLowercase(const StringPiece& str, char sep); + +/** + * Returns true if the string ends with suffix. + */ +bool stringEndsWith(const StringPiece& str, const StringPiece& suffix); + +/** + * Creates a new StringPiece16 that points to a substring + * of the original string without leading or trailing whitespace. + */ +StringPiece16 trimWhitespace(const StringPiece16& str); + +/** + * UTF-16 isspace(). It basically checks for lower range characters that are + * whitespace. + */ +inline bool isspace16(char16_t c) { + return c < 0x0080 && isspace(c); +} + +/** + * Returns an iterator to the first character that is not alpha-numeric and that + * is not in the allowedChars set. + */ +StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16& str, + const StringPiece16& allowedChars); + +/** + * Makes a std::unique_ptr<> with the template parameter inferred by the compiler. + * This will be present in C++14 and can be removed then. + */ +template <typename T, class... Args> +std::unique_ptr<T> make_unique(Args&&... args) { + return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); +} + +/** + * Writes a set of items to the std::ostream, joining the times with the provided + * separator. + */ +template <typename Iterator> +::std::function<::std::ostream&(::std::ostream&)> joiner(Iterator begin, Iterator end, + const char* sep) { + return [begin, end, sep](::std::ostream& out) -> ::std::ostream& { + for (auto iter = begin; iter != end; ++iter) { + if (iter != begin) { + out << sep; + } + out << *iter; + } + return out; + }; +} + +inline ::std::function<::std::ostream&(::std::ostream&)> formatSize(size_t size) { + return [size](::std::ostream& out) -> ::std::ostream& { + constexpr size_t K = 1024; + constexpr size_t M = K * K; + constexpr size_t G = M * M; + if (size < K) { + out << size << "B"; + } else if (size < M) { + out << (double(size) / K) << " KiB"; + } else if (size < G) { + out << (double(size) / M) << " MiB"; + } else { + out << (double(size) / G) << " GiB"; + } + return out; + }; +} + +/** + * Helper method to extract a string from a StringPool. + */ +inline StringPiece16 getString(const android::ResStringPool& pool, size_t idx) { + size_t len; + const char16_t* str = pool.stringAt(idx, &len); + if (str != nullptr) { + return StringPiece16(str, len); + } + return StringPiece16(); +} + +class StringBuilder { +public: + StringBuilder& append(const StringPiece16& str); + const std::u16string& str() const; + const std::string& error() const; + operator bool() const; + +private: + std::u16string mStr; + bool mQuote = false; + bool mTrailingSpace = false; + std::string mError; +}; + +inline const std::u16string& StringBuilder::str() const { + return mStr; +} + +inline const std::string& StringBuilder::error() const { + return mError; +} + +inline StringBuilder::operator bool() const { + return mError.empty(); +} + +/** + * Converts a UTF8 string to a UTF16 string. + */ +std::u16string utf8ToUtf16(const StringPiece& utf8); +std::string utf16ToUtf8(const StringPiece16& utf8); + +/** + * Writes the entire BigBuffer to the output stream. + */ +bool writeAll(std::ostream& out, const BigBuffer& buffer); + +/* + * Copies the entire BigBuffer into a single buffer. + */ +std::unique_ptr<uint8_t[]> copy(const BigBuffer& buffer); + +/** + * A Tokenizer implemented as an iterable collection. It does not allocate + * any memory on the heap nor use standard containers. + */ +template <typename Char> +class Tokenizer { +public: + class iterator { + public: + iterator(const iterator&) = default; + iterator& operator=(const iterator&) = default; + + iterator& operator++(); + BasicStringPiece<Char> operator*(); + bool operator==(const iterator& rhs) const; + bool operator!=(const iterator& rhs) const; + + private: + friend class Tokenizer<Char>; + + iterator(BasicStringPiece<Char> s, Char sep, BasicStringPiece<Char> tok); + + BasicStringPiece<Char> str; + Char separator; + BasicStringPiece<Char> token; + }; + + Tokenizer(BasicStringPiece<Char> str, Char sep); + iterator begin(); + iterator end(); + +private: + const iterator mBegin; + const iterator mEnd; +}; + +template <typename Char> +inline Tokenizer<Char> tokenize(BasicStringPiece<Char> str, Char sep) { + return Tokenizer<Char>(str, sep); +} + +template <typename Char> +typename Tokenizer<Char>::iterator& Tokenizer<Char>::iterator::operator++() { + const Char* start = token.end(); + const Char* end = str.end(); + if (start == end) { + token.assign(token.end(), 0); + return *this; + } + + start += 1; + const Char* current = start; + while (current != end) { + if (*current == separator) { + token.assign(start, current - start); + return *this; + } + ++current; + } + token.assign(start, end - start); + return *this; +} + +template <typename Char> +inline BasicStringPiece<Char> Tokenizer<Char>::iterator::operator*() { + return token; +} + +template <typename Char> +inline bool Tokenizer<Char>::iterator::operator==(const iterator& rhs) const { + // We check equality here a bit differently. + // We need to know that the addresses are the same. + return token.begin() == rhs.token.begin() && token.end() == rhs.token.end(); +} + +template <typename Char> +inline bool Tokenizer<Char>::iterator::operator!=(const iterator& rhs) const { + return !(*this == rhs); +} + +template <typename Char> +inline Tokenizer<Char>::iterator::iterator(BasicStringPiece<Char> s, Char sep, + BasicStringPiece<Char> tok) : + str(s), separator(sep), token(tok) { +} + +template <typename Char> +inline typename Tokenizer<Char>::iterator Tokenizer<Char>::begin() { + return mBegin; +} + +template <typename Char> +inline typename Tokenizer<Char>::iterator Tokenizer<Char>::end() { + return mEnd; +} + +template <typename Char> +inline Tokenizer<Char>::Tokenizer(BasicStringPiece<Char> str, Char sep) : + mBegin(++iterator(str, sep, BasicStringPiece<Char>(str.begin() - 1, 0))), + mEnd(str, sep, BasicStringPiece<Char>(str.end(), 0)) { +} + +} // namespace util + +/** + * Stream operator for functions. Calls the function with the stream as an argument. + * In the aapt namespace for lookup. + */ +inline ::std::ostream& operator<<(::std::ostream& out, + ::std::function<::std::ostream&(::std::ostream&)> f) { + return f(out); +} + +} // namespace aapt + +#endif // AAPT_UTIL_H diff --git a/tools/aapt2/Util_test.cpp b/tools/aapt2/Util_test.cpp new file mode 100644 index 0000000..7dbe7e0 --- /dev/null +++ b/tools/aapt2/Util_test.cpp @@ -0,0 +1,92 @@ +/* + * 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 <gtest/gtest.h> +#include <string> + +#include "StringPiece.h" +#include "Util.h" + +namespace aapt { + +TEST(UtilTest, TrimOnlyWhitespace) { + const std::u16string full = u"\n "; + + StringPiece16 trimmed = util::trimWhitespace(full); + EXPECT_TRUE(trimmed.empty()); + EXPECT_EQ(0u, trimmed.size()); +} + +TEST(UtilTest, StringEndsWith) { + EXPECT_TRUE(util::stringEndsWith("hello.xml", ".xml")); +} + +TEST(UtilTest, StringBuilderWhitespaceRemoval) { + EXPECT_EQ(StringPiece16(u"hey guys this is so cool"), + util::StringBuilder().append(u" hey guys ") + .append(u" this is so cool ") + .str()); + + EXPECT_EQ(StringPiece16(u" wow, so many \t spaces. what?"), + util::StringBuilder().append(u" \" wow, so many \t ") + .append(u"spaces. \"what? ") + .str()); + + EXPECT_EQ(StringPiece16(u"where is the pie?"), + util::StringBuilder().append(u" where \t ") + .append(u" \nis the "" pie?") + .str()); +} + +TEST(UtilTest, StringBuilderEscaping) { + EXPECT_EQ(StringPiece16(u"hey guys\n this \t is so\\ cool"), + util::StringBuilder().append(u" hey guys\\n ") + .append(u" this \\t is so\\\\ cool ") + .str()); + + EXPECT_EQ(StringPiece16(u"@?#\\\'"), + util::StringBuilder().append(u"\\@\\?\\#\\\\\\'") + .str()); +} + +TEST(UtilTest, StringBuilderMisplacedQuote) { + util::StringBuilder builder{}; + EXPECT_FALSE(builder.append(u"they're coming!")); +} + +TEST(UtilTest, StringBuilderUnicodeCodes) { + EXPECT_EQ(StringPiece16(u"\u00AF\u0AF0 woah"), + util::StringBuilder().append(u"\\u00AF\\u0AF0 woah") + .str()); + + EXPECT_FALSE(util::StringBuilder().append(u"\\u00 yo")); +} + +TEST(UtilTest, TokenizeInput) { + auto tokenizer = util::tokenize(StringPiece16(u"this| is|the|end"), u'|'); + auto iter = tokenizer.begin(); + ASSERT_EQ(*iter, StringPiece16(u"this")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u" is")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"the")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"end")); + ++iter; + ASSERT_EQ(tokenizer.end(), iter); +} + +} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.cpp b/tools/aapt2/XliffXmlPullParser.cpp new file mode 100644 index 0000000..f0950a3 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser.cpp @@ -0,0 +1,108 @@ +/* + * 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 "XliffXmlPullParser.h" + +#include <string> + +namespace aapt { + +XliffXmlPullParser::XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser) : + mParser(parser) { +} + +XmlPullParser::Event XliffXmlPullParser::next() { + while (XmlPullParser::isGoodEvent(mParser->next())) { + Event event = mParser->getEvent(); + if (event != Event::kStartElement && event != Event::kEndElement) { + break; + } + + if (mParser->getElementNamespace() != + u"urn:oasis:names:tc:xliff:document:1.2") { + break; + } + + const std::u16string& name = mParser->getElementName(); + if (name != u"bpt" + && name != u"ept" + && name != u"it" + && name != u"ph" + && name != u"g" + && name != u"bx" + && name != u"ex" + && name != u"x") { + break; + } + + // We hit a tag that was ignored, so get the next event. + } + return mParser->getEvent(); +} + +XmlPullParser::Event XliffXmlPullParser::getEvent() const { + return mParser->getEvent(); +} + +const std::string& XliffXmlPullParser::getLastError() const { + return mParser->getLastError(); +} + +const std::u16string& XliffXmlPullParser::getComment() const { + return mParser->getComment(); +} + +size_t XliffXmlPullParser::getLineNumber() const { + return mParser->getLineNumber(); +} + +size_t XliffXmlPullParser::getDepth() const { + return mParser->getDepth(); +} + +const std::u16string& XliffXmlPullParser::getText() const { + return mParser->getText(); +} + +const std::u16string& XliffXmlPullParser::getNamespacePrefix() const { + return mParser->getNamespacePrefix(); +} + +const std::u16string& XliffXmlPullParser::getNamespaceUri() const { + return mParser->getNamespaceUri(); +} + +const std::u16string& XliffXmlPullParser::getElementNamespace() const { + return mParser->getElementNamespace(); +} + +const std::u16string& XliffXmlPullParser::getElementName() const { + return mParser->getElementName(); +} + +size_t XliffXmlPullParser::getAttributeCount() const { + return mParser->getAttributeCount(); +} + +XmlPullParser::const_iterator XliffXmlPullParser::beginAttributes() const { + return mParser->beginAttributes(); +} + +XmlPullParser::const_iterator XliffXmlPullParser::endAttributes() const { + return mParser->endAttributes(); +} + +} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.h b/tools/aapt2/XliffXmlPullParser.h new file mode 100644 index 0000000..d362521 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser.h @@ -0,0 +1,61 @@ +/* + * 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_XLIFF_XML_PULL_PARSER_H +#define AAPT_XLIFF_XML_PULL_PARSER_H + +#include "XmlPullParser.h" + +#include <string> + +namespace aapt { + +/** + * Strips xliff elements and provides the caller with a view of the + * underlying XML without xliff. + */ +class XliffXmlPullParser : public XmlPullParser { +public: + XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser); + XliffXmlPullParser(const XliffXmlPullParser& rhs) = delete; + + Event getEvent() const; + const std::string& getLastError() const; + Event next(); + + const std::u16string& getComment() const; + size_t getLineNumber() const; + size_t getDepth() const; + + const std::u16string& getText() const; + + const std::u16string& getNamespacePrefix() const; + const std::u16string& getNamespaceUri() const; + + const std::u16string& getElementNamespace() const; + const std::u16string& getElementName() const; + + const_iterator beginAttributes() const; + const_iterator endAttributes() const; + size_t getAttributeCount() const; + +private: + std::shared_ptr<XmlPullParser> mParser; +}; + +} // namespace aapt + +#endif // AAPT_XLIFF_XML_PULL_PARSER_H diff --git a/tools/aapt2/XliffXmlPullParser_test.cpp b/tools/aapt2/XliffXmlPullParser_test.cpp new file mode 100644 index 0000000..f903072 --- /dev/null +++ b/tools/aapt2/XliffXmlPullParser_test.cpp @@ -0,0 +1,75 @@ +/* + * 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 "SourceXmlPullParser.h" +#include "XliffXmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(XliffXmlPullParserTest, IgnoreXliffTags) { + std::stringstream input; + input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl + << "<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">" << std::endl + << "<string name=\"foo\">" + << "Hey <xliff:g><xliff:it>there</xliff:it></xliff:g> world</string>" << std::endl + << "</resources>" << std::endl; + std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); + XliffXmlPullParser parser(sourceParser); + EXPECT_EQ(XmlPullParser::Event::kStartDocument, parser.getEvent()); + + EXPECT_EQ(XmlPullParser::Event::kStartNamespace, parser.next()); + EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); + EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); + + EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"resources"); + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. + + EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"string"); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u"Hey "); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u"there"); + + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); + EXPECT_EQ(parser.getText(), u" world"); + + EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"string"); + EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. + + EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); + EXPECT_EQ(parser.getElementNamespace(), u""); + EXPECT_EQ(parser.getElementName(), u"resources"); + + EXPECT_EQ(XmlPullParser::Event::kEndNamespace, parser.next()); + EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); + EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); + + EXPECT_EQ(XmlPullParser::Event::kEndDocument, parser.next()); +} + +} // namespace aapt diff --git a/tools/aapt2/XmlFlattener.cpp b/tools/aapt2/XmlFlattener.cpp new file mode 100644 index 0000000..b6ca6d5 --- /dev/null +++ b/tools/aapt2/XmlFlattener.cpp @@ -0,0 +1,437 @@ +/* + * 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 { + +struct AttributeValueFlattener : ValueVisitor { + struct Args : ValueVisitorArgs { + Args(std::shared_ptr<Resolver> r, SourceLogger& s, android::Res_value& oV, + std::shared_ptr<XmlPullParser> p, bool& e, StringPool::Ref& rV, + std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>& sR) : + resolver(r), logger(s), outValue(oV), parser(p), error(e), rawValue(rV), + stringRefs(sR) { + } + + std::shared_ptr<Resolver> resolver; + SourceLogger& logger; + android::Res_value& outValue; + std::shared_ptr<XmlPullParser> parser; + bool& error; + StringPool::Ref& rawValue; + std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>& stringRefs; + }; + + void visit(Reference& reference, ValueVisitorArgs& a) override { + Args& args = static_cast<Args&>(a); + + Maybe<ResourceId> result = args.resolver->findId(reference.name); + if (!result || !result.value().isValid()) { + args.logger.error(args.parser->getLineNumber()) + << "unresolved reference '" + << reference.name + << "'." + << std::endl; + args.error = true; + } else { + reference.id = result.value(); + reference.flatten(args.outValue); + } + } + + void visit(String& string, ValueVisitorArgs& a) override { + Args& args = static_cast<Args&>(a); + + args.outValue.dataType = android::Res_value::TYPE_STRING; + args.stringRefs.emplace_back(args.rawValue, + reinterpret_cast<android::ResStringPool_ref*>(&args.outValue.data)); + } + + void visitItem(Item& item, ValueVisitorArgs& a) override { + Args& args = static_cast<Args&>(a); + item.flatten(args.outValue); + } +}; + +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<Resolver>& resolver) : 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>(); + stringRefs.emplace_back( + pool.makeRef(parser->getElementNamespace(), lowPriority), &elem->ns); + 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 (attrIter->namespaceUri.empty()) { + // 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 { + StringPiece16 package; + if (attrIter->namespaceUri == u"http://schemas.android.com/apk/res-auto") { + package = mResolver->getDefaultPackage(); + } else { + // TODO(adamlesinski): Extract package from namespace. + // The package name appears like so: + // http://schemas.android.com/apk/res/<package name> + package = u"android"; + } + + // Find the Attribute object via our Resolver. + ResourceName attrName = { + package.toString(), ResourceType::kAttr, attrIter->name }; + Maybe<Resolver::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; + } + + if (options.maxSdkAttribute && package == u"android") { + size_t sdkVersion = findAttributeSdkLevel(attrIter->name); + if (sdkVersion > options.maxSdkAttribute.value()) { + // We will silently omit this attribute + smallestStrippedAttributeSdk = + std::min(smallestStrippedAttributeSdk, sdkVersion); + 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.toString()].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. + for (auto entry : sortedAttributes) { + android::ResXMLTree_attribute* attr = + out.nextBlock<android::ResXMLTree_attribute>(); + stringRefs.emplace_back( + pool.makeRef(entry.xmlAttr->namespaceUri, lowPriority), &attr->ns); + StringPool::Ref rawValueRef = pool.makeRef(entry.xmlAttr->value, lowPriority); + stringRefs.emplace_back(rawValueRef, &attr->rawValue); + stringRefs.emplace_back(entry.nameRef, &attr->name); + + if (entry.attr) { + std::unique_ptr<Item> value = ResourceParser::parseItemForAttribute( + entry.xmlAttr->value, *entry.attr, mResolver->getDefaultPackage()); + if (value) { + AttributeValueFlattener flattener; + value->accept(flattener, AttributeValueFlattener::Args{ + mResolver, + logger, + attr->typedValue, + parser, + error, + rawValueRef, + stringRefs + }); + } else if (!(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; + stringRefs.emplace_back(rawValueRef, + reinterpret_cast<android::ResStringPool_ref*>( + &attr->typedValue.data)); + } + } else { + attr->typedValue.dataType = android::Res_value::TYPE_STRING; + 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); + + // 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; + + // Flatten the StringPool. + StringPool::flattenUtf8(outBuffer, pool); + + // 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 diff --git a/tools/aapt2/XmlFlattener.h b/tools/aapt2/XmlFlattener.h new file mode 100644 index 0000000..abf64ab --- /dev/null +++ b/tools/aapt2/XmlFlattener.h @@ -0,0 +1,68 @@ +/* + * 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_FLATTENER_H +#define AAPT_XML_FLATTENER_H + +#include "BigBuffer.h" +#include "Maybe.h" +#include "Resolver.h" +#include "Source.h" +#include "XmlPullParser.h" + +namespace aapt { + +/** + * 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. + */ +class XmlFlattener { +public: + struct Options { + /** + * If set, tells the XmlFlattener to strip out + * attributes that have been introduced after + * max SDK. + */ + Maybe<size_t> maxSdkAttribute; + }; + + /** + * Creates a flattener with a Resolver to resolve references + * and attributes. + */ + XmlFlattener(const std::shared_ptr<Resolver>& resolver); + + XmlFlattener(const XmlFlattener&) = delete; // Not copyable. + + /** + * 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. + */ + Maybe<size_t> flatten(const Source& source, const std::shared_ptr<XmlPullParser>& parser, + BigBuffer* outBuffer, Options options); + +private: + std::shared_ptr<Resolver> mResolver; +}; + +} // namespace aapt + +#endif // AAPT_XML_FLATTENER_H diff --git a/tools/aapt2/XmlFlattener_test.cpp b/tools/aapt2/XmlFlattener_test.cpp new file mode 100644 index 0000000..79030be --- /dev/null +++ b/tools/aapt2/XmlFlattener_test.cpp @@ -0,0 +1,85 @@ +/* + * 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 "Resolver.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "SourceXmlPullParser.h" +#include "Util.h" +#include "XmlFlattener.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +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<ResourceTable> table = std::make_shared<ResourceTable>(); + table->setPackage(u"android"); + table->setPackageId(0x01); + + table->addResource(ResourceName{ {}, ResourceType::kAttr, u"id" }, + ResourceId{ 0x01010000 }, {}, {}, + util::make_unique<Attribute>(false, android::ResTable_map::TYPE_ANY)); + + table->addResource(ResourceName{ {}, ResourceType::kId, u"test" }, + ResourceId{ 0x01020000 }, {}, {}, util::make_unique<Id>()); + + mFlattener = std::make_shared<XmlFlattener>( + std::make_shared<Resolver>(table, std::make_shared<android::AssetManager>())); + } + + ::testing::AssertionResult testFlatten(std::istream& in, android::ResXMLTree* outTree) { + std::stringstream input(kXmlPreamble); + input << in.rdbuf() << std::endl; + std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(input); + BigBuffer outBuffer(1024); + if (!mFlattener->flatten(Source{ "test" }, xmlParser, &outBuffer, {})) { + return ::testing::AssertionFailure(); + } + + std::unique_ptr<uint8_t[]> data = util::copy(outBuffer); + if (outTree->setTo(data.get(), outBuffer.size(), true) != android::NO_ERROR) { + return ::testing::AssertionFailure(); + } + return ::testing::AssertionSuccess(); + } + + std::shared_ptr<XmlFlattener> mFlattener; +}; + +TEST_F(XmlFlattenerTest, ParseSimpleView) { + std::stringstream input; + input << "<View xmlns:android=\"http://schemas.android.com/apk/res/android\"" << std::endl + << " android:id=\"@id/test\">" << std::endl + << "</View>" << std::endl; + + android::ResXMLTree tree; + ASSERT_TRUE(testFlatten(input, &tree)); + + while (tree.next() != android::ResXMLTree::END_DOCUMENT) { + ASSERT_NE(tree.getEventType(), android::ResXMLTree::BAD_DOCUMENT); + } +} + +} // namespace aapt diff --git a/tools/aapt2/XmlPullParser.h b/tools/aapt2/XmlPullParser.h new file mode 100644 index 0000000..c667df2 --- /dev/null +++ b/tools/aapt2/XmlPullParser.h @@ -0,0 +1,234 @@ +/* + * 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_PULL_PARSER_H +#define AAPT_XML_PULL_PARSER_H + +#include <algorithm> +#include <ostream> +#include <string> +#include <vector> + +#include "StringPiece.h" + +namespace aapt { + +class XmlPullParser { +public: + enum class Event { + kBadDocument, + kStartDocument, + kEndDocument, + + kStartNamespace, + kEndNamespace, + kStartElement, + kEndElement, + kText, + kComment, + }; + + static void skipCurrentElement(XmlPullParser* parser); + static bool isGoodEvent(Event event); + + virtual ~XmlPullParser() {} + + /** + * Returns the current event that is being processed. + */ + virtual Event getEvent() const = 0; + + virtual const std::string& getLastError() const = 0; + + /** + * Note, unlike XmlPullParser, the first call to next() will return + * StartElement of the first element. + */ + virtual Event next() = 0; + + // + // These are available for all nodes. + // + + virtual const std::u16string& getComment() const = 0; + virtual size_t getLineNumber() const = 0; + virtual size_t getDepth() const = 0; + + /** + * Returns the character data for a Text event. + */ + virtual const std::u16string& getText() const = 0; + + /** + * Namespace prefix is available for StartNamespace and EndNamespace. + */ + virtual const std::u16string& getNamespacePrefix() const = 0; + + /** + * Namespace URI is available for StartNamespace. + */ + virtual const std::u16string& getNamespaceUri() const = 0; + + // + // These are available for StartElement and EndElement. + // + + virtual const std::u16string& getElementNamespace() const = 0; + virtual const std::u16string& getElementName() const = 0; + + // + // Remaining methods are for retrieving information about attributes + // associated with a StartElement. + // + // Attributes must be in sorted order (according to the less than operator + // of struct Attribute). + // + + struct Attribute { + std::u16string namespaceUri; + std::u16string name; + std::u16string value; + + int compare(const Attribute& rhs) const; + bool operator<(const Attribute& rhs) const; + bool operator==(const Attribute& rhs) const; + bool operator!=(const Attribute& rhs) const; + }; + + using const_iterator = std::vector<Attribute>::const_iterator; + + virtual const_iterator beginAttributes() const = 0; + virtual const_iterator endAttributes() const = 0; + virtual size_t getAttributeCount() const = 0; + const_iterator findAttribute(StringPiece16 namespaceUri, StringPiece16 name) const; +}; + +/* + * Automatically reads up to the end tag of the element it was initialized with + * when being destroyed. + */ +class AutoFinishElement { +public: + AutoFinishElement(const std::shared_ptr<XmlPullParser>& parser); + ~AutoFinishElement(); + +private: + std::shared_ptr<XmlPullParser> mParser; + int mDepth; +}; + +// +// Implementation +// + +inline ::std::ostream& operator<<(::std::ostream& out, XmlPullParser::Event event) { + switch (event) { + case XmlPullParser::Event::kBadDocument: return out << "BadDocument"; + case XmlPullParser::Event::kStartDocument: return out << "StartDocument"; + case XmlPullParser::Event::kEndDocument: return out << "EndDocument"; + case XmlPullParser::Event::kStartNamespace: return out << "StartNamespace"; + case XmlPullParser::Event::kEndNamespace: return out << "EndNamespace"; + case XmlPullParser::Event::kStartElement: return out << "StartElement"; + case XmlPullParser::Event::kEndElement: return out << "EndElement"; + case XmlPullParser::Event::kText: return out << "Text"; + case XmlPullParser::Event::kComment: return out << "Comment"; + } + return out; +} + +inline void XmlPullParser::skipCurrentElement(XmlPullParser* parser) { + int depth = 1; + while (depth > 0) { + switch (parser->next()) { + case Event::kEndDocument: + case Event::kBadDocument: + return; + case Event::kStartElement: + depth++; + break; + case Event::kEndElement: + depth--; + break; + default: + break; + } + } +} + +inline bool XmlPullParser::isGoodEvent(XmlPullParser::Event event) { + return event != Event::kBadDocument && event != Event::kEndDocument; +} + +inline int XmlPullParser::Attribute::compare(const Attribute& rhs) const { + int cmp = namespaceUri.compare(rhs.namespaceUri); + if (cmp != 0) return cmp; + return name.compare(rhs.name); +} + +inline bool XmlPullParser::Attribute::operator<(const Attribute& rhs) const { + return compare(rhs) < 0; +} + +inline bool XmlPullParser::Attribute::operator==(const Attribute& rhs) const { + return compare(rhs) == 0; +} + +inline bool XmlPullParser::Attribute::operator!=(const Attribute& rhs) const { + return compare(rhs) != 0; +} + +inline XmlPullParser::const_iterator XmlPullParser::findAttribute(StringPiece16 namespaceUri, + StringPiece16 name) const { + const auto endIter = endAttributes(); + const auto iter = std::lower_bound(beginAttributes(), endIter, + std::pair<StringPiece16, StringPiece16>(namespaceUri, name), + [](const Attribute& attr, const std::pair<StringPiece16, StringPiece16>& rhs) -> bool { + int cmp = attr.namespaceUri.compare(0, attr.namespaceUri.size(), + rhs.first.data(), rhs.first.size()); + if (cmp < 0) return true; + if (cmp > 0) return false; + cmp = attr.name.compare(0, attr.name.size(), rhs.second.data(), rhs.second.size()); + if (cmp < 0) return true; + return false; + } + ); + + if (iter != endIter && namespaceUri == iter->namespaceUri && name == iter->name) { + return iter; + } + return endIter; +} + +inline AutoFinishElement::AutoFinishElement(const std::shared_ptr<XmlPullParser>& parser) : + mParser(parser), mDepth(parser->getDepth()) { +} + +inline AutoFinishElement::~AutoFinishElement() { + int depth; + XmlPullParser::Event event; + while ((depth = mParser->getDepth()) >= mDepth && + XmlPullParser::isGoodEvent(event = mParser->getEvent())) { + if (depth == mDepth && (event == XmlPullParser::Event::kEndElement || + event == XmlPullParser::Event::kEndNamespace)) { + return; + } + mParser->next(); + } +} + +} // namespace aapt + +#endif // AAPT_XML_PULL_PARSER_H diff --git a/tools/aapt2/data/AndroidManifest.xml b/tools/aapt2/data/AndroidManifest.xml new file mode 100644 index 0000000..c017a0d --- /dev/null +++ b/tools/aapt2/data/AndroidManifest.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app"> + <application> + </application> +</manifest> diff --git a/tools/aapt2/data/res/drawable/image.xml b/tools/aapt2/data/res/drawable/image.xml new file mode 100644 index 0000000..9b38739 --- /dev/null +++ b/tools/aapt2/data/res/drawable/image.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector /> diff --git a/tools/aapt2/data/res/layout/main.xml b/tools/aapt2/data/res/layout/main.xml new file mode 100644 index 0000000..e0b55c0 --- /dev/null +++ b/tools/aapt2/data/res/layout/main.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/view" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <View xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/me" + android:layout_width="1dp" + android:layout_height="match_parent" + app:layout_width="false" + app:flags="complex|weak" + android:colorAccent="#ffffff"/> +</LinearLayout> diff --git a/tools/aapt2/data/res/values-v4/styles.xml b/tools/aapt2/data/res/values-v4/styles.xml new file mode 100644 index 0000000..979a82a --- /dev/null +++ b/tools/aapt2/data/res/values-v4/styles.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="App" parent="android:Theme.Material"> + <item name="android:colorAccent">@color/accent</item> + <item name="android:text">Hey</item> + </style> +</resources> diff --git a/tools/aapt2/data/res/values/colors.xml b/tools/aapt2/data/res/values/colors.xml new file mode 100644 index 0000000..89db5fb --- /dev/null +++ b/tools/aapt2/data/res/values/colors.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="primary">#f44336</color> + <color name="primary_dark">#b71c1c</color> + <color name="accent">#fdd835</color> +</resources> diff --git a/tools/aapt2/data/res/values/styles.xml b/tools/aapt2/data/res/values/styles.xml new file mode 100644 index 0000000..71ce388 --- /dev/null +++ b/tools/aapt2/data/res/values/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="App" parent="android:Theme.Material"> + <item name="android:background">@color/primary</item> + <item name="android:colorPrimary">@color/primary</item> + <item name="android:colorPrimaryDark">@color/primary_dark</item> + <item name="android:colorAccent">@color/accent</item> + </style> + <attr name="custom" format="reference" /> + <style name="Pop"> + <item name="custom">@drawable/image</item> + </style> + <string name="yo">@string/wow</string> + + <declare-styleable name="View"> + <attr name="custom" /> + <attr name="decor"> + <enum name="no-border" value="0"/> + <enum name="border" value="1"/> + <enum name="shadow" value="2"/> + </attr> + </declare-styleable> + +</resources> diff --git a/tools/aapt2/data/res/values/test.xml b/tools/aapt2/data/res/values/test.xml new file mode 100644 index 0000000..d3ead34 --- /dev/null +++ b/tools/aapt2/data/res/values/test.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="hooha"><font bgcolor="#ffffff">Hey guys!</font> <xliff:g>My</xliff:g> name is <b>Adam</b>. How <b><i>are</i></b> you?</string> + <public name="hooha" type="string" id="0x7f020001"/> + <string name="wow">@android:string/ok</string> + <public name="image" type="drawable" id="0x7f060000" /> + <attr name="layout_width" format="boolean" /> + <attr name="flags"> + <flag name="complex" value="1" /> + <flag name="pub" value="2" /> + <flag name="weak" value="4" /> + </attr> +</resources> diff --git a/tools/aapt2/data/resources.arsc b/tools/aapt2/data/resources.arsc Binary files differnew file mode 100644 index 0000000..6a416df --- /dev/null +++ b/tools/aapt2/data/resources.arsc diff --git a/tools/aapt2/data/resources_base.arsc b/tools/aapt2/data/resources_base.arsc Binary files differnew file mode 100644 index 0000000..f9d0610 --- /dev/null +++ b/tools/aapt2/data/resources_base.arsc diff --git a/tools/aapt2/data/resources_hdpi.arsc b/tools/aapt2/data/resources_hdpi.arsc Binary files differnew file mode 100644 index 0000000..97232a3 --- /dev/null +++ b/tools/aapt2/data/resources_hdpi.arsc diff --git a/tools/aapt2/process.dot b/tools/aapt2/process.dot new file mode 100644 index 0000000..a92405d --- /dev/null +++ b/tools/aapt2/process.dot @@ -0,0 +1,92 @@ +digraph aapt { + out_package [label="out/default/package.apk"]; + out_fr_package [label="out/fr/package.apk"]; + out_table_aligned [label="out/default/resources-aligned.arsc"]; + out_table_fr_aligned [label="out/fr/resources-aligned.arsc"]; + out_res_layout_main_xml [label="out/res/layout/main.xml"]; + out_res_layout_v21_main_xml [color=red,label="out/res/layout-v21/main.xml"]; + out_res_layout_fr_main_xml [label="out/res/layout-fr/main.xml"]; + out_res_layout_fr_v21_main_xml [color=red,label="out/res/layout-fr-v21/main.xml"]; + out_table [label="out/default/resources.arsc"]; + out_fr_table [label="out/fr/resources.arsc"]; + out_values_table [label="out/values/resources.arsc"]; + out_layout_table [label="out/layout/resources.arsc"]; + out_values_fr_table [label="out/values-fr/resources.arsc"]; + out_layout_fr_table [label="out/layout-fr/resources.arsc"]; + res_values_strings_xml [label="res/values/strings.xml"]; + res_values_attrs_xml [label="res/values/attrs.xml"]; + res_layout_main_xml [label="res/layout/main.xml"]; + res_layout_fr_main_xml [label="res/layout-fr/main.xml"]; + res_values_fr_strings_xml [label="res/values-fr/strings.xml"]; + + out_package -> package_default; + out_fr_package -> package_fr; + + package_default [shape=box,label="Assemble",color=blue]; + package_default -> out_table_aligned; + package_default -> out_res_layout_main_xml; + package_default -> out_res_layout_v21_main_xml [color=red]; + + package_fr [shape=box,label="Assemble",color=blue]; + package_fr -> out_table_fr_aligned; + package_fr -> out_res_layout_fr_main_xml; + package_fr -> out_res_layout_fr_v21_main_xml [color=red]; + + out_table_aligned -> align_tables; + out_table_fr_aligned -> align_tables; + + align_tables [shape=box,label="Align",color=blue]; + align_tables -> out_table; + align_tables -> out_fr_table; + + out_table -> link_tables; + + link_tables [shape=box,label="Link",color=blue]; + link_tables -> out_values_table; + link_tables -> out_layout_table; + + out_values_table -> compile_values; + + compile_values [shape=box,label="Collect",color=blue]; + compile_values -> res_values_strings_xml; + compile_values -> res_values_attrs_xml; + + out_layout_table -> collect_xml; + + collect_xml [shape=box,label="Collect",color=blue]; + collect_xml -> res_layout_main_xml; + + out_fr_table -> link_fr_tables; + + link_fr_tables [shape=box,label="Link",color=blue]; + link_fr_tables -> out_values_fr_table; + link_fr_tables -> out_layout_fr_table; + + out_values_fr_table -> compile_values_fr; + + compile_values_fr [shape=box,label="Compile",color=blue]; + compile_values_fr -> res_values_fr_strings_xml; + + out_layout_fr_table -> collect_xml_fr; + + collect_xml_fr [shape=box,label="Collect",color=blue]; + collect_xml_fr -> res_layout_fr_main_xml; + + compile_res_layout_main_xml [shape=box,label="Compile",color=blue]; + + out_res_layout_main_xml -> compile_res_layout_main_xml; + + out_res_layout_v21_main_xml -> compile_res_layout_main_xml [color=red]; + + compile_res_layout_main_xml -> res_layout_main_xml; + compile_res_layout_main_xml -> out_table_aligned; + + compile_res_layout_fr_main_xml [shape=box,label="Compile",color=blue]; + + out_res_layout_fr_main_xml -> compile_res_layout_fr_main_xml; + + out_res_layout_fr_v21_main_xml -> compile_res_layout_fr_main_xml [color=red]; + + compile_res_layout_fr_main_xml -> res_layout_fr_main_xml; + compile_res_layout_fr_main_xml -> out_table_fr_aligned; +} diff --git a/tools/aapt2/public_attr_map.py b/tools/aapt2/public_attr_map.py new file mode 100644 index 0000000..92136a8 --- /dev/null +++ b/tools/aapt2/public_attr_map.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +import sys +import xml.etree.ElementTree as ET + +def findSdkLevelForAttribute(id): + intId = int(id, 16) + packageId = 0x000000ff & (intId >> 24) + typeId = 0x000000ff & (intId >> 16) + entryId = 0x0000ffff & intId + + if packageId != 0x01 or typeId != 0x01: + return 0 + + levels = [(1, 0x021c), (2, 0x021d), (3, 0x0269), (4, 0x028d), + (5, 0x02ad), (6, 0x02b3), (7, 0x02b5), (8, 0x02bd), + (9, 0x02cb), (11, 0x0361), (12, 0x0366), (13, 0x03a6), + (16, 0x03ae), (17, 0x03cc), (18, 0x03da), (19, 0x03f1), + (20, 0x03f6), (21, 0x04ce)] + for level, attrEntryId in levels: + if entryId <= attrEntryId: + return level + return 22 + + +tree = None +with open(sys.argv[1], 'rt') as f: + tree = ET.parse(f) + +attrs = [] +for node in tree.iter('public'): + if node.get('type') == 'attr': + sdkLevel = findSdkLevelForAttribute(node.get('id', '0')) + if sdkLevel > 1 and sdkLevel < 22: + attrs.append("{{ u\"{}\", {} }}".format(node.get('name'), sdkLevel)) + +print "#include <string>" +print "#include <unordered_map>" +print +print "namespace aapt {" +print +print "static std::unordered_map<std::u16string, size_t> sAttrMap = {" +print ",\n ".join(attrs) +print "};" +print +print "size_t findAttributeSdkLevel(const std::u16string& name) {" +print " auto iter = sAttrMap.find(name);" +print " if (iter != sAttrMap.end()) {" +print " return iter->second;" +print " }" +print " return 0;" +print "}" +print +print "} // namespace aapt" +print diff --git a/tools/aapt2/todo.txt b/tools/aapt2/todo.txt new file mode 100644 index 0000000..acc8bfb --- /dev/null +++ b/tools/aapt2/todo.txt @@ -0,0 +1,29 @@ +XML Files +X Collect declared IDs +X Build StringPool +X Flatten + +Resource Table Operations +X Build Resource Table (with StringPool) from XML. +X Modify Resource Table. +X - Copy and transform resources. +X - Pre-17/21 attr correction. +X Perform analysis of types. +X Flatten. +X Assign resource IDs. +X Assign public resource IDs. +X Merge resource tables +- Assign private attributes to different typespace. +- Align resource tables + +Splits +- Collect all resources (ids from layouts). +- Generate resource table from base resources. +- Generate resource table from individual resources of the required type. +- Align resource tables (same type/name = same ID). + +Fat Apk +X Collect all resources (ids from layouts). +X Generate resource tables for all configurations. +- Align individual resource tables. +- Merge resource tables. |