androidfw: Add support for 16-bit entry offsets

Bug: 237583012

Most offsets to the entries can be well encoded in 16-bit,
and given entries are 4-byte aligned, this gives us a range
of entry offsets from 0x00000 to 0xfffe * 4u, with 0xffffu
to represent ResTable_type::NO_ENTRY.

For now, 16-bit entry offset will be enabled only when:

 * all the entry offsets can be represented in 16-bit
 * --enable-compact-entries switch is turned on

Change-Id: I1c815c052aa5fba6eab2529434d31d7714c13694
diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp
index e78f91e..386f718 100644
--- a/libs/androidfw/LoadedArsc.cpp
+++ b/libs/androidfw/LoadedArsc.cpp
@@ -88,7 +88,9 @@
   // Make sure that there is enough room for the entry offsets.
   const size_t offsets_offset = dtohs(header->header.headerSize);
   const size_t entries_offset = dtohl(header->entriesStart);
-  const size_t offsets_length = sizeof(uint32_t) * entry_count;
+  const size_t offsets_length = header->flags & ResTable_type::FLAG_OFFSET16
+                                    ? sizeof(uint16_t) * entry_count
+                                    : sizeof(uint32_t) * entry_count;
 
   if (offsets_offset > entries_offset || entries_offset - offsets_offset < offsets_length) {
     LOG(ERROR) << "RES_TABLE_TYPE_TYPE entry offsets overlap actual entry data.";
@@ -247,14 +249,13 @@
   // The configuration matches and is better than the previous selection.
   // Find the entry value if it exists for this configuration.
   const size_t entry_count = dtohl(type_chunk->entryCount);
-  const size_t offsets_offset = dtohs(type_chunk->header.headerSize);
+  const auto offsets = type_chunk.offset(dtohs(type_chunk->header.headerSize));
 
   // Check if there is the desired entry in this type.
   if (type_chunk->flags & ResTable_type::FLAG_SPARSE) {
     // This is encoded as a sparse map, so perform a binary search.
     bool error = false;
-    auto sparse_indices = type_chunk.offset(offsets_offset)
-                                    .convert<ResTable_sparseTypeEntry>().iterator();
+    auto sparse_indices = offsets.convert<ResTable_sparseTypeEntry>().iterator();
     auto sparse_indices_end = sparse_indices + entry_count;
     auto result = std::lower_bound(sparse_indices, sparse_indices_end, entry_index,
                                    [&error](const incfs::map_ptr<ResTable_sparseTypeEntry>& entry,
@@ -289,17 +290,26 @@
     return base::unexpected(std::nullopt);
   }
 
-  const auto entry_offset_ptr = type_chunk.offset(offsets_offset).convert<uint32_t>() + entry_index;
-  if (UNLIKELY(!entry_offset_ptr)) {
-    return base::unexpected(IOError::PAGES_MISSING);
+  uint32_t result;
+
+  if (type_chunk->flags & ResTable_type::FLAG_OFFSET16) {
+    const auto entry_offset_ptr = offsets.convert<uint16_t>() + entry_index;
+    if (UNLIKELY(!entry_offset_ptr)) {
+      return base::unexpected(IOError::PAGES_MISSING);
+    }
+    result = offset_from16(entry_offset_ptr.value());
+  } else {
+    const auto entry_offset_ptr = offsets.convert<uint32_t>() + entry_index;
+    if (UNLIKELY(!entry_offset_ptr)) {
+      return base::unexpected(IOError::PAGES_MISSING);
+    }
+    result = dtohl(entry_offset_ptr.value());
   }
 
-  const uint32_t value = dtohl(entry_offset_ptr.value());
-  if (value == ResTable_type::NO_ENTRY) {
+  if (result == ResTable_type::NO_ENTRY) {
     return base::unexpected(std::nullopt);
   }
-
-  return value;
+  return result;
 }
 
 base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
@@ -382,24 +392,35 @@
   for (const auto& type_entry : type_spec->type_entries) {
     const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type;
 
-    size_t entry_count = dtohl(type->entryCount);
-    for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
-      auto entry_offset_ptr = type.offset(dtohs(type->header.headerSize)).convert<uint32_t>() +
-          entry_idx;
-      if (!entry_offset_ptr) {
-        return base::unexpected(IOError::PAGES_MISSING);
-      }
+    const size_t entry_count = dtohl(type->entryCount);
+    const auto entry_offsets = type.offset(dtohs(type->header.headerSize));
 
+    for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
       uint32_t offset;
       uint16_t res_idx;
       if (type->flags & ResTable_type::FLAG_SPARSE) {
-        auto sparse_entry = entry_offset_ptr.convert<ResTable_sparseTypeEntry>();
+        auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx;
+        if (!sparse_entry) {
+          return base::unexpected(IOError::PAGES_MISSING);
+        }
         offset = dtohs(sparse_entry->offset) * 4u;
         res_idx  = dtohs(sparse_entry->idx);
+      } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+        auto entry = entry_offsets.convert<uint16_t>() + entry_idx;
+        if (!entry) {
+          return base::unexpected(IOError::PAGES_MISSING);
+        }
+        offset = offset_from16(entry.value());
+        res_idx = entry_idx;
       } else {
-        offset = dtohl(entry_offset_ptr.value());
+        auto entry = entry_offsets.convert<uint32_t>() + entry_idx;
+        if (!entry) {
+          return base::unexpected(IOError::PAGES_MISSING);
+        }
+        offset = dtohl(entry.value());
         res_idx = entry_idx;
       }
+
       if (offset != ResTable_type::NO_ENTRY) {
         auto entry = type.offset(dtohl(type->entriesStart) + offset).convert<ResTable_entry>();
         if (!entry) {
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index aac52b4..ba93546 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6521,8 +6521,12 @@
                     // Entry does not exist.
                     continue;
                 }
-
-                thisOffset = dtohl(eindex[realEntryIndex]);
+                if (thisType->flags & ResTable_type::FLAG_OFFSET16) {
+                    auto eindex16 = reinterpret_cast<const uint16_t*>(eindex);
+                    thisOffset = offset_from16(eindex16[realEntryIndex]);
+                } else {
+                    thisOffset = dtohl(eindex[realEntryIndex]);
+                }
             }
 
             if (thisOffset == ResTable_type::NO_ENTRY) {
@@ -7574,6 +7578,9 @@
                         if (type->flags & ResTable_type::FLAG_SPARSE) {
                             printf(" [sparse]");
                         }
+                        if (type->flags & ResTable_type::FLAG_OFFSET16) {
+                            printf(" [offset16]");
+                        }
                     }
 
                     printf(":\n");
@@ -7605,7 +7612,13 @@
                             thisOffset = static_cast<uint32_t>(dtohs(entry->offset)) * 4u;
                         } else {
                             entryId = entryIndex;
-                            thisOffset = dtohl(eindex[entryIndex]);
+                            if (type->flags & ResTable_type::FLAG_OFFSET16) {
+                                const auto eindex16 =
+                                    reinterpret_cast<const uint16_t*>(eindex);
+                                thisOffset = offset_from16(eindex16[entryIndex]);
+                            } else {
+                                thisOffset = dtohl(eindex[entryIndex]);
+                            }
                             if (thisOffset == ResTable_type::NO_ENTRY) {
                                 continue;
                             }
diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp
index 2c39a81..70d14a1 100644
--- a/libs/androidfw/TypeWrappers.cpp
+++ b/libs/androidfw/TypeWrappers.cpp
@@ -59,7 +59,9 @@
             + dtohl(type->header.size);
     const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>(
             reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize));
-    if (reinterpret_cast<uintptr_t>(entryIndices) + (sizeof(uint32_t) * entryCount) > containerEnd) {
+    const size_t indexSize = type->flags & ResTable_type::FLAG_OFFSET16 ?
+                                    sizeof(uint16_t) : sizeof(uint32_t);
+    if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) {
         ALOGE("Type's entry indices extend beyond its boundaries");
         return NULL;
     }
@@ -73,6 +75,9 @@
       }
 
       entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u;
+    } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+      auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices);
+      entryOffset = offset_from16(entryIndices16[mIndex]);
     } else {
       entryOffset = dtohl(entryIndices[mIndex]);
     }
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 42d8cbeb..d588b235 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1448,6 +1448,10 @@
         // Mark any types that use this with a v26 qualifier to prevent runtime issues on older
         // platforms.
         FLAG_SPARSE = 0x01,
+
+        // If set, the offsets to the entries are encoded in 16-bit, real_offset = offset * 4u
+        // An 16-bit offset of 0xffffu means a NO_ENTRY
+        FLAG_OFFSET16 = 0x02,
     };
     uint8_t flags;
 
@@ -1464,6 +1468,11 @@
     ResTable_config config;
 };
 
+// Convert a 16-bit offset to 32-bit if FLAG_OFFSET16 is set
+static inline uint32_t offset_from16(uint16_t off16) {
+    return dtohs(off16) == 0xffffu ? ResTable_type::NO_ENTRY : dtohs(off16) * 4u;
+}
+
 // The minimum size required to read any version of ResTable_type.
 constexpr size_t kResTableTypeMinSize =
     sizeof(ResTable_type) - sizeof(ResTable_config) + sizeof(ResTable_config::size);
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index c431730..f192234 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -16,6 +16,7 @@
 
 #include "format/binary/TableFlattener.h"
 
+#include <limits>
 #include <sstream>
 #include <type_traits>
 #include <variant>
@@ -191,6 +192,9 @@
       offsets[flat_entry.entry->id.value()] = res_entry_writer->Write(&flat_entry);
     }
 
+    // whether the offsets can be represented in 2 bytes
+    bool short_offsets = (values_buffer.size() / 4u) < std::numeric_limits<uint16_t>::max();
+
     bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
                          sparse_entries_ == SparseEntriesMode::Forced;
 
@@ -203,8 +207,7 @@
     }
 
     // Only sparse encode if the offsets are representable in 2 bytes.
-    sparse_encode =
-        sparse_encode && (values_buffer.size() / 4u) <= std::numeric_limits<uint16_t>::max();
+    sparse_encode = sparse_encode && short_offsets;
 
     // Only sparse encode if the ratio of populated entries to total entries is below some
     // threshold.
@@ -226,12 +229,22 @@
       }
     } else {
       type_header->entryCount = android::util::HostToDevice32(num_total_entries);
-      uint32_t* indices = type_writer.NextBlock<uint32_t>(num_total_entries);
-      for (size_t i = 0; i < num_total_entries; i++) {
-        indices[i] = android::util::HostToDevice32(offsets[i]);
+      if (compact_entry && short_offsets) {
+        // use 16-bit offset only when compact_entry is true
+        type_header->flags |= ResTable_type::FLAG_OFFSET16;
+        uint16_t* indices = type_writer.NextBlock<uint16_t>(num_total_entries);
+        for (size_t i = 0; i < num_total_entries; i++) {
+          indices[i] = android::util::HostToDevice16(offsets[i] / 4u);
+        }
+      } else {
+        uint32_t* indices = type_writer.NextBlock<uint32_t>(num_total_entries);
+        for (size_t i = 0; i < num_total_entries; i++) {
+          indices[i] = android::util::HostToDevice32(offsets[i]);
+        }
       }
     }
 
+    type_writer.buffer()->Align4();
     type_header->entriesStart = android::util::HostToDevice32(type_writer.size());
     type_writer.buffer()->AppendBuffer(std::move(values_buffer));
     type_writer.Finish();