atmos/kernel/
http_body.rs1pub fn body_start(raw: &[u8]) -> Option<usize> {
16 raw.windows(4).position(|w| w == b"\r\n\r\n").map(|i| i + 4)
17}
18
19pub fn content_length(raw: &[u8]) -> Option<usize> {
21 let end = body_start(raw)?;
22 const KEY: &[u8] = b"content-length:";
25 let head = &raw[..end];
26 let mut i = 0usize;
27 while i + KEY.len() <= head.len() {
28 let hit = head[i..i + KEY.len()]
29 .iter()
30 .zip(KEY.iter())
31 .all(|(a, b)| a.to_ascii_lowercase() == *b);
32 if hit {
33 let mut j = i + KEY.len();
35 let mut v: usize = 0;
36 let mut seen = false;
37 while j < head.len() && head[j] != 13 {
38 match head[j] {
39 d @ 48..=57 => {
40 v = v.checked_mul(10)?.checked_add((d - 48) as usize)?;
41 seen = true;
42 }
43 32 | 9 if !seen => {}
44 _ => break,
45 }
46 j += 1;
47 }
48 return if seen { Some(v) } else { None };
49 }
50 i += 1;
51 }
52 None
53}
54
55pub fn body_range(raw: &[u8]) -> Option<(usize, usize)> {
60 let start = body_start(raw)?;
61 let len = content_length(raw)?;
62 let end = start.checked_add(len)?;
63 if raw.len() >= end {
64 Some((start, end))
65 } else {
66 None
67 }
68}
69
70pub fn looks_truncated(raw: &[u8]) -> bool {
79 let Some(start) = body_start(raw) else {
80 return false;
81 };
82 let Some(len) = content_length(raw) else {
83 return false;
84 };
85 raw.len().saturating_sub(start) < len
86}
87
88pub fn is_framed_complete(raw: &[u8]) -> bool {
104 let Some(_) = body_start(raw) else {
105 return false;
106 };
107 let status = raw
108 .get(9..12)
109 .and_then(|d| core::str::from_utf8(d).ok())
110 .and_then(|d| d.parse::<u16>().ok())
111 .unwrap_or(200);
112 let no_body = (100..200).contains(&status) || status == 204 || status == 304;
113 let chunked = header_value(raw, b"transfer-encoding:")
114 .is_some_and(|v| v.to_ascii_lowercase().windows(7).any(|w| w == b"chunked"));
115 if !no_body && !chunked && content_length(raw).is_none() {
116 return false;
117 }
118 is_complete_response(raw)
119}
120
121pub fn is_complete_response(raw: &[u8]) -> bool {
122 let Some(start) = body_start(raw) else {
123 return false;
124 };
125 let status = raw
126 .get(9..12)
127 .and_then(|d| core::str::from_utf8(d).ok())
128 .and_then(|d| d.parse::<u16>().ok())
129 .unwrap_or(200);
130 if (100..200).contains(&status) || status == 204 || status == 304 {
131 return true;
132 }
133 let chunked = header_value(raw, b"transfer-encoding:")
134 .is_some_and(|v| v.to_ascii_lowercase().windows(7).any(|w| w == b"chunked"));
135 if chunked {
136 return chunked_terminated(raw.get(start..).unwrap_or(&[]));
137 }
138 match content_length(raw) {
139 Some(len) => raw.len().saturating_sub(start) >= len,
140 None => true,
141 }
142}
143
144fn chunked_terminated(body: &[u8]) -> bool {
146 let mut i = 0usize;
147 loop {
148 let Some(rest) = body.get(i..) else {
149 return false;
150 };
151 let Some(eol) = rest.windows(2).position(|w| w == b"\r\n") else {
152 return false;
153 };
154 let line = rest.get(..eol).unwrap_or(&[]);
155 let size_part = line.split(|&b| b == b';').next().unwrap_or(&[]);
156 let Some(size) = core::str::from_utf8(size_part)
157 .ok()
158 .and_then(|t| usize::from_str_radix(t.trim(), 16).ok())
159 else {
160 return false;
161 };
162 if size == 0 {
163 return true;
164 }
165 let next = i + eol + 2 + size + 2;
167 if next > body.len() {
168 return false;
169 }
170 i = next;
171 }
172}
173
174fn header_value<'a>(raw: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
176 let end = body_start(raw)?;
177 let head = raw.get(..end)?;
178 for line in head.split(|&b| b == b'\n') {
179 let line = line.strip_suffix(b"\r").unwrap_or(line);
180 if line.len() > name.len()
181 && line
182 .iter()
183 .zip(name.iter())
184 .all(|(a, b)| a.to_ascii_lowercase() == *b)
185 {
186 let v = line.get(name.len()..)?;
187 let start = v.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(v.len());
188 return v.get(start..);
189 }
190 }
191 None
192}
193
194pub fn resume_offset(raw: &[u8]) -> Option<usize> {
202 if !looks_truncated(raw) {
203 return None;
204 }
205 let ar = header_value(raw, b"accept-ranges:")?;
206 if !ar.eq_ignore_ascii_case(b"bytes") {
207 return None;
208 }
209 let got = raw.len().checked_sub(body_start(raw)?)?;
210 (got > 0).then_some(got)
211}
212
213pub fn merge_range_response(partial: &[u8], off: usize, resp: &[u8]) -> Option<alloc::vec::Vec<u8>> {
220 let status_ok = resp.starts_with(b"HTTP/1.1 206") || resp.starts_with(b"HTTP/1.0 206");
221 if !status_ok {
222 return None;
223 }
224 let total = content_length(partial)?;
225 let cr = header_value(resp, b"content-range:")?;
226 let cr = cr.strip_prefix(b"bytes ")?;
227 let dash = cr.iter().position(|&b| b == b'-')?;
228 let slash = cr.iter().position(|&b| b == b'/')?;
229 let parse = |s: &[u8]| -> Option<usize> { core::str::from_utf8(s).ok()?.trim().parse().ok() };
230 let start = parse(cr.get(..dash)?)?;
231 let whole = parse(cr.get(slash + 1..)?)?;
232 if start != off || whole != total {
233 return None;
234 }
235 let body = resp.get(body_start(resp)?..)?;
236 let pstart = body_start(partial)?;
237 if partial.len().checked_sub(pstart)? != off {
238 return None;
239 }
240 let mut out = alloc::vec::Vec::with_capacity(partial.len() + body.len());
241 out.extend_from_slice(partial);
242 out.extend_from_slice(body);
243 Some(out)
244}