1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.urlconnection; 6 7 import org.chromium.net.UploadDataProvider; 8 import org.chromium.net.UploadDataSink; 9 10 import androidx.annotation.VisibleForTesting; 11 12 import java.io.IOException; 13 import java.net.ProtocolException; 14 import java.nio.ByteBuffer; 15 16 /** 17 * An implementation of {@link java.io.OutputStream} that buffers entire request 18 * body in memory. This is used when neither 19 * {@link CronetHttpURLConnection#setFixedLengthStreamingMode} 20 * nor {@link CronetHttpURLConnection#setChunkedStreamingMode} is set. 21 */ 22 @VisibleForTesting 23 public final class CronetBufferedOutputStream extends CronetOutputStream { 24 // QUIC uses a read buffer of 14520 bytes, SPDY uses 2852 bytes, and normal 25 // stream uses 16384 bytes. Therefore, use 16384 for now to avoid growing 26 // the buffer too many times. 27 private static final int INITIAL_BUFFER_SIZE = 16384; 28 // If content length is not passed in the constructor, this is -1. 29 private final int mInitialContentLength; 30 private final CronetHttpURLConnection mConnection; 31 private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl(); 32 // Internal buffer that is used to buffer the request body. 33 private ByteBuffer mBuffer; 34 private boolean mConnected; 35 36 /** 37 * Package protected constructor. 38 * @param connection The CronetHttpURLConnection object. 39 * @param contentLength The content length of the request body. It must not 40 * be smaller than 0 or bigger than {@link Integer.MAX_VALUE}. 41 */ CronetBufferedOutputStream(final CronetHttpURLConnection connection, final long contentLength)42 CronetBufferedOutputStream(final CronetHttpURLConnection connection, final long contentLength) { 43 if (connection == null) { 44 throw new NullPointerException("Argument connection cannot be null."); 45 } 46 47 if (contentLength > Integer.MAX_VALUE) { 48 throw new IllegalArgumentException( 49 "Use setFixedLengthStreamingMode()" 50 + " or setChunkedStreamingMode() for requests larger than 2GB."); 51 } 52 if (contentLength < 0) { 53 throw new IllegalArgumentException("Content length < 0."); 54 } 55 mConnection = connection; 56 mInitialContentLength = (int) contentLength; 57 mBuffer = ByteBuffer.allocate(mInitialContentLength); 58 } 59 60 /** 61 * Package protected constructor used when content length is not known. 62 * @param connection The CronetHttpURLConnection object. 63 */ CronetBufferedOutputStream(final CronetHttpURLConnection connection)64 CronetBufferedOutputStream(final CronetHttpURLConnection connection) { 65 if (connection == null) { 66 throw new NullPointerException(); 67 } 68 69 mConnection = connection; 70 mInitialContentLength = -1; 71 // Buffering without knowing content-length. 72 mBuffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE); 73 } 74 75 @Override write(int oneByte)76 public void write(int oneByte) throws IOException { 77 checkNotClosed(); 78 ensureCanWrite(1); 79 mBuffer.put((byte) oneByte); 80 } 81 82 @Override write(byte[] buffer, int offset, int count)83 public void write(byte[] buffer, int offset, int count) throws IOException { 84 checkNotClosed(); 85 ensureCanWrite(count); 86 mBuffer.put(buffer, offset, count); 87 } 88 89 /** Ensures that {@code count} bytes can be written to the internal buffer. */ ensureCanWrite(int count)90 private void ensureCanWrite(int count) throws IOException { 91 if (mInitialContentLength != -1 && mBuffer.position() + count > mInitialContentLength) { 92 // Error message is to match that of the default implementation. 93 throw new ProtocolException( 94 "exceeded content-length limit of " + mInitialContentLength + " bytes"); 95 } 96 if (mConnected) { 97 throw new IllegalStateException( 98 "Use setFixedLengthStreamingMode() or " 99 + "setChunkedStreamingMode() for writing after connect"); 100 } 101 if (mInitialContentLength != -1) { 102 // If mInitialContentLength is known, the buffer should not grow. 103 return; 104 } 105 if (mBuffer.limit() - mBuffer.position() > count) { 106 // If there is enough capacity, the buffer should not grow. 107 return; 108 } 109 int afterSize = Math.max(mBuffer.capacity() * 2, mBuffer.capacity() + count); 110 ByteBuffer newByteBuffer = ByteBuffer.allocate(afterSize); 111 mBuffer.flip(); 112 newByteBuffer.put(mBuffer); 113 mBuffer = newByteBuffer; 114 } 115 116 // Below are CronetOutputStream implementations: 117 118 /** Sets {@link #mConnected} to {@code true}. */ 119 @Override setConnected()120 void setConnected() throws IOException { 121 mConnected = true; 122 if (mBuffer.position() < mInitialContentLength) { 123 throw new ProtocolException("Content received is less than Content-Length"); 124 } 125 // Flip the buffer to prepare it for UploadDataProvider read calls. 126 mBuffer.flip(); 127 } 128 129 @Override checkReceivedEnoughContent()130 void checkReceivedEnoughContent() throws IOException { 131 // Already checked in setConnected. Skip the check here, since mBuffer 132 // might be flipped. 133 } 134 135 @Override getUploadDataProvider()136 UploadDataProvider getUploadDataProvider() { 137 return mUploadDataProvider; 138 } 139 140 private class UploadDataProviderImpl extends UploadDataProvider { 141 @Override getLength()142 public long getLength() { 143 // This method is supposed to be called just before starting the request. 144 // If content length is not initially passed in, the number of bytes 145 // written will be used as the content length. 146 // TODO(xunjieli): Think of a less fragile way, since getLength() can be 147 // potentially called in other places in the future. 148 if (mInitialContentLength == -1) { 149 // Account for the fact that setConnected() flip()s mBuffer. 150 return mConnected ? mBuffer.limit() : mBuffer.position(); 151 } 152 return mInitialContentLength; 153 } 154 155 @Override read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer)156 public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { 157 final int availableSpace = byteBuffer.remaining(); 158 if (availableSpace < mBuffer.remaining()) { 159 byteBuffer.put(mBuffer.array(), mBuffer.position(), availableSpace); 160 mBuffer.position(mBuffer.position() + availableSpace); 161 } else { 162 byteBuffer.put(mBuffer); 163 } 164 uploadDataSink.onReadSucceeded(false); 165 } 166 167 @Override rewind(UploadDataSink uploadDataSink)168 public void rewind(UploadDataSink uploadDataSink) { 169 mBuffer.position(0); 170 uploadDataSink.onRewindSucceeded(); 171 } 172 } 173 } 174