mirror of
https://github.com/androidx/media.git
synced 2025-05-10 00:59:51 +08:00
Add upstream.crypto package (and friends).
This commit is contained in:
parent
52d47aa244
commit
55ca323cee
@ -27,7 +27,9 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Unit tests for {@link CacheDataSource}. */
|
||||
/**
|
||||
* Unit tests for {@link CacheDataSource}.
|
||||
*/
|
||||
public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
|
||||
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
|
181
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
vendored
Normal file
181
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
vendored
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSink;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;
|
||||
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Additional tests for {@link CacheDataSource}.
|
||||
*/
|
||||
public class CacheDataSourceTest2 extends AndroidTestCase {
|
||||
|
||||
private static final String EXO_CACHE_DIR = "exo";
|
||||
private static final int EXO_CACHE_MAX_FILESIZE = 128;
|
||||
|
||||
private static final Uri URI = Uri.parse("http://test.com/content");
|
||||
private static final String KEY = "key";
|
||||
private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);
|
||||
|
||||
// A DataSpec that covers the full file.
|
||||
private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);
|
||||
|
||||
private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;
|
||||
// A DataSpec that starts at 0 and extends to a cache file boundary.
|
||||
private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);
|
||||
// A DataSpec that starts on the same boundary and extends to the end of the file.
|
||||
private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,
|
||||
DATA.length - OFFSET_ON_BOUNDARY, KEY);
|
||||
|
||||
private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;
|
||||
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
|
||||
private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);
|
||||
// A DataSpec that starts on the same boundary and extends to the end of the file.
|
||||
private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,
|
||||
DATA.length - OFFSET_OFF_BOUNDARY, KEY);
|
||||
|
||||
public void testWithoutEncryption() throws IOException {
|
||||
testReads(false);
|
||||
}
|
||||
|
||||
public void testWithEncryption() throws IOException {
|
||||
testReads(true);
|
||||
}
|
||||
|
||||
private void testReads(boolean useEncryption) throws IOException {
|
||||
FakeDataSource upstreamSource = buildFakeUpstreamSource();
|
||||
CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption);
|
||||
// First read, should arrive from upstream.
|
||||
testRead(END_ON_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);
|
||||
// Second read, should arrive from upstream.
|
||||
testRead(START_OFF_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);
|
||||
// Second read, should arrive part from cache and part from upstream.
|
||||
testRead(END_OFF_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);
|
||||
// Third read, should arrive from cache.
|
||||
testRead(FULL, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
// Various reads, should all arrive from cache.
|
||||
testRead(FULL, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(START_ON_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(END_ON_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(START_OFF_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(END_OFF_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
}
|
||||
|
||||
private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {
|
||||
byte[] scratch = new byte[4096];
|
||||
Random random = new Random(0);
|
||||
source.open(dataSpec);
|
||||
int position = (int) dataSpec.absoluteStreamPosition;
|
||||
int bytesRead = 0;
|
||||
while (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
int maxBytesToRead = random.nextInt(scratch.length) + 1;
|
||||
bytesRead = source.read(scratch, 0, maxBytesToRead);
|
||||
if (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead),
|
||||
Arrays.copyOf(scratch, bytesRead));
|
||||
position += bytesRead;
|
||||
}
|
||||
}
|
||||
source.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
|
||||
* source, with the specified start (inclusive) and end (exclusive) positions.
|
||||
*/
|
||||
private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {
|
||||
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
|
||||
assertEquals(1, openedDataSpecs.length);
|
||||
assertEquals(start, openedDataSpecs[0].position);
|
||||
assertEquals(start, openedDataSpecs[0].absoluteStreamPosition);
|
||||
assertEquals(end - start, openedDataSpecs[0].length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the upstream source was not opened.
|
||||
*/
|
||||
private void assertNoOpen(FakeDataSource upstreamSource) {
|
||||
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
|
||||
assertEquals(0, openedDataSpecs.length);
|
||||
}
|
||||
|
||||
private static FakeDataSource buildFakeUpstreamSource() {
|
||||
return new FakeDataSource.Builder().appendReadData(DATA).build();
|
||||
}
|
||||
|
||||
private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,
|
||||
boolean useAesEncryption) throws CacheException {
|
||||
File cacheDir = context.getExternalCacheDir();
|
||||
Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());
|
||||
emptyCache(cache);
|
||||
|
||||
// Source and cipher
|
||||
final String secretKey = "testKey:12345678";
|
||||
DataSource file = new FileDataSource();
|
||||
DataSource cacheReadDataSource = useAesEncryption
|
||||
? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;
|
||||
|
||||
// Sink and cipher
|
||||
CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);
|
||||
byte[] scratch = new byte[3897];
|
||||
DataSink cacheWriteDataSink = useAesEncryption
|
||||
? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;
|
||||
|
||||
return new CacheDataSource(cache,
|
||||
upstreamSource,
|
||||
cacheReadDataSource,
|
||||
cacheWriteDataSink,
|
||||
CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||
null); // eventListener
|
||||
}
|
||||
|
||||
private static void emptyCache(Cache cache) throws CacheException {
|
||||
for (String key : cache.getKeys()) {
|
||||
for (CacheSpan span : cache.getCachedSpans(key)) {
|
||||
cache.removeSpan(span);
|
||||
}
|
||||
}
|
||||
// Sanity check that the cache really is empty now.
|
||||
assertTrue(cache.getKeys().isEmpty());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link CachedRegionTracker}.
|
||||
*/
|
||||
public final class CachedRegionTrackerTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String CACHE_KEY = "abc";
|
||||
private static final long MS_IN_US = 1000;
|
||||
|
||||
// 5 chunks, each 20 bytes long and 100 ms long.
|
||||
private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
|
||||
new int[] {20, 20, 20, 20, 20},
|
||||
new long[] {100, 120, 140, 160, 180},
|
||||
new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
|
||||
new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
|
||||
|
||||
@Mock private Cache cache;
|
||||
private CachedRegionTracker tracker;
|
||||
|
||||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
|
||||
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
|
||||
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testGetRegion_noSpansInCache() {
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
|
||||
}
|
||||
|
||||
public void testGetRegion_fullyCached() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 100));
|
||||
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_partiallyCached() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 40));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 20));
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(120, 20));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
|
||||
// Start with the full stream in cache.
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 100));
|
||||
|
||||
// Remove the middle bit.
|
||||
tracker.onSpanRemoved(
|
||||
cache,
|
||||
newCacheSpan(140, 40));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
|
||||
}
|
||||
|
||||
public void testGetRegion_subchunkEstimation() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 10));
|
||||
|
||||
assertEquals(50, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
|
||||
}
|
||||
|
||||
private CacheSpan newCacheSpan(int position, int length) throws IOException {
|
||||
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.upstream.crypto;
|
||||
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Random;
|
||||
import javax.crypto.Cipher;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AesFlushingCipher}.
|
||||
*/
|
||||
public class AesFlushingCipherTest extends TestCase {
|
||||
|
||||
private static final int DATA_LENGTH = 65536;
|
||||
private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678");
|
||||
private static final long NONCE = 0;
|
||||
private static final long START_OFFSET = 11;
|
||||
private static final long RANDOM_SEED = 0x12345678;
|
||||
|
||||
private AesFlushingCipher encryptCipher;
|
||||
private AesFlushingCipher decryptCipher;
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);
|
||||
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() {
|
||||
encryptCipher = null;
|
||||
decryptCipher = null;
|
||||
}
|
||||
|
||||
private long getMaxUnchangedBytesAllowedPostEncryption(long length) {
|
||||
// Assuming that not more than 10% of the resultant bytes should be identical.
|
||||
// The value of 10% is arbitrary, ciphers standards do not name a value.
|
||||
return length / 10;
|
||||
}
|
||||
|
||||
// Count the number of bytes that do not match.
|
||||
private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {
|
||||
int count = 0;
|
||||
for (int i = startOffset; i < data1.length; i++) {
|
||||
if (data1[i] != data2[i]) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Count the number of bytes that do not match.
|
||||
private int getDifferingByteCount(byte[] data1, byte[] data2) {
|
||||
return getDifferingByteCount(data1, data2, 0);
|
||||
}
|
||||
|
||||
// Test a single encrypt and decrypt call
|
||||
public void testSingle() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
|
||||
encryptCipher.updateInPlace(data, 0, data.length);
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
decryptCipher.updateInPlace(data, 0, data.length);
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
|
||||
public void testAligned() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = (1 + random.nextInt(50)) * 16;
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
assertEquals(0, bytes % 16);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = (1 + random.nextInt(50)) * 16;
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
assertEquals(0, bytes % 16);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test several encrypt and decrypt calls, not aligned on block boundary
|
||||
public void testUnAligned() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
// Encrypt
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test decryption starting from the middle of an encrypted block
|
||||
public void testMidJoin() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
// Encrypt
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
// Verify
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
// Setup decryption from random location
|
||||
offset = random.nextInt(4096);
|
||||
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);
|
||||
int remainingLength = data.length - offset;
|
||||
int originalOffset = offset;
|
||||
|
||||
// Decrypt
|
||||
while (remainingLength > 0) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, remainingLength);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
remainingLength -= bytes;
|
||||
}
|
||||
|
||||
// Verify
|
||||
int differingByteCount = getDifferingByteCount(reference, data, originalOffset);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
}
|
205
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
vendored
Normal file
205
library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.upstream.cache;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility class for efficiently tracking regions of data that are stored in a {@link Cache}
|
||||
* for a given cache key.
|
||||
*/
|
||||
public final class CachedRegionTracker implements Cache.Listener {
|
||||
|
||||
private static final String TAG = "CachedRegionTracker";
|
||||
|
||||
public static final int NOT_CACHED = -1;
|
||||
public static final int CACHED_TO_END = -2;
|
||||
|
||||
private final Cache cache;
|
||||
private final String cacheKey;
|
||||
private final ChunkIndex chunkIndex;
|
||||
|
||||
private final TreeSet<Region> regions;
|
||||
private final Region lookupRegion;
|
||||
|
||||
public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) {
|
||||
this.cache = cache;
|
||||
this.cacheKey = cacheKey;
|
||||
this.chunkIndex = chunkIndex;
|
||||
this.regions = new TreeSet<>();
|
||||
this.lookupRegion = new Region(0, 0);
|
||||
|
||||
synchronized (this) {
|
||||
NavigableSet<CacheSpan> cacheSpans = cache.addListener(cacheKey, this);
|
||||
if (cacheSpans != null) {
|
||||
// Merge the spans into regions. mergeSpan is more efficient when merging from high to low,
|
||||
// which is why a descending iterator is used here.
|
||||
Iterator<CacheSpan> spanIterator = cacheSpans.descendingIterator();
|
||||
while (spanIterator.hasNext()) {
|
||||
CacheSpan span = spanIterator.next();
|
||||
mergeSpan(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
cache.removeListener(cacheKey, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* When provided with a byte offset, this method locates the cached region within which the
|
||||
* offset falls, and returns the approximate end position in milliseconds of that region. If the
|
||||
* byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.
|
||||
* If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.
|
||||
*
|
||||
* @param byteOffset The byte offset in the underlying stream.
|
||||
* @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or
|
||||
* {@link #CACHED_TO_END}.
|
||||
*/
|
||||
public synchronized int getRegionEndTimeMs(long byteOffset) {
|
||||
lookupRegion.startOffset = byteOffset;
|
||||
Region floorRegion = regions.floor(lookupRegion);
|
||||
if (floorRegion == null || byteOffset > floorRegion.endOffset
|
||||
|| floorRegion.endOffsetIndex == -1) {
|
||||
return NOT_CACHED;
|
||||
}
|
||||
int index = floorRegion.endOffsetIndex;
|
||||
if (index == chunkIndex.length - 1
|
||||
&& floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) {
|
||||
return CACHED_TO_END;
|
||||
}
|
||||
long segmentFractionUs = (chunkIndex.durationsUs[index]
|
||||
* (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index];
|
||||
return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onSpanAdded(Cache cache, CacheSpan span) {
|
||||
mergeSpan(span);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onSpanRemoved(Cache cache, CacheSpan span) {
|
||||
Region removedRegion = new Region(span.position, span.position + span.length);
|
||||
|
||||
// Look up a region this span falls into.
|
||||
Region floorRegion = regions.floor(removedRegion);
|
||||
if (floorRegion == null) {
|
||||
Log.e(TAG, "Removed a span we were not aware of");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it.
|
||||
regions.remove(floorRegion);
|
||||
|
||||
// Add new floor and ceiling regions, if necessary.
|
||||
if (floorRegion.startOffset < removedRegion.startOffset) {
|
||||
Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset);
|
||||
|
||||
int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset);
|
||||
newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
|
||||
regions.add(newFloorRegion);
|
||||
}
|
||||
|
||||
if (floorRegion.endOffset > removedRegion.endOffset) {
|
||||
Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset);
|
||||
newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex;
|
||||
regions.add(newCeilingRegion);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void mergeSpan(CacheSpan span) {
|
||||
Region newRegion = new Region(span.position, span.position + span.length);
|
||||
Region floorRegion = regions.floor(newRegion);
|
||||
Region ceilingRegion = regions.ceiling(newRegion);
|
||||
boolean floorConnects = regionsConnect(floorRegion, newRegion);
|
||||
boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion);
|
||||
|
||||
if (ceilingConnects) {
|
||||
if (floorConnects) {
|
||||
// Extend floorRegion to cover both newRegion and ceilingRegion.
|
||||
floorRegion.endOffset = ceilingRegion.endOffset;
|
||||
floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
|
||||
} else {
|
||||
// Extend newRegion to cover ceilingRegion. Add it.
|
||||
newRegion.endOffset = ceilingRegion.endOffset;
|
||||
newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
|
||||
regions.add(newRegion);
|
||||
}
|
||||
regions.remove(ceilingRegion);
|
||||
} else if (floorConnects) {
|
||||
// Extend floorRegion to the right to cover newRegion.
|
||||
floorRegion.endOffset = newRegion.endOffset;
|
||||
int index = floorRegion.endOffsetIndex;
|
||||
while (index < chunkIndex.length - 1
|
||||
&& (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) {
|
||||
index++;
|
||||
}
|
||||
floorRegion.endOffsetIndex = index;
|
||||
} else {
|
||||
// This is a new region.
|
||||
int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset);
|
||||
newRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
|
||||
regions.add(newRegion);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean regionsConnect(Region lower, Region upper) {
|
||||
return lower != null && upper != null && lower.endOffset == upper.startOffset;
|
||||
}
|
||||
|
||||
private static class Region implements Comparable<Region> {
|
||||
|
||||
/**
|
||||
* The first byte of the region (inclusive).
|
||||
*/
|
||||
public long startOffset;
|
||||
/**
|
||||
* End offset of the region (exclusive).
|
||||
*/
|
||||
public long endOffset;
|
||||
/**
|
||||
* The index in chunkIndex that contains the end offset. May be -1 if the end offset comes
|
||||
* before the start of the first media chunk (i.e. if the end offset is within the stream
|
||||
* header).
|
||||
*/
|
||||
public int endOffsetIndex;
|
||||
|
||||
public Region(long position, long endOffset) {
|
||||
this.startOffset = position;
|
||||
this.endOffset = endOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Region another) {
|
||||
return startOffset < another.startOffset ? -1
|
||||
: startOffset == another.startOffset ? 0 : 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.upstream.crypto;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSink;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import java.io.IOException;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
/**
|
||||
* A wrapping {@link DataSink} that encrypts the data being consumed.
|
||||
*/
|
||||
public final class AesCipherDataSink implements DataSink {
|
||||
|
||||
private final DataSink wrappedDataSink;
|
||||
private final byte[] secretKey;
|
||||
private final byte[] scratch;
|
||||
|
||||
private AesFlushingCipher cipher;
|
||||
|
||||
/**
|
||||
* Create an instance whose {@code write} methods have the side effect of overwriting the input
|
||||
* {@code data}. Use this constructor for maximum efficiency in the case that there is no
|
||||
* requirement for the input data arrays to remain unchanged.
|
||||
*
|
||||
* @param secretKey The key data.
|
||||
* @param wrappedDataSink The wrapped {@link DataSink}.
|
||||
*/
|
||||
public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) {
|
||||
this(secretKey, wrappedDataSink, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance whose {@code write} methods are free of side effects. Use this constructor
|
||||
* when the input data arrays are required to remain unchanged.
|
||||
*
|
||||
* @param secretKey The key data.
|
||||
* @param wrappedDataSink The wrapped {@link DataSink}.
|
||||
* @param scratch Scratch space. Data is decrypted into this array before being written to the
|
||||
* wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a
|
||||
* write is larger than the size of this array the write will still succeed, but multiple
|
||||
* cipher calls will be required to complete the operation.
|
||||
*/
|
||||
public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) {
|
||||
this.wrappedDataSink = wrappedDataSink;
|
||||
this.secretKey = secretKey;
|
||||
this.scratch = scratch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(DataSpec dataSpec) throws IOException {
|
||||
wrappedDataSink.open(dataSpec);
|
||||
long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
|
||||
cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,
|
||||
dataSpec.absoluteStreamPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] data, int offset, int length) throws IOException {
|
||||
if (scratch == null) {
|
||||
// In-place mode. Writes over the input data.
|
||||
cipher.updateInPlace(data, offset, length);
|
||||
wrappedDataSink.write(data, offset, length);
|
||||
} else {
|
||||
// Use scratch space. The original data remains intact.
|
||||
int bytesProcessed = 0;
|
||||
while (bytesProcessed < length) {
|
||||
int bytesToProcess = Math.min(length - bytesProcessed, scratch.length);
|
||||
cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0);
|
||||
wrappedDataSink.write(scratch, 0, bytesToProcess);
|
||||
bytesProcessed += bytesToProcess;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
cipher = null;
|
||||
wrappedDataSink.close();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.upstream.crypto;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import java.io.IOException;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
/**
|
||||
* A {@link DataSource} that decrypts the data read from an upstream source.
|
||||
*/
|
||||
public final class AesCipherDataSource implements DataSource {
|
||||
|
||||
private final DataSource upstream;
|
||||
private final byte[] secretKey;
|
||||
|
||||
private AesFlushingCipher cipher;
|
||||
|
||||
public AesCipherDataSource(byte[] secretKey, DataSource upstream) {
|
||||
this.upstream = upstream;
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
long dataLength = upstream.open(dataSpec);
|
||||
long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
|
||||
cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce,
|
||||
dataSpec.absoluteStreamPosition);
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] data, int offset, int readLength) throws IOException {
|
||||
if (readLength == 0) {
|
||||
return 0;
|
||||
}
|
||||
int read = upstream.read(data, offset, readLength);
|
||||
if (read == C.RESULT_END_OF_INPUT) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
cipher.updateInPlace(data, offset, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
cipher = null;
|
||||
upstream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return upstream.getUri();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.upstream.crypto;
|
||||
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* A flushing variant of a AES/CTR/NoPadding {@link Cipher}.
|
||||
*
|
||||
* Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all
|
||||
* of the bytes input (and hence output the same number of bytes).
|
||||
*/
|
||||
public final class AesFlushingCipher {
|
||||
|
||||
private final Cipher cipher;
|
||||
private final int blockSize;
|
||||
private final byte[] zerosBlock;
|
||||
private final byte[] flushedBlock;
|
||||
|
||||
private int pendingXorBytes;
|
||||
|
||||
public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
blockSize = cipher.getBlockSize();
|
||||
zerosBlock = new byte[blockSize];
|
||||
flushedBlock = new byte[blockSize];
|
||||
long counter = offset / blockSize;
|
||||
int startPadding = (int) (offset % blockSize);
|
||||
cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]),
|
||||
new IvParameterSpec(getInitializationVector(nonce, counter)));
|
||||
if (startPadding != 0) {
|
||||
updateInPlace(new byte[startPadding], 0, startPadding);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException e) {
|
||||
// Should never happen.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateInPlace(byte[] data, int offset, int length) {
|
||||
update(data, offset, length, data, offset);
|
||||
}
|
||||
|
||||
public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
|
||||
// If we previously flushed the cipher by inputting zeros up to a block boundary, then we need
|
||||
// to manually transform the data that actually ended the block. See the comment below for more
|
||||
// details.
|
||||
while (pendingXorBytes > 0) {
|
||||
out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]);
|
||||
outOffset++;
|
||||
inOffset++;
|
||||
pendingXorBytes--;
|
||||
length--;
|
||||
if (length == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Do the bulk of the update.
|
||||
int written = nonFlushingUpdate(in, inOffset, length, out, outOffset);
|
||||
if (length == written) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,
|
||||
// so that the corresponding bytes output by the cipher are those that would have been XORed
|
||||
// against the real end-of-block data to transform it. We store these bytes so that we can
|
||||
// perform the transformation manually in the case of a subsequent call to this method with
|
||||
// the real data.
|
||||
int bytesToFlush = length - written;
|
||||
Assertions.checkState(bytesToFlush < blockSize);
|
||||
outOffset += written;
|
||||
pendingXorBytes = blockSize - bytesToFlush;
|
||||
written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0);
|
||||
Assertions.checkState(written == blockSize);
|
||||
// The first part of xorBytes contains the flushed data, which we copy out. The remainder
|
||||
// contains the bytes that will be needed for manual transformation in a subsequent call.
|
||||
for (int i = 0; i < bytesToFlush; i++) {
|
||||
out[outOffset++] = flushedBlock[i];
|
||||
}
|
||||
}
|
||||
|
||||
private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
|
||||
try {
|
||||
return cipher.update(in, inOffset, length, out, outOffset);
|
||||
} catch (ShortBufferException e) {
|
||||
// Should never happen.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getInitializationVector(long nonce, long counter) {
|
||||
return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.upstream.crypto;
|
||||
|
||||
/**
|
||||
* Utility functions for the crypto package.
|
||||
*/
|
||||
/* package */ final class CryptoUtil {
|
||||
|
||||
private CryptoUtil() {}
|
||||
|
||||
/**
|
||||
* Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash
|
||||
* values produced by this function are less likely to collide than those produced by
|
||||
* {@link #hashCode()}.
|
||||
*/
|
||||
public static long getFNV64Hash(String input) {
|
||||
if (input == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long hash = 0;
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
hash ^= input.charAt(i);
|
||||
// This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).
|
||||
hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user