/*
* 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.testutil;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import java.io.EOFException;
import java.io.IOException;
import junit.framework.Assert;
/**
* A fake {@link ExtractorInput} capable of simulating various scenarios.
* <p>
* Read, skip and peek errors can be simulated using {@link Builder#setSimulateIOErrors}. When
* enabled each read and skip will throw a {@link SimulatedIOException} unless one has already been
* thrown from the current position. Each peek will throw {@link SimulatedIOException} unless one
* has already been thrown from the current peek position. When a {@link SimulatedIOException} is
* thrown the read position is left unchanged and the peek position is reset back to the read
* position.
* <p>
* Partial reads and skips can be simulated using {@link Builder#setSimulatePartialReads}. When
* enabled, {@link #read(byte[], int, int)} and {@link #skip(int)} calls will only read or skip a
* single byte unless a partial read or skip has already been performed that had the same target
* position. For example, a first read request for 10 bytes will be partially satisfied by reading
* a single byte and advancing the position to 1. If the following read request attempts to read 9
* bytes then it will be fully satisfied, since it has the same target position of 10.
* <p>
* Unknown data length can be simulated using {@link Builder#setSimulateUnknownLength}. When enabled
* {@link #getLength()} will return {@link C#LENGTH_UNSET} rather than the length of the data.
*/
public final class FakeExtractorInput implements ExtractorInput {
/**
* Thrown when simulating an {@link IOException}.
*/
public static final class SimulatedIOException extends IOException {
public SimulatedIOException(String message) {
super(message);
}
}
private final byte[] data;
private final boolean simulateUnknownLength;
private final boolean simulatePartialReads;
private final boolean simulateIOErrors;
private int readPosition;
private int peekPosition;
private final SparseBooleanArray partiallySatisfiedTargetPositions;
private final SparseBooleanArray failedReadPositions;
private final SparseBooleanArray failedPeekPositions;
private FakeExtractorInput(byte[] data, boolean simulateUnknownLength,
boolean simulatePartialReads, boolean simulateIOErrors) {
this.data = data;
this.simulateUnknownLength = simulateUnknownLength;
this.simulatePartialReads = simulatePartialReads;
this.simulateIOErrors = simulateIOErrors;
partiallySatisfiedTargetPositions = new SparseBooleanArray();
failedReadPositions = new SparseBooleanArray();
failedPeekPositions = new SparseBooleanArray();
}
/**
* Sets the read and peek positions.
*
* @param position The position to set.
*/
public void setPosition(int position) {
Assert.assertTrue(0 <= position && position <= data.length);
readPosition = position;
peekPosition = position;
}
@Override
public int read(byte[] target, int offset, int length) throws IOException {
length = getReadLength(length);
if (readFully(target, offset, length, true)) {
return length;
}
return C.RESULT_END_OF_INPUT;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) {
return false;
}
System.arraycopy(data, readPosition, target, offset, length);
readPosition += length;
peekPosition = readPosition;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length) throws IOException {
readFully(target, offset, length, false);
}
@Override
public int skip(int length) throws IOException {
length = getReadLength(length);
if (skipFully(length, true)) {
return length;
}
return C.RESULT_END_OF_INPUT;
}
@Override
public boolean skipFully(int length, boolean allowEndOfInput) throws IOException {
if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) {
return false;
}
readPosition += length;
peekPosition = readPosition;
return true;
}
@Override
public void skipFully(int length) throws IOException {
skipFully(length, false);
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) {
return false;
}
System.arraycopy(data, peekPosition, target, offset, length);
peekPosition += length;
return true;
}
@Override
public void peekFully(byte[] target, int offset, int length) throws IOException {
peekFully(target, offset, length, false);
}
@Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException {
if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) {
return false;
}
peekPosition += length;
return true;
}
@Override
public void advancePeekPosition(int length) throws IOException {
advancePeekPosition(length, false);
}
@Override
public void resetPeekPosition() {
peekPosition = readPosition;
}
@Override
public long getPeekPosition() {
return peekPosition;
}
@Override
public long getPosition() {
return readPosition;
}
@Override
public long getLength() {
return simulateUnknownLength ? C.LENGTH_UNSET : data.length;
}
@Override
public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
Assert.assertTrue(position >= 0);
readPosition = (int) position;
throw e;
}
private boolean checkXFully(boolean allowEndOfInput, int position, int length,
SparseBooleanArray failedPositions) throws IOException {
if (simulateIOErrors && !failedPositions.get(position)) {
failedPositions.put(position, true);
peekPosition = readPosition;
throw new SimulatedIOException("Simulated IO error at position: " + position);
}
if (length > 0 && position == data.length) {
if (allowEndOfInput) {
return false;
}
throw new EOFException();
}
if (position + length > data.length) {
throw new EOFException("Attempted to move past end of data: (" + position + " + "
+ length + ") > " + data.length);
}
return true;
}
private int getReadLength(int requestedLength) {
if (readPosition == data.length) {
// If the requested length is non-zero, the end of the input will be read.
return requestedLength == 0 ? 0 : Integer.MAX_VALUE;
}
int targetPosition = readPosition + requestedLength;
if (simulatePartialReads && requestedLength > 1
&& !partiallySatisfiedTargetPositions.get(targetPosition)) {
partiallySatisfiedTargetPositions.put(targetPosition, true);
return 1;
}
return Math.min(requestedLength, data.length - readPosition);
}
/**
* Builder of {@link FakeExtractorInput} instances.
*/
public static final class Builder {
private byte[] data;
private boolean simulateUnknownLength;
private boolean simulatePartialReads;
private boolean simulateIOErrors;
public Builder() {
data = new byte[0];
}
public Builder setData(byte[] data) {
this.data = data;
return this;
}
public Builder setSimulateUnknownLength(boolean simulateUnknownLength) {
this.simulateUnknownLength = simulateUnknownLength;
return this;
}
public Builder setSimulatePartialReads(boolean simulatePartialReads) {
this.simulatePartialReads = simulatePartialReads;
return this;
}
public Builder setSimulateIOErrors(boolean simulateIOErrors) {
this.simulateIOErrors = simulateIOErrors;
return this;
}
public FakeExtractorInput build() {
return new FakeExtractorInput(data, simulateUnknownLength, simulatePartialReads,
simulateIOErrors);
}
}
}