Skip to content

Commit 0e1d5c8

Browse files
committed
more efficient header parsing
1 parent 6c90693 commit 0e1d5c8

File tree

2 files changed

+166
-107
lines changed

2 files changed

+166
-107
lines changed

src/main/java/robaho/net/httpserver/Request.java

Lines changed: 101 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,12 @@
3535
*/
3636
class Request {
3737

38-
static final int BUF_LEN = 2048;
39-
static final byte CR = 13;
40-
static final byte LF = 10;
38+
static final char CR = 13;
39+
static final char LF = 10;
4140

4241
private String startLine;
43-
private InputStream is;
44-
private OutputStream os;
42+
private final InputStream is;
43+
private final OutputStream os;
4544

4645
Request(InputStream rawInputStream, OutputStream rawout) throws IOException {
4746
is = rawInputStream;
@@ -52,10 +51,6 @@ class Request {
5251
} while ("".equals(startLine));
5352
}
5453

55-
char[] buf = new char[BUF_LEN];
56-
int pos;
57-
StringBuffer lineBuf;
58-
5954
public InputStream inputStream() {
6055
return is;
6156
}
@@ -64,22 +59,73 @@ public OutputStream outputStream() {
6459
return os;
6560
}
6661

62+
static class PushbackStream {
63+
private final InputStream is;
64+
private int pushback = -1;
65+
private boolean eof=false;
66+
67+
public PushbackStream(InputStream is) {
68+
this.is = is;
69+
}
70+
public int read() throws IOException {
71+
if(pushback!=-1) {
72+
try {
73+
return pushback;
74+
} finally {
75+
pushback=-1;
76+
}
77+
}
78+
if(eof) return -1;
79+
return is.read();
80+
}
81+
public void skipWhitespace() throws IOException {
82+
int c;
83+
for(c=read();c==' ' || c=='\t';c=read()){}
84+
if(c==-1) eof=true; else pushback=c;
85+
}
86+
}
87+
88+
// efficient building of trimmed strings
89+
static class StrBuilder {
90+
private char buffer[] = new char[128];
91+
private int count=0;
92+
public void append(int c) {
93+
if(count==0 && c==' ') return;
94+
if(count==buffer.length) {
95+
char tmp[] = new char[buffer.length*2];
96+
System.arraycopy(buffer,0,tmp,0,count);
97+
buffer=tmp;
98+
}
99+
buffer[count++]=(char)c;
100+
}
101+
@Override
102+
public String toString() {
103+
while(count>0 && buffer[count-1]==' ') count--;
104+
return new String(buffer,0,count);
105+
}
106+
public boolean isEmpty() {
107+
return count==0;
108+
}
109+
public void clear() {
110+
count=0;
111+
}
112+
}
113+
67114
/**
68115
* read a line from the stream returning as a String.
69116
* Not used for reading headers.
70117
*/
118+
private String readLine() throws IOException {
119+
StringBuilder lineBuf = new StringBuilder();
71120

72-
public String readLine() throws IOException {
73-
boolean gotCR = false, gotLF = false;
74-
pos = 0;
75-
lineBuf = new StringBuffer();
76-
while (!gotLF) {
121+
boolean gotCR = false;
122+
while (true) {
77123
int c;
78124

79125
try {
80126
c = is.read();
81127
} catch(IOException e) {
82-
if(pos==0) return null;
128+
if(lineBuf.length()==0) return null;
83129
throw e;
84130
}
85131

@@ -88,30 +134,20 @@ public String readLine() throws IOException {
88134
}
89135
if (gotCR) {
90136
if (c == LF) {
91-
gotLF = true;
137+
return new String(lineBuf);
92138
} else {
93139
gotCR = false;
94-
consume(CR);
95-
consume(c);
140+
lineBuf.append(CR);
141+
lineBuf.append((char)c);
96142
}
97143
} else {
98144
if (c == CR) {
99145
gotCR = true;
100146
} else {
101-
consume(c);
147+
lineBuf.append((char)c);
102148
}
103149
}
104150
}
105-
lineBuf.append(buf, 0, pos);
106-
return new String(lineBuf);
107-
}
108-
109-
private void consume(int c) {
110-
if (pos == BUF_LEN) {
111-
lineBuf.append(buf);
112-
pos = 0;
113-
}
114-
buf[pos++] = (char) c;
115151
}
116152

117153
/**
@@ -130,91 +166,49 @@ Headers headers() throws IOException {
130166
}
131167
hdrs = new Headers();
132168

133-
char s[] = new char[10];
134-
int len = 0;
169+
StrBuilder key = new StrBuilder();
170+
StrBuilder value = new StrBuilder();
135171

136-
int firstc = is.read();
172+
PushbackStream pbs = new PushbackStream(is);
137173

138-
// check for empty headers
139-
if (firstc == CR || firstc == LF) {
140-
int c = is.read();
141-
if (c == CR || c == LF) {
142-
return hdrs;
143-
}
144-
s[0] = (char) firstc;
145-
len = 1;
146-
firstc = c;
147-
}
174+
boolean inKey = true;
175+
boolean prevCR = false;
176+
boolean sol = true;
177+
int c;
148178

149-
while (firstc != LF && firstc != CR && firstc >= 0) {
150-
int keyend = -1;
151-
int c;
152-
boolean inKey = firstc > ' ';
153-
s[len++] = (char) firstc;
154-
parseloop: {
155-
while ((c = is.read()) >= 0) {
156-
switch (c) {
157-
/* fallthrough */
158-
case ':':
159-
if (inKey && len > 0)
160-
keyend = len;
161-
inKey = false;
162-
break;
163-
case '\t':
164-
c = ' ';
165-
case ' ':
166-
inKey = false;
167-
break;
168-
case CR:
169-
case LF:
170-
firstc = is.read();
171-
if (c == CR && firstc == LF) {
172-
firstc = is.read();
173-
if (firstc == CR)
174-
firstc = is.read();
175-
}
176-
if (firstc == LF || firstc == CR || firstc > ' ')
177-
break parseloop;
178-
/* continuation */
179-
c = ' ';
180-
break;
179+
while((c=pbs.read())!=-1) {
180+
if(c==CR) { prevCR = true; }
181+
else if(c==LF && prevCR) {
182+
if(key.isEmpty() && value.isEmpty()) break;
183+
if(sol) {
184+
hdrs.add(key.toString(),value.toString());
185+
break;
186+
}
187+
prevCR=false;
188+
sol=true;
189+
} else {
190+
if((c==' ' || c=='\t') && sol) {
191+
pbs.skipWhitespace();
192+
inKey=false;
193+
sol=false;
194+
} else {
195+
if(sol) {
196+
if(!key.isEmpty() || !value.isEmpty()) {
197+
hdrs.add(key.toString(),value.toString());
198+
key.clear();
199+
value.clear();
200+
}
201+
inKey=true;
202+
sol=false;
181203
}
182-
if (len >= s.length) {
183-
char ns[] = new char[s.length * 2];
184-
System.arraycopy(s, 0, ns, 0, len);
185-
s = ns;
204+
if(c==':' && inKey) {
205+
inKey=false;
206+
pbs.skipWhitespace();
207+
} else {
208+
(inKey ? key : value).append(c);
186209
}
187-
s[len++] = (char) c;
188210
}
189-
firstc = -1;
190-
}
191-
while (len > 0 && s[len - 1] <= ' ')
192-
len--;
193-
String k;
194-
if (keyend <= 0) {
195-
k = null;
196-
keyend = 0;
197-
} else {
198-
k = String.copyValueOf(s, 0, keyend);
199-
if (keyend < len && s[keyend] == ':')
200-
keyend++;
201-
while (keyend < len && s[keyend] <= ' ')
202-
keyend++;
203-
}
204-
String v;
205-
if (keyend >= len)
206-
v = new String();
207-
else
208-
v = String.copyValueOf(s, keyend, len - keyend);
209-
210-
if (hdrs.size() >= ServerConfig.getMaxReqHeaders()) {
211-
throw new IOException("maximum number of headers exceeded");
212-
}
213-
if (k == null) { // Headers disallows null keys, use empty string
214-
k = ""; // instead to represent invalid key
215211
}
216-
hdrs.add(k, v);
217-
len = 0;
218212
}
219213
return hdrs;
220214
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package robaho.net.httpserver;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.IOException;
6+
7+
import static org.testng.Assert.assertEquals;
8+
import org.testng.annotations.Test;
9+
10+
public class RequestHeadersTest {
11+
@Test
12+
public void TestHeaderParse() throws IOException {
13+
String request = "GET blah\r\nKEY:VALUE\r\n\r\n";
14+
var is = new ByteArrayInputStream(request.getBytes());
15+
var os = new ByteArrayOutputStream();
16+
17+
Request r = new Request(is,os);
18+
assertEquals(r.requestLine(),"GET blah");
19+
assertEquals(r.headers().getFirst("KEY"),"VALUE");
20+
}
21+
@Test
22+
public void TestMultiLineHeaderParse() throws IOException {
23+
String request = "GET blah\r\nKEY:VAL\r\n 2\r\n\r\n";
24+
var is = new ByteArrayInputStream(request.getBytes());
25+
var os = new ByteArrayOutputStream();
26+
27+
Request r = new Request(is,os);
28+
assertEquals(r.requestLine(),"GET blah");
29+
assertEquals(r.headers().getFirst("KEY"),"VAL2");
30+
}
31+
@Test
32+
public void TestMultipleHeaders() throws IOException {
33+
String request = "GET blah\r\nKEY:VAL\r\nKEY2:VAL2\r\n\r\n";
34+
var is = new ByteArrayInputStream(request.getBytes());
35+
var os = new ByteArrayOutputStream();
36+
37+
Request r = new Request(is,os);
38+
assertEquals(r.requestLine(),"GET blah");
39+
assertEquals(r.headers().getFirst("KEY"),"VAL");
40+
assertEquals(r.headers().getFirst("KEY2"),"VAL2");
41+
}
42+
@Test
43+
public void TestMultipleHeadersThenBody() throws IOException {
44+
String request = "GET blah\r\nKEY:VAL\r\nKEY2:VAL2\r\n\r\nSome Body Data";
45+
var is = new ByteArrayInputStream(request.getBytes());
46+
var os = new ByteArrayOutputStream();
47+
48+
Request r = new Request(is,os);
49+
assertEquals(r.requestLine(),"GET blah");
50+
assertEquals(r.headers().getFirst("KEY"),"VAL");
51+
assertEquals(r.headers().getFirst("KEY2"),"VAL2");
52+
}
53+
@Test
54+
public void TestWhitespace() throws IOException {
55+
String request = "GET blah\r\nKEY : VAL\r\nKEY2:VAL2 \r\n\r\nSome Body Data";
56+
var is = new ByteArrayInputStream(request.getBytes());
57+
var os = new ByteArrayOutputStream();
58+
59+
Request r = new Request(is,os);
60+
assertEquals(r.requestLine(),"GET blah");
61+
assertEquals(r.headers().getFirst("KEY"),"VAL");
62+
assertEquals(r.headers().getFirst("KEY2"),"VAL2");
63+
}
64+
65+
}

0 commit comments

Comments
 (0)