Commit f182c0c1 by olly Committed by Oliver Woodman

Centralize serialization in CachedContentIndex

We need to support serialization to/from an SQLite table. The
model of passing something around for each class to write into
doesn't work well for SQL, and it would be messy to have two
different structural designs for serialization. This change
centralizes the logic in CachedContentIndex, where a centralized
SQL based version can more easily sit alongside it.

PiperOrigin-RevId: 230692291
parent 49b9775d
......@@ -18,17 +18,11 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.TreeSet;
/** Defines the cached content for a single stream. */
/* package */ final class CachedContent {
private static final int VERSION_METADATA_INTRODUCED = 2;
private static final int VERSION_MAX = Integer.MAX_VALUE;
/** The cache file id that uniquely identifies the original stream. */
public final int id;
/** The cache key that uniquely identifies the original stream. */
......@@ -41,55 +35,24 @@ import java.util.TreeSet;
private boolean locked;
/**
* Reads an instance from a {@link DataInputStream}.
*
* @param version Version of the encoded data.
* @param input Input stream containing values needed to initialize CachedContent instance.
* @throws IOException If an error occurs during reading values.
*/
public static CachedContent readFromStream(int version, DataInputStream input)
throws IOException {
int id = input.readInt();
String key = input.readUTF();
CachedContent cachedContent = new CachedContent(id, key);
if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, length);
cachedContent.applyMetadataMutations(mutations);
} else {
cachedContent.metadata = DefaultContentMetadata.readFromStream(input);
}
return cachedContent;
}
/**
* Creates a CachedContent.
*
* @param id The cache file id.
* @param key The cache stream key.
*/
public CachedContent(int id, String key) {
this(id, key, DefaultContentMetadata.EMPTY);
}
public CachedContent(int id, String key, DefaultContentMetadata metadata) {
this.id = id;
this.key = key;
this.metadata = DefaultContentMetadata.EMPTY;
this.metadata = metadata;
this.cachedSpans = new TreeSet<>();
}
/**
* Writes the instance 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(id);
output.writeUTF(key);
metadata.writeToStream(output);
}
/** Returns the metadata. */
public ContentMetadata getMetadata() {
public DefaultContentMetadata getMetadata() {
return metadata;
}
......@@ -208,26 +171,11 @@ import java.util.TreeSet;
return false;
}
/**
* Calculates a hash code for the header of this {@code CachedContent} which is compatible with
* the index file with {@code version}.
*/
public int headerHashCode(int version) {
int result = id;
result = 31 * result + key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadata.getContentLength(metadata);
result = 31 * result + (int) (length ^ (length >>> 32));
} else {
result = 31 * result + metadata.hashCode();
}
return result;
}
@Override
public int hashCode() {
int result = headerHashCode(VERSION_MAX);
result = 31 * result + cachedSpans.hashCode();
int result = id;
result = 31 * result + key.hashCode();
result = 31 * result + metadata.hashCode();
return result;
}
......
......@@ -33,8 +33,10 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.crypto.Cipher;
......@@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public static final String FILE_NAME = "cached_content_index.exi";
private static final int VERSION = 2;
private static final int VERSION_METADATA_INTRODUCED = 2;
private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;
private static final int FLAG_ENCRYPTED_INDEX = 1;
......@@ -245,6 +249,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
}
private CachedContent addNew(String key) {
int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key);
add(cachedContent);
changed = true;
return cachedContent;
}
private void add(CachedContent cachedContent) {
keyToContent.put(cachedContent.key, cachedContent);
idToKey.put(cachedContent.id, cachedContent.key);
}
private boolean readFile() {
DataInputStream input = null;
try {
......@@ -276,9 +293,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int count = input.readInt();
int hashCode = 0;
for (int i = 0; i < count; i++) {
CachedContent cachedContent = CachedContent.readFromStream(version, input);
CachedContent cachedContent = readCachedContent(version, input);
add(cachedContent);
hashCode += cachedContent.headerHashCode(version);
hashCode += hashCachedContent(cachedContent, version);
}
int fileHashCode = input.readInt();
boolean isEOF = input.read() == -1;
......@@ -327,8 +344,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
output.writeInt(keyToContent.size());
int hashCode = 0;
for (CachedContent cachedContent : keyToContent.values()) {
cachedContent.writeToStream(output);
hashCode += cachedContent.headerHashCode(VERSION);
writeCachedContent(cachedContent, output);
hashCode += hashCachedContent(cachedContent, VERSION);
}
output.writeInt(hashCode);
atomicFile.endWrite(output);
......@@ -342,17 +359,108 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
private CachedContent addNew(String key) {
int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key);
add(cachedContent);
changed = true;
return cachedContent;
/**
* Calculates a hash code for a {@link CachedContent} which is compatible with a particular index
* version.
*/
private int hashCachedContent(CachedContent cachedContent, int version) {
int result = cachedContent.id;
result = 31 * result + cachedContent.key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadata.getContentLength(cachedContent.getMetadata());
result = 31 * result + (int) (length ^ (length >>> 32));
} else {
result = 31 * result + cachedContent.getMetadata().hashCode();
}
return result;
}
private void add(CachedContent cachedContent) {
keyToContent.put(cachedContent.key, cachedContent);
idToKey.put(cachedContent.id, cachedContent.key);
/**
* Reads a {@link CachedContent} from a {@link DataInputStream}.
*
* @param version Version of the encoded data.
* @param input Input stream containing values needed to initialize CachedContent instance.
* @throws IOException If an error occurs during reading values.
*/
private static CachedContent readCachedContent(int version, DataInputStream input)
throws IOException {
int id = input.readInt();
String key = input.readUTF();
DefaultContentMetadata metadata;
if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, length);
metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);
} else {
metadata = readContentMetadata(input);
}
return new CachedContent(id, key, metadata);
}
/**
* Writes a {@link CachedContent} to a {@link DataOutputStream}.
*
* @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output.
*/
private static void writeCachedContent(CachedContent cachedContent, DataOutputStream output)
throws IOException {
output.writeInt(cachedContent.id);
output.writeUTF(cachedContent.key);
writeContentMetadata(cachedContent.getMetadata(), output);
}
/**
* 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.
*/
private static DefaultContentMetadata readContentMetadata(DataInputStream input)
throws IOException {
int size = input.readInt();
HashMap<String, byte[]> metadata = new HashMap<>();
for (int i = 0; i < size; i++) {
String name = input.readUTF();
int valueSize = input.readInt();
if (valueSize < 0) {
throw new IOException("Invalid value size: " + valueSize);
}
// Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very
// large) valueSize was read. In such cases the implementation below is expected to throw
// IOException from one of the readFully calls, due to the end of the input being reached.
int bytesRead = 0;
int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH);
byte[] value = Util.EMPTY_BYTE_ARRAY;
while (bytesRead != valueSize) {
value = Arrays.copyOf(value, bytesRead + nextBytesToRead);
input.readFully(value, bytesRead, nextBytesToRead);
bytesRead += nextBytesToRead;
nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH);
}
metadata.put(name, value);
}
return new DefaultContentMetadata(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.
*/
private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)
throws IOException {
Set<Map.Entry<String, byte[]>> entrySet = metadata.entrySet();
output.writeInt(entrySet.size());
for (Map.Entry<String, byte[]> entry : entrySet) {
output.writeUTF(entry.getKey());
byte[] value = entry.getValue();
output.writeInt(value.length);
output.write(value);
}
}
private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
......
......@@ -17,9 +17,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
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.Arrays;
......@@ -28,6 +25,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */
public final class DefaultContentMetadata implements ContentMetadata {
......@@ -36,39 +34,16 @@ public final class DefaultContentMetadata implements ContentMetadata {
public static final DefaultContentMetadata EMPTY =
new DefaultContentMetadata(Collections.emptyMap());
private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024;
private int hashCode;
/**
* 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<String, byte[]> 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<String, byte[]> metadata;
public DefaultContentMetadata() {
this(Collections.emptyMap());
}
private DefaultContentMetadata(Map<String, byte[]> metadata) {
/** @param metadata The metadata entries in their raw byte array form. */
public DefaultContentMetadata(Map<String, byte[]> metadata) {
this.metadata = Collections.unmodifiableMap(metadata);
}
......@@ -84,20 +59,9 @@ public final class DefaultContentMetadata implements ContentMetadata {
return new DefaultContentMetadata(mutatedMetadata);
}
/**
* 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<String, byte[]> entry : metadata.entrySet()) {
output.writeUTF(entry.getKey());
byte[] value = entry.getValue();
output.writeInt(value.length);
output.write(value);
}
/** Returns the set of metadata entries in their raw byte array form. */
public Set<Entry<String, byte[]>> entrySet() {
return metadata.entrySet();
}
@Override
......@@ -190,18 +154,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> values) {
for (String name : values.keySet()) {
Object value = values.get(name);
byte[] bytes = getBytes(value);
if (bytes.length > MAX_VALUE_LENGTH) {
throw new IllegalArgumentException(
"The size of "
+ name
+ " ("
+ bytes.length
+ ") is greater than maximum allowed: "
+ MAX_VALUE_LENGTH);
}
metadata.put(name, bytes);
metadata.put(name, getBytes(values.get(name)));
}
}
......
......@@ -17,10 +17,6 @@ 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;
......@@ -134,24 +130,6 @@ public class DefaultContentMetadataTest {
}
@Test
public void testSerializeDeserialize() throws Exception {
byte[] metadata3 = {1, 2, 3};
contentMetadata =
createContentMetadata(
"metadata1 name", "value", "metadata2 name", 12345, "metadata3 name", metadata3);
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);
}
@Test
public void testEqualsStringValues() throws Exception {
DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value");
DefaultContentMetadata metadata2 = createContentMetadata("metadata1", "value");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment