Commit 2c54b834 by olly Committed by Oliver Woodman

Move CachedContentIndex storage behind an interface

This interface will get an SQLite implementation in a subsequent CL

PiperOrigin-RevId: 230693881
parent f182c0c1
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
...@@ -83,12 +84,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -83,12 +84,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*/ */
private final SparseBooleanArray removedIds; private final SparseBooleanArray removedIds;
private final AtomicFile atomicFile; private final Storage storage;
private final Cipher cipher;
private final SecretKeySpec secretKeySpec;
private final boolean encrypt;
private boolean changed;
private ReusableBufferedOutputStream bufferedOutputStream;
/** /**
* Creates a CachedContentIndex which works on the index file in the given cacheDir. * Creates a CachedContentIndex which works on the index file in the given cacheDir.
...@@ -118,7 +114,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -118,7 +114,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* secretKey} is null. * secretKey} is null.
*/ */
public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) { public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) {
this.encrypt = encrypt; Cipher cipher = null;
SecretKeySpec secretKeySpec = null;
if (secretKey != null) { if (secretKey != null) {
Assertions.checkArgument(secretKey.length == 16); Assertions.checkArgument(secretKey.length == 16);
try { try {
...@@ -129,20 +126,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -129,20 +126,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
} else { } else {
Assertions.checkState(!encrypt); Assertions.checkState(!encrypt);
cipher = null;
secretKeySpec = null;
} }
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray(); removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); storage = new AtomicFileStorage(new File(cacheDir, FILE_NAME), encrypt, cipher, secretKeySpec);
} }
/** Loads the index file. */ /** Loads the index file. */
public void load() { public void load() {
Assertions.checkState(!changed); if (!storage.load(keyToContent, idToKey)) {
if (!readFile()) {
atomicFile.delete();
keyToContent.clear(); keyToContent.clear();
idToKey.clear(); idToKey.clear();
} }
...@@ -150,11 +143,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -150,11 +143,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Stores the index data to index file if there is a change. */ /** Stores the index data to index file if there is a change. */
public void store() throws CacheException { public void store() throws CacheException {
if (!changed) { storage.store(keyToContent);
return;
}
writeFile();
changed = false;
// Make ids that were removed since the index was last stored eligible for re-use. // Make ids that were removed since the index was last stored eligible for re-use.
int removedIdCount = removedIds.size(); int removedIdCount = removedIds.size();
for (int i = 0; i < removedIdCount; i++) { for (int i = 0; i < removedIdCount; i++) {
...@@ -205,7 +194,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -205,7 +194,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
CachedContent cachedContent = keyToContent.get(key); CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key); keyToContent.remove(key);
changed = true; storage.onRemove(cachedContent);
// Keep an entry in idToKey to stop the id from being reused until the index is next stored. // Keep an entry in idToKey to stop the id from being reused until the index is next stored.
idToKey.put(cachedContent.id, /* value= */ null); idToKey.put(cachedContent.id, /* value= */ null);
// Track that the entry should be removed from idToKey when the index is next stored. // Track that the entry should be removed from idToKey when the index is next stored.
...@@ -239,7 +228,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -239,7 +228,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) { public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) {
CachedContent cachedContent = getOrAdd(key); CachedContent cachedContent = getOrAdd(key);
if (cachedContent.applyMetadataMutations(mutations)) { if (cachedContent.applyMetadataMutations(mutations)) {
changed = true; storage.onUpdate(cachedContent);
} }
} }
...@@ -252,17 +241,187 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -252,17 +241,187 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private CachedContent addNew(String key) { private CachedContent addNew(String key) {
int id = getNewId(idToKey); int id = getNewId(idToKey);
CachedContent cachedContent = new CachedContent(id, key); CachedContent cachedContent = new CachedContent(id, key);
add(cachedContent); keyToContent.put(cachedContent.key, cachedContent);
changed = true; idToKey.put(cachedContent.id, cachedContent.key);
storage.onUpdate(cachedContent);
return cachedContent; return cachedContent;
} }
private void add(CachedContent cachedContent) { private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
keyToContent.put(cachedContent.key, cachedContent); // Workaround for https://issuetracker.google.com/issues/36976726
idToKey.put(cachedContent.id, cachedContent.key); if (Util.SDK_INT == 18) {
try {
return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC");
} catch (Throwable ignored) {
// ignored
}
}
return Cipher.getInstance("AES/CBC/PKCS5PADDING");
}
/**
* Returns an id which isn't used in the given array. If the maximum id in the array is smaller
* than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
* returns the smallest unused non-negative integer.
*/
@VisibleForTesting
/* package */ static int getNewId(SparseArray<String> idToKey) {
int size = idToKey.size();
int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
if (id < 0) { // In case if we pass max int value.
// TODO optimization: defragmentation or binary search?
for (id = 0; id < size; id++) {
if (id != idToKey.keyAt(id)) {
break;
}
}
}
return id;
} }
private boolean readFile() { /**
* 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);
}
}
/** Interface for the persistent index. */
private interface Storage {
/**
* Loads the persisted index into {@code content} and {@code idToKey}.
*
* @param content The key to content map to populate with persisted data.
* @param idToKey The id to key map to populate with persisted data.
* @return Whether the load was successful.
*/
boolean load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey);
/**
* Ensures all changes in the in-memory table are persisted.
*
* @param content The key to content map to persist.
* @throws CacheException If an error occurs persisting the index.
*/
void store(HashMap<String, CachedContent> content) throws CacheException;
/**
* Called when a {@link CachedContent} is added or updated in the in-memory index.
*
* @param cachedContent The updated {@link CachedContent}.
*/
void onUpdate(CachedContent cachedContent);
/**
* Called when a {@link CachedContent} is removed from the in-memory index.
*
* @param cachedContent The removed {@link CachedContent}.
*/
void onRemove(CachedContent cachedContent);
}
/** {@link Storage} implementation that uses an {@link AtomicFile}. */
private static class AtomicFileStorage implements Storage {
private final boolean encrypt;
@Nullable private final Cipher cipher;
@Nullable private final SecretKeySpec secretKeySpec;
private final AtomicFile atomicFile;
private final Random random;
private boolean changed;
@Nullable private ReusableBufferedOutputStream bufferedOutputStream;
public AtomicFileStorage(
File fileName,
boolean encrypt,
@Nullable Cipher cipher,
@Nullable SecretKeySpec secretKeySpec) {
this.encrypt = encrypt;
this.cipher = cipher;
this.secretKeySpec = secretKeySpec;
atomicFile = new AtomicFile(fileName);
random = new Random();
}
@Override
public boolean load(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
Assertions.checkState(!changed);
if (!readFile(content, idToKey)) {
atomicFile.delete();
return false;
}
return true;
}
@Override
public void store(HashMap<String, CachedContent> content) throws CacheException {
if (!changed) {
return;
}
writeFile(content);
changed = false;
}
@Override
public void onUpdate(CachedContent cachedContent) {
changed = true;
}
@Override
public void onRemove(CachedContent cachedContent) {
changed = true;
}
private boolean readFile(
HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
DataInputStream input = null; DataInputStream input = null;
try { try {
InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
...@@ -294,7 +453,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -294,7 +453,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int hashCode = 0; int hashCode = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
CachedContent cachedContent = readCachedContent(version, input); CachedContent cachedContent = readCachedContent(version, input);
add(cachedContent); content.put(cachedContent.key, cachedContent);
idToKey.put(cachedContent.id, cachedContent.key);
hashCode += hashCachedContent(cachedContent, version); hashCode += hashCachedContent(cachedContent, version);
} }
int fileHashCode = input.readInt(); int fileHashCode = input.readInt();
...@@ -312,7 +472,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -312,7 +472,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return true; return true;
} }
private void writeFile() throws CacheException { private void writeFile(HashMap<String, CachedContent> content) throws CacheException {
DataOutputStream output = null; DataOutputStream output = null;
try { try {
OutputStream outputStream = atomicFile.startWrite(); OutputStream outputStream = atomicFile.startWrite();
...@@ -329,7 +489,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -329,7 +489,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (encrypt) { if (encrypt) {
byte[] initializationVector = new byte[16]; byte[] initializationVector = new byte[16];
new Random().nextBytes(initializationVector); random.nextBytes(initializationVector);
output.write(initializationVector); output.write(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
try { try {
...@@ -341,9 +501,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -341,9 +501,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));
} }
output.writeInt(keyToContent.size()); output.writeInt(content.size());
int hashCode = 0; int hashCode = 0;
for (CachedContent cachedContent : keyToContent.values()) { for (CachedContent cachedContent : content.values()) {
writeCachedContent(cachedContent, output); writeCachedContent(cachedContent, output);
hashCode += hashCachedContent(cachedContent, VERSION); hashCode += hashCachedContent(cachedContent, VERSION);
} }
...@@ -360,8 +520,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -360,8 +520,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
/** /**
* Calculates a hash code for a {@link CachedContent} which is compatible with a particular index * Calculates a hash code for a {@link CachedContent} which is compatible with a particular
* version. * index version.
*/ */
private int hashCachedContent(CachedContent cachedContent, int version) { private int hashCachedContent(CachedContent cachedContent, int version) {
int result = cachedContent.id; int result = cachedContent.id;
...@@ -382,8 +542,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -382,8 +542,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* @param input Input stream containing values needed to initialize CachedContent instance. * @param input Input stream containing values needed to initialize CachedContent instance.
* @throws IOException If an error occurs during reading values. * @throws IOException If an error occurs during reading values.
*/ */
private static CachedContent readCachedContent(int version, DataInputStream input) private CachedContent readCachedContent(int version, DataInputStream input) throws IOException {
throws IOException {
int id = input.readInt(); int id = input.readInt();
String key = input.readUTF(); String key = input.readUTF();
DefaultContentMetadata metadata; DefaultContentMetadata metadata;
...@@ -404,95 +563,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -404,95 +563,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* @param output Output stream to store the values. * @param output Output stream to store the values.
* @throws IOException If an error occurs during writing values to output. * @throws IOException If an error occurs during writing values to output.
*/ */
private static void writeCachedContent(CachedContent cachedContent, DataOutputStream output) private void writeCachedContent(CachedContent cachedContent, DataOutputStream output)
throws IOException { throws IOException {
output.writeInt(cachedContent.id); output.writeInt(cachedContent.id);
output.writeUTF(cachedContent.key); output.writeUTF(cachedContent.key);
writeContentMetadata(cachedContent.getMetadata(), output); 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 {
// Workaround for https://issuetracker.google.com/issues/36976726
if (Util.SDK_INT == 18) {
try {
return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC");
} catch (Throwable ignored) {
// ignored
}
}
return Cipher.getInstance("AES/CBC/PKCS5PADDING");
}
/**
* Returns an id which isn't used in the given array. If the maximum id in the array is smaller
* than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
* returns the smallest unused non-negative integer.
*/
@VisibleForTesting
public static int getNewId(SparseArray<String> idToKey) {
int size = idToKey.size();
int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
if (id < 0) { // In case if we pass max int value.
// TODO optimization: defragmentation or binary search?
for (id = 0; id < size; id++) {
if (id != idToKey.keyAt(id)) {
break;
}
}
}
return id;
}
} }
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