1use 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_visits(1);
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 #[inline]
213 pub(crate) fn bare_step(&self, m: usize, c: u8) -> usize {
214 if m > 0 && self.delimiter.get(m.wrapping_add(1)) == Some(&c) {
215 m.wrapping_add(1)
216 } else {
217 usize::from(crate::scan::is_break(c))
218 }
219 }
220
221 pub(crate) fn run(&self, mut k: usize, mut m: usize, hay: &[u8]) -> (usize, usize, Hit) {
240 let len = self.len();
241 let bare = len.wrapping_sub(1);
242 let delimiter = self.delimiter();
243 let dashes = delimiter.get(2..).unwrap_or(&[]);
244 let mut i = 0usize;
245 if k == 1 {
246 let Some(&c) = hay.first() else {
249 return (k, m, Hit::None);
250 };
251 k = self.step(k, c);
252 m = self.bare_step(m, c);
253 i = 1;
254 if k == 1 {
255 (i, k, m) = (0, 0, 0);
257 }
258 }
259 if m > 0 {
260 let need = delimiter.get(m.wrapping_add(1)..).unwrap_or(&[]);
263 let rest = hay.get(i..).unwrap_or(&[]);
264 let same = shared(need, rest);
265 if same == need.len() {
266 let end = i.wrapping_add(same);
267 return if k > 0 {
268 (len, 0, Hit::Delimiter(end))
269 } else {
270 (0, bare, Hit::BareBreak(end))
271 };
272 }
273 if same == rest.len() {
274 let k = if k > 0 { k.wrapping_add(same) } else { 0 };
276 return (k, m.wrapping_add(same), Hit::None);
277 }
278 i = i.wrapping_add(same);
279 }
280 while i < hay.len() {
281 let at = crate::scan::find_break_dash(hay, i);
282 if at >= hay.len() {
283 break;
284 }
285 let start = at.wrapping_add(1);
286 let after_cr = hay.get(at) == Some(&b'\n')
287 && at > 0
288 && hay.get(at.wrapping_sub(1)) == Some(&b'\r');
289 let rest = hay.get(start..).unwrap_or(&[]);
290 let same = shared(dashes, rest);
291 if same == dashes.len() {
292 let end = start.wrapping_add(same);
293 return if after_cr {
294 (len, 0, Hit::Delimiter(end))
295 } else {
296 (0, bare, Hit::BareBreak(end))
297 };
298 }
299 if same == rest.len() {
300 let k = if after_cr {
303 same.wrapping_add(2)
304 } else {
305 usize::from(same == 0 && hay.get(at) == Some(&b'\r'))
306 };
307 return (k, same.wrapping_add(1), Hit::None);
308 }
309 i = start.wrapping_add(same);
310 }
311 (0, 0, Hit::None)
312 }
313}
314
315#[inline]
317pub(crate) fn shared(a: &[u8], b: &[u8]) -> usize {
318 let n = crate::scan::common_prefix(a, b);
319 #[cfg(test)]
320 tests::count_visits(n.wrapping_add(1));
321 n
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub(crate) enum Hit {
327 None,
329 Delimiter(usize),
331 BareBreak(usize),
334}
335
336fn failure_table(d: &[u8; MAX_DELIMITER_LEN], len: usize) -> [u8; MAX_DELIMITER_LEN] {
339 let mut fail = [0u8; MAX_DELIMITER_LEN];
340 let mut k = 0usize;
341 for i in 1..len {
342 let c = *d.get(i).unwrap_or(&0);
343 while k > 0 && d.get(k) != Some(&c) {
344 k = usize::from(*fail.get(k.wrapping_sub(1)).unwrap_or(&0));
345 }
346 if d.get(k) == Some(&c) {
347 k = k.wrapping_add(1);
348 }
349 if let Some(slot) = fail.get_mut(i) {
350 *slot = k as u8;
351 }
352 }
353 fail
354}
355
356impl fmt::Debug for Boundary {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 let text = core::str::from_utf8(self.as_bytes()).unwrap_or("");
360 f.debug_tuple("Boundary").field(&text).finish()
361 }
362}
363
364#[cfg(test)]
365pub(crate) mod tests {
366 use super::*;
367 use alloc::vec::Vec;
368 use core::cell::Cell;
369
370 std::thread_local! {
371 static VISITS: Cell<u64> = const { Cell::new(0) };
372 }
373
374 pub(crate) fn count_visits(n: usize) {
378 VISITS.with(|v| v.set(v.get() + n as u64));
379 }
380
381 #[cfg(feature = "alloc")]
382 pub(crate) fn visits() -> u64 {
383 VISITS.with(Cell::get)
384 }
385
386 #[cfg(feature = "alloc")]
389 fn visits_for(body: &[u8], chunk: usize) -> u64 {
390 use crate::{Event, Limits, MultipartParser, Policy};
391 let limits = Limits::DEFAULT
392 .with_max_body_bytes(64 << 20)
393 .with_max_file_bytes(64 << 20);
394 let mut p = MultipartParser::new(
395 Boundary::new(b"boundary").unwrap(),
396 &limits,
397 &Policy::DEFAULT,
398 )
399 .unwrap();
400 let before = visits();
401 let chunks: Vec<&[u8]> = body.chunks(chunk).collect();
402 for (i, c) in chunks.iter().enumerate() {
403 let mut rest: &[u8] = c;
404 loop {
405 let step = p.parse(rest, i + 1 == chunks.len()).unwrap();
406 rest = &rest[step.consumed..];
407 if matches!(step.event, Event::NeedInput | Event::End) {
408 break;
409 }
410 }
411 }
412 visits() - before
413 }
414
415 #[cfg(feature = "alloc")]
416 #[test]
417 fn test_matcher_visits_are_linear_for_adversarial_bodies() {
418 let head = b"--boundary\r\nContent-Disposition: form-data; name=f; filename=a\r\n\r\n";
419 let shapes: [&[u8]; 4] = [b"\r", b"\r\n--boundar", b"\r\n-", b"-"];
420 for shape in shapes {
421 for chunk in [1usize, 7, 4096] {
422 let mut body = head.to_vec();
423 while body.len() < head.len() + 64 * 1024 {
424 body.extend_from_slice(shape);
425 }
426 let data = (body.len() - head.len()) as u64;
427 body.extend_from_slice(b"\r\n--boundary--");
428 let v = visits_for(&body, chunk);
429 let bound = 2 * data + (body.len() / chunk + 1) as u64 * 16 + 64;
433 assert!(
434 v <= bound,
435 "{shape:?} chunk {chunk}: {v} visits for {data} bytes"
436 );
437 }
438 }
439 }
440
441 #[test]
442 fn test_boundary_every_byte_value() {
443 for b in 0u8..=255 {
444 let r = Boundary::new(&[b'a', b, b'z']);
445 assert_eq!(r.is_ok(), is_bchar(b), "byte {b:#x}");
446 }
447 }
448
449 #[test]
450 fn test_boundary_lengths_and_trailing_space() {
451 assert!(Boundary::new(b"").is_err());
452 assert!(Boundary::new(b"a").is_ok());
453 assert!(Boundary::new(&[b'a'; 69]).is_ok());
454 assert!(Boundary::new(&[b'a'; 70]).is_ok());
455 assert!(Boundary::new(&[b'a'; 71]).is_err());
456 assert!(Boundary::new(b"a ").is_err());
457 assert!(Boundary::new(b"a b").is_ok());
458 assert!(Boundary::new(b" ").is_err());
459 }
460
461 #[test]
462 fn test_failure_table_matches_definition() {
463 for raw in [&b"a"[..], b"aaaa", b"-", b"--", b"-a-", b"---", b"a-b--a"] {
464 let b = Boundary::new(raw).unwrap();
465 let d = b.delimiter();
466 for i in 0..d.len() {
467 let best = (0..=i)
468 .rev()
469 .find(|&l| l <= i && d[..l] == d[i + 1 - l..=i])
470 .unwrap();
471 assert_eq!(usize::from(b.fail[i]), best, "{raw:?} at {i}");
472 }
473 }
474 }
475
476 fn naive_hit(b: &Boundary, hay: &[u8]) -> Hit {
479 let d = b.delimiter();
480 let dashes = &d[2..];
481 for end in 0..=hay.len() {
482 if end >= d.len() && &hay[end - d.len()..end] == d {
483 return Hit::Delimiter(end);
484 }
485 if end > dashes.len() && &hay[end - dashes.len()..end] == dashes {
486 let p = end - dashes.len() - 1;
487 if hay[p] == b'\r' || hay[p] == b'\n' {
488 return Hit::BareBreak(end);
489 }
490 }
491 }
492 Hit::None
493 }
494
495 fn chunked_hit(b: &Boundary, chunks: &[&[u8]]) -> Hit {
498 let (mut k, mut m, mut base) = (0, 0, 0);
499 for chunk in chunks {
500 let (k2, m2, hit) = b.run(k, m, chunk);
501 match hit {
502 Hit::Delimiter(e) => return Hit::Delimiter(base + e),
503 Hit::BareBreak(e) => return Hit::BareBreak(base + e),
504 Hit::None => (k, m, base) = (k2, m2, base + chunk.len()),
505 }
506 }
507 Hit::None
508 }
509
510 #[test]
511 fn test_run_finds_like_naive_and_carries_state() {
512 let b = Boundary::new(b"-ab").unwrap();
513 let d = b.delimiter().to_vec();
514 let mut hay = Vec::new();
515 hay.extend_from_slice(b"x\r\n--a\r\n\r\n--\r\n--");
516 hay.extend_from_slice(&d);
517 hay.extend_from_slice(b"tail");
518 let expected = naive_hit(&b, &hay);
519 assert!(matches!(expected, Hit::Delimiter(_)));
520 assert_eq!(chunked_hit(&b, &[&hay]), expected);
521 for split in 0..hay.len() {
522 let (x, y) = hay.split_at(split);
523 assert_eq!(chunked_hit(&b, &[x, y]), expected, "split {split}");
524 }
525 }
526
527 #[test]
531 fn test_bare_breaks_and_delimiters_match_naive_at_every_split() {
532 let mut seed = 11u64;
533 for boundary in [&b"-ab"[..], b"--", b"x", b"ab-"] {
534 let b = Boundary::new(boundary).unwrap();
535 for len in 0..40 {
536 for _ in 0..60 {
537 let hay: Vec<u8> = (0..len)
538 .map(|_| {
539 seed ^= seed << 13;
540 seed ^= seed >> 7;
541 seed ^= seed << 17;
542 b"\r\n--abx"[(seed % 7) as usize]
543 })
544 .collect();
545 let expected = naive_hit(&b, &hay);
546 assert_eq!(chunked_hit(&b, &[&hay]), expected, "{hay:?}");
547 for split in 0..=hay.len() {
548 let (x, y) = hay.split_at(split);
549 assert_eq!(chunked_hit(&b, &[x, y]), expected, "{hay:?} at {split}");
550 }
551 let bytes: Vec<&[u8]> = hay.chunks(1).collect();
552 assert_eq!(chunked_hit(&b, &bytes), expected, "{hay:?} bytewise");
553 }
554 }
555 }
556 }
557
558 #[test]
561 fn test_run_leaves_the_automata_states_at_every_split() {
562 let mut seed = 5u64;
563 for boundary in [&b"-ab"[..], b"--", b"x", b"ab-", b"abcdefghijklmnop"] {
564 let b = Boundary::new(boundary).unwrap();
565 let bare = b.len() - 1;
566 let hay_bytes: &[u8] = &[b'\r', b'\n', b'-', b'-', boundary[0], b'x'];
567 for len in 0..48 {
568 for _ in 0..40 {
569 let mut hay: Vec<u8> = (0..len)
570 .map(|_| {
571 seed ^= seed << 13;
572 seed ^= seed >> 7;
573 seed ^= seed << 17;
574 hay_bytes[(seed % 6) as usize]
575 })
576 .collect();
577 let d = b.delimiter();
579 hay.extend_from_slice(&d[..len % d.len()]);
580 if !matches!(naive_hit(&b, &hay), Hit::None) {
581 continue;
582 }
583 let (mut k, mut m) = (0, 0);
584 for &c in &hay {
585 k = b.step(k, c);
586 m = b.bare_step(m, c);
587 assert!(k < b.len() && m < bare);
588 }
589 assert_eq!(&b.run(0, 0, &hay), &(k, m, Hit::None), "{hay:?}");
590 for split in 0..=hay.len() {
591 let (x, y) = hay.split_at(split);
592 let (k1, m1, _) = b.run(0, 0, x);
593 assert_eq!(&b.run(k1, m1, y), &(k, m, Hit::None), "{hay:?} at {split}");
594 }
595 }
596 }
597 }
598 }
599
600 #[test]
601 fn test_parse_reports_unsupported_for_urlencoded() {
602 assert_eq!(
603 Boundary::parse(b"application/x-www-form-urlencoded"),
604 Err(FormError::UnsupportedMediaType)
605 );
606 }
607}