1use crate::error::{FormError, Location, SyntaxError};
5use crate::multipart::boundary::Boundary;
6
7const DISPOSITION: &[u8] = b"Content-Disposition: form-data; name=\"";
8const FILENAME: &[u8] = b"\"; filename=\"";
9const CONTENT_TYPE: &[u8] = b"Content-Type: ";
10
11trait Out {
13 fn put(&mut self, bytes: &[u8]) -> Result<(), FormError>;
14}
15
16struct SliceOut<'o> {
17 out: &'o mut [u8],
18 pos: usize,
19}
20
21impl Out for SliceOut<'_> {
22 fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
23 let end = self.pos.saturating_add(bytes.len());
24 match self.out.get_mut(self.pos..end) {
25 Some(dst) => {
26 dst.copy_from_slice(bytes);
27 self.pos = end;
28 Ok(())
29 }
30 None => Err(FormError::BufferTooSmall { needed: end }),
31 }
32 }
33}
34
35fn syntax(reason: SyntaxError) -> FormError {
36 FormError::syntax(reason, Location::NONE)
37}
38
39fn escaped_len(text: &str, is_name: bool) -> Result<usize, FormError> {
47 let bytes = text.as_bytes();
48 if is_name && bytes.is_empty() {
49 return Err(syntax(SyntaxError::EmptyName));
50 }
51 if crate::multipart::filename::has_html_escape(bytes) {
52 return Err(syntax(SyntaxError::AmbiguousQuote));
53 }
54 if (is_name && bytes.contains(&b'\\')) || (!is_name && bytes.last() == Some(&b'\\')) {
55 return Err(syntax(SyntaxError::AmbiguousQuote));
56 }
57 let mut len = 0usize;
58 for &b in bytes {
59 len = len.saturating_add(match b {
60 b'\n' | b'\r' | b'"' => 3,
61 b'\t' => 1,
62 b if b < 0x20 || b == 0x7F => return Err(syntax(SyntaxError::ControlInHeader)),
63 _ => 1,
64 });
65 }
66 Ok(len)
67}
68
69fn put_escaped(out: &mut impl Out, text: &str) -> Result<(), FormError> {
71 let bytes = text.as_bytes();
72 let mut from = 0usize;
73 for (i, &b) in bytes.iter().enumerate() {
74 let esc: &[u8] = match b {
75 b'\n' => b"%0A",
76 b'\r' => b"%0D",
77 b'"' => b"%22",
78 _ => continue,
79 };
80 out.put(bytes.get(from..i).unwrap_or(&[]))?;
81 out.put(esc)?;
82 from = i.saturating_add(1);
83 }
84 out.put(bytes.get(from..).unwrap_or(&[]))
85}
86
87fn check_content_type(ct: &str) -> Result<(), FormError> {
89 let raw = ct.as_bytes();
90 if raw.iter().any(|&b| (b < 0x20 && b != b'\t') || b == 0x7F) {
91 return Err(syntax(SyntaxError::ControlInHeader));
92 }
93 if raw.windows(2).any(|w| w == b"\\\"") {
94 return Err(syntax(SyntaxError::AmbiguousQuote));
95 }
96 let invalid = || syntax(SyntaxError::InvalidContentType);
97 if crate::kind::has_empty_parameter(raw) {
98 return Err(invalid());
101 }
102 let mt = parse_net::media::parse(raw, &crate::kind::HEADER_LIMITS).map_err(|_| invalid())?;
103 if crate::kind::repeated_name(&mt.params()).is_some()
104 || mt.type_().eq_ignore_ascii_case(b"multipart")
105 {
106 return Err(invalid());
107 }
108 Ok(())
109}
110
111fn head_len(
113 boundary: &Boundary,
114 first: bool,
115 name: &str,
116 filename: Option<&str>,
117 content_type: Option<&str>,
118) -> Result<usize, FormError> {
119 if crate::kind::holds_name_assignment(name.as_bytes())
122 || filename.is_some_and(|f| crate::kind::holds_name_assignment(f.as_bytes()))
123 {
124 return Err(syntax(SyntaxError::MalformedParameter));
125 }
126 let mut len = if first { 0usize } else { 2 };
127 len = len
128 .saturating_add(2)
129 .saturating_add(boundary.as_bytes().len())
130 .saturating_add(2)
131 .saturating_add(DISPOSITION.len())
132 .saturating_add(escaped_len(name, true)?);
133 if let Some(f) = filename {
134 len = len
135 .saturating_add(FILENAME.len())
136 .saturating_add(escaped_len(f, false)?);
137 }
138 len = len.saturating_add(3);
140 if let Some(ct) = content_type {
141 check_content_type(ct)?;
142 len = len
143 .saturating_add(CONTENT_TYPE.len())
144 .saturating_add(ct.len())
145 .saturating_add(2);
146 }
147 Ok(len.saturating_add(2))
148}
149
150fn put_head(
152 out: &mut impl Out,
153 boundary: &Boundary,
154 first: bool,
155 name: &str,
156 filename: Option<&str>,
157 content_type: Option<&str>,
158) -> Result<(), FormError> {
159 if !first {
160 out.put(b"\r\n")?;
161 }
162 out.put(b"--")?;
163 out.put(boundary.as_bytes())?;
164 out.put(b"\r\n")?;
165 out.put(DISPOSITION)?;
166 put_escaped(out, name)?;
167 if let Some(f) = filename {
168 out.put(FILENAME)?;
169 put_escaped(out, f)?;
170 }
171 out.put(b"\"\r\n")?;
172 if let Some(ct) = content_type {
173 out.put(CONTENT_TYPE)?;
174 out.put(ct.as_bytes())?;
175 out.put(b"\r\n")?;
176 }
177 out.put(b"\r\n")
178}
179
180pub fn encode_part_head(
206 boundary: &Boundary,
207 first: bool,
208 name: &str,
209 filename: Option<&str>,
210 content_type: Option<&str>,
211 out: &mut [u8],
212) -> Result<usize, FormError> {
213 let needed = head_len(boundary, first, name, filename, content_type)?;
214 if out.len() < needed {
215 return Err(FormError::BufferTooSmall { needed });
216 }
217 let mut sink = SliceOut { out, pos: 0 };
218 put_head(&mut sink, boundary, first, name, filename, content_type)?;
219 Ok(sink.pos)
220}
221
222#[cfg(feature = "std")]
223pub use writer_std::MultipartWriter;
224
225#[cfg(feature = "std")]
226mod writer_std {
227 use alloc::string::String;
228 use core::fmt;
229 use std::io::{Read, Write};
230
231 use super::{Out, head_len, put_head};
232 use crate::error::{EncodingError, FormError, Location, SyntaxError};
233 use crate::multipart::boundary::Boundary;
234
235 struct IoOut<'w, W: Write>(&'w mut W);
236
237 impl<W: Write> Out for IoOut<'_, W> {
238 fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
239 self.0
240 .write_all(bytes)
241 .map_err(|e| FormError::Io { kind: e.kind() })
242 }
243 }
244
245 pub struct MultipartWriter<W: Write> {
271 out: W,
272 boundary: Boundary,
273 verify: bool,
274 first: bool,
275 in_file: bool,
276 poisoned: bool,
277 k: usize,
279 m: usize,
281 pending_cr: bool,
283 }
284
285 impl<W: Write> fmt::Debug for MultipartWriter<W> {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 f.debug_struct("MultipartWriter")
288 .field("boundary", &self.boundary)
289 .field("in_file", &self.in_file)
290 .finish_non_exhaustive()
291 }
292 }
293
294 impl<W: Write> MultipartWriter<W> {
295 #[cfg(feature = "random")]
301 #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
302 pub fn new(out: W) -> Result<Self, FormError> {
303 let mut w = Self::with_boundary(out, Boundary::generate()?);
304 w.verify = false;
306 Ok(w)
307 }
308
309 pub fn with_boundary(out: W, boundary: Boundary) -> Self {
311 MultipartWriter {
312 out,
313 boundary,
314 verify: true,
315 first: true,
316 in_file: false,
317 poisoned: false,
318 k: 2,
319 m: 1,
320 pending_cr: false,
321 }
322 }
323
324 pub const fn boundary(&self) -> &Boundary {
326 &self.boundary
327 }
328
329 #[must_use]
333 pub fn content_type(&self) -> String {
334 let b = self.boundary.as_bytes();
335 let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
336 let b = core::str::from_utf8(b).unwrap_or("");
337 if token {
338 alloc::format!("multipart/form-data; boundary={b}")
339 } else {
340 alloc::format!("multipart/form-data; boundary=\"{b}\"")
341 }
342 }
343
344 pub fn verify_boundary(&mut self, on: bool) {
347 self.verify = on;
348 }
349
350 pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
361 self.guard(|w| {
362 if name == "_charset_"
363 && !(value.eq_ignore_ascii_case("utf-8")
364 || value.eq_ignore_ascii_case("us-ascii"))
365 {
366 return Err(FormError::encoding(
367 EncodingError::UnsupportedCharset,
368 Location::NONE,
369 ));
370 }
371 w.head(name, None, None)?;
372 w.in_file = false;
373 w.pending_cr = false;
374 let bytes = value.as_bytes();
375 let mut from = 0usize;
376 for (i, &b) in bytes.iter().enumerate() {
377 match b {
378 b'\r' => {
379 w.data(bytes.get(from..i).unwrap_or(&[]))?;
380 w.data(b"\r\n")?;
381 w.pending_cr = true;
382 from = i.saturating_add(1);
383 }
384 b'\n' => {
385 w.data(bytes.get(from..i).unwrap_or(&[]))?;
386 if !w.pending_cr {
387 w.data(b"\r\n")?;
388 }
389 w.pending_cr = false;
390 from = i.saturating_add(1);
391 }
392 _ => w.pending_cr = false,
393 }
394 }
395 w.data(bytes.get(from..).unwrap_or(&[]))
396 })
397 }
398
399 pub fn file_start(
406 &mut self,
407 name: &str,
408 filename: &str,
409 content_type: Option<&str>,
410 ) -> Result<(), FormError> {
411 self.guard(|w| {
412 w.head(name, Some(filename), content_type)?;
413 w.in_file = true;
414 Ok(())
415 })
416 }
417
418 pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
425 if !self.in_file {
426 self.poisoned = true;
427 return Err(FormError::Poisoned);
428 }
429 self.guard(|w| w.data(chunk))
430 }
431
432 pub fn file<R: Read>(
439 &mut self,
440 name: &str,
441 filename: &str,
442 content_type: Option<&str>,
443 mut data: R,
444 ) -> Result<u64, FormError> {
445 self.file_start(name, filename, content_type)?;
446 let mut buf = [0u8; 8192];
447 let mut total = 0u64;
448 let r = loop {
449 let n = match data.read(&mut buf) {
450 Ok(0) => break Ok(total),
451 Ok(n) => n,
452 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
453 Err(e) => {
454 self.poisoned = true;
455 break Err(FormError::Io { kind: e.kind() });
456 }
457 };
458 if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
459 break Err(e);
460 }
461 total = total.saturating_add(n as u64);
462 };
463 crate::wipe::wipe(&mut buf);
464 r
465 }
466
467 pub fn finish(mut self) -> Result<W, FormError> {
473 self.guard(|w| {
474 let mut out = IoOut(&mut w.out);
475 if !w.first {
476 out.put(b"\r\n")?;
477 }
478 out.put(b"--")?;
479 out.put(w.boundary.as_bytes())?;
480 out.put(b"--\r\n")?;
481 w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
482 })?;
483 Ok(self.out)
484 }
485
486 fn guard(
488 &mut self,
489 f: impl FnOnce(&mut Self) -> Result<(), FormError>,
490 ) -> Result<(), FormError> {
491 if self.poisoned {
492 return Err(FormError::Poisoned);
493 }
494 let r = f(self);
495 if r.is_err() {
496 self.poisoned = true;
497 }
498 r
499 }
500
501 fn head(
502 &mut self,
503 name: &str,
504 filename: Option<&str>,
505 content_type: Option<&str>,
506 ) -> Result<(), FormError> {
507 head_len(&self.boundary, self.first, name, filename, content_type)?;
509 put_head(
510 &mut IoOut(&mut self.out),
511 &self.boundary,
512 self.first,
513 name,
514 filename,
515 content_type,
516 )?;
517 self.first = false;
518 self.k = 2;
520 self.m = 1;
521 Ok(())
522 }
523
524 fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
528 if self.verify {
529 let (k, m, found) = self.boundary.run(self.k, self.m, bytes);
530 if found != crate::multipart::boundary::Hit::None {
531 return Err(FormError::syntax(
532 SyntaxError::BoundaryInContent,
533 Location::NONE,
534 ));
535 }
536 self.k = k;
537 self.m = m;
538 }
539 IoOut(&mut self.out).put(bytes)
540 }
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 fn head(
549 first: bool,
550 name: &str,
551 filename: Option<&str>,
552 ct: Option<&str>,
553 ) -> Result<alloc::vec::Vec<u8>, FormError> {
554 let b = Boundary::new(b"X").unwrap();
555 let mut out = [0u8; 256];
556 let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
557 Ok(out[..n].to_vec())
558 }
559
560 #[test]
561 fn test_head_shape_and_escapes() {
562 assert_eq!(
563 head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
564 b"\r\n--X\r\nContent-Disposition: form-data; name=\"a%0D%0Ab\"; filename=\"c%22.txt\"\r\nContent-Type: text/plain\r\n\r\n"
565 );
566 assert_eq!(
567 head(true, "t\tab", None, None).unwrap(),
568 b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
569 );
570 }
571
572 #[test]
573 fn test_head_refusals() {
574 let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
575 Err(FormError::Syntax { reason, .. }) => reason,
576 other => panic!("{other:?}"),
577 };
578 assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
579 assert_eq!(
580 refused(head(true, "a\0", None, None)),
581 SyntaxError::ControlInHeader
582 );
583 assert_eq!(
584 refused(head(true, "a\\b", None, None)),
585 SyntaxError::AmbiguousQuote
586 );
587 assert_eq!(
588 refused(head(true, "a%22", None, None)),
589 SyntaxError::AmbiguousQuote
590 );
591 assert_eq!(
592 refused(head(true, "a", Some("x\\"), None)),
593 SyntaxError::AmbiguousQuote
594 );
595 assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
596 assert_eq!(
597 refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
598 SyntaxError::ControlInHeader
599 );
600 assert_eq!(
601 refused(head(true, "a", None, Some("not a type"))),
602 SyntaxError::InvalidContentType
603 );
604 assert_eq!(
605 refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
606 SyntaxError::InvalidContentType
607 );
608 assert_eq!(
609 refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
610 SyntaxError::InvalidContentType
611 );
612 }
613
614 #[test]
615 fn test_head_buffer_too_small_writes_nothing() {
616 let b = Boundary::new(b"X").unwrap();
617 let mut out = [7u8; 10];
618 let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
619 assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
620 assert!(out.iter().all(|&x| x == 7));
621 }
622}