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.http.impl.nio.codecs;
29
30 import java.io.IOException;
31 import java.nio.ByteBuffer;
32 import java.nio.channels.ReadableByteChannel;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 import org.apache.http.Header;
37 import org.apache.http.MalformedChunkCodingException;
38 import org.apache.http.ParseException;
39 import org.apache.http.TruncatedChunkException;
40 import org.apache.http.annotation.NotThreadSafe;
41 import org.apache.http.impl.io.HttpTransportMetricsImpl;
42 import org.apache.http.message.BufferedHeader;
43 import org.apache.http.nio.reactor.SessionInputBuffer;
44 import org.apache.http.protocol.HTTP;
45 import org.apache.http.util.Args;
46 import org.apache.http.util.CharArrayBuffer;
47
48
49
50
51
52
53
54 @NotThreadSafe
55 public class ChunkDecoder extends AbstractContentDecoder {
56
57 private static final int READ_CONTENT = 0;
58 private static final int READ_FOOTERS = 1;
59 private static final int COMPLETED = 2;
60
61 private int state;
62 private boolean endOfChunk;
63 private boolean endOfStream;
64
65 private CharArrayBuffer lineBuf;
66 private int chunkSize;
67 private int pos;
68
69 private final List<CharArrayBuffer> trailerBufs;
70
71 private Header[] footers;
72
73 public ChunkDecoder(
74 final ReadableByteChannel channel,
75 final SessionInputBuffer buffer,
76 final HttpTransportMetricsImpl metrics) {
77 super(channel, buffer, metrics);
78 this.state = READ_CONTENT;
79 this.chunkSize = -1;
80 this.pos = 0;
81 this.endOfChunk = false;
82 this.endOfStream = false;
83 this.trailerBufs = new ArrayList<CharArrayBuffer>();
84 }
85
86 private void readChunkHead() throws IOException {
87 if (this.endOfChunk) {
88 if (this.buffer.length() < 2) {
89 return;
90 }
91 final int cr = this.buffer.read();
92 final int lf = this.buffer.read();
93 if (cr != HTTP.CR || lf != HTTP.LF) {
94 throw new MalformedChunkCodingException("CRLF expected at end of chunk");
95 }
96 this.endOfChunk = false;
97 }
98 if (this.lineBuf == null) {
99 this.lineBuf = new CharArrayBuffer(32);
100 } else {
101 this.lineBuf.clear();
102 }
103 if (this.buffer.readLine(this.lineBuf, this.endOfStream)) {
104 int separator = this.lineBuf.indexOf(';');
105 if (separator < 0) {
106 separator = this.lineBuf.length();
107 }
108 try {
109 final String s = this.lineBuf.substringTrimmed(0, separator);
110 this.chunkSize = Integer.parseInt(s, 16);
111 } catch (final NumberFormatException e) {
112 throw new MalformedChunkCodingException("Bad chunk header");
113 }
114 this.pos = 0;
115 }
116 }
117
118 private void parseHeader() {
119 final CharArrayBuffer current = this.lineBuf;
120 final int count = this.trailerBufs.size();
121 if ((this.lineBuf.charAt(0) == ' ' || this.lineBuf.charAt(0) == '\t') && count > 0) {
122
123 final CharArrayBuffer previous = this.trailerBufs.get(count - 1);
124 int i = 0;
125 while (i < current.length()) {
126 final char ch = current.charAt(i);
127 if (ch != ' ' && ch != '\t') {
128 break;
129 }
130 i++;
131 }
132 previous.append(' ');
133 previous.append(current, i, current.length() - i);
134 } else {
135 this.trailerBufs.add(current);
136 this.lineBuf = null;
137 }
138 }
139
140 private void processFooters() throws IOException {
141 final int count = this.trailerBufs.size();
142 if (count > 0) {
143 this.footers = new Header[this.trailerBufs.size()];
144 for (int i = 0; i < this.trailerBufs.size(); i++) {
145 final CharArrayBuffer buffer = this.trailerBufs.get(i);
146 try {
147 this.footers[i] = new BufferedHeader(buffer);
148 } catch (final ParseException ex) {
149 throw new IOException(ex.getMessage());
150 }
151 }
152 }
153 this.trailerBufs.clear();
154 }
155
156 public int read(final ByteBuffer dst) throws IOException {
157 Args.notNull(dst, "Byte buffer");
158 if (this.state == COMPLETED) {
159 return -1;
160 }
161
162 int totalRead = 0;
163 while (this.state != COMPLETED) {
164
165 if (!this.buffer.hasData() || this.chunkSize == -1) {
166 final int bytesRead = fillBufferFromChannel();
167 if (bytesRead == -1) {
168 this.endOfStream = true;
169 }
170 }
171
172 switch (this.state) {
173 case READ_CONTENT:
174
175 if (this.chunkSize == -1) {
176 readChunkHead();
177 if (this.chunkSize == -1) {
178
179 if (this.endOfStream) {
180 this.state = COMPLETED;
181 this.completed = true;
182 }
183 return totalRead;
184 }
185 if (this.chunkSize == 0) {
186
187 this.chunkSize = -1;
188 this.state = READ_FOOTERS;
189 break;
190 }
191 }
192 final int maxLen = this.chunkSize - this.pos;
193 final int len = this.buffer.read(dst, maxLen);
194 if (len > 0) {
195 this.pos += len;
196 totalRead += len;
197 } else {
198 if (!this.buffer.hasData() && this.endOfStream) {
199 this.state = COMPLETED;
200 this.completed = true;
201 throw new TruncatedChunkException("Truncated chunk "
202 + "( expected size: " + this.chunkSize
203 + "; actual size: " + this.pos + ")");
204 }
205 }
206
207 if (this.pos == this.chunkSize) {
208
209 this.chunkSize = -1;
210 this.pos = 0;
211 this.endOfChunk = true;
212 break;
213 }
214 return totalRead;
215 case READ_FOOTERS:
216 if (this.lineBuf == null) {
217 this.lineBuf = new CharArrayBuffer(32);
218 } else {
219 this.lineBuf.clear();
220 }
221 if (!this.buffer.readLine(this.lineBuf, this.endOfStream)) {
222
223 if (this.endOfStream) {
224 this.state = COMPLETED;
225 this.completed = true;
226 }
227 return totalRead;
228 }
229 if (this.lineBuf.length() > 0) {
230 parseHeader();
231 } else {
232 this.state = COMPLETED;
233 this.completed = true;
234 processFooters();
235 }
236 break;
237 }
238
239 }
240 return totalRead;
241 }
242
243 public Header[] getFooters() {
244 if (this.footers != null) {
245 return this.footers.clone();
246 } else {
247 return new Header[] {};
248 }
249 }
250
251 @Override
252 public String toString() {
253 final StringBuilder buffer = new StringBuilder();
254 buffer.append("[chunk-coded; completed: ");
255 buffer.append(this.completed);
256 buffer.append("]");
257 return buffer.toString();
258 }
259
260 }