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 let mt = parse_net::media::parse(raw, &crate::kind::HEADER_LIMITS).map_err(|_| invalid())?;
98 if crate::kind::repeated_name(&mt.params()).is_some()
99 || mt.type_().eq_ignore_ascii_case(b"multipart")
100 {
101 return Err(invalid());
102 }
103 Ok(())
104}
105
106fn head_len(
108 boundary: &Boundary,
109 first: bool,
110 name: &str,
111 filename: Option<&str>,
112 content_type: Option<&str>,
113) -> Result<usize, FormError> {
114 let mut len = if first { 0usize } else { 2 };
115 len = len
116 .saturating_add(2)
117 .saturating_add(boundary.as_bytes().len())
118 .saturating_add(2)
119 .saturating_add(DISPOSITION.len())
120 .saturating_add(escaped_len(name, true)?);
121 if let Some(f) = filename {
122 len = len
123 .saturating_add(FILENAME.len())
124 .saturating_add(escaped_len(f, false)?);
125 }
126 len = len.saturating_add(3);
128 if let Some(ct) = content_type {
129 check_content_type(ct)?;
130 len = len
131 .saturating_add(CONTENT_TYPE.len())
132 .saturating_add(ct.len())
133 .saturating_add(2);
134 }
135 Ok(len.saturating_add(2))
136}
137
138fn put_head(
140 out: &mut impl Out,
141 boundary: &Boundary,
142 first: bool,
143 name: &str,
144 filename: Option<&str>,
145 content_type: Option<&str>,
146) -> Result<(), FormError> {
147 if !first {
148 out.put(b"\r\n")?;
149 }
150 out.put(b"--")?;
151 out.put(boundary.as_bytes())?;
152 out.put(b"\r\n")?;
153 out.put(DISPOSITION)?;
154 put_escaped(out, name)?;
155 if let Some(f) = filename {
156 out.put(FILENAME)?;
157 put_escaped(out, f)?;
158 }
159 out.put(b"\"\r\n")?;
160 if let Some(ct) = content_type {
161 out.put(CONTENT_TYPE)?;
162 out.put(ct.as_bytes())?;
163 out.put(b"\r\n")?;
164 }
165 out.put(b"\r\n")
166}
167
168pub fn encode_part_head(
192 boundary: &Boundary,
193 first: bool,
194 name: &str,
195 filename: Option<&str>,
196 content_type: Option<&str>,
197 out: &mut [u8],
198) -> Result<usize, FormError> {
199 let needed = head_len(boundary, first, name, filename, content_type)?;
200 if out.len() < needed {
201 return Err(FormError::BufferTooSmall { needed });
202 }
203 let mut sink = SliceOut { out, pos: 0 };
204 put_head(&mut sink, boundary, first, name, filename, content_type)?;
205 Ok(sink.pos)
206}
207
208#[cfg(feature = "std")]
209pub use writer_std::MultipartWriter;
210
211#[cfg(feature = "std")]
212mod writer_std {
213 use alloc::string::String;
214 use core::fmt;
215 use std::io::{Read, Write};
216
217 use super::{Out, head_len, put_head};
218 use crate::error::{EncodingError, FormError, Location, SyntaxError};
219 use crate::multipart::boundary::Boundary;
220
221 struct IoOut<'w, W: Write>(&'w mut W);
222
223 impl<W: Write> Out for IoOut<'_, W> {
224 fn put(&mut self, bytes: &[u8]) -> Result<(), FormError> {
225 self.0
226 .write_all(bytes)
227 .map_err(|e| FormError::Io { kind: e.kind() })
228 }
229 }
230
231 pub struct MultipartWriter<W: Write> {
257 out: W,
258 boundary: Boundary,
259 verify: bool,
260 first: bool,
261 in_file: bool,
262 poisoned: bool,
263 k: usize,
265 m: usize,
267 pending_cr: bool,
269 }
270
271 impl<W: Write> fmt::Debug for MultipartWriter<W> {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.debug_struct("MultipartWriter")
274 .field("boundary", &self.boundary)
275 .field("in_file", &self.in_file)
276 .finish_non_exhaustive()
277 }
278 }
279
280 impl<W: Write> MultipartWriter<W> {
281 #[cfg(feature = "random")]
287 #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
288 pub fn new(out: W) -> Result<Self, FormError> {
289 let mut w = Self::with_boundary(out, Boundary::generate()?);
290 w.verify = false;
292 Ok(w)
293 }
294
295 pub fn with_boundary(out: W, boundary: Boundary) -> Self {
297 MultipartWriter {
298 out,
299 boundary,
300 verify: true,
301 first: true,
302 in_file: false,
303 poisoned: false,
304 k: 2,
305 m: 1,
306 pending_cr: false,
307 }
308 }
309
310 pub const fn boundary(&self) -> &Boundary {
312 &self.boundary
313 }
314
315 #[must_use]
319 pub fn content_type(&self) -> String {
320 let b = self.boundary.as_bytes();
321 let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
322 let b = core::str::from_utf8(b).unwrap_or("");
323 if token {
324 alloc::format!("multipart/form-data; boundary={b}")
325 } else {
326 alloc::format!("multipart/form-data; boundary=\"{b}\"")
327 }
328 }
329
330 pub fn verify_boundary(&mut self, on: bool) {
333 self.verify = on;
334 }
335
336 pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
347 self.guard(|w| {
348 if name == "_charset_"
349 && !(value.eq_ignore_ascii_case("utf-8")
350 || value.eq_ignore_ascii_case("us-ascii"))
351 {
352 return Err(FormError::encoding(
353 EncodingError::UnsupportedCharset,
354 Location::NONE,
355 ));
356 }
357 w.head(name, None, None)?;
358 w.in_file = false;
359 w.pending_cr = false;
360 let bytes = value.as_bytes();
361 let mut from = 0usize;
362 for (i, &b) in bytes.iter().enumerate() {
363 match b {
364 b'\r' => {
365 w.data(bytes.get(from..i).unwrap_or(&[]))?;
366 w.data(b"\r\n")?;
367 w.pending_cr = true;
368 from = i.saturating_add(1);
369 }
370 b'\n' => {
371 w.data(bytes.get(from..i).unwrap_or(&[]))?;
372 if !w.pending_cr {
373 w.data(b"\r\n")?;
374 }
375 w.pending_cr = false;
376 from = i.saturating_add(1);
377 }
378 _ => w.pending_cr = false,
379 }
380 }
381 w.data(bytes.get(from..).unwrap_or(&[]))
382 })
383 }
384
385 pub fn file_start(
392 &mut self,
393 name: &str,
394 filename: &str,
395 content_type: Option<&str>,
396 ) -> Result<(), FormError> {
397 self.guard(|w| {
398 w.head(name, Some(filename), content_type)?;
399 w.in_file = true;
400 Ok(())
401 })
402 }
403
404 pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
411 if !self.in_file {
412 self.poisoned = true;
413 return Err(FormError::Poisoned);
414 }
415 self.guard(|w| w.data(chunk))
416 }
417
418 pub fn file<R: Read>(
425 &mut self,
426 name: &str,
427 filename: &str,
428 content_type: Option<&str>,
429 mut data: R,
430 ) -> Result<u64, FormError> {
431 self.file_start(name, filename, content_type)?;
432 let mut buf = [0u8; 8192];
433 let mut total = 0u64;
434 let r = loop {
435 let n = match data.read(&mut buf) {
436 Ok(0) => break Ok(total),
437 Ok(n) => n,
438 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
439 Err(e) => {
440 self.poisoned = true;
441 break Err(FormError::Io { kind: e.kind() });
442 }
443 };
444 if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
445 break Err(e);
446 }
447 total = total.saturating_add(n as u64);
448 };
449 crate::wipe::wipe(&mut buf);
450 r
451 }
452
453 pub fn finish(mut self) -> Result<W, FormError> {
459 self.guard(|w| {
460 let mut out = IoOut(&mut w.out);
461 if !w.first {
462 out.put(b"\r\n")?;
463 }
464 out.put(b"--")?;
465 out.put(w.boundary.as_bytes())?;
466 out.put(b"--\r\n")?;
467 w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
468 })?;
469 Ok(self.out)
470 }
471
472 fn guard(
474 &mut self,
475 f: impl FnOnce(&mut Self) -> Result<(), FormError>,
476 ) -> Result<(), FormError> {
477 if self.poisoned {
478 return Err(FormError::Poisoned);
479 }
480 let r = f(self);
481 if r.is_err() {
482 self.poisoned = true;
483 }
484 r
485 }
486
487 fn head(
488 &mut self,
489 name: &str,
490 filename: Option<&str>,
491 content_type: Option<&str>,
492 ) -> Result<(), FormError> {
493 head_len(&self.boundary, self.first, name, filename, content_type)?;
495 put_head(
496 &mut IoOut(&mut self.out),
497 &self.boundary,
498 self.first,
499 name,
500 filename,
501 content_type,
502 )?;
503 self.first = false;
504 self.k = 2;
506 self.m = 1;
507 Ok(())
508 }
509
510 fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
514 if self.verify {
515 let (k, m, found) = self.boundary.run(self.k, self.m, bytes);
516 if found != crate::multipart::boundary::Hit::None {
517 return Err(FormError::syntax(
518 SyntaxError::BoundaryInContent,
519 Location::NONE,
520 ));
521 }
522 self.k = k;
523 self.m = m;
524 }
525 IoOut(&mut self.out).put(bytes)
526 }
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 fn head(
535 first: bool,
536 name: &str,
537 filename: Option<&str>,
538 ct: Option<&str>,
539 ) -> Result<alloc::vec::Vec<u8>, FormError> {
540 let b = Boundary::new(b"X").unwrap();
541 let mut out = [0u8; 256];
542 let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
543 Ok(out[..n].to_vec())
544 }
545
546 #[test]
547 fn test_head_shape_and_escapes() {
548 assert_eq!(
549 head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
550 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"
551 );
552 assert_eq!(
553 head(true, "t\tab", None, None).unwrap(),
554 b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
555 );
556 }
557
558 #[test]
559 fn test_head_refusals() {
560 let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
561 Err(FormError::Syntax { reason, .. }) => reason,
562 other => panic!("{other:?}"),
563 };
564 assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
565 assert_eq!(
566 refused(head(true, "a\0", None, None)),
567 SyntaxError::ControlInHeader
568 );
569 assert_eq!(
570 refused(head(true, "a\\b", None, None)),
571 SyntaxError::AmbiguousQuote
572 );
573 assert_eq!(
574 refused(head(true, "a%22", None, None)),
575 SyntaxError::AmbiguousQuote
576 );
577 assert_eq!(
578 refused(head(true, "a", Some("x\\"), None)),
579 SyntaxError::AmbiguousQuote
580 );
581 assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
582 assert_eq!(
583 refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
584 SyntaxError::ControlInHeader
585 );
586 assert_eq!(
587 refused(head(true, "a", None, Some("not a type"))),
588 SyntaxError::InvalidContentType
589 );
590 assert_eq!(
591 refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
592 SyntaxError::InvalidContentType
593 );
594 assert_eq!(
595 refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
596 SyntaxError::InvalidContentType
597 );
598 }
599
600 #[test]
601 fn test_head_buffer_too_small_writes_nothing() {
602 let b = Boundary::new(b"X").unwrap();
603 let mut out = [7u8; 10];
604 let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
605 assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
606 assert!(out.iter().all(|&x| x == 7));
607 }
608}