1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.hc.core5.http.impl.io;
29
30 import java.io.IOException;
31 import java.io.InputStream;
32
33 import org.apache.hc.core5.http.ConnectionClosedException;
34 import org.apache.hc.core5.http.Header;
35 import org.apache.hc.core5.http.HttpException;
36 import org.apache.hc.core5.http.MalformedChunkCodingException;
37 import org.apache.hc.core5.http.StreamClosedException;
38 import org.apache.hc.core5.http.TruncatedChunkException;
39 import org.apache.hc.core5.http.config.Http1Config;
40 import org.apache.hc.core5.http.io.SessionInputBuffer;
41 import org.apache.hc.core5.util.Args;
42 import org.apache.hc.core5.util.CharArrayBuffer;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 public class ChunkedInputStream extends InputStream {
61
62 private enum State {
63 CHUNK_LEN, CHUNK_DATA, CHUNK_CRLF, CHUNK_INVALID
64 }
65
66 private static final int BUFFER_SIZE = 2048;
67 private static final Headerder.html#Header">Header[] EMPTY_FOOTERS = new Header[0];
68
69
70 private final SessionInputBuffer buffer;
71 private final InputStream inputStream;
72 private final CharArrayBuffer lineBuffer;
73 private final Http1Config http1Config;
74
75 private State state;
76
77
78 private long chunkSize;
79
80
81 private long pos;
82
83
84 private boolean eof;
85
86
87 private boolean closed;
88
89 private Header[] footers = EMPTY_FOOTERS;
90
91
92
93
94
95
96
97
98
99
100 public ChunkedInputStream(final SessionInputBuffer buffer, final InputStream inputStream, final Http1Config http1Config) {
101 super();
102 this.buffer = Args.notNull(buffer, "Session input buffer");
103 this.inputStream = Args.notNull(inputStream, "Input stream");
104 this.pos = 0L;
105 this.lineBuffer = new CharArrayBuffer(16);
106 this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT;
107 this.state = State.CHUNK_LEN;
108 }
109
110
111
112
113
114
115
116 public ChunkedInputStream(final SessionInputBuffer buffer, final InputStream inputStream) {
117 this(buffer, inputStream, null);
118 }
119
120 @Override
121 public int available() throws IOException {
122 final int len = this.buffer.length();
123 return (int) Math.min(len, this.chunkSize - this.pos);
124 }
125
126
127
128
129
130
131
132
133
134
135
136
137
138 @Override
139 public int read() throws IOException {
140 if (this.closed) {
141 throw new StreamClosedException();
142 }
143 if (this.eof) {
144 return -1;
145 }
146 if (state != State.CHUNK_DATA) {
147 nextChunk();
148 if (this.eof) {
149 return -1;
150 }
151 }
152 final int b = buffer.read(inputStream);
153 if (b != -1) {
154 pos++;
155 if (pos >= chunkSize) {
156 state = State.CHUNK_CRLF;
157 }
158 }
159 return b;
160 }
161
162
163
164
165
166
167
168
169
170
171
172 @Override
173 public int read (final byte[] b, final int off, final int len) throws IOException {
174
175 if (closed) {
176 throw new StreamClosedException();
177 }
178
179 if (eof) {
180 return -1;
181 }
182 if (state != State.CHUNK_DATA) {
183 nextChunk();
184 if (eof) {
185 return -1;
186 }
187 }
188 final int bytesRead = buffer.read(b, off, (int) Math.min(len, chunkSize - pos), inputStream);
189 if (bytesRead != -1) {
190 pos += bytesRead;
191 if (pos >= chunkSize) {
192 state = State.CHUNK_CRLF;
193 }
194 return bytesRead;
195 }
196 eof = true;
197 throw new TruncatedChunkException("Truncated chunk (expected size: %d; actual size: %d)",
198 chunkSize, pos);
199 }
200
201
202
203
204
205
206
207
208 @Override
209 public int read (final byte[] b) throws IOException {
210 return read(b, 0, b.length);
211 }
212
213
214
215
216
217 private void nextChunk() throws IOException {
218 if (state == State.CHUNK_INVALID) {
219 throw new MalformedChunkCodingException("Corrupt data stream");
220 }
221 try {
222 chunkSize = getChunkSize();
223 if (chunkSize < 0L) {
224 throw new MalformedChunkCodingException("Negative chunk size");
225 }
226 state = State.CHUNK_DATA;
227 pos = 0L;
228 if (chunkSize == 0L) {
229 eof = true;
230 parseTrailerHeaders();
231 }
232 } catch (final MalformedChunkCodingException ex) {
233 state = State.CHUNK_INVALID;
234 throw ex;
235 }
236 }
237
238
239
240
241
242
243 private long getChunkSize() throws IOException {
244 final State st = this.state;
245 switch (st) {
246 case CHUNK_CRLF:
247 lineBuffer.clear();
248 final int bytesRead1 = this.buffer.readLine(lineBuffer, inputStream);
249 if (bytesRead1 == -1) {
250 throw new MalformedChunkCodingException(
251 "CRLF expected at end of chunk");
252 }
253 if (!lineBuffer.isEmpty()) {
254 throw new MalformedChunkCodingException(
255 "Unexpected content at the end of chunk");
256 }
257 state = State.CHUNK_LEN;
258
259 case CHUNK_LEN:
260 lineBuffer.clear();
261 final int bytesRead2 = this.buffer.readLine(lineBuffer, inputStream);
262 if (bytesRead2 == -1) {
263 throw new ConnectionClosedException(
264 "Premature end of chunk coded message body: closing chunk expected");
265 }
266 int separator = lineBuffer.indexOf(';');
267 if (separator < 0) {
268 separator = lineBuffer.length();
269 }
270 final String s = this.lineBuffer.substringTrimmed(0, separator);
271 try {
272 return Long.parseLong(s, 16);
273 } catch (final NumberFormatException e) {
274 throw new MalformedChunkCodingException("Bad chunk header: " + s);
275 }
276 default:
277 throw new IllegalStateException("Inconsistent codec state");
278 }
279 }
280
281
282
283
284
285 private void parseTrailerHeaders() throws IOException {
286 try {
287 this.footers = AbstractMessageParser.parseHeaders(buffer, inputStream,
288 http1Config.getMaxHeaderCount(),
289 http1Config.getMaxLineLength(),
290 null);
291 } catch (final HttpException ex) {
292 final IOException ioe = new MalformedChunkCodingException("Invalid trailing header: "
293 + ex.getMessage());
294 ioe.initCause(ex);
295 throw ioe;
296 }
297 }
298
299
300
301
302
303
304
305 @Override
306 public void close() throws IOException {
307 if (!closed) {
308 try {
309 if (!eof && state != State.CHUNK_INVALID) {
310
311
312
313
314 if (chunkSize == pos && chunkSize > 0 && read() == -1) {
315 return;
316 }
317
318 final byte[] buff = new byte[BUFFER_SIZE];
319 while (read(buff) >= 0) {
320 }
321 }
322 } finally {
323 eof = true;
324 closed = true;
325 }
326 }
327 }
328
329 public Header[] getFooters() {
330 return footers.length > 0 ? footers.clone() : EMPTY_FOOTERS;
331 }
332
333 }