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 pending_cr: bool,
267 }
268
269 impl<W: Write> fmt::Debug for MultipartWriter<W> {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("MultipartWriter")
272 .field("boundary", &self.boundary)
273 .field("in_file", &self.in_file)
274 .finish_non_exhaustive()
275 }
276 }
277
278 impl<W: Write> MultipartWriter<W> {
279 #[cfg(feature = "random")]
285 #[cfg_attr(docsrs, doc(cfg(feature = "random")))]
286 pub fn new(out: W) -> Result<Self, FormError> {
287 let mut w = Self::with_boundary(out, Boundary::generate()?);
288 w.verify = false;
290 Ok(w)
291 }
292
293 pub fn with_boundary(out: W, boundary: Boundary) -> Self {
295 MultipartWriter {
296 out,
297 boundary,
298 verify: true,
299 first: true,
300 in_file: false,
301 poisoned: false,
302 k: 2,
303 pending_cr: false,
304 }
305 }
306
307 pub const fn boundary(&self) -> &Boundary {
309 &self.boundary
310 }
311
312 #[must_use]
316 pub fn content_type(&self) -> String {
317 let b = self.boundary.as_bytes();
318 let token = b.iter().all(|&c| parse_net::class::is_tchar(c));
319 let b = core::str::from_utf8(b).unwrap_or("");
320 if token {
321 alloc::format!("multipart/form-data; boundary={b}")
322 } else {
323 alloc::format!("multipart/form-data; boundary=\"{b}\"")
324 }
325 }
326
327 pub fn verify_boundary(&mut self, on: bool) {
330 self.verify = on;
331 }
332
333 pub fn text(&mut self, name: &str, value: &str) -> Result<(), FormError> {
344 self.guard(|w| {
345 if name == "_charset_"
346 && !(value.eq_ignore_ascii_case("utf-8")
347 || value.eq_ignore_ascii_case("us-ascii"))
348 {
349 return Err(FormError::encoding(
350 EncodingError::UnsupportedCharset,
351 Location::NONE,
352 ));
353 }
354 w.head(name, None, None)?;
355 w.in_file = false;
356 w.pending_cr = false;
357 let bytes = value.as_bytes();
358 let mut from = 0usize;
359 for (i, &b) in bytes.iter().enumerate() {
360 match b {
361 b'\r' => {
362 w.data(bytes.get(from..i).unwrap_or(&[]))?;
363 w.data(b"\r\n")?;
364 w.pending_cr = true;
365 from = i.saturating_add(1);
366 }
367 b'\n' => {
368 w.data(bytes.get(from..i).unwrap_or(&[]))?;
369 if !w.pending_cr {
370 w.data(b"\r\n")?;
371 }
372 w.pending_cr = false;
373 from = i.saturating_add(1);
374 }
375 _ => w.pending_cr = false,
376 }
377 }
378 w.data(bytes.get(from..).unwrap_or(&[]))
379 })
380 }
381
382 pub fn file_start(
389 &mut self,
390 name: &str,
391 filename: &str,
392 content_type: Option<&str>,
393 ) -> Result<(), FormError> {
394 self.guard(|w| {
395 w.head(name, Some(filename), content_type)?;
396 w.in_file = true;
397 Ok(())
398 })
399 }
400
401 pub fn file_data(&mut self, chunk: &[u8]) -> Result<(), FormError> {
408 if !self.in_file {
409 self.poisoned = true;
410 return Err(FormError::Poisoned);
411 }
412 self.guard(|w| w.data(chunk))
413 }
414
415 pub fn file<R: Read>(
422 &mut self,
423 name: &str,
424 filename: &str,
425 content_type: Option<&str>,
426 mut data: R,
427 ) -> Result<u64, FormError> {
428 self.file_start(name, filename, content_type)?;
429 let mut buf = [0u8; 8192];
430 let mut total = 0u64;
431 let r = loop {
432 let n = match data.read(&mut buf) {
433 Ok(0) => break Ok(total),
434 Ok(n) => n,
435 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
436 Err(e) => {
437 self.poisoned = true;
438 break Err(FormError::Io { kind: e.kind() });
439 }
440 };
441 if let Err(e) = self.file_data(buf.get(..n).unwrap_or(&[])) {
442 break Err(e);
443 }
444 total = total.saturating_add(n as u64);
445 };
446 crate::wipe::wipe(&mut buf);
447 r
448 }
449
450 pub fn finish(mut self) -> Result<W, FormError> {
456 self.guard(|w| {
457 let mut out = IoOut(&mut w.out);
458 if !w.first {
459 out.put(b"\r\n")?;
460 }
461 out.put(b"--")?;
462 out.put(w.boundary.as_bytes())?;
463 out.put(b"--\r\n")?;
464 w.out.flush().map_err(|e| FormError::Io { kind: e.kind() })
465 })?;
466 Ok(self.out)
467 }
468
469 fn guard(
471 &mut self,
472 f: impl FnOnce(&mut Self) -> Result<(), FormError>,
473 ) -> Result<(), FormError> {
474 if self.poisoned {
475 return Err(FormError::Poisoned);
476 }
477 let r = f(self);
478 if r.is_err() {
479 self.poisoned = true;
480 }
481 r
482 }
483
484 fn head(
485 &mut self,
486 name: &str,
487 filename: Option<&str>,
488 content_type: Option<&str>,
489 ) -> Result<(), FormError> {
490 head_len(&self.boundary, self.first, name, filename, content_type)?;
492 put_head(
493 &mut IoOut(&mut self.out),
494 &self.boundary,
495 self.first,
496 name,
497 filename,
498 content_type,
499 )?;
500 self.first = false;
501 self.k = 2;
503 Ok(())
504 }
505
506 fn data(&mut self, bytes: &[u8]) -> Result<(), FormError> {
508 if self.verify {
509 let (k, found) = self.boundary.run(self.k, bytes);
510 if found.is_some() {
511 return Err(FormError::syntax(
512 SyntaxError::BoundaryInContent,
513 Location::NONE,
514 ));
515 }
516 self.k = k;
517 }
518 IoOut(&mut self.out).put(bytes)
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 fn head(
528 first: bool,
529 name: &str,
530 filename: Option<&str>,
531 ct: Option<&str>,
532 ) -> Result<alloc::vec::Vec<u8>, FormError> {
533 let b = Boundary::new(b"X").unwrap();
534 let mut out = [0u8; 256];
535 let n = encode_part_head(&b, first, name, filename, ct, &mut out)?;
536 Ok(out[..n].to_vec())
537 }
538
539 #[test]
540 fn test_head_shape_and_escapes() {
541 assert_eq!(
542 head(false, "a\r\nb", Some("c\".txt"), Some("text/plain")).unwrap(),
543 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"
544 );
545 assert_eq!(
546 head(true, "t\tab", None, None).unwrap(),
547 b"--X\r\nContent-Disposition: form-data; name=\"t\tab\"\r\n\r\n"
548 );
549 }
550
551 #[test]
552 fn test_head_refusals() {
553 let refused = |r: Result<alloc::vec::Vec<u8>, FormError>| match r {
554 Err(FormError::Syntax { reason, .. }) => reason,
555 other => panic!("{other:?}"),
556 };
557 assert_eq!(refused(head(true, "", None, None)), SyntaxError::EmptyName);
558 assert_eq!(
559 refused(head(true, "a\0", None, None)),
560 SyntaxError::ControlInHeader
561 );
562 assert_eq!(
563 refused(head(true, "a\\b", None, None)),
564 SyntaxError::AmbiguousQuote
565 );
566 assert_eq!(
567 refused(head(true, "a%22", None, None)),
568 SyntaxError::AmbiguousQuote
569 );
570 assert_eq!(
571 refused(head(true, "a", Some("x\\"), None)),
572 SyntaxError::AmbiguousQuote
573 );
574 assert!(head(true, "a", Some("C:\\x\\y.txt"), None).is_ok());
575 assert_eq!(
576 refused(head(true, "a", None, Some("text/plain\r\nX: y"))),
577 SyntaxError::ControlInHeader
578 );
579 assert_eq!(
580 refused(head(true, "a", None, Some("not a type"))),
581 SyntaxError::InvalidContentType
582 );
583 assert_eq!(
584 refused(head(true, "a", None, Some("multipart/mixed; boundary=y"))),
585 SyntaxError::InvalidContentType
586 );
587 assert_eq!(
588 refused(head(true, "a", None, Some("text/plain; a=1; a=2"))),
589 SyntaxError::InvalidContentType
590 );
591 }
592
593 #[test]
594 fn test_head_buffer_too_small_writes_nothing() {
595 let b = Boundary::new(b"X").unwrap();
596 let mut out = [7u8; 10];
597 let e = encode_part_head(&b, true, "name", None, None, &mut out).unwrap_err();
598 assert_eq!(e, FormError::BufferTooSmall { needed: 52 });
599 assert!(out.iter().all(|&x| x == 7));
600 }
601}