content: /*
   * Copyright (C) 2018 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.upstream.cache;
  
  import static com.google.android.exoplayer2.testutil.TestUtil.createTestFile;
  import static com.google.common.truth.Truth.assertThat;
  import static com.google.common.truth.Truth.assertWithMessage;
  
  import android.net.Uri;
  import android.util.SparseArray;
  import androidx.annotation.Nullable;
  import androidx.test.core.app.ApplicationProvider;
  import androidx.test.ext.junit.runners.AndroidJUnit4;
  import com.google.android.exoplayer2.testutil.TestUtil;
  import com.google.android.exoplayer2.util.Util;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.Collection;
  import java.util.Set;
  import org.junit.After;
  import org.junit.Before;
  import org.junit.Test;
  import org.junit.runner.RunWith;
  
  /** Tests {@link CachedContentIndex}. */
  @RunWith(AndroidJUnit4.class)
  public class CachedContentIndexTest {
  
    private final byte[] testIndexV1File = {
      0,
      0,
      0,
      1, // version
      0,
      0,
      0,
      0, // flags
      0,
      0,
      0,
      2, // number_of_CachedContent
      0,
      0,
      0,
      5, // cache_id 5
      0,
      5,
      65,
      66,
      67,
      68,
      69, // cache_key "ABCDE"
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      10, // original_content_length
      0,
      0,
      0,
      2, // cache_id 2
      0,
      5,
      75,
      76,
      77,
      78,
      79, // cache_key "KLMNO"
      0,
      0,
      0,
      0,
      0,
      0,
      10,
      0, // original_content_length
      (byte) 0xF6,
      (byte) 0xFB,
      0x50,
      0x41 // hashcode_of_CachedContent_array
    };
  
    private final byte[] testIndexV2File = {
      0,
      0,
      0,
      2, // version
      0,
      0,
      0,
      0, // flags
      0,
      0,
      0,
      2, // number_of_CachedContent
      0,
      0,
      0,
      5, // cache_id 5
      0,
      5,
      65,
      66,
      67,
      68,
      69, // cache_key "ABCDE"
      0,
      0,
      0,
      2, // metadata count
      0,
      9,
      101,
      120,
      111,
      95,
      114,
      101,
      100,
      105,
      114, // "exo_redir"
      0,
      0,
      0,
      5, // value length
      97,
      98,
      99,
      100,
      101, // Redirected Uri "abcde"
      0,
      7,
      101,
      120,
      111,
      95,
      108,
      101,
      110, // "exo_len"
      0,
      0,
      0,
      8, // value length
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      10, // original_content_length
      0,
      0,
      0,
      2, // cache_id 2
      0,
      5,
      75,
      76,
      77,
      78,
      79, // cache_key "KLMNO"
      0,
      0,
      0,
      1, // metadata count
      0,
      7,
      101,
      120,
      111,
      95,
      108,
      101,
      110, // "exo_len"
      0,
      0,
      0,
      8, // value length
      0,
      0,
      0,
      0,
      0,
      0,
      10,
      0, // original_content_length
      0x12,
      0x15,
      0x66,
      (byte) 0x8A // hashcode_of_CachedContent_array
    };
    private File cacheDir;
  
    @Before
    public void setUp() throws Exception {
      cacheDir =
          Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
    }
  
    @After
    public void tearDown() {
      Util.recursiveDelete(cacheDir);
    }
  
    @Test
    public void addGetRemove() throws Exception {
      final String key1 = "key1";
      final String key2 = "key2";
      final String key3 = "key3";
  
      CachedContentIndex index = newInstance();
  
      // Add two CachedContents with add methods
      CachedContent cachedContent1 = index.getOrAdd(key1);
      CachedContent cachedContent2 = index.getOrAdd(key2);
      assertThat(cachedContent1.id != cachedContent2.id).isTrue();
  
      // add a span
      int cacheFileLength = 20;
      File cacheSpanFile =
          SimpleCacheSpan.getCacheFile(
              cacheDir, cachedContent1.id, /* position= */ 10, /* timestamp= */ 30);
      createTestFile(cacheSpanFile, cacheFileLength);
      SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index);
      assertThat(span).isNotNull();
      cachedContent1.addSpan(span);
  
      // Check if they are added and get method returns null if the key isn't found
      assertThat(index.get(key1)).isEqualTo(cachedContent1);
      assertThat(index.get(key2)).isEqualTo(cachedContent2);
      assertThat(index.get(key3)).isNull();
  
      // test getAll()
      Collection<CachedContent> cachedContents = index.getAll();
      assertThat(cachedContents).containsExactly(cachedContent1, cachedContent2);
  
      // test getKeys()
      Set<String> keys = index.getKeys();
      assertThat(keys).containsExactly(key1, key2);
  
      // test getKeyForId()
      assertThat(index.getKeyForId(cachedContent1.id)).isEqualTo(key1);
      assertThat(index.getKeyForId(cachedContent2.id)).isEqualTo(key2);
  
      // test remove()
      index.maybeRemove(key2);
      index.maybeRemove(key3);
      assertThat(index.get(key1)).isEqualTo(cachedContent1);
      assertThat(index.get(key2)).isNull();
      assertThat(cacheSpanFile.exists()).isTrue();
  
      // test removeEmpty()
      index.getOrAdd(key2);
      index.removeEmpty();
      assertThat(index.get(key1)).isEqualTo(cachedContent1);
      assertThat(index.get(key2)).isNull();
      assertThat(cacheSpanFile.exists()).isTrue();
    }
  
    @Test
    public void legacyStoreAndLoad() throws Exception {
      assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance());
    }
  
    @Test
    public void legacyLoadV1() throws Exception {
      CachedContentIndex index = newLegacyInstance();
  
      FileOutputStream fos =
          new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
      fos.write(testIndexV1File);
      fos.close();
  
      index.initialize(/* uid= */ 0);
      assertThat(index.getAll()).hasSize(2);
  
      assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
      ContentMetadata metadata = index.get("ABCDE").getMetadata();
      assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
  
      assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
      ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
      assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
    }
  
    @Test
    public void legacyLoadV2() throws Exception {
      CachedContentIndex index = newLegacyInstance();
  
      FileOutputStream fos =
          new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));
      fos.write(testIndexV2File);
      fos.close();
  
      index.initialize(/* uid= */ 0);
      assertThat(index.getAll()).hasSize(2);
  
      assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
      ContentMetadata metadata = index.get("ABCDE").getMetadata();
      assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
      assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
  
      assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
      ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
      assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
    }
  
    @Test
    public void assignIdForKeyAndGetKeyForId() {
      CachedContentIndex index = newInstance();
      final String key1 = "key1";
      final String key2 = "key2";
      int id1 = index.assignIdForKey(key1);
      int id2 = index.assignIdForKey(key2);
      assertThat(index.getKeyForId(id1)).isEqualTo(key1);
      assertThat(index.getKeyForId(id2)).isEqualTo(key2);
      assertThat(id1 != id2).isTrue();
      assertThat(index.assignIdForKey(key1)).isEqualTo(id1);
      assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
    }
  
    @Test
    public void getNewId() {
      SparseArray<String> idToKey = new SparseArray<>();
      assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
      idToKey.put(10, "");
      assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(11);
      idToKey.put(Integer.MAX_VALUE, "");
      assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
      idToKey.put(0, "");
      assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
    }
  
    @Test
    public void legacyEncryption() throws Exception {
      byte[] key = Util.getUtf8Bytes("Bar12345Bar12345"); // 128 bit key
      byte[] key2 = Util.getUtf8Bytes("Foo12345Foo12345"); // 128 bit key
  
      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));
  
      // Rename the index file from the test above
      File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC);
      File file2 = new File(cacheDir, "file2compare");
      assertThat(file1.renameTo(file2)).isTrue();
  
      // Write a new index file
      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));
  
      assertThat(file1.length()).isEqualTo(file2.length());
      // Assert file content is different
      FileInputStream fis1 = new FileInputStream(file1);
      FileInputStream fis2 = new FileInputStream(file2);
      for (int b; (b = fis1.read()) == fis2.read(); ) {
        assertThat(b != -1).isTrue();
      }
  
      boolean threw = false;
      try {
        assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key2));
      } catch (AssertionError e) {
        threw = true;
      }
      assertWithMessage("Encrypted index file can not be read with different encryption key")
          .that(threw)
          .isTrue();
  
      try {
        assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance());
      } catch (AssertionError e) {
        threw = true;
      }
      assertWithMessage("Encrypted index file can not be read without encryption key")
          .that(threw)
          .isTrue();
  
      // Non encrypted index file can be read even when encryption key provided.
      assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance(key));
  
      // Test multiple store() calls
      CachedContentIndex index = newLegacyInstance(key);
      index.getOrAdd("key3");
      index.store();
      assertStoredAndLoadedEqual(index, newLegacyInstance(key));
    }
  
    @Test
    public void removeEmptyNotLockedCachedContent() {
      CachedContentIndex index = newInstance();
      CachedContent cachedContent = index.getOrAdd("key1");
  
      index.maybeRemove(cachedContent.key);
  
      assertThat(index.get(cachedContent.key)).isNull();
    }
  
    @Test
    public void cantRemoveNotEmptyCachedContent() throws Exception {
      CachedContentIndex index = newInstance();
  
      CachedContent cachedContent = index.getOrAdd("key1");
      long cacheFileLength = 20;
      File cacheFile =
          SimpleCacheSpan.getCacheFile(
              cacheDir, cachedContent.id, /* position= */ 10, /* timestamp= */ 30);
      createTestFile(cacheFile, cacheFileLength);
      SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index);
      cachedContent.addSpan(span);
  
      index.maybeRemove(cachedContent.key);
  
      assertThat(index.get(cachedContent.key)).isNotNull();
    }
  
    @Test
    public void cantRemoveLockedCachedContent() {
      CachedContentIndex index = newInstance();
      CachedContent cachedContent = index.getOrAdd("key1");
      cachedContent.lockRange(0, 1);
  
      index.maybeRemove(cachedContent.key);
  
      assertThat(index.get(cachedContent.key)).isNotNull();
    }
  
    private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
        throws IOException {
      ContentMetadataMutations mutations1 = new ContentMetadataMutations();
      ContentMetadataMutations.setContentLength(mutations1, 2560);
      index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
      ContentMetadataMutations mutations2 = new ContentMetadataMutations();
      ContentMetadataMutations.setContentLength(mutations2, 10);
      ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse("abcde"));
      index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
      index.store();
  
      index2.initialize(/* uid= */ 0);
      Set<String> keys = index.getKeys();
      Set<String> keys2 = index2.getKeys();
      assertThat(keys2).isEqualTo(keys);
      for (String key : keys) {
        assertThat(index2.get(key)).isEqualTo(index.get(key));
      }
    }
  
    private CachedContentIndex newInstance() {
      return new CachedContentIndex(TestUtil.getInMemoryDatabaseProvider());
    }
  
    private CachedContentIndex newLegacyInstance() {
      return newLegacyInstance(null);
    }
  
    private CachedContentIndex newLegacyInstance(@Nullable byte[] key) {
      return new CachedContentIndex(
          /* databaseProvider= */ null,
          cacheDir,
          /* legacyStorageSecretKey= */ key,
          /* legacyStorageEncrypt= */ key != null,
          /* preferLegacyStorage= */ true);
    }
  }
  