1use simoom_http1::write::{DateCache, HeadWriter, StaticHead};
10use simoom_http1::*;
11use simoom_sys::scan::{force, level, Level};
12use std::hint::black_box;
13use std::time::Instant;
14
15const CURL: &[u8] = b"GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.5.0\r\nAccept: */*\r\n\r\n";
16
17const BROWSER: &[u8] = b"GET /index.html?q=1 HTTP/1.1\r\nHost: www.example.com\r\nConnection: keep-alive\r\nsec-ch-ua: \"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: \"Windows\"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nAccept-Language: en-US,en;q=0.9\r\nCookie: session=8f2a9c1d4e5b6a7f8091a2b3c4d5e6f7; theme=dark; _ga=GA1.1.123456789.1700000000\r\n\r\n";
18
19fn twenty_headers() -> Vec<u8> {
20 let mut s = b"POST /api/v1/orders HTTP/1.1\r\nHost: api.internal:8443\r\nContent-Type: application/json\r\nContent-Length: 128\r\n".to_vec();
21 for i in 0..17 {
22 s.extend_from_slice(format!("X-Custom-Header-{i}: value-{i}-abcdefghijklmnop\r\n").as_bytes());
23 }
24 s.extend_from_slice(b"\r\n");
25 s
26}
27
28fn measure(mut f: impl FnMut()) -> f64 {
30 let mut iters = 1_000u64;
31 loop {
32 let t = Instant::now();
33 for _ in 0..iters {
34 f();
35 }
36 if t.elapsed().as_millis() >= 50 {
37 break;
38 }
39 iters *= 4;
40 }
41 (0..7)
42 .map(|_| {
43 let t = Instant::now();
44 for _ in 0..iters {
45 f();
46 }
47 t.elapsed().as_nanos() as f64 / iters as f64
48 })
49 .fold(f64::INFINITY, f64::min)
50}
51
52fn simoom(req: &[u8]) -> usize {
53 let mut hs = [EMPTY_HEADER; 32];
54 match parse_request(black_box(req), &mut hs, &Limits::default()) {
55 Ok(Status::Complete(r)) => r.head_len + r.headers.len(),
56 other => panic!("{other:?}"),
57 }
58}
59
60fn main() {
61 let twenty = twenty_headers();
62 let cases: [(&str, &[u8]); 3] = [("curl (3 fields)", CURL), ("browser (15 fields)", BROWSER), ("20 fields", &twenty)];
63 let best = level();
64 println!("scan level detected: {best:?}");
65 println!("{:<22} {:>6} {:>12} {:>12} {:>14} {:>9}", "request", "bytes", "simd ns", "scalar ns", "cameleer ns", "GB/s simd");
66 for (name, req) in cases {
67 assert!(cameleer::parse(req).is_ok());
68 force(best);
69 let simd = measure(|| {
70 black_box(simoom(req));
71 });
72 force(Level::Scalar);
73 let scalar = measure(|| {
74 black_box(simoom(req));
75 });
76 force(best);
77 let cam = measure(|| {
78 black_box(cameleer::parse(black_box(req)).ok());
79 });
80 println!(
81 "{name:<22} {:>6} {simd:>12.1} {scalar:>12.1} {cam:>14.1} {:>9.2}",
82 req.len(),
83 req.len() as f64 / simd
84 );
85 }
86 let date = DateCache::new(1_700_000_000);
88 let mut out = Vec::with_capacity(256);
89 let head = measure(|| {
90 out.clear();
91 let mut w = HeadWriter::response(&mut out, 200);
92 w.header(b"Content-Type", b"application/json").unwrap().date(&date).content_length(1234);
93 w.finish();
94 black_box(&out);
95 });
96 let mut fixed = StaticHead::new(200, &[(b"Content-Type", b"text/plain"), (b"Server", b"simoom")], b"Hello, World!").unwrap();
97 let mut secs = 1_700_000_000;
98 let mut dc = DateCache::new(secs);
99 let stat = measure(|| {
100 black_box(fixed.full(&dc).len());
101 });
102 let stat_tick = measure(|| {
103 secs += 1;
104 dc.update(secs);
105 black_box(fixed.full(&dc).len());
106 });
107 println!("HeadWriter (status, 1 field, Date, Content-Length): {head:.1} ns");
108 println!("StaticHead::full (fixed reply, same second): {stat:.1} ns; with a new second every call: {stat_tick:.1} ns");
109}
110
111mod cameleer {
116 #![allow(dead_code)]
118
119 use std::io::BufRead;
120
121 #[derive(Debug)]
122 pub enum HttpError {
123 BadRequest(String),
124 HeaderTooLarge,
125 UnexpectedEof,
126 NotImplemented(String),
127 PayloadTooLarge,
128 Io,
129 }
130
131 #[derive(Default)]
132 pub struct HttpHeaders {
133 entries: Vec<(String, String)>,
134 }
135
136 impl HttpHeaders {
137 fn get_all(&self, name: &str) -> Vec<&str> {
138 self.entries.iter().filter(|(k, _)| k.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str()).collect()
139 }
140 fn contains(&self, name: &str) -> bool {
141 self.entries.iter().any(|(k, _)| k.eq_ignore_ascii_case(name))
142 }
143 }
144
145 pub struct HttpRequest {
146 pub method: String,
147 pub target: String,
148 pub headers: HttpHeaders,
149 }
150
151 #[derive(Debug)]
152 pub enum BodyKind {
153 None,
154 Length(usize),
155 Chunked,
156 }
157
158 fn read_line<R: BufRead>(r: &mut R, budget: &mut usize) -> Result<Option<String>, HttpError> {
159 let mut buf = Vec::new();
160 loop {
161 let available = r.fill_buf().map_err(|_| HttpError::Io)?;
162 if available.is_empty() {
163 if buf.is_empty() {
164 return Ok(None);
165 }
166 return Err(HttpError::UnexpectedEof);
167 }
168 let (consume, done) = match available.iter().position(|&b| b == b'\n') {
169 Some(i) => (i + 1, true),
170 None => (available.len(), false),
171 };
172 if consume > *budget {
173 return Err(HttpError::HeaderTooLarge);
174 }
175 *budget -= consume;
176 buf.extend_from_slice(&available[..consume]);
177 r.consume(consume);
178 if done {
179 break;
180 }
181 }
182 buf.pop();
183 if buf.last() == Some(&b'\r') {
184 buf.pop();
185 }
186 String::from_utf8(buf).map(Some).map_err(|_| HttpError::BadRequest("invalid UTF-8 in header".into()))
187 }
188
189 fn is_token(s: &str) -> bool {
190 !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
191 }
192
193 fn read_head<R: BufRead>(r: &mut R) -> Result<Option<(String, HttpHeaders)>, HttpError> {
194 let mut budget = 16 * 1024;
195 let start = loop {
196 match read_line(r, &mut budget)? {
197 None => return Ok(None),
198 Some(l) if l.is_empty() => continue,
199 Some(l) => break l,
200 }
201 };
202 let mut headers = HttpHeaders::default();
203 loop {
204 let line = read_line(r, &mut budget)?.ok_or(HttpError::UnexpectedEof)?;
205 if line.is_empty() {
206 break;
207 }
208 if line.starts_with(' ') || line.starts_with('\t') {
209 return Err(HttpError::BadRequest("obsolete header line folding".into()));
210 }
211 let colon = line.find(':').ok_or_else(|| HttpError::BadRequest(format!("malformed header: {}", line)))?;
212 let name = &line[..colon];
213 if !is_token(name) {
214 return Err(HttpError::BadRequest(format!("invalid header name: {}", name)));
215 }
216 if headers.entries.len() >= 100 {
217 return Err(HttpError::HeaderTooLarge);
218 }
219 headers.entries.push((name.to_string(), line[colon + 1..].trim().to_string()));
220 }
221 Ok(Some((start, headers)))
222 }
223
224 fn body_kind(headers: &HttpHeaders) -> Result<BodyKind, HttpError> {
225 if let Some(te) = headers.get_all("Transfer-Encoding").last().copied() {
226 let last = te.split(',').next_back().map(|s| s.trim().to_ascii_lowercase()).unwrap_or_default();
227 if last == "chunked" {
228 return Ok(BodyKind::Chunked);
229 }
230 return Err(HttpError::NotImplemented(format!("Transfer-Encoding: {}", te)));
231 }
232 let lengths = headers.get_all("Content-Length");
233 if !lengths.is_empty() {
234 let mut value: Option<usize> = None;
235 for l in lengths {
236 for part in l.split(',') {
237 let part = part.trim();
238 if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
239 return Err(HttpError::BadRequest(format!("invalid Content-Length: {}", l)));
240 }
241 let n: usize = part.parse().map_err(|_| HttpError::PayloadTooLarge)?;
242 if value.is_some_and(|v| v != n) {
243 return Err(HttpError::BadRequest("conflicting Content-Length values".into()));
244 }
245 value = Some(n);
246 }
247 }
248 return Ok(BodyKind::Length(value.unwrap_or(0)));
249 }
250 Ok(BodyKind::None)
251 }
252
253 pub fn parse(mut input: &[u8]) -> Result<(HttpRequest, BodyKind), HttpError> {
254 let (start, headers) = read_head(&mut input)?.ok_or(HttpError::UnexpectedEof)?;
255 let mut parts = start.split(' ').filter(|s| !s.is_empty());
256 let (method, target, version) = match (parts.next(), parts.next(), parts.next(), parts.next()) {
257 (Some(m), Some(t), Some(v), None) => (m, t, v),
258 _ => return Err(HttpError::BadRequest(format!("malformed request line: {}", start))),
259 };
260 if !is_token(method) {
261 return Err(HttpError::BadRequest(format!("invalid method: {}", method)));
262 }
263 if version != "HTTP/1.1" && version != "HTTP/1.0" {
264 return Err(HttpError::BadRequest(format!("unsupported version: {}", version)));
265 }
266 if version == "HTTP/1.1" && !headers.contains("Host") {
267 return Err(HttpError::BadRequest("missing Host header".into()));
268 }
269 let target = if let Some(rest) = target.strip_prefix("http://").or_else(|| target.strip_prefix("https://")) {
270 match rest.find('/') {
271 Some(i) => rest[i..].to_string(),
272 None => "/".to_string(),
273 }
274 } else {
275 target.to_string()
276 };
277 if !target.starts_with('/') && target != "*" {
278 return Err(HttpError::BadRequest(format!("invalid request target: {}", target)));
279 }
280 let kind = body_kind(&headers)?;
281 Ok((HttpRequest { method: method.to_string(), target, headers }, kind))
282 }
283}