use super::cfws;
use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
ext_value::{Charset, ExtValue, is_mime_attribute_char},
limits::{Limits, MAX_RFC2231_SECTIONS},
param::{MAX_CHECKED_PARAMS, Names},
scan::control_kind,
text::{Word, parse_quoted},
write::{CountSink, Sink, reserve},
};
const RELAXED: Limits = Limits::DEFAULT
.max_params(u16::MAX)
.max_comment_depth(u8::MAX);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Section {
Plain,
Ext,
Part { n: usize, ext: bool },
}
fn split_name(name: &[u8]) -> (&[u8], Section) {
let (body, ext) = match name.split_last() {
Some((b'*', body)) => (body, true),
_ => (name, false),
};
if let Some(star) = body.iter().rposition(|&b| b == b'*') {
let digits = body.get(star + 1..).unwrap_or(&[]);
let canonical = !digits.is_empty()
&& digits.iter().all(u8::is_ascii_digit)
&& (digits.len() == 1 || digits.first() != Some(&b'0'));
if canonical {
let n = digits.iter().fold(0usize, |a, &d| {
a.saturating_mul(10).saturating_add(usize::from(d - b'0'))
});
return (body.get(..star).unwrap_or(&[]), Section::Part { n, ext });
}
}
if ext {
(body, Section::Ext)
} else {
(name, Section::Plain)
}
}
fn refused(v: &[u8], at: usize, kind: ErrorKind) -> ParseError {
match v.get(at) {
Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
_ => ParseError::new(kind, at),
}
}
fn param_at<'a>(
v: &'a [u8],
i: usize,
limits: &Limits,
) -> Result<(&'a [u8], Word<'a>, usize, usize)> {
let j = cfws(v, i + 1, limits)?;
let n = v
.get(j..)
.unwrap_or(&[])
.iter()
.take_while(|&&b| class::is_mime_token_char(b))
.count();
if n == 0 {
return Err(refused(v, j, ErrorKind::NotToken));
}
let name = v.get(j..j + n).unwrap_or(&[]);
let k = cfws(v, j + n, limits)?;
if v.get(k) != Some(&b'=') {
return Err(refused(v, k, ErrorKind::MissingValue));
}
let at = cfws(v, k + 1, limits)?;
let rest = v.get(at..).unwrap_or(&[]);
if rest.first() == Some(&b'"') {
let (q, used) = parse_quoted(rest).map_err(|e| e.shifted(at))?;
return Ok((name, Word::Quoted(q), at, at + used));
}
let t = rest
.iter()
.take_while(|&&b| class::is_mime_token_char(b))
.count();
if t == 0 {
return Err(refused(v, at, ErrorKind::MissingValue));
}
Ok((name, Word::Token(rest.get(..t).unwrap_or(&[])), at, at + t))
}
fn pct_chars(t: &[u8]) -> Option<usize> {
let mut i = 0usize;
while let Some(&b) = t.get(i) {
if b == b'%' {
match (t.get(i + 1), t.get(i + 2)) {
(Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
_ => return Some(i),
}
} else if is_mime_attribute_char(b) {
i += 1;
} else {
return Some(i);
}
}
None
}
pub fn parse_mime_params<'a>(s: &'a [u8], limits: &Limits) -> Result<(MimeParams<'a>, usize)> {
let (mut i, mut count) = (0usize, 0usize);
let max = usize::from(limits.params).min(MAX_CHECKED_PARAMS);
let mut names = Names::new();
loop {
let j = cfws(s, i, limits)?;
if s.get(j) != Some(&b';') {
break;
}
let (name, value, at, end) = param_at(s, j, limits)?;
count += 1;
if count > max {
return err(ErrorKind::TooManyMembers, j);
}
let name_at = (name.as_ptr() as usize).saturating_sub(s.as_ptr() as usize);
if names.repeats(s, name_at, name, class::is_mime_token_char) {
return err(ErrorKind::DuplicateParameter, j);
}
match (split_name(name).1, value) {
(Section::Ext | Section::Part { n: 0, ext: true }, Word::Token(t)) => {
let ext = ExtValue::parse_mime(t).map_err(|e| e.shifted(at))?;
if split_name(name).1 == Section::Ext {
ext.check_text()
.map_err(|e| ParseError::new(e.kind(), at))?;
}
}
(Section::Part { ext: true, .. }, Word::Token(t)) => {
if let Some(bad) = pct_chars(t) {
return err(ErrorKind::InvalidPercentEncoding, at + bad);
}
}
(Section::Ext | Section::Part { ext: true, .. }, Word::Quoted(_)) => {
return err(ErrorKind::InvalidCharset, at);
}
_ => {}
}
i = end;
}
let params = MimeParams {
text: s.get(..i).unwrap_or(&[]),
count,
};
for (name, _) in params.iter() {
if let (base, Section::Part { n: 0, .. }) = split_name(name) {
let c = Continuation { params, name: base };
let _ = c.walk(limits, &mut |_| {})?;
} else if let (base, Section::Part { .. }) = split_name(name) {
let has_zero = params
.iter()
.any(|(n, _)| matches!(split_name(n), (b, Section::Part { n: 0, .. }) if b.eq_ignore_ascii_case(base)));
if !has_zero {
let at = (name.as_ptr() as usize).saturating_sub(s.as_ptr() as usize);
return err(ErrorKind::SectionGap, at);
}
}
}
Ok((params, i))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MimeParams<'a> {
text: &'a [u8],
count: usize,
}
impl<'a> MimeParams<'a> {
#[must_use]
pub fn get(&self, name: &str) -> Option<MimeValue<'a>> {
let want = name.as_bytes();
let mut ext = None;
let mut plain = None;
for (n, w) in self.iter() {
match split_name(n) {
(b, Section::Part { n: 0, .. }) if b.eq_ignore_ascii_case(want) => {
return Some(MimeValue::Continued(Continuation {
params: *self,
name: b,
}));
}
(b, Section::Ext) if b.eq_ignore_ascii_case(want) => {
if let Word::Token(t) = w {
ext = ExtValue::parse_mime(t).ok();
}
}
(b, Section::Plain) if b.eq_ignore_ascii_case(want) => plain = Some(w),
_ => {}
}
}
ext.map(MimeValue::Extended).or(plain.map(MimeValue::Plain))
}
pub fn decode_into<'b>(&self, name: &str, out: &'b mut [u8]) -> Result<Option<&'b str>> {
match self.get(name) {
None => Ok(None),
Some(MimeValue::Continued(c)) => c.decode_into(out).map(Some),
Some(MimeValue::Extended(e)) => e.decode_into(out).map(Some),
Some(MimeValue::Plain(w)) => {
let Some(dst) = out.get_mut(..w.len()) else {
return err(ErrorKind::BufferTooSmall, 0);
};
let (mut utf8, text) = (crate::utf8::Utf8::new(), self.value_text(&w));
let valid = match w {
Word::Token(t) => t.iter().all(|&b| utf8.push(b)),
Word::Quoted(q) => q.bytes().all(|b| utf8.push(b)),
};
if !valid || !utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, text);
}
core::str::from_utf8(w.unescape_into(dst)?)
.map(Some)
.map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
}
}
}
fn value_text(&self, w: &Word<'_>) -> usize {
let raw = match w {
Word::Token(t) => t,
Word::Quoted(q) => q.raw(),
};
(raw.as_ptr() as usize).saturating_sub(self.text.as_ptr() as usize)
}
#[must_use]
pub fn iter(&self) -> MimeParamIter<'a> {
MimeParamIter { s: self.text, i: 0 }
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
#[derive(Clone, Debug)]
pub struct MimeParamIter<'a> {
s: &'a [u8],
i: usize,
}
impl<'a> Iterator for MimeParamIter<'a> {
type Item = (&'a [u8], Word<'a>);
fn next(&mut self) -> Option<Self::Item> {
let j = cfws(self.s, self.i, &RELAXED).ok()?;
if self.s.get(j) != Some(&b';') {
return None;
}
let (name, value, _, end) = param_at(self.s, j, &RELAXED).ok()?;
self.i = end;
Some((name, value))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MimeValue<'a> {
Plain(Word<'a>),
Extended(ExtValue<'a>),
Continued(Continuation<'a>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Continuation<'a> {
params: MimeParams<'a>,
name: &'a [u8],
}
impl Continuation<'_> {
#[must_use]
pub fn sections(&self) -> usize {
self.params
.iter()
.filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
.count()
}
#[must_use]
pub fn max_decoded_len(&self) -> usize {
self.params
.iter()
.filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
.map(|(_, w)| w.len().saturating_mul(2))
.sum()
}
pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
let len = self.walk(&RELAXED, &mut |_| {})?;
let Some(dst) = out.get_mut(..len) else {
return err(ErrorKind::BufferTooSmall, 0);
};
let mut n = 0usize;
let _ = self.walk(&RELAXED, &mut |b: u8| {
if let Some(d) = dst.get_mut(n) {
*d = b;
}
n += 1;
})?;
core::str::from_utf8(dst)
.map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
}
fn walk(&self, limits: &Limits, emit: &mut impl FnMut(u8)) -> Result<usize> {
let base = self.params.text.as_ptr() as usize;
let at = |s: &[u8]| (s.as_ptr() as usize).saturating_sub(base);
let mut slots: [Option<(bool, Word<'_>)>; MAX_RFC2231_SECTIONS as usize] =
[None; MAX_RFC2231_SECTIONS as usize];
let max = usize::from(limits.rfc2231_sections.min(MAX_RFC2231_SECTIONS));
let mut highest = 0usize;
for (name, w) in self.params.iter() {
if let (b, Section::Part { n, ext }) = split_name(name) {
if !b.eq_ignore_ascii_case(self.name) {
continue;
}
if n >= max {
return err(ErrorKind::TooManySections, at(name));
}
match slots.get_mut(n) {
Some(Some(_)) => return err(ErrorKind::SectionGap, at(name)),
Some(slot) => *slot = Some((ext, w)),
None => {}
}
highest = highest.max(n);
}
}
let used = slots.get(..=highest).unwrap_or(&[]);
if let Some(gap) = used.iter().position(Option::is_none) {
let after = self.params.iter().find(|(name, _)| {
matches!(split_name(name), (b, Section::Part { n, .. })
if b.eq_ignore_ascii_case(self.name) && n > gap)
});
let offset = after.map_or(0, |(name, _)| at(name));
return err(ErrorKind::SectionGap, offset);
}
let mut charset = Charset::Utf8;
let mut utf8 = crate::utf8::Utf8::new();
let (mut count, mut prev) = (0usize, 0u8);
let mut put = |b: u8, charset: Charset, strict: bool, off: usize| -> Result<()> {
if (b < 0x20 && (strict || b != b'\t')) || b == 0x7f {
return err(ErrorKind::ControlInDecoded, off);
}
if charset == Charset::UsAscii && b >= 0x80 {
return err(ErrorKind::InvalidByte, off);
}
let c1 = if charset == Charset::Iso8859_1 {
(0x80..=0x9f).contains(&b)
} else {
prev == 0xc2 && (0x80..=0x9f).contains(&b)
};
if c1 {
return err(ErrorKind::ControlInDecoded, off);
}
prev = b;
let decoded = [0xc0 | (b >> 6), 0x80 | (b & 0x3f)];
let bytes: &[u8] = if charset == Charset::Iso8859_1 && b >= 0x80 {
&decoded
} else {
core::slice::from_ref(&b)
};
for &x in bytes {
if !utf8.push(x) {
return err(ErrorKind::InvalidUtf8, off);
}
emit(x);
count += 1;
}
Ok(())
};
for (k, slot) in used.iter().enumerate() {
let Some((ext, w)) = *slot else { continue };
let raw: &[u8] = match (k, ext, w) {
(0, true, Word::Token(t)) => {
let e = ExtValue::parse_mime(t)?;
charset = e.charset();
e.encoded()
}
(_, true, Word::Token(t)) => t,
_ => &[],
};
if ext {
let mut i = 0usize;
while let Some(&b) = raw.get(i) {
let start = i;
let byte = if b == b'%' {
let hi = raw
.get(i + 1)
.copied()
.and_then(class::hex_value)
.unwrap_or(0);
let lo = raw
.get(i + 2)
.copied()
.and_then(class::hex_value)
.unwrap_or(0);
i += 3;
(hi << 4) | lo
} else {
i += 1;
b
};
put(byte, charset, true, at(raw) + start)?;
}
} else {
match w {
Word::Token(t) => {
for (i, &byte) in t.iter().enumerate() {
put(byte, charset, false, at(t) + i)?;
}
}
Word::Quoted(q) => {
for byte in q.bytes() {
put(byte, charset, false, at(q.raw()))?;
}
}
}
}
}
if !utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, self.params.text.len());
}
Ok(count)
}
}
fn is_attribute(name: &[u8]) -> bool {
!name.is_empty()
&& name
.iter()
.all(|&b| class::is_mime_token_char(b) && !matches!(b, b'*' | b'\'' | b'%'))
}
const LINE: usize = 78;
const fn unit_len(b: u8) -> usize {
if class::is_attr_char(b) { 1 } else { 3 }
}
fn put_unit(b: u8, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
if class::is_attr_char(b) {
out.put(&[b])?;
} else {
let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
out.put(&[b'%', hi, lo])?;
}
Ok(())
}
#[derive(Clone, Copy)]
enum Form {
Token,
Quoted,
Extended,
}
fn form(n: &[u8], v: &[u8]) -> (Form, usize) {
if !v.is_empty() && v.iter().all(|&b| class::is_mime_token_char(b)) {
return (Form::Token, 3 + n.len() + v.len());
}
if v.iter().all(|&b| (0x20..=0x7e).contains(&b)) {
let escaped = v.iter().filter(|&&b| b == b'"' || b == b'\\').count();
return (Form::Quoted, 5 + n.len() + v.len() + escaped);
}
let encoded: usize = v.iter().map(|&b| unit_len(b)).sum();
(Form::Extended, 11 + n.len() + encoded)
}
fn put_form(
form: Form,
n: &[u8],
v: &[u8],
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
out.put(n)?;
match form {
Form::Token => {
out.put(b"=")?;
out.put(v)?;
}
Form::Quoted => {
out.put(b"=\"")?;
let mut rest = v;
while let Some(p) = rest.iter().position(|&b| b == b'"' || b == b'\\') {
out.put(rest.get(..p).unwrap_or(&[]))?;
out.put(b"\\")?;
out.put(rest.get(p..p + 1).unwrap_or(&[]))?;
rest = rest.get(p + 1..).unwrap_or(&[]);
}
out.put(rest)?;
out.put(b"\"")?;
}
Form::Extended => {
out.put(b"*=UTF-8''")?;
let mut rest = v;
while let Some(p) = rest.iter().position(|&b| !class::is_attr_char(b)) {
out.put(rest.get(..p).unwrap_or(&[]))?;
put_unit(rest.get(p).copied().unwrap_or(0), out)?;
rest = rest.get(p + 1..).unwrap_or(&[]);
}
out.put(rest)?;
}
}
Ok(())
}
fn put_param(
n: &[u8],
v: &[u8],
(f, len): (Form, usize),
room: usize,
out: &mut impl Sink,
) -> core::result::Result<usize, WriteError> {
if len <= room {
out.put(b"; ")?;
put_form(f, n, v, out)?;
return Ok(room - len);
}
if len <= LINE {
out.put(b";\r\n ")?;
put_form(f, n, v, out)?;
return Ok(LINE - (len - 1));
}
if 1 + n.len() + 12 + 3 + 1 > LINE {
return Err(WriteError::OutOfRange);
}
out.put(b";")?;
let mut rest = v;
let mut k = 0usize;
let mut col = 0usize;
while !rest.is_empty() || k == 0 {
if k >= usize::from(MAX_RFC2231_SECTIONS) {
return Err(WriteError::OutOfRange);
}
let mut digits = [0u8; 2];
let dl = if k < 10 {
digits[0] = b'0' + k as u8;
1
} else {
digits[0] = b'0' + (k / 10) as u8;
digits[1] = b'0' + (k % 10) as u8;
2
};
if k > 0 {
out.put(b";")?;
}
out.put(b"\r\n ")?;
out.put(n)?;
out.put(b"*")?;
out.put(digits.get(..dl).unwrap_or(&[]))?;
out.put(b"*=")?;
col = 1 + n.len() + 1 + dl + 2;
if k == 0 {
out.put(b"UTF-8''")?;
col += 7;
}
let mut took = 0usize;
for &b in rest {
let u = unit_len(b);
if col + u + 1 > LINE && took > 0 {
break;
}
put_unit(b, out)?;
col += u;
took += 1;
}
rest = rest.get(took..).unwrap_or(&[]);
k += 1;
}
Ok(LINE.saturating_sub(col))
}
fn write_param(
name: &str,
value: &str,
room: usize,
out: &mut impl Sink,
) -> core::result::Result<usize, WriteError> {
if !is_attribute(name.as_bytes()) {
return Err(WriteError::NotToken);
}
let control = if value.is_ascii() {
value.bytes().position(|b| b < 0x20 || b == 0x7f)
} else {
value
.char_indices()
.find(|&(_, c)| c.is_control())
.map(|(at, _)| at)
};
if let Some(at) = control {
return Err(WriteError::forbidden(at));
}
let (n, v) = (name.as_bytes(), value.as_bytes());
let f = form(n, v);
let mut count = CountSink::new();
let _ = put_param(n, v, f, room, &mut count)?;
reserve(out, count.count())?;
put_param(n, v, f, room, out)
}
pub fn write_mime_param(
name: &str,
value: &str,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
write_param(name, value, LINE, out).map(|_| ())
}
pub fn write_mime_param_at(
name: &str,
value: &str,
room_first_line: usize,
out: &mut impl Sink,
) -> core::result::Result<usize, WriteError> {
write_param(name, value, room_first_line, out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{format, string::String, vec, vec::Vec};
fn p(v: &[u8]) -> MimeParams<'_> {
parse_mime_params(v, &Limits::DEFAULT).unwrap().0
}
fn k(v: &[u8]) -> ErrorKind {
parse_mime_params(v, &Limits::DEFAULT).unwrap_err().kind()
}
fn get(v: &[u8], name: &str) -> String {
let mut buf = vec![0u8; 512];
String::from(p(v).decode_into(name, &mut buf).unwrap().unwrap())
}
#[test]
fn test_mime_params_basic() {
assert_eq!(get(b"; a=b; c=\"d e\"", "C"), "d e");
assert_eq!(get(b"; boundary={x}", "boundary"), "{x}");
assert_eq!(get(b" ; a = (c) b (d)", "a"), "b");
assert_eq!(get(b";\r\n a=b", "a"), "b");
assert!(
p(b"; a=b")
.decode_into("z", &mut [0u8; 4])
.unwrap()
.is_none()
);
assert_eq!(parse_mime_params(b"", &Limits::DEFAULT).unwrap().1, 0);
assert_eq!(
parse_mime_params(b"; a=b x", &Limits::DEFAULT).unwrap().1,
5
);
assert_eq!(k(b"; =b"), ErrorKind::NotToken);
assert_eq!(k(b"; a"), ErrorKind::MissingValue);
assert_eq!(k(b"; a="), ErrorKind::MissingValue);
assert_eq!(k(b"; a=\"b"), ErrorKind::UnterminatedQuote);
assert_eq!(k(b"; a=1; A=2"), ErrorKind::DuplicateParameter);
assert_eq!(k(b"; a=1 (x"), ErrorKind::UnbalancedComment);
let l = Limits::DEFAULT.max_params(2);
assert!(parse_mime_params(b"; a=1; b=2", &l).is_ok());
assert_eq!(
parse_mime_params(b"; a=1; b=2; c=3", &l)
.unwrap_err()
.kind(),
ErrorKind::TooManyMembers
);
}
#[test]
fn test_mime_params_rfc2231() {
assert_eq!(
get(
b"; t*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
"t"
),
"This is ***fun***"
);
assert_eq!(get(b"; t*0=\"foo\"; t*1=bar", "t"), "foobar");
assert_eq!(get(b"; t*1=bar; t*0=\"foo\"", "t"), "foobar");
assert_eq!(get(b"; t*0*=UTF-8''%E2%82; t*1*=%AC", "t"), "\u{20ac}");
assert_eq!(get(b"; t=plain; t*=UTF-8''ext", "t"), "ext");
assert_eq!(get(b"; t=plain; t*0=cont", "t"), "cont");
assert_eq!(get(b"; t*00=x; t=plain", "t"), "plain");
assert_eq!(k(b"; t*0=a; t*2=c"), ErrorKind::SectionGap);
assert_eq!(k(b"; t*1=a"), ErrorKind::SectionGap);
assert_eq!(k(b"; t*0=a; t*0=b"), ErrorKind::DuplicateParameter);
assert_eq!(
k(b"; t*0*=UTF-8''a; t*1*=UTF-8''b"),
ErrorKind::InvalidPercentEncoding
);
assert_eq!(k(b"; t*0*=UTF-8''%C3"), ErrorKind::InvalidUtf8);
assert_eq!(k(b"; t*0*=UTF-8''%0D"), ErrorKind::ControlInDecoded);
assert_eq!(k(b"; t*=\"UTF-8''x\""), ErrorKind::InvalidCharset);
let mut v = Vec::new();
for i in 0..32 {
v.extend_from_slice(format!("; t*{i}=x").as_bytes());
}
assert_eq!(
p(&v)
.get("t")
.map(|m| matches!(m, MimeValue::Continued(c) if c.sections() == 32)),
Some(true)
);
v.extend_from_slice(b"; t*32=x");
assert_eq!(k(&v), ErrorKind::TooManySections);
let l = Limits::DEFAULT.max_rfc2231_sections(2);
assert!(parse_mime_params(b"; t*0=a; t*1=b", &l).is_ok());
assert_eq!(
parse_mime_params(b"; t*0=a; t*1=b; t*2=c", &l)
.unwrap_err()
.kind(),
ErrorKind::TooManySections
);
}
fn written(name: &str, value: &str) -> Vec<u8> {
let mut buf = vec![0u8; 4096];
let mut out = crate::SliceSink::new(&mut buf);
write_mime_param(name, value, &mut out).unwrap();
out.written().to_vec()
}
#[test]
fn test_write_mime_param_forms_and_round_trip() {
assert_eq!(written("charset", "utf-8"), b"; charset=utf-8");
assert_eq!(written("name", "a \"b\""), b"; name=\"a \\\"b\\\"\"");
assert_eq!(written("name", ""), b"; name=\"\"");
let long = "x".repeat(200);
let nonascii = "\u{e9}".repeat(60);
for value in [
String::from("plain"),
long,
nonascii,
String::from("caf\u{e9} au lait.txt"),
] {
let w = written("filename", &value);
for line in w.split(|&b| b == b'\n') {
assert!(line.len() <= 79, "{}", String::from_utf8_lossy(line));
}
let (params, used) = parse_mime_params(&w, &Limits::DEFAULT).unwrap();
assert_eq!(used, w.len());
let mut buf = vec![0u8; 1024];
assert_eq!(
params.decode_into("filename", &mut buf).unwrap(),
Some(value.as_str())
);
}
let mut buf = [0u8; 8];
let mut out = crate::SliceSink::new(&mut buf);
assert_eq!(
write_mime_param("a*b", "x", &mut out),
Err(WriteError::NotToken)
);
assert_eq!(
write_mime_param("a", "x\ny", &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
write_mime_param("a", "toolongvalue", &mut out),
Err(WriteError::Full)
);
assert!(out.is_empty());
let mut big = vec![0u8; 1 << 16];
let mut out = crate::SliceSink::new(&mut big);
assert_eq!(
write_mime_param("f", &"\u{e9}".repeat(1000), &mut out),
Err(WriteError::OutOfRange)
);
}
fn written_at(name: &str, value: &str, room: usize) -> (String, usize) {
let mut buf = vec![0u8; 4096];
let mut out = crate::SliceSink::new(&mut buf);
let left = write_mime_param_at(name, value, room, &mut out).unwrap();
(String::from_utf8(out.written().to_vec()).unwrap(), left)
}
#[test]
fn test_write_mime_param_at_room() {
assert_eq!(
written_at("charset", "utf-8", 15),
(String::from("; charset=utf-8"), 0)
);
assert_eq!(
written_at("charset", "utf-8", 14),
(String::from(";\r\n charset=utf-8"), 64)
);
assert_eq!(
written_at("charset", "utf-8", 0),
(String::from(";\r\n charset=utf-8"), 64)
);
assert_eq!(written_at("n", "a b", 9), (String::from("; n=\"a b\""), 0));
assert_eq!(
written_at("n", "a b", 8),
(String::from(";\r\n n=\"a b\""), 70)
);
assert_eq!(
written_at("f", "caf\u{e9}", 21),
(String::from("; f*=UTF-8''caf%C3%A9"), 0)
);
assert_eq!(
written_at("f", "caf\u{e9}", 20),
(String::from(";\r\n f*=UTF-8''caf%C3%A9"), 58)
);
let v = "x".repeat(78 - 3 - 1);
let (w, left) = written_at("a", &v, 78);
assert_eq!((w.len(), left), (78, 0));
let (w, left) = written_at("a", &v, 10);
assert_eq!((w.len(), left), (80, 1));
let long = "x".repeat(200);
for room in [0, 10, 78] {
let (w, left) = written_at("filename", &long, room);
assert!(w.starts_with(";\r\n filename*0*=UTF-8''"), "{w}");
let last = w.rsplit("\r\n").next().unwrap();
assert_eq!(left, 78 - last.len());
}
let mut buf = vec![0u8; 256];
let mut out = crate::SliceSink::new(&mut buf);
let mut room = 78 - "Content-Type: text/plain".len();
for (n, v) in [
("charset", "utf-8"),
("format", "flowed"),
("delsp", "yes"),
("name", "a long attachment name.txt"),
] {
room = write_mime_param_at(n, v, room, &mut out).unwrap();
}
assert_eq!(
out.written(),
b"; charset=utf-8; format=flowed; delsp=yes;\r\n name=\"a long attachment name.txt\""
);
assert_eq!(room, 78 - 34);
assert_eq!(
write_mime_param_at("a*", "x", 78, &mut crate::SliceSink::new(&mut [0u8; 8])),
Err(WriteError::NotToken)
);
}
}