Bug: 237583012
Given the large number of simple resources such as strings in
Android resources, their ResTable_entry and Res_value can be
encoded together in a compact way. This allows a significant
saving in both storage and memory footprint.
The basic observations for simple resources are:
* ResTable_entry.size will always be sizeof(ResTable_entry)
unless it's a complex entry
* ResTable_entry.key is unlikely to exceed 16-bit
* ResTable_entry.flags only uses 3 bits for now
* Res_value.size will always be sizeof(Res_value)
Given the above, we could well encode the information into
a compact/compatible structure.
struct compact {
uint16_t key;
uint16_t flags;
uint32_t data;
};
The layout of this structure will allow maximum backward
compatibility. e.g. the flags will be at the same offset,
and a
`dtohs((ResTable_entry *)entry->flags) & FLAG_COMPACT`
would tell if this entry is a compact one or not. For a
compact entry:
struct compact *entry;
entry_size == sizeof(*entry)
entry_key == static_cast<uint32_t>(dtohs(entry->key))
entry_flags == dtohs(entry->flags) & 0xff // low 8-bit
data_type == dtohs(entry->flags) >> 8 // high 8-bit
data_size == sizeof(Res_value)
data_value == dtohl(entry->data)
To allow minimum code change and backward compatibility,
we change 'struct ResTable_entry' to 'union ResTable_entry',
with an anonymous structure inside that's fully backward
compatible. Thus, any existing reference such as:
ResTable_entry *entry = ...
if (dtohs(entry->flags) & ResTable_entry::FLAG_COMPLEX) ...
would still work.
However, special care needs to be taken after an entry is
obtained, and when value needs to be extracted.
A compact entry will not encode a complex value, and hence
complex entries/values are handled the same way.
Change-Id: I15d97a4f5e85fab28c075496f7f0cf6b1fcd73e3
191 lines
7.3 KiB
C++
191 lines
7.3 KiB
C++
/*
|
|
* Copyright (C) 2022 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 "format/binary/ResEntryWriter.h"
|
|
|
|
#include "androidfw/BigBuffer.h"
|
|
#include "format/binary/ResourceTypeExtensions.h"
|
|
#include "test/Test.h"
|
|
#include "util/Util.h"
|
|
|
|
using ::android::BigBuffer;
|
|
using ::android::Res_value;
|
|
using ::android::ResTable_map;
|
|
using ::testing::Eq;
|
|
using ::testing::Ge;
|
|
using ::testing::IsNull;
|
|
using ::testing::Ne;
|
|
using ::testing::NotNull;
|
|
|
|
namespace aapt {
|
|
|
|
using SequentialResEntryWriterTest = CommandTestFixture;
|
|
using DeduplicateItemsResEntryWriterTest = CommandTestFixture;
|
|
|
|
std::vector<int32_t> WriteAllEntries(const ResourceTableView& table, ResEntryWriter& writer) {
|
|
std::vector<int32_t> result = {};
|
|
for (const auto& type : table.packages[0].types) {
|
|
for (const auto& entry : type.entries) {
|
|
for (const auto& value : entry.values) {
|
|
auto flat_entry = FlatEntry{&entry, value->value.get(), 0};
|
|
result.push_back(writer.Write(&flat_entry));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TEST_F(SequentialResEntryWriterTest, WriteEntriesOneByOne) {
|
|
std::unique_ptr<ResourceTable> table =
|
|
test::ResourceTableBuilder()
|
|
.AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
|
|
.AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
|
|
.AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
|
|
.Build();
|
|
|
|
{
|
|
BigBuffer out(512);
|
|
SequentialResEntryWriter<false> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair),
|
|
2 * sizeof(ResEntryValuePair)};
|
|
EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
|
|
{
|
|
/* expect a compact entry to only take sizeof(ResTable_entry) */
|
|
BigBuffer out(512);
|
|
SequentialResEntryWriter<true> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry),
|
|
2 * sizeof(ResTable_entry)};
|
|
EXPECT_EQ(out.size(), 3 * sizeof(ResTable_entry));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
};
|
|
|
|
TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) {
|
|
std::unique_ptr<Array> array1 = util::make_unique<Array>();
|
|
array1->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
|
|
array1->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
|
|
std::unique_ptr<Array> array2 = util::make_unique<Array>();
|
|
array2->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
|
|
array2->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
|
|
|
|
std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
|
|
.AddValue("com.app.test:array/arr1", std::move(array1))
|
|
.AddValue("com.app.test:array/arr2", std::move(array2))
|
|
.Build();
|
|
|
|
{
|
|
BigBuffer out(512);
|
|
SequentialResEntryWriter<false> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
|
|
EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
|
|
{
|
|
/* compact_entry should have no impact to map items */
|
|
BigBuffer out(512);
|
|
SequentialResEntryWriter<true> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
|
|
EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
};
|
|
|
|
TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) {
|
|
std::unique_ptr<ResourceTable> table =
|
|
test::ResourceTableBuilder()
|
|
.AddSimple("com.app.test:id/id1", ResourceId(0x7f010000))
|
|
.AddSimple("com.app.test:id/id2", ResourceId(0x7f010001))
|
|
.AddSimple("com.app.test:id/id3", ResourceId(0x7f010002))
|
|
.Build();
|
|
|
|
{
|
|
BigBuffer out(512);
|
|
DeduplicateItemsResEntryWriter<false> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, 0, 0};
|
|
EXPECT_EQ(out.size(), sizeof(ResEntryValuePair));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
|
|
{
|
|
/* expect a compact entry to only take sizeof(ResTable_entry) */
|
|
BigBuffer out(512);
|
|
DeduplicateItemsResEntryWriter<true> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, 0, 0};
|
|
EXPECT_EQ(out.size(), sizeof(ResTable_entry));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
};
|
|
|
|
TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) {
|
|
std::unique_ptr<Array> array1 = util::make_unique<Array>();
|
|
array1->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
|
|
array1->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
|
|
std::unique_ptr<Array> array2 = util::make_unique<Array>();
|
|
array2->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u));
|
|
array2->elements.push_back(
|
|
util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u));
|
|
|
|
std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
|
|
.AddValue("com.app.test:array/arr1", std::move(array1))
|
|
.AddValue("com.app.test:array/arr2", std::move(array2))
|
|
.Build();
|
|
|
|
{
|
|
BigBuffer out(512);
|
|
DeduplicateItemsResEntryWriter<false> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
|
|
EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
|
|
{
|
|
/* compact_entry should have no impact to map items */
|
|
BigBuffer out(512);
|
|
DeduplicateItemsResEntryWriter<true> writer(&out);
|
|
auto offsets = WriteAllEntries(table->GetPartitionedView(), writer);
|
|
|
|
std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)};
|
|
EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)));
|
|
EXPECT_EQ(offsets, expected_offsets);
|
|
}
|
|
};
|
|
|
|
} // namespace aapt
|