Commit 3069251b by ibaker Committed by Oliver Woodman

Add gzip support to WebServerDispatcher

Add a test to DataSourceContractTest that asserts the gzip flag is
either ignored or handled correctly.

Add a test resource to DefaultHttpDataSourceContracTest that enables
gzip compression on the 'server' and checks it's handled correctly by
the client.

PiperOrigin-RevId: 352574359
parent dd1b1c08
...@@ -36,6 +36,7 @@ dependencies { ...@@ -36,6 +36,7 @@ dependencies {
testImplementation 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils')
} }
ext { ext {
......
...@@ -91,6 +91,7 @@ import java.util.concurrent.Executors; ...@@ -91,6 +91,7 @@ import java.util.concurrent.Executors;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
import java.util.zip.GZIPOutputStream;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
...@@ -2121,6 +2122,17 @@ public final class Util { ...@@ -2121,6 +2122,17 @@ public final class Util {
return initialValue; return initialValue;
} }
/** Compresses {@code input} using gzip and returns the result in a newly allocated byte array. */
public static byte[] gzip(byte[] input) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (GZIPOutputStream os = new GZIPOutputStream(output)) {
os.write(input);
} catch (IOException e) {
throw new AssertionError(e);
}
return output.toByteArray();
}
/** /**
* Absolute <i>get</i> method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link * Absolute <i>get</i> method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link
* ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by * ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchFloor; ...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchFloor;
import static com.google.android.exoplayer2.util.Util.escapeFileName; import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType; import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.getStringForTime; import static com.google.android.exoplayer2.util.Util.getStringForTime;
import static com.google.android.exoplayer2.util.Util.gzip;
import static com.google.android.exoplayer2.util.Util.minValue; import static com.google.android.exoplayer2.util.Util.minValue;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.parseXsDuration;
...@@ -37,6 +38,9 @@ import android.text.style.UnderlineSpan; ...@@ -37,6 +38,9 @@ import android.text.style.UnderlineSpan;
import android.util.SparseLongArray; import android.util.SparseLongArray;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -45,6 +49,7 @@ import java.util.Formatter; ...@@ -45,6 +49,7 @@ import java.util.Formatter;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Random; import java.util.Random;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -928,6 +933,17 @@ public class UtilTest { ...@@ -928,6 +933,17 @@ public class UtilTest {
} }
@Test @Test
public void gzip_resultInflatesBackToOriginalValue() throws Exception {
byte[] input = TestUtil.buildTestData(20);
byte[] deflated = gzip(input);
byte[] inflated =
ByteStreams.toByteArray(new GZIPInputStream(new ByteArrayInputStream(deflated)));
assertThat(inflated).isEqualTo(input);
}
@Test
public void getBigEndianInt_fromBigEndian() { public void getBigEndianInt_fromBigEndian() {
byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C}; byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C};
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
......
...@@ -194,6 +194,43 @@ public abstract class DataSourceContractTest { ...@@ -194,6 +194,43 @@ public abstract class DataSourceContractTest {
} }
} }
/**
* {@link DataSpec#FLAG_ALLOW_GZIP} should either be ignored by {@link DataSource}
* implementations, or correctly handled (i.e. the data is decompressed before being returned from
* {@link DataSource#read(byte[], int, int)}).
*/
@Test
public void gzipFlagDoesntAffectReturnedData() throws Exception {
ImmutableList<TestResource> resources = getTestResources();
Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource.");
for (int i = 0; i < resources.size(); i++) {
additionalFailureInfo.setInfo(getFailureLabel(resources, i));
TestResource resource = resources.get(i);
DataSource dataSource = createDataSource();
try {
long length =
dataSource.open(
new DataSpec.Builder()
.setUri(resource.getUri())
.setFlags(DataSpec.FLAG_ALLOW_GZIP)
.build());
byte[] data =
resource.isEndOfInputExpected()
? Util.readToEnd(dataSource)
: Util.readExactly(dataSource, resource.getExpectedBytes().length);
if (length != C.LENGTH_UNSET) {
assertThat(length).isEqualTo(resource.getExpectedBytes().length);
}
assertThat(data).isEqualTo(resource.getExpectedBytes());
} finally {
dataSource.close();
}
additionalFailureInfo.setInfo(null);
}
}
@Test @Test
public void resourceNotFound() throws Exception { public void resourceNotFound() throws Exception {
DataSource dataSource = createDataSource(); DataSource dataSource = createDataSource();
......
...@@ -60,6 +60,20 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -60,6 +60,20 @@ public class HttpDataSourceTestEnv extends ExternalResource {
.resolvesToUnknownLength(true) .resolvesToUnknownLength(true)
.build(); .build();
private static final WebServerDispatcher.Resource GZIP_ENABLED =
new WebServerDispatcher.Resource.Builder()
.setPath("/gzip/enabled")
.setData(TestUtil.buildTestData(/* length= */ 20, seed++))
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED)
.build();
private static final WebServerDispatcher.Resource GZIP_FORCED =
new WebServerDispatcher.Resource.Builder()
.setPath("/gzip/forced")
.setData(TestUtil.buildTestData(/* length= */ 20, seed++))
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED)
.build();
private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED = private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED =
RANGE_SUPPORTED.buildUpon().setPath("/redirects/to/range/supported").build(); RANGE_SUPPORTED.buildUpon().setPath("/redirects/to/range/supported").build();
...@@ -74,6 +88,8 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -74,6 +88,8 @@ public class HttpDataSourceTestEnv extends ExternalResource {
createTestResource("range not supported", RANGE_NOT_SUPPORTED), createTestResource("range not supported", RANGE_NOT_SUPPORTED),
createTestResource( createTestResource(
"range not supported, length unknown", RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN), "range not supported, length unknown", RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN),
createTestResource("gzip enabled", GZIP_ENABLED),
createTestResource("gzip forced", GZIP_FORCED),
createTestResource( createTestResource(
"302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, /* server= */ redirectionServer)); "302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, /* server= */ redirectionServer));
} }
...@@ -91,7 +107,9 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -91,7 +107,9 @@ public class HttpDataSourceTestEnv extends ExternalResource {
RANGE_SUPPORTED, RANGE_SUPPORTED,
RANGE_SUPPORTED_LENGTH_UNKNOWN, RANGE_SUPPORTED_LENGTH_UNKNOWN,
RANGE_NOT_SUPPORTED, RANGE_NOT_SUPPORTED,
RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN))); RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN,
GZIP_ENABLED,
GZIP_FORCED)));
redirectionServer.start(); redirectionServer.start();
redirectionServer.setDispatcher( redirectionServer.setDispatcher(
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.Arrays; import java.util.Arrays;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
...@@ -29,6 +30,26 @@ import org.junit.Test; ...@@ -29,6 +30,26 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Tests for {@link WebServerDispatcher}. */ /** Tests for {@link WebServerDispatcher}. */
// We use the OkHttp client library for these tests because it's generally nicer to use than Java's
// HttpURLConnection.
//
// However, OkHttp's 'transparent compression' behaviour is annoying when trying to test the edge
// cases of the WebServerDispatcher's Accept-Encoding header handling. If passed a request with no
// Accept-Encoding header, the OkHttp client library will silently add one that accepts gzip and
// then silently unzip the response data (and remove the Content-Coding header) before returning it.
//
// This gets in the way of some test cases, for example testing how the WebServerDispatcher handles
// a request with *no* Accept-Encoding header (since it's impossible to send this using OkHttp).
//
// Under Robolectric, the Java HttpURLConnection doesn't have this transparent compression
// behaviour, but that's a Robolectric 'bug' (internal: b/177071755) because the Android platform
// implementation of HttpURLConnection does (it uses OkHttp under the hood). So we can't really use
// HttpURLConnection to test these edge cases either (even though it would work for now) because
// ideally Robolectric will in the future make the implementation more realistic and suddenly our
// tests would be wrong.
//
// So instead we just don't test these cases that require passing header combinations that are
// impossible with OkHttp.
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class WebServerDispatcherTest { public class WebServerDispatcherTest {
...@@ -49,6 +70,10 @@ public class WebServerDispatcherTest { ...@@ -49,6 +70,10 @@ public class WebServerDispatcherTest {
"/range/requests/not-supported-length-unknown"; "/range/requests/not-supported-length-unknown";
private static final byte[] RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA = private static final byte[] RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA =
TestUtil.buildTestData(/* length= */ 20, seed++); TestUtil.buildTestData(/* length= */ 20, seed++);
private static final String GZIP_ENABLED_PATH = "/gzip/enabled";
private static final byte[] GZIP_ENABLED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++);
private static final String GZIP_FORCED_PATH = "/gzip/forced";
private static final byte[] GZIP_FORCED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++);
private MockWebServer mockWebServer; private MockWebServer mockWebServer;
...@@ -79,6 +104,16 @@ public class WebServerDispatcherTest { ...@@ -79,6 +104,16 @@ public class WebServerDispatcherTest {
.setData(RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA) .setData(RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA)
.supportsRangeRequests(false) .supportsRangeRequests(false)
.resolvesToUnknownLength(true) .resolvesToUnknownLength(true)
.build(),
new WebServerDispatcher.Resource.Builder()
.setPath(GZIP_ENABLED_PATH)
.setData(GZIP_ENABLED_DATA)
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED)
.build(),
new WebServerDispatcher.Resource.Builder()
.setPath(GZIP_FORCED_PATH)
.setData(GZIP_FORCED_DATA)
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED)
.build()))); .build())));
} }
...@@ -392,4 +427,155 @@ public class WebServerDispatcherTest { ...@@ -392,4 +427,155 @@ public class WebServerDispatcherTest {
assertThat(response.body().bytes()).isEqualTo(RANGE_UNSUPPORTED_DATA); assertThat(response.body().bytes()).isEqualTo(RANGE_UNSUPPORTED_DATA);
} }
} }
@Test
public void gzipDisabled_acceptEncodingHeaderAllowsAnyCoding_identityResponse() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "*")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(RANGE_SUPPORTED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(RANGE_SUPPORTED_DATA);
}
}
@Test
public void gzipDisabled_acceptEncodingHeaderRequiresGzip_406Response() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "gzip;q=1.0")
.addHeader("Accept-Encoding", "identity;q=0")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(406);
}
}
@Test
public void gzipDisabled_acceptEncodingHeaderRequiresGzipViaAsterisk_406Response()
throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "gzip;q=1.0")
.addHeader("Accept-Encoding", "*;q=0")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(406);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "gzip")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderAllowsAnyCoding_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "*")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderPrefersIdentity_responseNotGzipped()
throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "identity;q=0.8, gzip;q=0.2")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "identity")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA);
}
}
@Test
public void gzipForced_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_FORCED_PATH))
.addHeader("Accept-Encoding", "gzip")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_FORCED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipForced_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_FORCED_PATH))
.addHeader("Accept-Encoding", "identity")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_FORCED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_FORCED_DATA);
}
}
} }
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