diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index 7aa420d68e..227d4bf0e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -16,20 +16,50 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; /** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ public final class DefaultContentMetadata implements ContentMetadata { + private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024; + + /** + * Deserializes a {@link DefaultContentMetadata} from the given input stream. + * + * @param input Input stream to read from. + * @return a {@link DefaultContentMetadata} instance. + * @throws IOException If an error occurs during reading from input. + */ + public static DefaultContentMetadata readFromStream(DataInputStream input) throws IOException { + int size = input.readInt(); + HashMap metadata = new HashMap<>(); + for (int i = 0; i < size; i++) { + String name = input.readUTF(); + int valueSize = input.readInt(); + if (valueSize < 0 || valueSize > MAX_VALUE_LENGTH) { + throw new IOException("Invalid value size: " + valueSize); + } + byte[] value = new byte[valueSize]; + input.readFully(value); + metadata.put(name, value); + } + return new DefaultContentMetadata(metadata); + } + private final Map metadata; /** Constructs an empty {@link DefaultContentMetadata}. */ public DefaultContentMetadata() { - this.metadata = new HashMap<>(); + this(Collections.emptyMap()); } /** @@ -37,53 +67,66 @@ public final class DefaultContentMetadata implements ContentMetadata { * applying {@code mutations}. */ public DefaultContentMetadata(DefaultContentMetadata other, ContentMetadataMutations mutations) { - this.metadata = new HashMap<>(other.metadata); - applyMutations(mutations); + this(applyMutations(other.metadata, mutations)); + } + + private DefaultContentMetadata(Map metadata) { + this.metadata = Collections.unmodifiableMap(metadata); + } + + /** + * Serializes itself to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + public void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(metadata.size()); + for (Entry entry : metadata.entrySet()) { + output.writeUTF(entry.getKey()); + byte[] value = entry.getValue(); + output.writeInt(value.length); + output.write(value); + } } @Override public final byte[] get(String name, byte[] defaultValue) { - synchronized (metadata) { - if (metadata.containsKey(name)) { - return metadata.get(name); - } else { - return defaultValue; - } + if (metadata.containsKey(name)) { + return metadata.get(name); + } else { + return defaultValue; } } @Override public final String get(String name, String defaultValue) { - synchronized (metadata) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); - return new String(bytes, Charset.forName(C.UTF8_NAME)); - } else { - return defaultValue; - } + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } else { + return defaultValue; } } @Override public final long get(String name, long defaultValue) { - synchronized (metadata) { - if (metadata.containsKey(name)) { - byte[] bytes = metadata.get(name); - return ByteBuffer.wrap(bytes).getLong(); - } else { - return defaultValue; - } + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return ByteBuffer.wrap(bytes).getLong(); + } else { + return defaultValue; } } @Override public final boolean contains(String name) { - synchronized (metadata) { - return metadata.containsKey(name); - } + return metadata.containsKey(name); } - private void applyMutations(ContentMetadataMutations mutations) { + private static Map applyMutations( + Map otherMetadata, ContentMetadataMutations mutations) { + HashMap metadata = new HashMap<>(otherMetadata); List removedValues = mutations.getRemovedValues(); for (int i = 0; i < removedValues.size(); i++) { metadata.remove(removedValues.get(i)); @@ -91,8 +134,16 @@ public final class DefaultContentMetadata implements ContentMetadata { Map editedValues = mutations.getEditedValues(); for (String name : editedValues.keySet()) { Object value = editedValues.get(name); - metadata.put(name, getBytes(value)); + byte[] bytes = getBytes(value); + if (bytes.length > MAX_VALUE_LENGTH) { + throw new IllegalArgumentException( + String.format( + "The size of %s (%d) is greater than maximum allowed: %d", + name, bytes.length, MAX_VALUE_LENGTH)); + } + metadata.put(name, bytes); } + return metadata; } private static byte[] getBytes(Object value) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java index 5554329e0e..34d2cdd5b4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java @@ -17,6 +17,10 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,7 +34,7 @@ public class DefaultContentMetadataTest { @Before public void setUp() throws Exception { - contentMetadata = createAbstractContentMetadata(); + contentMetadata = createContentMetadata(); } @Test @@ -40,7 +44,7 @@ public class DefaultContentMetadataTest { @Test public void testContainsReturnsTrueForInitialValue() throws Exception { - contentMetadata = createAbstractContentMetadata("metadata name", "value"); + contentMetadata = createContentMetadata("metadata name", "value"); assertThat(contentMetadata.contains("metadata name")).isTrue(); } @@ -51,7 +55,7 @@ public class DefaultContentMetadataTest { @Test public void testGetReturnsInitialValue() throws Exception { - contentMetadata = createAbstractContentMetadata("metadata name", "value"); + contentMetadata = createContentMetadata("metadata name", "value"); assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); } @@ -95,7 +99,7 @@ public class DefaultContentMetadataTest { @Test public void testEditMetadata() throws Exception { - contentMetadata = createAbstractContentMetadata("metadata name", "value"); + contentMetadata = createContentMetadata("metadata name", "value"); ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.set("metadata name", "edited value"); contentMetadata = new DefaultContentMetadata(contentMetadata, mutations); @@ -104,7 +108,7 @@ public class DefaultContentMetadataTest { @Test public void testRemoveMetadata() throws Exception { - contentMetadata = createAbstractContentMetadata("metadata name", "value"); + contentMetadata = createContentMetadata("metadata name", "value"); ContentMetadataMutations mutations = new ContentMetadataMutations(); mutations.remove("metadata name"); contentMetadata = new DefaultContentMetadata(contentMetadata, mutations); @@ -129,7 +133,27 @@ public class DefaultContentMetadataTest { assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); } - private DefaultContentMetadata createAbstractContentMetadata(String... pairs) { + @Test + public void testSerializeDeserialize() throws Exception { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + mutations.set("metadata1 name", "value"); + mutations.set("metadata2 name", 12345); + byte[] metadata3 = {1, 2, 3}; + mutations.set("metadata3 name", metadata3); + contentMetadata = new DefaultContentMetadata(contentMetadata, mutations); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + contentMetadata.writeToStream(new DataOutputStream(outputStream)); + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + DefaultContentMetadata contentMetadata2 = + DefaultContentMetadata.readFromStream(new DataInputStream(inputStream)); + + assertThat(contentMetadata2.get("metadata1 name", "default value")).isEqualTo("value"); + assertThat(contentMetadata2.get("metadata2 name", 0)).isEqualTo(12345); + assertThat(contentMetadata2.get("metadata3 name", new byte[] {})).isEqualTo(metadata3); + } + + private DefaultContentMetadata createContentMetadata(String... pairs) { assertThat(pairs.length % 2).isEqualTo(0); ContentMetadataMutations mutations = new ContentMetadataMutations(); for (int i = 0; i < pairs.length; i += 2) {