form_net/multipart/
boundary.rs1use core::fmt;
5
6use crate::error::{FormError, Location, SyntaxError};
7
8pub const MAX_BOUNDARY_LEN: usize = 70;
10
11pub const MAX_DELIMITER_LEN: usize = 74;
13
14const fn is_bchar(b: u8) -> bool {
16 b.is_ascii_alphanumeric()
17 || matches!(
18 b,
19 b'\''
20 | b'('
21 | b')'
22 | b'+'
23 | b'_'
24 | b','
25 | b'-'
26 | b'.'
27 | b'/'
28 | b':'
29 | b'='
30 | b'?'
31 | b' '
32 )
33}
34
35#[must_use]
55#[derive(Clone, Copy, PartialEq, Eq)]
56pub struct Boundary {
57 delimiter: [u8; MAX_DELIMITER_LEN],
58 fail: [u8; MAX_DELIMITER_LEN],
59 len: u8,
60}
61
62impl Boundary {
63 pub fn new(boundary: &[u8]) -> Result<Boundary, FormError> {
77 let invalid =
78 |at: usize| FormError::syntax(SyntaxError::InvalidBoundary, Location::at(at as u64));
79 if boundary.is_empty() {
80 return Err(invalid(0));
81 }
82 if boundary.len() > MAX_BOUNDARY_LEN {
83 return Err(invalid(MAX_BOUNDARY_LEN));
84 }
85 if let Some(at) = boundary.iter().position(|&b| !is_bchar(b)) {
86 return Err(invalid(at));
87 }
88 if boundary.last() == Some(&b' ') {
89 return Err(invalid(boundary.len().wrapping_sub(1)));
90 }
91 let mut delimiter = [0u8; MAX_DELIMITER_LEN];
92 let len = boundary.len().wrapping_add(4);
93 if let Some(head) = delimiter.get_mut(..4) {
94 head.copy_from_slice(b"\r\n--");
95 }
96 if let Some(rest) = delimiter.get_mut(4..len) {
97 rest.copy_from_slice(boundary);
98 }
99 let fail = failure_table(&delimiter, len);
100 Ok(Boundary {
101 delimiter,
102 fail,
103 len: len as u8,
104 })
105 }
106
107 #[cfg(feature = "random")]
124 #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
125 pub fn generate() -> Result<Boundary, FormError> {
126 let hex = crate::random::random_hex()?;
127 let mut name = [0u8; 41];
128 if let Some(head) = name.get_mut(..9) {
129 head.copy_from_slice(b"form-net-");
130 }
131 if let Some(tail) = name.get_mut(9..) {
132 tail.copy_from_slice(&hex);
133 }
134 Boundary::new(&name)
135 }
136
137 pub fn parse(content_type: &[u8]) -> Result<Boundary, FormError> {
154 match crate::kind::FormKind::detect(content_type)? {
155 crate::kind::FormKind::Multipart(b) => Ok(b),
156 crate::kind::FormKind::Urlencoded => Err(FormError::UnsupportedMediaType),
157 }
158 }
159
160 #[must_use]
169 pub fn as_bytes(&self) -> &[u8] {
170 self.delimiter.get(4..usize::from(self.len)).unwrap_or(&[])
171 }
172
173 #[must_use]
182 pub fn delimiter(&self) -> &[u8] {
183 self.delimiter.get(..usize::from(self.len)).unwrap_or(&[])
184 }
185
186 #[inline]
189 pub(crate) fn step(&self, mut k: usize, c: u8) -> usize {
190 #[cfg(test)]
191 tests::count_visit();
192 loop {
193 if self.delimiter.get(k) == Some(&c) {
194 return k.wrapping_add(1);
195 }
196 if k == 0 {
197 return 0;
198 }
199 k = usize::from(*self.fail.get(k.wrapping_sub(1)).unwrap_or(&0));
200 }
201 }
202
203 pub(crate) fn len(&self) -> usize {
204 usize::from(self.len)
205 }
206
207 pub(crate) fn run(&self, mut k: usize, hay: &[u8]) -> (usize, Option<usize>) {
213 let len = self.len();
214 let mut i = 0usize;
215 while i < hay.len() {
216 if k == 0 {
217 i = crate::scan::find_byte(hay, i, b'\r');
218 if i >= hay.len() {
219 break;
220 }
221 }
222 let c = *hay.get(i).unwrap_or(&0);
223 k = self.step(k, c);
224 i = i.wrapping_add(1);
225 if k == len {
226 return (k, Some(i));
227 }
228 }
229 (k, None)
230 }
231}
232
233fn failure_table(d: &[u8; MAX_DELIMITER_LEN], len: usize) -> [u8; MAX_DELIMITER_LEN] {
236 let mut fail = [0u8; MAX_DELIMITER_LEN];
237 let mut k = 0usize;
238 for i in 1..len {
239 let c = *d.get(i).unwrap_or(&0);
240 while k > 0 && d.get(k) != Some(&c) {
241 k = usize::from(*fail.get(k.wrapping_sub(1)).unwrap_or(&0));
242 }
243 if d.get(k) == Some(&c) {
244 k = k.wrapping_add(1);
245 }
246 if let Some(slot) = fail.get_mut(i) {
247 *slot = k as u8;
248 }
249 }
250 fail
251}
252
253impl fmt::Debug for Boundary {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 let text = core::str::from_utf8(self.as_bytes()).unwrap_or("");
257 f.debug_tuple("Boundary").field(&text).finish()
258 }
259}
260
261#[cfg(test)]
262pub(crate) mod tests {
263 use super::*;
264 use alloc::vec::Vec;
265 use core::cell::Cell;
266
267 std::thread_local! {
268 static VISITS: Cell<u64> = const { Cell::new(0) };
269 }
270
271 pub(crate) fn count_visit() {
275 VISITS.with(|v| v.set(v.get() + 1));
276 }
277
278 #[cfg(feature = "alloc")]
279 pub(crate) fn visits() -> u64 {
280 VISITS.with(Cell::get)
281 }
282
283 #[cfg(feature = "alloc")]
286 fn visits_for(body: &[u8], chunk: usize) -> u64 {
287 use crate::{Event, Limits, MultipartParser, Policy};
288 let limits = Limits::DEFAULT
289 .with_max_body_bytes(64 << 20)
290 .with_max_file_bytes(64 << 20);
291 let mut p = MultipartParser::new(
292 Boundary::new(b"boundary").unwrap(),
293 &limits,
294 &Policy::DEFAULT,
295 )
296 .unwrap();
297 let before = visits();
298 let chunks: Vec<&[u8]> = body.chunks(chunk).collect();
299 for (i, c) in chunks.iter().enumerate() {
300 let mut rest: &[u8] = c;
301 loop {
302 let step = p.parse(rest, i + 1 == chunks.len()).unwrap();
303 rest = &rest[step.consumed..];
304 if matches!(step.event, Event::NeedInput | Event::End) {
305 break;
306 }
307 }
308 }
309 visits() - before
310 }
311
312 #[cfg(feature = "alloc")]
313 #[test]
314 fn test_matcher_visits_are_linear_for_adversarial_bodies() {
315 let head = b"--boundary\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n";
316 let shapes: [&[u8]; 4] = [b"\r", b"\r\n--boundar", b"\r\n-", b"-"];
317 for shape in shapes {
318 for chunk in [1usize, 7, 4096] {
319 let mut body = head.to_vec();
320 while body.len() < head.len() + 64 * 1024 {
321 body.extend_from_slice(shape);
322 }
323 let data = (body.len() - head.len()) as u64;
324 body.extend_from_slice(b"\r\n--boundary--");
325 let v = visits_for(&body, chunk);
326 let bound = 2 * data + (body.len() / chunk + 1) as u64 * 16 + 64;
330 assert!(
331 v <= bound,
332 "{shape:?} chunk {chunk}: {v} visits for {data} bytes"
333 );
334 }
335 }
336 }
337
338 fn naive_find(hay: &[u8], needle: &[u8]) -> Option<usize> {
339 hay.windows(needle.len())
340 .position(|w| w == needle)
341 .map(|p| p + needle.len())
342 }
343
344 #[test]
345 fn test_boundary_every_byte_value() {
346 for b in 0u8..=255 {
347 let r = Boundary::new(&[b'a', b, b'z']);
348 assert_eq!(r.is_ok(), is_bchar(b), "byte {b:#x}");
349 }
350 }
351
352 #[test]
353 fn test_boundary_lengths_and_trailing_space() {
354 assert!(Boundary::new(b"").is_err());
355 assert!(Boundary::new(b"a").is_ok());
356 assert!(Boundary::new(&[b'a'; 69]).is_ok());
357 assert!(Boundary::new(&[b'a'; 70]).is_ok());
358 assert!(Boundary::new(&[b'a'; 71]).is_err());
359 assert!(Boundary::new(b"a ").is_err());
360 assert!(Boundary::new(b"a b").is_ok());
361 assert!(Boundary::new(b" ").is_err());
362 }
363
364 #[test]
365 fn test_failure_table_matches_definition() {
366 for raw in [&b"a"[..], b"aaaa", b"-", b"--", b"-a-", b"---", b"a-b--a"] {
367 let b = Boundary::new(raw).unwrap();
368 let d = b.delimiter();
369 for i in 0..d.len() {
370 let best = (0..=i)
371 .rev()
372 .find(|&l| l <= i && d[..l] == d[i + 1 - l..=i])
373 .unwrap();
374 assert_eq!(usize::from(b.fail[i]), best, "{raw:?} at {i}");
375 }
376 }
377 }
378
379 #[test]
380 fn test_run_finds_like_naive_and_carries_state() {
381 let b = Boundary::new(b"-ab").unwrap();
382 let d = b.delimiter().to_vec();
383 let mut hay = Vec::new();
384 hay.extend_from_slice(b"x\r\n--a\r\n\r\n--\r\n--");
385 hay.extend_from_slice(&d);
386 hay.extend_from_slice(b"tail");
387 let expected = naive_find(&hay, &d);
388 assert_eq!(b.run(0, &hay).1, expected);
389 for split in 0..hay.len() {
391 let (k, found) = b.run(0, &hay[..split]);
392 let total = match found {
393 Some(i) => Some(i),
394 None => b.run(k, &hay[split..]).1.map(|i| i + split),
395 };
396 assert_eq!(total, expected, "split {split}");
397 }
398 }
399
400 #[test]
401 fn test_parse_reports_unsupported_for_urlencoded() {
402 assert_eq!(
403 Boundary::parse(b"application/x-www-form-urlencoded"),
404 Err(FormError::UnsupportedMediaType)
405 );
406 }
407}