|
| 1 | +package com.google.api.client.http; |
| 2 | + |
| 3 | +import java.io.FilterInputStream; |
| 4 | +import java.io.IOException; |
| 5 | +import java.io.InputStream; |
| 6 | +import java.util.zip.GZIPInputStream; |
| 7 | + |
| 8 | +final class GzipSupport { |
| 9 | + |
| 10 | + private GzipSupport() {} |
| 11 | + |
| 12 | + static GZIPInputStream newGzipInputStream(InputStream in) throws IOException { |
| 13 | + return new GZIPInputStream(new OptimisticAvailabilityInputStream(in)); |
| 14 | + } |
| 15 | + |
| 16 | + /** |
| 17 | + * When {@link GZIPInputStream} completes processing an individual member it will call {@link |
| 18 | + * InputStream#available()} to determine if there is more stream to try and process. If the call |
| 19 | + * to {@code available()} returns 0 {@code GZIPInputStream} will determine it has processed the |
| 20 | + * entirety of the underlying stream. This is spurious, as {@link InputStream#available()} is |
| 21 | + * allowed to return 0 if it would require blocking in order for more bytes to be available. When |
| 22 | + * {@code GZIPInputStream} is reading from a {@code Transfer-Encoding: chunked} response, if the |
| 23 | + * chunk boundary happens to align closely enough to the member boundary {@code GZIPInputStream} |
| 24 | + * won't consume the whole response. |
| 25 | + * |
| 26 | + * <p>This class, provides an optimistic "estimate" (in actuality, a lie) of the number of {@code |
| 27 | + * available()} bytes in the underlying stream. It does this by tracking the last number of bytes |
| 28 | + * read. If the last number of bytes read is grater than -1, we return {@link Integer#MAX_VALUE} |
| 29 | + * to any call of {@link #available()}. |
| 30 | + * |
| 31 | + * <p>We're breaking the contract of available() in that we're lying about how much data we have |
| 32 | + * accessible without blocking, however in the case where we're weaving {@link GZIPInputStream} |
| 33 | + * into response processing we already know there are going to be blocking calls to read before |
| 34 | + * the stream is exhausted. |
| 35 | + * |
| 36 | + * <p>This scenario isn't unique to processing of chunked responses, and can be replicated |
| 37 | + * reliably using a {@link java.io.SequenceInputStream} with two underlying {@link |
| 38 | + * java.io.ByteArrayInputStream}. See the corresponding test class for a reproduction. |
| 39 | + * |
| 40 | + * <p>The need for this class has been verified for the following JVMs: |
| 41 | + * |
| 42 | + * <ol> |
| 43 | + * <li> |
| 44 | + * <pre> |
| 45 | + * openjdk version "1.8.0_292" |
| 46 | + * OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_292-b10) |
| 47 | + * OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.292-b10, mixed mode) |
| 48 | + * </pre> |
| 49 | + * <li> |
| 50 | + * <pre> |
| 51 | + * openjdk version "11.0.14.1" 2022-02-08 |
| 52 | + * OpenJDK Runtime Environment Temurin-11.0.14.1+1 (build 11.0.14.1+1) |
| 53 | + * OpenJDK 64-Bit Server VM Temurin-11.0.14.1+1 (build 11.0.14.1+1, mixed mode) |
| 54 | + * </pre> |
| 55 | + * <li> |
| 56 | + * <pre> |
| 57 | + * openjdk version "17" 2021-09-14 |
| 58 | + * OpenJDK Runtime Environment Temurin-17+35 (build 17+35) |
| 59 | + * OpenJDK 64-Bit Server VM Temurin-17+35 (build 17+35, mixed mode, sharing) |
| 60 | + * </pre> |
| 61 | + * </ol> |
| 62 | + */ |
| 63 | + private static final class OptimisticAvailabilityInputStream extends FilterInputStream { |
| 64 | + private int lastRead = 0; |
| 65 | + |
| 66 | + OptimisticAvailabilityInputStream(InputStream delegate) { |
| 67 | + super(delegate); |
| 68 | + } |
| 69 | + |
| 70 | + @Override |
| 71 | + public int available() throws IOException { |
| 72 | + return lastRead > -1 ? Integer.MAX_VALUE : 0; |
| 73 | + } |
| 74 | + |
| 75 | + @Override |
| 76 | + public int read() throws IOException { |
| 77 | + return lastRead = super.read(); |
| 78 | + } |
| 79 | + |
| 80 | + @Override |
| 81 | + public int read(byte[] b) throws IOException { |
| 82 | + return lastRead = super.read(b); |
| 83 | + } |
| 84 | + |
| 85 | + @Override |
| 86 | + public int read(byte[] b, int off, int len) throws IOException { |
| 87 | + return lastRead = super.read(b, off, len); |
| 88 | + } |
| 89 | + } |
| 90 | +} |
0 commit comments