From 972007abefcfbfc92afb61735dd692eafdab9c2b Mon Sep 17 00:00:00 2001 From: ktrajkovski Date: Mon, 8 Jul 2024 10:25:47 -0700 Subject: [PATCH] Add readLeb128 and readLeb128ToInt to ParsableByteArray. Leb128 is a little-endian long of variable byte length. The format is used during the extraction of the size of the OBU configuration for the iacb configuration box. PiperOrigin-RevId: 650295002 --- .../media3/common/util/ParsableByteArray.java | 36 +++++++++++ .../common/util/ParsableByteArrayTest.java | 64 +++++++++++++++++++ .../media3/extractor/mp4/AtomParsers.java | 12 +--- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java index 2f267a241b..db93658458 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Chars; +import com.google.common.primitives.Ints; import com.google.common.primitives.UnsignedBytes; import com.google.errorprone.annotations.CheckReturnValue; import java.nio.ByteBuffer; @@ -597,6 +598,41 @@ public final class ParsableByteArray { return value; } + /** + * Reads a little endian long of variable length. + * + * @throws IllegalStateException if the byte to be read is over the limit of the parsable byte + * array + * @return long value + */ + public long readUnsignedLeb128ToLong() { + long value = 0; + // At most, 63 bits of unsigned data can be stored in a long, which corresponds to 63/7=9 bytes + // in LEB128. + for (int i = 0; i < 9; i++) { + if (this.position == limit) { + throw new IllegalStateException("Attempting to read a byte over the limit."); + } + long currentByte = this.readUnsignedByte(); + value |= (currentByte & 0x7F) << (i * 7); + if ((currentByte & 0x80) == 0) { + break; + } + } + return value; + } + + /** + * Reads a little endian integer of variable length. + * + * @throws IllegalArgumentException if the read value is greater than {@link Integer#MAX_VALUE} or + * less than {@link Integer#MIN_VALUE} + * @return integer value + */ + public int readUnsignedLeb128ToInt() { + return Ints.checkedCast(readUnsignedLeb128ToLong()); + } + /** * Reads a UTF byte order mark (BOM) and returns the UTF {@link Charset} it represents. Returns * {@code null} without advancing {@link #getPosition() position} if no BOM is found. diff --git a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java index 2a97c0dfd9..0000b2d97e 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/ParsableByteArrayTest.java @@ -18,6 +18,7 @@ package androidx.media3.common.util; import static androidx.media3.test.utils.TestUtil.createByteArray; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.Charset.forName; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -917,4 +918,67 @@ public final class ParsableByteArrayTest { assertThat(parser.getPosition()).isEqualTo(22); assertThat(parser.readLine(Charsets.UTF_16LE)).isNull(); } + + @Test + public void readUnsignedLeb128ToLong() { + byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F}; + ParsableByteArray testArray = new ParsableByteArray(bytes); + + long readValue = testArray.readUnsignedLeb128ToLong(); + + assertThat(readValue).isEqualTo(0xFFFFFFFFL); + assertThat(testArray.getPosition()).isEqualTo(5); + } + + @Test + public void readMaxUnsignedLeb128ToLong() { + byte[] bytes = + new byte[] { + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0xFF, + (byte) 0x7F + }; + ParsableByteArray testArray = new ParsableByteArray(bytes); + + long readValue = testArray.readUnsignedLeb128ToLong(); + + assertThat(readValue).isEqualTo(Long.MAX_VALUE); + assertThat(testArray.getPosition()).isEqualTo(9); + } + + @Test + public void readUnsignedLeb128ToInt() { + byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F}; + ParsableByteArray testArray = new ParsableByteArray(bytes); + + int readValue = testArray.readUnsignedLeb128ToInt(); + + assertThat(readValue).isEqualTo(0x1FFFFFF); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void readMaxUnsignedLeb128ToInt() { + byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x07}; + ParsableByteArray testArray = new ParsableByteArray(bytes); + + int readValue = testArray.readUnsignedLeb128ToInt(); + + assertThat(readValue).isEqualTo(Integer.MAX_VALUE); + assertThat(testArray.getPosition()).isEqualTo(5); + } + + @Test + public void readTooLongUnsignedLeb128ToInt() { + byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F}; + ParsableByteArray testArray = new ParsableByteArray(bytes); + + assertThrows(IllegalArgumentException.class, testArray::readUnsignedLeb128ToInt); + } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 46fb1e36ce..5349f31d3f 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1920,17 +1920,7 @@ import java.util.Objects; } else if (childAtomType == Atom.TYPE_iacb) { parent.setPosition( childPosition + Atom.HEADER_SIZE + 1); // header and configuration version - int configObusSize = 0; - for (int i = 0; i <= 4; i++) { - int currentByte = parent.readUnsignedByte(); - configObusSize |= (currentByte & 0x7F) << (i * 7); - if ((currentByte & 0x80) == 0) { - break; - } - } - if (configObusSize < 0) { - throw ParserException.createForUnsupportedContainerFeature("OBU too large."); - } + int configObusSize = parent.readUnsignedLeb128ToInt(); byte[] initializationDataBytes = new byte[configObusSize]; parent.readBytes(initializationDataBytes, /* offset= */ 0, configObusSize); initializationData = ImmutableList.of(initializationDataBytes);