Commit 92a98d1c by eguven Committed by Oliver Woodman

Encrypt SimpleCache index file.

Clean up AtomicFile and make it return a custom FileOutputStream
for writing which handles IOException automatically during write
operations.

It also syncs the file descriptor and deletes the backup file on
close() call. This fixes the order of flush and close operations
when the fileoutputstream is wrapped by another OutputStream.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138779187
parent 16ddc84d
...@@ -8,9 +8,11 @@ import com.google.android.exoplayer2.testutil.TestUtil; ...@@ -8,9 +8,11 @@ import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import junit.framework.AssertionFailedError;
/** /**
* Tests {@link CachedContentIndex}. * Tests {@link CachedContentIndex}.
...@@ -91,21 +93,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -91,21 +93,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
} }
public void testStoreAndLoad() throws Exception { public void testStoreAndLoad() throws Exception {
index.addNew(new CachedContent(5, "key1", 10)); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
index.add("key2");
index.store();
CachedContentIndex index2 = new CachedContentIndex(cacheDir);
index2.load();
Set<String> keys = index.getKeys();
Set<String> keys2 = index2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(index.getContentLength(key), index2.getContentLength(key));
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
}
} }
public void testLoadV1() throws Exception { public void testLoadV1() throws Exception {
...@@ -168,4 +156,66 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -168,4 +156,66 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertEquals(1, CachedContentIndex.getNewId(idToKey)); assertEquals(1, CachedContentIndex.getNewId(idToKey));
} }
public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
// Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
File file2 = new File(cacheDir, "file2compare");
assertTrue(file1.renameTo(file2));
// Write a new index file
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
assertEquals(file2.length(), file1.length());
// Assert file content is different
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) {
assertTrue(b != -1);
}
boolean threw = false;
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key2));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read with different encryption key", threw);
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read without encryption key", threw);
// Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key));
}
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
index.addNew(new CachedContent(5, "key1", 10));
index.add("key2");
index.store();
index2.load();
Set<String> keys = index.getKeys();
Set<String> keys2 = index2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(index.getContentLength(key), index2.getContentLength(key));
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
}
}
} }
/*
* Copyright (C) 2016 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.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Tests {@link AtomicFile}.
*/
public class AtomicFileTest extends InstrumentationTestCase {
private File tempFolder;
private File file;
private AtomicFile atomicFile;
@Override
public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder);
}
public void testDelete() throws Exception {
assertTrue(file.createNewFile());
atomicFile.delete();
assertFalse(file.exists());
}
public void testWriteEndRead() throws Exception {
OutputStream output = atomicFile.startWrite();
output.write(5);
atomicFile.endWrite(output);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(5);
output.write(6);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(6);
assertRead();
output = atomicFile.startWrite();
assertRead();
}
private void assertRead() throws IOException {
InputStream input = atomicFile.openRead();
assertEquals(5, input.read());
assertEquals(-1, input.read());
input.close();
}
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.NavigableSet; import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
...@@ -60,6 +61,21 @@ public interface Cache { ...@@ -60,6 +61,21 @@ public interface Cache {
void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
} }
/**
* Thrown when an error is encountered when writing data.
*/
class CacheException extends IOException {
public CacheException(String message) {
super(message);
}
public CacheException(IOException cause) {
super(cause);
}
}
/** /**
* Registers a listener to listen for changes to a given key. * Registers a listener to listen for changes to a given key.
...@@ -125,7 +141,7 @@ public interface Cache { ...@@ -125,7 +141,7 @@ public interface Cache {
* @return The {@link CacheSpan}. * @return The {@link CacheSpan}.
* @throws InterruptedException * @throws InterruptedException
*/ */
CacheSpan startReadWrite(String key, long position) throws InterruptedException; CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;
/** /**
* Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
...@@ -135,7 +151,7 @@ public interface Cache { ...@@ -135,7 +151,7 @@ public interface Cache {
* @param position The position of the data being requested. * @param position The position of the data being requested.
* @return The {@link CacheSpan}. Or null if the cache entry is locked. * @return The {@link CacheSpan}. Or null if the cache entry is locked.
*/ */
CacheSpan startReadWriteNonBlocking(String key, long position); CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
/** /**
* Obtains a cache file into which data can be written. Must only be called when holding a * Obtains a cache file into which data can be written. Must only be called when holding a
...@@ -147,7 +163,7 @@ public interface Cache { ...@@ -147,7 +163,7 @@ public interface Cache {
* is enough space in the cache. * is enough space in the cache.
* @return The file into which data should be written. * @return The file into which data should be written.
*/ */
File startFile(String key, long position, long maxLength); File startFile(String key, long position, long maxLength) throws CacheException;
/** /**
* Commits a file into the cache. Must only be called when holding a corresponding hole * Commits a file into the cache. Must only be called when holding a corresponding hole
...@@ -155,7 +171,7 @@ public interface Cache { ...@@ -155,7 +171,7 @@ public interface Cache {
* *
* @param file A newly written cache file. * @param file A newly written cache file.
*/ */
void commitFile(File file); void commitFile(File file) throws CacheException;
/** /**
* Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
...@@ -170,7 +186,7 @@ public interface Cache { ...@@ -170,7 +186,7 @@ public interface Cache {
* *
* @param span The {@link CacheSpan} to remove. * @param span The {@link CacheSpan} to remove.
*/ */
void removeSpan(CacheSpan span); void removeSpan(CacheSpan span) throws CacheException;
/** /**
* Queries if a range is entirely available in the cache. * Queries if a range is entirely available in the cache.
...@@ -188,7 +204,7 @@ public interface Cache { ...@@ -188,7 +204,7 @@ public interface Cache {
* @param key The cache key for the data. * @param key The cache key for the data.
* @param length The length of the data. * @param length The length of the data.
*/ */
void setContentLength(String key, long length); void setContentLength(String key, long length) throws CacheException;
/** /**
* Returns the content length for the given key if one set, or {@link * Returns the content length for the given key if one set, or {@link
......
...@@ -18,10 +18,10 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -18,10 +18,10 @@ package com.google.android.exoplayer2.upstream.cache;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
...@@ -42,7 +42,7 @@ public final class CacheDataSink implements DataSink { ...@@ -42,7 +42,7 @@ public final class CacheDataSink implements DataSink {
/** /**
* Thrown when IOException is encountered when writing data into sink. * Thrown when IOException is encountered when writing data into sink.
*/ */
public static class CacheDataSinkException extends IOException { public static class CacheDataSinkException extends CacheException {
public CacheDataSinkException(IOException cause) { public CacheDataSinkException(IOException cause) {
super(cause); super(cause);
...@@ -50,7 +50,6 @@ public final class CacheDataSink implements DataSink { ...@@ -50,7 +50,6 @@ public final class CacheDataSink implements DataSink {
} }
/** /**
* @param cache The cache into which data should be written. * @param cache The cache into which data should be written.
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
...@@ -71,7 +70,7 @@ public final class CacheDataSink implements DataSink { ...@@ -71,7 +70,7 @@ public final class CacheDataSink implements DataSink {
dataSpecBytesWritten = 0; dataSpecBytesWritten = 0;
try { try {
openNextOutputStream(); openNextOutputStream();
} catch (FileNotFoundException e) { } catch (IOException e) {
throw new CacheDataSinkException(e); throw new CacheDataSinkException(e);
} }
} }
...@@ -112,7 +111,7 @@ public final class CacheDataSink implements DataSink { ...@@ -112,7 +111,7 @@ public final class CacheDataSink implements DataSink {
} }
} }
private void openNextOutputStream() throws FileNotFoundException { private void openNextOutputStream() throws IOException {
file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize));
outputStream = new FileOutputStream(file); outputStream = new FileOutputStream(file);
......
...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.upstream.DataSourceException; ...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TeeDataSource; import com.google.android.exoplayer2.upstream.TeeDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink.CacheDataSinkException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
...@@ -328,7 +328,7 @@ public final class CacheDataSource implements DataSource { ...@@ -328,7 +328,7 @@ public final class CacheDataSource implements DataSource {
return successful; return successful;
} }
private void setContentLength(long length) { private void setContentLength(long length) throws IOException {
cache.setContentLength(key, length); cache.setContentLength(key, length);
} }
...@@ -349,7 +349,7 @@ public final class CacheDataSource implements DataSource { ...@@ -349,7 +349,7 @@ public final class CacheDataSource implements DataSource {
} }
private void handleBeforeThrow(IOException exception) { private void handleBeforeThrow(IOException exception) {
if (currentDataSource == cacheReadDataSource || exception instanceof CacheDataSinkException) { if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) {
seenCacheError = true; seenCacheError = true;
} }
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
...@@ -150,14 +151,18 @@ import java.util.TreeSet; ...@@ -150,14 +151,18 @@ import java.util.TreeSet;
* *
* @param cacheSpan Span to be copied and updated. * @param cacheSpan Span to be copied and updated.
* @return a span with the updated last access time. * @return a span with the updated last access time.
* @throws CacheException If renaming of the underlying span file failed.
*/ */
public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) { public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException {
// Remove the old span from the in-memory representation. // Remove the old span from the in-memory representation.
Assertions.checkState(cachedSpans.remove(cacheSpan)); Assertions.checkState(cachedSpans.remove(cacheSpan));
// Obtain a new span with updated last access timestamp. // Obtain a new span with updated last access timestamp.
SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id);
// Rename the cache file // Rename the cache file
cacheSpan.file.renameTo(newCacheSpan.file); if (!cacheSpan.file.renameTo(newCacheSpan.file)) {
throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file
+ " failed.");
}
// Add the updated span back into the in-memory representation. // Add the updated span back into the in-memory representation.
cachedSpans.add(newCacheSpan); cachedSpans.add(newCacheSpan);
return newCacheSpan; return newCacheSpan;
......
...@@ -17,18 +17,30 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -17,18 +17,30 @@ package com.google.android.exoplayer2.upstream.cache;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.AtomicFile;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Random;
import java.util.Set; import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/** /**
* This class maintains the index of cached content. * This class maintains the index of cached content.
...@@ -36,15 +48,36 @@ import java.util.Set; ...@@ -36,15 +48,36 @@ import java.util.Set;
/*package*/ final class CachedContentIndex { /*package*/ final class CachedContentIndex {
public static final String FILE_NAME = "cached_content_index.exi"; public static final String FILE_NAME = "cached_content_index.exi";
private static final int VERSION = 1; private static final int VERSION = 1;
private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap<String, CachedContent> keyToContent; private final HashMap<String, CachedContent> keyToContent;
private final SparseArray<String> idToKey; private final SparseArray<String> idToKey;
private final AtomicFile atomicFile; private final AtomicFile atomicFile;
private final Cipher cipher;
private final SecretKeySpec secretKeySpec;
private boolean changed; private boolean changed;
/** 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. */
public CachedContentIndex(File cacheDir) { public CachedContentIndex(File cacheDir) {
this(cacheDir, null);
}
/** Creates a CachedContentIndex which works on the index file in the given cacheDir. */
public CachedContentIndex(File cacheDir, byte[] secretKey) {
if (secretKey != null) {
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
secretKeySpec = new SecretKeySpec(secretKey, "AES");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e); // Should never happen.
}
} else {
cipher = null;
secretKeySpec = null;
}
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
...@@ -53,18 +86,15 @@ import java.util.Set; ...@@ -53,18 +86,15 @@ import java.util.Set;
/** Loads the index file. */ /** Loads the index file. */
public void load() { public void load() {
Assertions.checkState(!changed); Assertions.checkState(!changed);
File cacheIndex = atomicFile.getBaseFile(); if (!readFile()) {
if (cacheIndex.exists()) { atomicFile.delete();
if (!readFile()) { keyToContent.clear();
cacheIndex.delete(); idToKey.clear();
keyToContent.clear();
idToKey.clear();
}
} }
} }
/** 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() { public void store() throws CacheException {
if (!changed) { if (!changed) {
return; return;
} }
...@@ -177,13 +207,30 @@ import java.util.Set; ...@@ -177,13 +207,30 @@ import java.util.Set;
private boolean readFile() { private boolean readFile() {
DataInputStream input = null; DataInputStream input = null;
try { try {
input = new DataInputStream(atomicFile.openRead()); InputStream inputStream = atomicFile.openRead();
input = new DataInputStream(inputStream);
int version = input.readInt(); int version = input.readInt();
if (version != VERSION) { if (version != VERSION) {
// Currently there is no other version // Currently there is no other version
return false; return false;
} }
input.readInt(); // ignore flags placeholder
int flags = input.readInt();
if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
if (cipher == null) {
return false;
}
byte[] initializationVector = new byte[16];
input.read(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
try {
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalStateException(e);
}
input = new DataInputStream(new CipherInputStream(inputStream, cipher));
}
int count = input.readInt(); int count = input.readInt();
int hashCode = 0; int hashCode = 0;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
...@@ -204,14 +251,30 @@ import java.util.Set; ...@@ -204,14 +251,30 @@ import java.util.Set;
return true; return true;
} }
private void writeFile() { private void writeFile() throws CacheException {
FileOutputStream outputStream = null; DataOutputStream output = null;
try { try {
outputStream = atomicFile.startWrite(); OutputStream outputStream = atomicFile.startWrite();
DataOutputStream output = new DataOutputStream(outputStream); output = new DataOutputStream(outputStream);
output.writeInt(VERSION); output.writeInt(VERSION);
output.writeInt(0); // flags placeholder
int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0;
output.writeInt(flags);
if (cipher != null) {
byte[] initializationVector = new byte[16];
new Random().nextBytes(initializationVector);
output.write(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalStateException(e); // Should never happen.
}
output.flush();
output = new DataOutputStream(new CipherOutputStream(outputStream, cipher));
}
output.writeInt(keyToContent.size()); output.writeInt(keyToContent.size());
int hashCode = 0; int hashCode = 0;
for (CachedContent cachedContent : keyToContent.values()) { for (CachedContent cachedContent : keyToContent.values()) {
...@@ -219,12 +282,11 @@ import java.util.Set; ...@@ -219,12 +282,11 @@ import java.util.Set;
hashCode += cachedContent.headerHashCode(); hashCode += cachedContent.headerHashCode();
} }
output.writeInt(hashCode); output.writeInt(hashCode);
atomicFile.endWrite(output);
output.flush();
atomicFile.finishWrite(outputStream);
} catch (IOException e) { } catch (IOException e) {
atomicFile.failWrite(outputStream); throw new CacheException(e);
throw new RuntimeException("Writing the new cache index file failed.", e); } finally {
Util.closeQuietly(output);
} }
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import java.util.Comparator; import java.util.Comparator;
import java.util.TreeSet; import java.util.TreeSet;
...@@ -74,7 +75,11 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar ...@@ -74,7 +75,11 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar
private void evictCache(Cache cache, long requiredSpace) { private void evictCache(Cache cache, long requiredSpace) {
while (currentSize + requiredSpace > maxBytes) { while (currentSize + requiredSpace > maxBytes) {
cache.removeSpan(leastRecentlyUsed.first()); try {
cache.removeSpan(leastRecentlyUsed.first());
} catch (CacheException e) {
// do nothing.
}
} }
} }
......
...@@ -38,18 +38,33 @@ public final class SimpleCache implements Cache { ...@@ -38,18 +38,33 @@ public final class SimpleCache implements Cache {
private final CachedContentIndex index; private final CachedContentIndex index;
private final HashMap<String, ArrayList<Listener>> listeners; private final HashMap<String, ArrayList<Listener>> listeners;
private long totalSpace = 0; private long totalSpace = 0;
private CacheException initializationException;
/** /**
* Constructs the cache. The cache will delete any unrecognized files from the directory. Hence * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
* the directory cannot be used to store other files. * the directory cannot be used to store other files.
* *
* @param cacheDir A dedicated cache directory. * @param cacheDir A dedicated cache directory.
* @param evictor The evictor to be used.
*/ */
public SimpleCache(File cacheDir, CacheEvictor evictor) { public SimpleCache(File cacheDir, CacheEvictor evictor) {
this(cacheDir, evictor, null);
}
/**
* Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
* the directory cannot be used to store other files.
*
* @param cacheDir A dedicated cache directory.
* @param evictor The evictor to be used.
* @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
* The key must be 16 bytes long.
*/
public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) {
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
this.evictor = evictor; this.evictor = evictor;
this.lockedSpans = new HashMap<>(); this.lockedSpans = new HashMap<>();
this.index = new CachedContentIndex(cacheDir); this.index = new CachedContentIndex(cacheDir, secretKey);
this.listeners = new HashMap<>(); this.listeners = new HashMap<>();
// Start cache initialization. // Start cache initialization.
final ConditionVariable conditionVariable = new ConditionVariable(); final ConditionVariable conditionVariable = new ConditionVariable();
...@@ -58,7 +73,11 @@ public final class SimpleCache implements Cache { ...@@ -58,7 +73,11 @@ public final class SimpleCache implements Cache {
public void run() { public void run() {
synchronized (SimpleCache.this) { synchronized (SimpleCache.this) {
conditionVariable.open(); conditionVariable.open();
initialize(); try {
initialize();
} catch (CacheException e) {
initializationException = e;
}
SimpleCache.this.evictor.onCacheInitialized(); SimpleCache.this.evictor.onCacheInitialized();
} }
} }
...@@ -106,7 +125,7 @@ public final class SimpleCache implements Cache { ...@@ -106,7 +125,7 @@ public final class SimpleCache implements Cache {
@Override @Override
public synchronized SimpleCacheSpan startReadWrite(String key, long position) public synchronized SimpleCacheSpan startReadWrite(String key, long position)
throws InterruptedException { throws InterruptedException, CacheException {
while (true) { while (true) {
SimpleCacheSpan span = startReadWriteNonBlocking(key, position); SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
if (span != null) { if (span != null) {
...@@ -122,7 +141,12 @@ public final class SimpleCache implements Cache { ...@@ -122,7 +141,12 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) { public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
throws CacheException {
if (initializationException != null) {
throw initializationException;
}
SimpleCacheSpan cacheSpan = getSpan(key, position); SimpleCacheSpan cacheSpan = getSpan(key, position);
// Read case. // Read case.
...@@ -144,7 +168,8 @@ public final class SimpleCache implements Cache { ...@@ -144,7 +168,8 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized File startFile(String key, long position, long maxLength) { public synchronized File startFile(String key, long position, long maxLength)
throws CacheException {
Assertions.checkState(lockedSpans.containsKey(key)); Assertions.checkState(lockedSpans.containsKey(key));
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it. // For some reason the cache directory doesn't exist. Make a best effort to create it.
...@@ -157,7 +182,7 @@ public final class SimpleCache implements Cache { ...@@ -157,7 +182,7 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void commitFile(File file) { public synchronized void commitFile(File file) throws CacheException {
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index);
Assertions.checkState(span != null); Assertions.checkState(span != null);
Assertions.checkState(lockedSpans.containsKey(span.key)); Assertions.checkState(lockedSpans.containsKey(span.key));
...@@ -199,7 +224,7 @@ public final class SimpleCache implements Cache { ...@@ -199,7 +224,7 @@ public final class SimpleCache implements Cache {
* @param position The position of the span being requested. * @param position The position of the span being requested.
* @return The corresponding cache {@link SimpleCacheSpan}. * @return The corresponding cache {@link SimpleCacheSpan}.
*/ */
private SimpleCacheSpan getSpan(String key, long position) { private SimpleCacheSpan getSpan(String key, long position) throws CacheException {
CachedContent cachedContent = index.get(key); CachedContent cachedContent = index.get(key);
if (cachedContent == null) { if (cachedContent == null) {
return SimpleCacheSpan.createOpenHole(key, position); return SimpleCacheSpan.createOpenHole(key, position);
...@@ -219,7 +244,7 @@ public final class SimpleCache implements Cache { ...@@ -219,7 +244,7 @@ public final class SimpleCache implements Cache {
/** /**
* Ensures that the cache's in-memory representation has been initialized. * Ensures that the cache's in-memory representation has been initialized.
*/ */
private void initialize() { private void initialize() throws CacheException {
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
cacheDir.mkdirs(); cacheDir.mkdirs();
return; return;
...@@ -259,7 +284,7 @@ public final class SimpleCache implements Cache { ...@@ -259,7 +284,7 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span); notifySpanAdded(span);
} }
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) { private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
CachedContent cachedContent = index.get(span.key); CachedContent cachedContent = index.get(span.key);
Assertions.checkState(cachedContent.removeSpan(span)); Assertions.checkState(cachedContent.removeSpan(span));
totalSpace -= span.length; totalSpace -= span.length;
...@@ -271,7 +296,7 @@ public final class SimpleCache implements Cache { ...@@ -271,7 +296,7 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void removeSpan(CacheSpan span) { public synchronized void removeSpan(CacheSpan span) throws CacheException {
removeSpan(span, true); removeSpan(span, true);
} }
...@@ -279,7 +304,7 @@ public final class SimpleCache implements Cache { ...@@ -279,7 +304,7 @@ public final class SimpleCache implements Cache {
* Scans all of the cached spans in the in-memory representation, removing any for which files * Scans all of the cached spans in the in-memory representation, removing any for which files
* no longer exist. * no longer exist.
*/ */
private void removeStaleSpansAndCachedContents() { private void removeStaleSpansAndCachedContents() throws CacheException {
LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>(); LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
for (CachedContent cachedContent : index.getAll()) { for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) { for (CacheSpan span : cachedContent.getSpans()) {
...@@ -336,7 +361,7 @@ public final class SimpleCache implements Cache { ...@@ -336,7 +361,7 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void setContentLength(String key, long length) { public synchronized void setContentLength(String key, long length) throws CacheException {
index.setContentLength(key, length); index.setContentLength(key, length);
index.store(); index.store();
} }
......
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