[automerger skipped] Merge 24Q3 to AOSP main am: a52694bd06 -s ours am: e882cf8d69 -s ours

am skip reason: Merged-In Id9603a32265dfc88c8bcb5503cc48e9a1da25a81 with SHA-1 f032935ca1 is already in history

Original change: https://android-review.googlesource.com/c/platform/packages/modules/GeoTZ/+/3258357

Change-Id: If979ab644fa03889c7a9356d09d0676bda2c01d4
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/s2storage/src/readonly/java/com/android/storage/block/read/Block.java b/s2storage/src/readonly/java/com/android/storage/block/read/Block.java
index 930785d..c931099 100644
--- a/s2storage/src/readonly/java/com/android/storage/block/read/Block.java
+++ b/s2storage/src/readonly/java/com/android/storage/block/read/Block.java
@@ -18,7 +18,6 @@
 
 import com.android.storage.util.Visitor;
 
-import java.nio.ByteBuffer;
 import java.util.Objects;
 
 /**
@@ -32,11 +31,11 @@
 
     private final BlockData mBlockData;
 
-    /** Creates a Block. The {@link ByteBuffer} must be read-only and is not copied. */
-    public Block(int id, int type, ByteBuffer dataBytes) {
+    /** Creates a Block. */
+    public Block(int id, int type, BlockData blockData) {
         mId = id;
         mType = type;
-        mBlockData = new BlockData(Objects.requireNonNull(dataBytes));
+        mBlockData = Objects.requireNonNull(blockData);
     }
 
     /** Returns the ID for this block. */
diff --git a/s2storage/src/readonly/java/com/android/storage/block/read/BlockData.java b/s2storage/src/readonly/java/com/android/storage/block/read/BlockData.java
index 4fe5bc4..c1eafaa 100644
--- a/s2storage/src/readonly/java/com/android/storage/block/read/BlockData.java
+++ b/s2storage/src/readonly/java/com/android/storage/block/read/BlockData.java
@@ -26,10 +26,9 @@
 /**
  * Provides typed, absolute position, random access to a block's data.
  *
- * <p>See also {@link TypedInputStream} for a streamed
- * equivalent.
+ * <p>See also {@link TypedInputStream} for a streamed equivalent.
  */
-public final class BlockData {
+public final class BlockData implements TypedData {
 
     private final ByteBuffer mDataBytes;
 
@@ -46,55 +45,82 @@
 
     /** Returns a copy of the underlying {@link ByteBuffer}. */
     public ByteBuffer getByteBuffer() {
-        return mDataBytes.duplicate();
+        ByteBuffer buffer = mDataBytes.duplicate();
+
+        // mDataBytes shouldn't have a position set, but make sure the duplicate doesn't anyway.
+        buffer.position(0);
+
+        return buffer;
     }
 
-    /** Returns the value of the byte at the specified position. */
+    @Override
+    public TypedData slice(int startPos, int length) {
+        // None of this code is thread safe, but this is especially not thread safe because
+        // it uses position / limit as part of the slicing, so synchronize.
+        int newLimit = startPos + length;
+
+        synchronized (mDataBytes) {
+            // mDataBytes shouldn't have a position or mark, but preserve and reset them
+            // again afterwards just in case.
+            int oldPosition = mDataBytes.position();
+            int oldLimit = mDataBytes.limit();
+
+            // Avoid creating a new slice that could fail when accessed, e.g. because its limit
+            // is outside of the original buffer.
+            if (newLimit > oldLimit) {
+                throw new IllegalArgumentException(
+                        "startPos(" + startPos + ") + length(" + length + ") > size()");
+            }
+
+            mDataBytes.position(startPos);
+            mDataBytes.limit(newLimit);
+            ByteBuffer sliceByteBuffer = mDataBytes.slice();
+
+            mDataBytes.limit(oldLimit);
+            mDataBytes.position(oldPosition);
+
+            return new BlockData(sliceByteBuffer);
+        }
+    }
+
+    @Override
     public byte getByte(int byteOffset) {
         return mDataBytes.get(byteOffset);
     }
 
-    /** Returns the value of the byte at the specified position as an unsigned value. */
+    @Override
     public int getUnsignedByte(int byteOffset) {
         return mDataBytes.get(byteOffset) & 0xFF;
     }
 
-    /** Returns the value of the 16-bit char at the specified position as an unsigned value. */
+    @Override
     public char getChar(int byteOffset) {
         return mDataBytes.getChar(byteOffset);
     }
 
-    /** Returns the value of the 32-bit int at the specified position as an signed value. */
+    @Override
     public int getInt(int byteOffset) {
         return mDataBytes.getInt(byteOffset);
     }
 
-    /** Returns the value of the 64-bit long at the specified position as an signed value. */
+    @Override
     public long getLong(int byteOffset) {
         return mDataBytes.getLong(byteOffset);
     }
 
-    /**
-     * Returns a tiny (<= 255 entries) array of signed bytes starting at the specified position,
-     * where the length is encoded in the data.
-     */
+    @Override
     public byte[] getTinyByteArray(int byteOffset) {
         int size = getUnsignedByte(byteOffset);
         return getBytes(byteOffset + 1, size);
     }
 
-    /**
-     * Returns an array of signed bytes starting at the specified position, where the 4-byte length
-     * is encoded in the data.
-     */
+    @Override
     public byte[] getByteArray(int byteOffset) {
         int size = getInt(byteOffset);
         return getBytes(byteOffset + Integer.BYTES, size);
     }
 
-    /**
-     * Returns an array of signed bytes starting at the specified position.
-     */
+    @Override
     public byte[] getBytes(int byteOffset, int byteCount) {
         byte[] bytes = new byte[byteCount];
         for (int i = 0; i < byteCount; i++) {
@@ -103,18 +129,13 @@
         return bytes;
     }
 
-    /**
-     * Returns a tiny (<= 255 entries) array of chars starting at the specified position, where the
-     * length is encoded in the data.
-     */
+    @Override
     public char[] getTinyCharArray(int byteOffset) {
         int size = getUnsignedByte(byteOffset);
         return getChars(byteOffset + 1, size);
     }
 
-    /**
-     * Returns an array of chars starting at the specified position.
-     */
+    @Override
     public char[] getChars(int byteOffset, int charCount) {
         char[] array = new char[charCount];
         for (int i = 0; i < charCount; i++) {
@@ -124,11 +145,7 @@
         return array;
     }
 
-    /**
-     * Returns 1-8 bytes ({@code valueSizeBytes}) starting as the specified position as a
-     * {@code long}. The value can be interpreted as signed or unsigned depending on
-     * {@code signExtend}.
-     */
+    @Override
     public long getValueAsLong(int valueSizeBytes, int byteOffset, boolean signExtend) {
         if (valueSizeBytes < 0 || valueSizeBytes > Long.BYTES) {
             throw new IllegalArgumentException("valueSizeBytes must be <= 8 bytes");
@@ -136,7 +153,7 @@
         return getValueInternal(valueSizeBytes, byteOffset, signExtend);
     }
 
-    /** Returns the size of the block data. */
+    @Override
     public int getSize() {
         return mDataBytes.limit();
     }
diff --git a/s2storage/src/readonly/java/com/android/storage/block/read/BlockFileReader.java b/s2storage/src/readonly/java/com/android/storage/block/read/BlockFileReader.java
index 4395bfd..62117d1 100644
--- a/s2storage/src/readonly/java/com/android/storage/block/read/BlockFileReader.java
+++ b/s2storage/src/readonly/java/com/android/storage/block/read/BlockFileReader.java
@@ -32,6 +32,7 @@
 public final class BlockFileReader implements AutoCloseable {
 
     private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0).asReadOnlyBuffer();
+    private static final BlockData EMPTY_BLOCK_DATA = new BlockData(EMPTY_BYTE_BUFFER);
 
     private Character mRequiredMagic;
 
@@ -146,7 +147,7 @@
 
         BlockInfo blockInfo = mBlockInfos[blockId];
         if (blockInfo.getBlockSizeBytes() == 0) {
-            return new Block(blockId, blockInfo.getType(), EMPTY_BYTE_BUFFER);
+            return new Block(blockId, blockInfo.getType(), EMPTY_BLOCK_DATA);
         }
 
         ByteBuffer allBlockBuffer;
@@ -193,8 +194,8 @@
         }
 
         // The part of the block that holds the data.
-        ByteBuffer blockDataBytes = allBlockBuffer.slice();
-        return new Block(actualId, actualType, blockDataBytes);
+        BlockData blockData = new BlockData(allBlockBuffer.slice());
+        return new Block(actualId, actualType, blockData);
     }
 
     /** Returns the number of blocks in the file. */
diff --git a/s2storage/src/readonly/java/com/android/storage/block/read/TypedData.java b/s2storage/src/readonly/java/com/android/storage/block/read/TypedData.java
new file mode 100644
index 0000000..c394a80
--- /dev/null
+++ b/s2storage/src/readonly/java/com/android/storage/block/read/TypedData.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.android.storage.block.read;
+
+import com.android.storage.io.read.TypedInputStream;
+
+/**
+ * Provides typed, absolute position, random access to data.
+ *
+ * <p>See also {@link TypedInputStream} for a streamed equivalent.
+ */
+public interface TypedData {
+
+    /**
+     * Returns a new read-only view into the data.
+     *
+     * @param startPos the start of the slice
+     * @param length the length of the slice to create
+     */
+    TypedData slice(int startPos, int length);
+
+    /** Returns the value of the byte at the specified position. */
+    byte getByte(int byteOffset);
+
+    /** Returns the value of the byte at the specified position as an unsigned value. */
+    int getUnsignedByte(int byteOffset);
+
+    /** Returns the value of the 16-bit char at the specified position as an unsigned value. */
+    char getChar(int byteOffset);
+
+    /** Returns the value of the 32-bit int at the specified position as an signed value. */
+    int getInt(int byteOffset);
+
+    /** Returns the value of the 64-bit long at the specified position as an signed value. */
+    long getLong(int byteOffset);
+
+    /**
+     * Returns a tiny (<= 255 entries) array of signed bytes starting at the specified position,
+     * where the length is encoded in the data.
+     */
+    byte[] getTinyByteArray(int byteOffset);
+
+    /**
+     * Returns an array of signed bytes starting at the specified position, where the 4-byte length
+     * is encoded in the data.
+     */
+    byte[] getByteArray(int byteOffset);
+
+    /**
+     * Returns an array of signed bytes starting at the specified position.
+     */
+    byte[] getBytes(int byteOffset, int byteCount);
+
+    /**
+     * Returns a tiny (<= 255 entries) array of chars starting at the specified position, where the
+     * length is encoded in the data.
+     */
+    char[] getTinyCharArray(int byteOffset);
+
+    /**
+     * Returns an array of chars starting at the specified position.
+     */
+    char[] getChars(int byteOffset, int charCount);
+
+    /**
+     * Returns 1-8 bytes ({@code valueSizeBytes}) starting as the specified position as a
+     * {@code long}. The value can be interpreted as signed or unsigned depending on
+     * {@code signExtend}.
+     */
+    long getValueAsLong(int valueSizeBytes, int byteOffset, boolean signExtend);
+
+    /** Returns the size of the block data. */
+    int getSize();
+}
diff --git a/s2storage/src/readonly/java/com/android/storage/table/packed/read/BaseTypedPackedTable.java b/s2storage/src/readonly/java/com/android/storage/table/packed/read/BaseTypedPackedTable.java
index 9a61c3c..6a530ab 100644
--- a/s2storage/src/readonly/java/com/android/storage/table/packed/read/BaseTypedPackedTable.java
+++ b/s2storage/src/readonly/java/com/android/storage/table/packed/read/BaseTypedPackedTable.java
@@ -17,6 +17,7 @@
 package com.android.storage.table.packed.read;
 
 import com.android.storage.block.read.BlockData;
+import com.android.storage.block.read.TypedData;
 import com.android.storage.table.reader.Table;
 
 import java.util.Objects;
@@ -51,6 +52,11 @@
     }
 
     @Override
+    public TypedData getSharedDataAsTyped() {
+        return mTableReader.getSharedDataAsTyped();
+    }
+
+    @Override
     public E getEntryByIndex(int i) {
         return createEntry(mTableReader.getEntryByIndex(i));
     }
diff --git a/s2storage/src/readonly/java/com/android/storage/table/packed/read/PackedTableReader.java b/s2storage/src/readonly/java/com/android/storage/table/packed/read/PackedTableReader.java
index d4354d1..9a02734 100644
--- a/s2storage/src/readonly/java/com/android/storage/table/packed/read/PackedTableReader.java
+++ b/s2storage/src/readonly/java/com/android/storage/table/packed/read/PackedTableReader.java
@@ -17,6 +17,7 @@
 package com.android.storage.table.packed.read;
 
 import com.android.storage.block.read.BlockData;
+import com.android.storage.block.read.TypedData;
 import com.android.storage.table.reader.IntValueTable.IntValueEntryMatcher;
 import com.android.storage.table.reader.LongValueTable.LongValueEntryMatcher;
 import com.android.storage.util.BitwiseUtils;
@@ -58,7 +59,7 @@
     private final int mEntryCount;
 
     /** Domain-specific data that should be common to all entries. */
-    private final byte[] mSharedData;
+    private final TypedData mSharedData;
 
     /**
      * True if the value is to be treated as a signed value, i.e. whether its sign should be
@@ -81,13 +82,16 @@
 
         int offset = 0;
 
+        int sharedDataLength;
         if (useBigSharedData) {
-            mSharedData = blockData.getByteArray(offset);
-            offset += Integer.BYTES + mSharedData.length;
+            sharedDataLength = blockData.getInt(offset);
+            offset += Integer.BYTES;
         } else {
-            mSharedData = blockData.getTinyByteArray(offset);
-            offset += Byte.BYTES + mSharedData.length;
+            sharedDataLength = blockData.getByte(offset);
+            offset += Byte.BYTES;
         }
+        mSharedData = blockData.slice(offset, sharedDataLength);
+        offset += sharedDataLength;
 
         // Boolean properties are extracted from a 32-bit bit field.
         int bitField = blockData.getUnsignedByte(offset);
@@ -147,8 +151,25 @@
         return mValueSizeBits;
     }
 
-    /** Returns the table's shared data. */
+    /**
+     * Returns the table's unstructured shared data that can be used, for example, to hold
+     * information shared by all entries in the table.
+     *
+     * <p>See {@link #getSharedDataAsTyped()} for an alternative that consumes less memory for
+     * large shared data and provides type conversions.
+     */
     public byte[] getSharedData() {
+        return mSharedData.getBytes(0, mSharedData.getSize());
+    }
+
+    /**
+     * Returns the table's unstructured shared data that can be used, for example, to hold
+     * information shared by all entries in the table.
+     *
+     * <p>Unlike {@link #getSharedData()}, this method will not allocate a byte array, which
+     * can save memory if the shared data is large.
+     */
+    public TypedData getSharedDataAsTyped() {
         return mSharedData;
     }
 
diff --git a/s2storage/src/readonly/java/com/android/storage/table/reader/Table.java b/s2storage/src/readonly/java/com/android/storage/table/reader/Table.java
index 2caf5d0..6b886b4 100644
--- a/s2storage/src/readonly/java/com/android/storage/table/reader/Table.java
+++ b/s2storage/src/readonly/java/com/android/storage/table/reader/Table.java
@@ -16,6 +16,8 @@
 
 package com.android.storage.table.reader;
 
+import com.android.storage.block.read.TypedData;
+
 /**
  * A table containing entries with a signed, int key. A table can also have an array of shared data
  * that can be used, for example, to hold information shared by all entries in the table.
@@ -27,10 +29,22 @@
     /**
      * Returns the table's unstructured shared data that can be used, for example, to hold
      * information shared by all entries in the table.
+     *
+     * <p>See {@link #getSharedDataAsTyped()} for an alternative that consumes less memory for
+     * large shared data and provides type conversions.
      */
     byte[] getSharedData();
 
     /**
+     * Returns the table's unstructured shared data that can be used, for example, to hold
+     * information shared by all entries in the table.
+     *
+     * <p>Unlike {@link #getSharedData()}, this method will not allocate a byte array, which
+     * can save memory if the shared data is large.
+     */
+    TypedData getSharedDataAsTyped();
+
+    /**
      * Returns a table entry associated with the key, or {@code null} if there isn't one. If
      * multiple entries have the key, then an arbitrary entry with the key is returned.
      */
diff --git a/s2storage/src/test/java/com/android/storage/block/read/BlockDataTest.java b/s2storage/src/test/java/com/android/storage/block/read/BlockDataTest.java
index af56bae..7624cf0 100644
--- a/s2storage/src/test/java/com/android/storage/block/read/BlockDataTest.java
+++ b/s2storage/src/test/java/com/android/storage/block/read/BlockDataTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
 
 import com.android.storage.io.write.TypedOutputStream;
 
@@ -70,6 +71,65 @@
     }
 
     @Test
+    public void slice() throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        TypedOutputStream typedOutputStream = new TypedOutputStream(baos);
+        byte[] tinyByteArray = "Tiny Byte Array".getBytes(StandardCharsets.UTF_8);
+        typedOutputStream.writeTinyByteArray(tinyByteArray);
+        typedOutputStream.close();
+
+        byte[] blockDataBytes = baos.toByteArray();
+        ByteBuffer originalByteBuffer = ByteBuffer.wrap(blockDataBytes).asReadOnlyBuffer();
+        BlockData blockData = new BlockData(originalByteBuffer);
+
+        assertSliceEmptyBehavior(blockData, 0, 0);
+        assertSliceNonEmptyBehavior(blockData, 0, 1);
+        assertSliceNonEmptyBehavior(blockData, 0, blockData.getSize());
+
+        assertSliceEmptyBehavior(blockData, 1, 0);
+        assertSliceNonEmptyBehavior(blockData, 1, 1);
+        assertSliceNonEmptyBehavior(blockData, 1, blockData.getSize() - 1);
+
+        assertSliceEmptyBehavior(blockData, blockData.getSize() - 2, 0);
+        assertSliceNonEmptyBehavior(blockData, blockData.getSize() - 2, 1);
+        assertSliceNonEmptyBehavior(blockData, blockData.getSize() - 2, 2);
+
+        assertSliceEmptyBehavior(blockData, blockData.getSize() - 1, 0);
+        assertSliceNonEmptyBehavior(blockData, blockData.getSize() - 1, 1);
+
+        assertSliceEmptyBehavior(blockData, blockData.getSize(), 0);
+
+        // Edge cases: length of slice puts the top of the new slice outside of the original buffer.
+        assertSliceTypedDataBadArguments(blockData, -1, 0);
+        assertSliceTypedDataBadArguments(blockData, 0, blockData.getSize() + 1);
+        assertSliceTypedDataBadArguments(blockData, 0, blockData.getSize() * 2);
+        assertSliceTypedDataBadArguments(blockData, 1, blockData.getSize());
+        assertSliceTypedDataBadArguments(blockData, blockData.getSize() - 2, 3);
+        assertSliceTypedDataBadArguments(blockData, blockData.getSize() - 1, 2);
+        assertSliceTypedDataBadArguments(blockData, blockData.getSize(), 1);
+    }
+
+    private static void assertSliceEmptyBehavior(BlockData blockData, int offset, int length) {
+        TypedData slice = blockData.slice(offset, length);
+        assertEquals(0, slice.getSize());
+        assertThrows(IndexOutOfBoundsException.class, () -> slice.getInt(0));
+    }
+
+    private static void assertSliceNonEmptyBehavior(BlockData blockData, int offset, int length) {
+        TypedData slice = blockData.slice(offset, length);
+        assertEquals(length, slice.getSize());
+        for (int i = 0; i < length; i++) {
+            assertEquals(blockData.getByte(offset + i), slice.getByte(i));
+        }
+        assertThrows(IndexOutOfBoundsException.class, () -> slice.getInt(length));
+    }
+
+    private static void assertSliceTypedDataBadArguments(
+            BlockData blockData, int offset, int length) {
+        assertThrows(IllegalArgumentException.class, () -> blockData.slice(offset, length));
+    }
+
+    @Test
     public void typedRandomAccess() throws IOException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         TypedOutputStream typedOutputStream = new TypedOutputStream(baos);
diff --git a/s2storage/src/test/java/com/android/storage/block/read/BlockTest.java b/s2storage/src/test/java/com/android/storage/block/read/BlockTest.java
index b79e1cb..b94d640 100644
--- a/s2storage/src/test/java/com/android/storage/block/read/BlockTest.java
+++ b/s2storage/src/test/java/com/android/storage/block/read/BlockTest.java
@@ -32,8 +32,8 @@
     public void visit() throws Exception {
         int id = 1234;
         int type = 2345;
-        ByteBuffer blockData = ByteBuffer.wrap("Data Bytes".getBytes()).asReadOnlyBuffer();
-        Block block = new Block(id, type, blockData);
+        ByteBuffer blockDataBuffer = ByteBuffer.wrap("Data Bytes".getBytes()).asReadOnlyBuffer();
+        Block block = new Block(id, type, new BlockData(blockDataBuffer));
         Block.BlockVisitor mockVisitor = mock(Block.BlockVisitor.class);
         block.visit(mockVisitor);
 
diff --git a/s2storage/src/test/java/com/android/storage/table/packed/PackedTableReaderWriterTest.java b/s2storage/src/test/java/com/android/storage/table/packed/PackedTableReaderWriterTest.java
index ab51f36..079dd73 100644
--- a/s2storage/src/test/java/com/android/storage/table/packed/PackedTableReaderWriterTest.java
+++ b/s2storage/src/test/java/com/android/storage/table/packed/PackedTableReaderWriterTest.java
@@ -91,6 +91,8 @@
         assertEquals(keyBits, tableReader.getKeySizeBits());
         assertEquals(signedValue, tableReader.isValueSigned());
         assertArrayEquals(sharedData, tableReader.getSharedData());
+        assertArrayEquals(
+                sharedData, tableReader.getSharedDataAsTyped().getBytes(0, sharedData.length));
         assertEquals((entrySizeBytes * Byte.SIZE) - keyBits, tableReader.getValueSizeBits());
         assertEquals(0, tableReader.getEntryCount());
     }
@@ -229,6 +231,8 @@
         BlockData blockData = new BlockData(createByteBuffer(baos.toByteArray()));
         PackedTableReader tableReader = new PackedTableReader(blockData, useBigSharedData);
         assertArrayEquals(sharedData, tableReader.getSharedData());
+        assertArrayEquals(
+                sharedData, tableReader.getSharedDataAsTyped().getBytes(0, sharedData.length));
     }
 
     @Test
@@ -279,6 +283,7 @@
         BlockData blockData = new BlockData(createByteBuffer(baos.toByteArray()));
         PackedTableReader tableReader = new PackedTableReader(blockData);
         assertArrayEquals(new byte[0], tableReader.getSharedData());
+        assertEquals(0, tableReader.getSharedDataAsTyped().getSize());
 
         assertNull(tableReader.getEntry(12));
     }
@@ -294,6 +299,7 @@
         BlockData blockData = new BlockData(createByteBuffer(baos.toByteArray()));
         PackedTableReader tableReader = new PackedTableReader(blockData);
         assertArrayEquals(new byte[0], tableReader.getSharedData());
+        assertEquals(0, tableReader.getSharedDataAsTyped().getSize());
 
         int negativeKey = -1;
         assertThrows(IllegalArgumentException.class, () -> tableReader.getEntry(negativeKey));