1use crate::{
18 class,
19 error::{ErrorKind, ParseError, Result, WriteError, err},
20 ext_value::{Charset, ExtValue, decode_checked, write_ext_value},
21 limits::{DuplicatePolicy, Limits},
22 param::{self, Params},
23 scan::control_kind,
24 text::{Word, skip_ows, trim_ows},
25 write::{CountSink, Sink, reserve},
26};
27
28#[non_exhaustive]
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum DispositionType<'a> {
32 Inline,
34 Attachment,
36 FormData,
38 Other(&'a [u8]),
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub struct ContentDisposition<'a> {
46 kind: DispositionType<'a>,
47 params: Params<'a>,
48}
49
50pub fn parse_content_disposition(value: &[u8]) -> Result<ContentDisposition<'_>> {
65 parse(value, &Limits::DEFAULT)
66}
67
68pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<ContentDisposition<'a>> {
93 let lead = skip_ows(value);
94 let v = trim_ows(value);
95 let type_len = v.iter().take_while(|&&b| class::is_tchar(b)).count();
96 if type_len == 0 {
97 return Err(match v.first() {
98 Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), lead),
99 _ => ParseError::new(ErrorKind::NotToken, lead),
100 });
101 }
102 let type_ = v.get(..type_len).unwrap_or(&[]);
103 let refuse = limits.duplicate_params(DuplicatePolicy::Refuse);
104 let rest = v.get(type_len..).unwrap_or(&[]);
105 let (params, used) = param::parse(rest, &refuse).map_err(|e| e.shifted(lead + type_len))?;
106 let end = type_len + used;
107 if end < v.len() {
108 let at = end + skip_ows(v.get(end..).unwrap_or(&[]));
109 return Err(match v.get(at) {
110 Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), lead + at),
111 _ => ParseError::new(ErrorKind::TrailingData, lead + at),
112 });
113 }
114 for p in params.iter() {
115 if p.name.eq_ignore_ascii_case(b"filename*") {
116 let at = lead
117 + (p.name.as_ptr() as usize).saturating_sub(v.as_ptr() as usize)
118 + p.name.len()
119 + 1;
120 let Word::Token(t) = p.value else {
121 return err(ErrorKind::InvalidCharset, at);
122 };
123 let ext = ExtValue::parse(t).map_err(|e| e.shifted(at))?;
124 ext.check_text()
127 .map_err(|e| ParseError::new(e.kind(), at))?;
128 }
129 }
130 let kind = if type_.eq_ignore_ascii_case(b"inline") {
131 DispositionType::Inline
132 } else if type_.eq_ignore_ascii_case(b"attachment") {
133 DispositionType::Attachment
134 } else if type_.eq_ignore_ascii_case(b"form-data") {
135 DispositionType::FormData
136 } else {
137 DispositionType::Other(type_)
138 };
139 Ok(ContentDisposition { kind, params })
140}
141
142impl<'a> ContentDisposition<'a> {
143 #[must_use]
145 pub const fn kind(&self) -> DispositionType<'a> {
146 self.kind
147 }
148
149 #[must_use]
151 pub const fn params(&self) -> Params<'a> {
152 self.params
153 }
154
155 #[must_use]
157 pub fn name(&self) -> Option<Word<'a>> {
158 self.params.get("name")
159 }
160
161 #[must_use]
164 pub fn filename(&self) -> Option<Filename<'a>> {
165 self.filename_ext()
166 .map(Filename::Ext)
167 .or_else(|| self.filename_plain().map(Filename::Plain))
168 }
169
170 #[must_use]
172 pub fn filename_plain(&self) -> Option<Word<'a>> {
173 self.params.get("filename")
174 }
175
176 #[must_use]
178 pub fn filename_ext(&self) -> Option<ExtValue<'a>> {
179 match self.params.get("filename*")? {
180 Word::Token(t) => ExtValue::parse(t).ok(),
181 Word::Quoted(_) => None,
182 }
183 }
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
188pub enum Filename<'a> {
189 Plain(Word<'a>),
191 Ext(ExtValue<'a>),
193}
194
195#[derive(Clone)]
197enum WordBytes<'a> {
198 Token(core::slice::Iter<'a, u8>),
199 Quoted(crate::text::Unescape<'a>),
200}
201
202impl Iterator for WordBytes<'_> {
203 type Item = u8;
204
205 fn next(&mut self) -> Option<u8> {
206 match self {
207 WordBytes::Token(t) => t.next().copied(),
208 WordBytes::Quoted(q) => q.next(),
209 }
210 }
211}
212
213fn word_bytes<'a>(w: &Word<'a>) -> WordBytes<'a> {
214 match w {
215 Word::Token(t) => WordBytes::Token(t.iter()),
216 Word::Quoted(q) => WordBytes::Quoted(q.bytes()),
217 }
218}
219
220#[derive(Clone)]
223struct PctDecode<I> {
224 inner: I,
225}
226
227impl<I: Iterator<Item = u8> + Clone> Iterator for PctDecode<I> {
228 type Item = u8;
229
230 fn next(&mut self) -> Option<u8> {
231 let b = self.inner.next()?;
232 if b != b'%' {
233 return Some(b);
234 }
235 let mut look = self.inner.clone();
236 match (
237 look.next().and_then(class::hex_value),
238 look.next().and_then(class::hex_value),
239 ) {
240 (Some(h), Some(l)) => {
241 self.inner = look;
242 Some((h << 4) | l)
243 }
244 _ => Some(b),
245 }
246 }
247}
248
249fn has_bad_percent(bytes: impl Iterator<Item = u8>) -> Option<usize> {
251 let mut pending: Option<usize> = None;
252 for (i, b) in bytes.enumerate() {
253 match pending {
254 Some(at) if !class::is_hexdig(b) => return Some(at),
255 Some(at) if i == at + 2 => pending = None,
256 Some(_) => {}
257 None if b == b'%' => pending = Some(i),
258 None => {}
259 }
260 }
261 pending
262}
263
264impl Filename<'_> {
265 #[must_use]
267 pub fn max_decoded_len(&self) -> usize {
268 match self {
269 Filename::Plain(w) => w.len(),
270 Filename::Ext(e) => e.max_decoded_len(),
271 }
272 }
273
274 pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
283 match self {
284 Filename::Plain(w) => decode_checked(word_bytes(w), Charset::Utf8, out),
285 Filename::Ext(e) => e.decode_into(out),
286 }
287 }
288
289 pub fn decode_percent_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
307 match self {
308 Filename::Plain(w) => {
309 if let Some(at) = has_bad_percent(word_bytes(w)) {
310 return err(ErrorKind::InvalidPercentEncoding, at);
311 }
312 decode_checked(
313 PctDecode {
314 inner: word_bytes(w),
315 },
316 Charset::Utf8,
317 out,
318 )
319 }
320 Filename::Ext(e) => e.decode_into(out),
321 }
322 }
323
324 #[must_use]
328 pub fn has_path_separator(&self) -> bool {
329 let sep = |b: u8| b == b'/' || b == b'\\';
330 match self {
331 Filename::Plain(w) => {
332 word_bytes(w).any(sep)
333 || PctDecode {
334 inner: word_bytes(w),
335 }
336 .any(sep)
337 }
338 Filename::Ext(e) => e.raw_bytes().any(sep),
339 }
340 }
341
342 #[must_use]
354 pub fn has_bidi_control(&self) -> bool {
355 fn scan(bytes: impl Iterator<Item = u8>) -> bool {
356 let (mut a, mut b) = (0u8, 0u8);
357 for c in bytes {
358 if a == 0xe2
359 && ((b == 0x80 && (0xaa..=0xae).contains(&c))
360 || (b == 0x81 && (0xa6..=0xa9).contains(&c)))
361 {
362 return true;
363 }
364 a = b;
365 b = c;
366 }
367 false
368 }
369 match self {
370 Filename::Plain(w) => {
371 scan(word_bytes(w))
372 || scan(PctDecode {
373 inner: word_bytes(w),
374 })
375 }
376 Filename::Ext(e) if e.charset() == Charset::Utf8 => scan(e.raw_bytes()),
377 Filename::Ext(_) => false,
378 }
379 }
380}
381
382fn needs_fallback(c: char) -> bool {
384 !c.is_ascii() || c == '"' || c == '\\' || c.is_ascii_control()
385}
386
387fn put_attachment(filename: &str, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
388 out.put(b"attachment; filename=\"")?;
389 for c in filename.chars() {
390 if needs_fallback(c) {
391 out.put(b"_")?;
392 } else {
393 let mut buf = [0u8; 4];
394 out.put(c.encode_utf8(&mut buf).as_bytes())?;
395 }
396 }
397 out.put(b"\"")?;
398 if filename.chars().any(needs_fallback) {
399 out.put(b"; filename*=")?;
400 write_ext_value(filename, out)?;
401 }
402 Ok(())
403}
404
405pub fn write_attachment(
425 filename: &str,
426 out: &mut impl Sink,
427) -> core::result::Result<(), WriteError> {
428 for (i, c) in filename.char_indices() {
429 let p = u32::from(c);
430 if p < 0x20 || (0x7f..=0x9f).contains(&p) {
431 return Err(WriteError::forbidden(i));
432 }
433 }
434 let mut count = CountSink::new();
435 put_attachment(filename, &mut count)?;
436 reserve(out, count.count())?;
437 put_attachment(filename, out)
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 fn cd(v: &[u8]) -> Result<ContentDisposition<'_>> {
445 parse_content_disposition(v)
446 }
447
448 fn name(v: &[u8]) -> std::string::String {
449 let mut buf = [0u8; 128];
450 cd(v)
451 .unwrap()
452 .filename()
453 .unwrap()
454 .decode_into(&mut buf)
455 .unwrap()
456 .into()
457 }
458
459 #[test]
460 fn test_disposition_types_and_params() {
461 assert_eq!(cd(b"inline").unwrap().kind(), DispositionType::Inline);
462 assert_eq!(
463 cd(b"ATTACHMENT;").unwrap().kind(),
464 DispositionType::Attachment
465 );
466 assert_eq!(
467 cd(b"form-data; name=\"a\"; filename=\"\"").unwrap().kind(),
468 DispositionType::FormData
469 );
470 assert_eq!(
471 cd(b"x-custom").unwrap().kind(),
472 DispositionType::Other(b"x-custom")
473 );
474 assert_eq!(cd(b"").unwrap_err().kind(), ErrorKind::NotToken);
475 assert_eq!(
476 cd(b"attachment x").unwrap_err().kind(),
477 ErrorKind::TrailingData
478 );
479 assert_eq!(
480 cd(b"attachment; filename=a\rb").unwrap_err().kind(),
481 ErrorKind::BareCr
482 );
483 assert!(cd(b"attachment").unwrap().filename().is_none());
484 }
485
486 #[test]
487 fn test_filename_precedence_and_refusals() {
488 assert_eq!(
489 name(b"attachment; filename=\"a.txt\"; filename*=UTF-8''b.txt"),
490 "b.txt"
491 );
492 assert_eq!(
493 name(b"attachment; filename*=UTF-8''b.txt; filename=\"a.txt\""),
494 "b.txt"
495 );
496 assert_eq!(
497 name(b"attachment; filename=\"a \\\"q\\\".txt\""),
498 "a \"q\".txt"
499 );
500 let k = |v: &[u8]| cd(v).unwrap_err().kind();
501 assert_eq!(
503 k(b"attachment; filename=\"a.txt\"; filename*=UTF-8''%C3"),
504 ErrorKind::InvalidUtf8
505 );
506 assert_eq!(
507 k(b"attachment; filename*=\"UTF-8''a\""),
508 ErrorKind::InvalidCharset
509 );
510 assert_eq!(
511 k(b"attachment; filename*=UTF-16''a"),
512 ErrorKind::UnsupportedCharset
513 );
514 let last = Limits::DEFAULT.duplicate_params(DuplicatePolicy::Last);
516 assert_eq!(
517 parse(b"attachment; filename=a; FILENAME=b", &last)
518 .unwrap_err()
519 .kind(),
520 ErrorKind::DuplicateParameter
521 );
522 }
523
524 #[test]
525 fn test_filename_nul_refused() {
526 let mut buf = [0u8; 32];
527 let e = cd(b"attachment; filename*=UTF-8''a%00.txt").unwrap_err();
528 assert_eq!((e.kind(), e.offset()), (ErrorKind::ControlInDecoded, 22));
529 let p = cd(b"form-data; name=x; filename=\"a%00b\"").unwrap();
530 assert_eq!(
531 p.filename()
532 .unwrap()
533 .decode_percent_into(&mut buf)
534 .unwrap_err()
535 .kind(),
536 ErrorKind::ControlInDecoded
537 );
538 assert_eq!(
539 p.filename().unwrap().decode_into(&mut buf).unwrap(),
540 "a%00b"
541 );
542 }
543
544 #[test]
545 fn test_filename_overlong_utf8_refused() {
546 let mut buf = [0u8; 32];
547 assert_eq!(
548 cd(b"attachment; filename*=UTF-8''%C0%AF")
549 .unwrap_err()
550 .kind(),
551 ErrorKind::InvalidUtf8
552 );
553 let p = cd(b"attachment; filename=\"\xc0\xaf\"").unwrap();
554 assert_eq!(
555 p.filename()
556 .unwrap()
557 .decode_into(&mut buf)
558 .unwrap_err()
559 .kind(),
560 ErrorKind::InvalidUtf8
561 );
562 }
563
564 #[test]
565 fn test_filename_separators_and_bidi_reported() {
566 fn f(v: &[u8]) -> Filename<'_> {
567 cd(v).unwrap().filename().unwrap()
568 }
569 assert!(f(b"attachment; filename=\"../x\"").has_path_separator());
570 assert!(f(b"attachment; filename=\"a\\\\b\"").has_path_separator());
571 assert!(f(b"attachment; filename=\"a%2Fb\"").has_path_separator());
572 assert!(f(b"attachment; filename*=UTF-8''a%2Fb").has_path_separator());
573 assert!(!f(b"attachment; filename=plain.txt").has_path_separator());
574 assert!(f(b"attachment; filename*=UTF-8''%E2%81%A6x").has_bidi_control());
575 assert!(!f(b"attachment; filename*=UTF-8''%E2%82%AC").has_bidi_control());
576 assert!(!f(b"attachment; filename*=ISO-8859-1''%E2%A1%AE").has_bidi_control());
577 assert_eq!(
578 cd(b"attachment; filename*=ISO-8859-1''%85")
579 .unwrap_err()
580 .kind(),
581 ErrorKind::ControlInDecoded
582 );
583 }
584
585 #[test]
586 fn test_decode_percent_into_errors() {
587 let mut buf = [0u8; 32];
588 let p = cd(b"form-data; name=x; filename=\"a%zz\"").unwrap();
589 assert_eq!(
590 p.filename()
591 .unwrap()
592 .decode_percent_into(&mut buf)
593 .unwrap_err()
594 .kind(),
595 ErrorKind::InvalidPercentEncoding
596 );
597 let p = cd(b"form-data; name=x; filename=\"a%4\"").unwrap();
598 assert_eq!(
599 p.filename()
600 .unwrap()
601 .decode_percent_into(&mut buf)
602 .unwrap_err()
603 .kind(),
604 ErrorKind::InvalidPercentEncoding
605 );
606 }
607
608 #[test]
609 fn test_write_attachment_cases() {
610 let mut buf = [0u8; 128];
611 let mut out = crate::SliceSink::new(&mut buf);
612 write_attachment("report.pdf", &mut out).unwrap();
613 assert_eq!(out.written(), b"attachment; filename=\"report.pdf\"");
614 let back = cd(out.written()).unwrap();
615 assert!(back.filename_ext().is_none());
616 out.clear();
617 write_attachment("a\"b\\c", &mut out).unwrap();
618 assert_eq!(
619 out.written(),
620 b"attachment; filename=\"a_b_c\"; filename*=UTF-8''a%22b%5Cc"
621 );
622 let mut dec = [0u8; 16];
623 assert_eq!(
624 cd(out.written())
625 .unwrap()
626 .filename()
627 .unwrap()
628 .decode_into(&mut dec)
629 .unwrap(),
630 "a\"b\\c"
631 );
632 assert_eq!(
633 write_attachment("a\nb", &mut out),
634 Err(WriteError::Forbidden { at: 1 })
635 );
636 let mut small = [0u8; 10];
637 let mut out = crate::SliceSink::new(&mut small);
638 assert_eq!(write_attachment("x", &mut out), Err(WriteError::Full));
639 assert!(out.is_empty());
640 }
641}