Skip to main content

parse_net/
ext_value.rs

1//! RFC 8187 extended parameter values: `charset "'" [ language ] "'"
2//! value-chars`, as in `filename*` and `title*`.
3//!
4//! # Examples
5//!
6//! ```
7//! use parse_net::{ExtValue, Charset};
8//!
9//! let v = ExtValue::parse(b"UTF-8'en'%E2%82%AC%20rates.txt")?;
10//! assert_eq!((v.charset(), v.language()), (Charset::Utf8, &b"en"[..]));
11//! let mut buf = [0u8; 32];
12//! assert_eq!(v.decode_into(&mut buf)?, "\u{20ac} rates.txt");
13//! # Ok::<(), parse_net::ParseError>(())
14//! ```
15
16use crate::{
17    class,
18    error::{ErrorKind, Result, WriteError, err},
19    pct::{EncodeSet, HexCase},
20    scan::control_kind,
21    utf8::Utf8,
22    write::{Sink, reserve},
23};
24
25/// The charsets RFC 8187 values are decoded from.
26///
27/// # Examples
28///
29/// ```
30/// use parse_net::{Charset, ExtValue};
31///
32/// assert_eq!(ExtValue::parse(b"iso-8859-1''%A3")?.charset(), Charset::Iso8859_1);
33/// assert_eq!(ExtValue::parse(b"US-ASCII''abc")?.charset(), Charset::UsAscii);
34/// # Ok::<(), parse_net::ParseError>(())
35/// ```
36#[non_exhaustive]
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum Charset {
39    /// UTF-8, the charset producers must use (RFC 8187 section 3.2.1).
40    Utf8,
41    /// ISO-8859-1, decoded only (D-010).
42    Iso8859_1,
43    /// US-ASCII, decoded only: a decoded byte above 0x7F is refused (D-058).
44    UsAscii,
45}
46
47/// A validated extended value.
48///
49/// # Examples
50///
51/// ```
52/// let v = parse_net::ExtValue::parse(b"UTF-8''na%C3%AFve.txt")?;
53/// let mut buf = [0u8; 16];
54/// assert_eq!(v.decode_into(&mut buf)?, "na\u{ef}ve.txt");
55/// # Ok::<(), parse_net::ParseError>(())
56/// ```
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58pub struct ExtValue<'a> {
59    charset: Charset,
60    language: &'a [u8],
61    encoded: &'a [u8],
62}
63
64/// Whether `b` may appear in a `mime-charset` (RFC 8187 section 3.2.1).
65const fn is_mime_charset_char(b: u8) -> bool {
66    b.is_ascii_alphanumeric()
67        || matches!(
68            b,
69            b'!' | b'#'
70                | b'$'
71                | b'%'
72                | b'&'
73                | b'+'
74                | b'-'
75                | b'^'
76                | b'_'
77                | b'`'
78                | b'{'
79                | b'}'
80                | b'~'
81        )
82}
83
84/// An RFC 2231 `attribute-char`: a MIME token character but `*`, `'`, and `%`.
85pub(crate) fn is_mime_attribute_char(b: u8) -> bool {
86    class::is_mime_token_char(b) && !matches!(b, b'*' | b'\'' | b'%')
87}
88
89/// Whether a decoded code point is a control: C0, DEL, or C1.
90const fn is_control_point(c: u32) -> bool {
91    c < 0x20 || (c >= 0x7f && c <= 0x9f)
92}
93
94impl<'a> ExtValue<'a> {
95    /// Parses an extended value.
96    ///
97    /// The charset is `UTF-8` or `ISO-8859-1` (case-insensitive); the
98    /// language is empty or letters, digits, and `-`; the value is
99    /// `attr-char` bytes and `%XX` triples.
100    ///
101    /// # Errors
102    ///
103    /// [`ErrorKind::InvalidCharset`] (no `'`, an empty or malformed charset,
104    /// a malformed language); [`ErrorKind::UnsupportedCharset`];
105    /// [`ErrorKind::InvalidPercentEncoding`]; [`ErrorKind::InvalidByte`], or a
106    /// control kind, for a byte outside the value grammar.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use parse_net::{ErrorKind, ExtValue};
112    ///
113    /// assert!(ExtValue::parse(b"UTF-8''a%20b").is_ok());
114    /// assert_eq!(ExtValue::parse(b"UTF-8'a%20b").unwrap_err().kind(), ErrorKind::InvalidCharset);
115    /// assert_eq!(ExtValue::parse(b"Shift_JIS''x").unwrap_err().kind(), ErrorKind::UnsupportedCharset);
116    /// assert_eq!(ExtValue::parse(b"UTF-8''a%2").unwrap_err().kind(), ErrorKind::InvalidPercentEncoding);
117    /// ```
118    pub fn parse(value: &'a [u8]) -> Result<Self> {
119        Self::parse_with(value, class::is_attr_char)
120    }
121
122    /// As [`parse`](Self::parse), for mail: RFC 2231's `attribute-char` also
123    /// allows the bytes RFC 8187 took out, such as `{` and `}`.
124    pub(crate) fn parse_mime(value: &'a [u8]) -> Result<Self> {
125        Self::parse_with(value, is_mime_attribute_char)
126    }
127
128    fn parse_with(value: &'a [u8], in_value: fn(u8) -> bool) -> Result<Self> {
129        let (head, encoded_at) = Self::head(value)?;
130        let mut i = 0usize;
131        while let Some(&b) = head.encoded.get(i) {
132            if b == b'%' {
133                match (head.encoded.get(i + 1), head.encoded.get(i + 2)) {
134                    (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
135                    _ => return err(ErrorKind::InvalidPercentEncoding, encoded_at + i),
136                }
137            } else if in_value(b) {
138                i += 1;
139            } else if crate::scan::is_control(b) {
140                return err(control_kind(b), encoded_at + i);
141            } else {
142                return err(ErrorKind::InvalidByte, encoded_at + i);
143            }
144        }
145        Ok(head)
146    }
147
148    /// An extended value whose text [`parse`](Self::parse) already accepted:
149    /// the value bytes are not checked again.
150    pub(crate) fn from_validated(value: &'a [u8]) -> Option<Self> {
151        Self::head(value).ok().map(|(head, _)| head)
152    }
153
154    /// Reads and checks the charset and language; returns the value with its
155    /// `value-chars` unchecked, and where they start.
156    fn head(value: &'a [u8]) -> Result<(Self, usize)> {
157        let Some(q1) = value.iter().position(|&b| b == b'\'') else {
158            return err(ErrorKind::InvalidCharset, value.len());
159        };
160        let charset_text = value.get(..q1).unwrap_or(&[]);
161        if charset_text.is_empty() || !charset_text.iter().all(|&b| is_mime_charset_char(b)) {
162            return err(ErrorKind::InvalidCharset, 0);
163        }
164        let charset = if charset_text.eq_ignore_ascii_case(b"UTF-8") {
165            Charset::Utf8
166        } else if charset_text.eq_ignore_ascii_case(b"ISO-8859-1") {
167            Charset::Iso8859_1
168        } else if charset_text.eq_ignore_ascii_case(b"US-ASCII") {
169            Charset::UsAscii
170        } else {
171            return err(ErrorKind::UnsupportedCharset, 0);
172        };
173        let after = value.get(q1 + 1..).unwrap_or(&[]);
174        let Some(q2) = after.iter().position(|&b| b == b'\'') else {
175            return err(ErrorKind::InvalidCharset, value.len());
176        };
177        let language = after.get(..q2).unwrap_or(&[]);
178        if let Some(i) = language
179            .iter()
180            .position(|&b| !(b.is_ascii_alphanumeric() || b == b'-'))
181        {
182            return err(ErrorKind::InvalidCharset, q1 + 1 + i);
183        }
184        let encoded = after.get(q2 + 1..).unwrap_or(&[]);
185        Ok((
186            Self {
187                charset,
188                language,
189                encoded,
190            },
191            q1 + 1 + q2 + 1,
192        ))
193    }
194
195    /// The declared charset.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// let v = parse_net::ExtValue::parse(b"utf-8''x")?;
201    /// assert_eq!(v.charset(), parse_net::Charset::Utf8);
202    /// # Ok::<(), parse_net::ParseError>(())
203    /// ```
204    #[must_use]
205    pub const fn charset(&self) -> Charset {
206        self.charset
207    }
208
209    /// The language tag, possibly empty, as sent.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use parse_net::ExtValue;
215    ///
216    /// assert_eq!(ExtValue::parse(b"UTF-8'de-CH'Gr%C3%BCezi")?.language(), b"de-CH");
217    /// assert!(ExtValue::parse(b"UTF-8''Gr%C3%BCezi")?.language().is_empty());
218    /// # Ok::<(), parse_net::ParseError>(())
219    /// ```
220    #[must_use]
221    pub const fn language(&self) -> &'a [u8] {
222        self.language
223    }
224
225    /// The `value-chars`, still percent-encoded.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// let v = parse_net::ExtValue::parse(b"UTF-8''a%20b")?;
231    /// assert_eq!(v.encoded(), b"a%20b");
232    /// # Ok::<(), parse_net::ParseError>(())
233    /// ```
234    #[must_use]
235    pub const fn encoded(&self) -> &'a [u8] {
236        self.encoded
237    }
238
239    /// The decoded bytes, before charset handling.
240    pub(crate) fn raw_bytes(&self) -> PctBytes<'a> {
241        PctBytes {
242            rest: self.encoded.iter(),
243        }
244    }
245
246    /// An upper bound on the length [`decode_into`](Self::decode_into)
247    /// writes: exact for UTF-8, doubled for ISO-8859-1.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// let v = parse_net::ExtValue::parse(b"ISO-8859-1''%E9t%E9")?;
253    /// assert_eq!(v.max_decoded_len(), 6);
254    /// let mut buf = [0u8; 6];
255    /// assert_eq!(v.decode_into(&mut buf)?.len(), 5);
256    /// # Ok::<(), parse_net::ParseError>(())
257    /// ```
258    #[must_use]
259    pub fn max_decoded_len(&self) -> usize {
260        let n = self.raw_bytes().count();
261        match self.charset {
262            Charset::Utf8 | Charset::UsAscii => n,
263            Charset::Iso8859_1 => n.saturating_mul(2),
264        }
265    }
266
267    /// Checks that the value decodes: valid UTF-8 (for UTF-8) and no
268    /// control character.
269    pub(crate) fn check_text(&self) -> Result<()> {
270        decoded_text_len(self.raw_bytes(), self.charset).map(|_| ())
271    }
272
273    /// Decodes into `out` and returns the text: UTF-8 is validated
274    /// (overlong forms and surrogates refused), ISO-8859-1 is transcoded.
275    /// A decoded control character (C0, DEL, or C1, such as `%0D%0A` or
276    /// `%00`) is refused: never copy such a value into another header or a
277    /// log.
278    ///
279    /// # Errors
280    ///
281    /// [`ErrorKind::InvalidUtf8`], [`ErrorKind::ControlInDecoded`],
282    /// [`ErrorKind::BufferTooSmall`]. Nothing is written on error.
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use parse_net::{ErrorKind, ExtValue};
288    ///
289    /// let mut buf = [0u8; 8];
290    /// assert_eq!(ExtValue::parse(b"ISO-8859-1''%A3%205")?.decode_into(&mut buf)?, "\u{a3} 5");
291    /// let crlf = ExtValue::parse(b"UTF-8''a%0D%0Ab")?;
292    /// assert_eq!(crlf.decode_into(&mut buf).unwrap_err().kind(), ErrorKind::ControlInDecoded);
293    /// # Ok::<(), parse_net::ParseError>(())
294    /// ```
295    pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
296        decode_checked(self.raw_bytes(), self.charset, out)
297    }
298}
299
300/// Decodes `bytes` in `charset` into `out`, refusing controls, after a first
301/// pass that validates and sizes the output so nothing is written on error.
302pub(crate) fn decode_checked(
303    bytes: impl Iterator<Item = u8> + Clone,
304    charset: Charset,
305    out: &mut [u8],
306) -> Result<&str> {
307    let len = decoded_text_len(bytes.clone(), charset)?;
308    let Some(dst) = out.get_mut(..len) else {
309        return err(ErrorKind::BufferTooSmall, 0);
310    };
311    let mut slots = dst.iter_mut();
312    for b in bytes {
313        match charset {
314            Charset::Utf8 | Charset::UsAscii => {
315                if let Some(s) = slots.next() {
316                    *s = b;
317                }
318            }
319            Charset::Iso8859_1 => {
320                if b < 0x80 {
321                    if let Some(s) = slots.next() {
322                        *s = b;
323                    }
324                } else {
325                    // U+0080 to U+00FF in UTF-8: 0xC2 or 0xC3, then 0x80 | low six bits.
326                    if let Some(s) = slots.next() {
327                        *s = 0xc0 | (b >> 6);
328                    }
329                    if let Some(s) = slots.next() {
330                        *s = 0x80 | (b & 0x3f);
331                    }
332                }
333            }
334        }
335    }
336    core::str::from_utf8(dst)
337        .map_err(|e| crate::ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
338}
339
340/// Validates `bytes` as text in `charset` with no control character, and
341/// returns its UTF-8 length.
342pub(crate) fn decoded_text_len(bytes: impl Iterator<Item = u8>, charset: Charset) -> Result<usize> {
343    let mut len = 0usize;
344    match charset {
345        Charset::Utf8 => {
346            let mut v = Utf8::new();
347            let mut point = 0u32;
348            for (i, b) in bytes.enumerate() {
349                if !v.push(b) {
350                    return err(ErrorKind::InvalidUtf8, i);
351                }
352                // Accumulate the code point; `v` has already refused any
353                // byte out of place, so the masks below see valid UTF-8.
354                point = match b {
355                    0x00..=0x7f => u32::from(b),
356                    0x80..=0xbf => (point << 6) | u32::from(b & 0x3f),
357                    0xc0..=0xdf => u32::from(b & 0x1f),
358                    0xe0..=0xef => u32::from(b & 0x0f),
359                    _ => u32::from(b & 0x07),
360                };
361                if v.is_complete() && is_control_point(point) {
362                    return err(ErrorKind::ControlInDecoded, i);
363                }
364                len += 1;
365            }
366            if !v.is_complete() {
367                return err(ErrorKind::InvalidUtf8, len);
368            }
369        }
370        Charset::Iso8859_1 => {
371            for (i, b) in bytes.enumerate() {
372                if is_control_point(u32::from(b)) {
373                    return err(ErrorKind::ControlInDecoded, i);
374                }
375                len += if b < 0x80 { 1 } else { 2 };
376            }
377        }
378        Charset::UsAscii => {
379            for (i, b) in bytes.enumerate() {
380                if b >= 0x80 {
381                    return err(ErrorKind::InvalidByte, i);
382                }
383                if is_control_point(u32::from(b)) {
384                    return err(ErrorKind::ControlInDecoded, i);
385                }
386                len += 1;
387            }
388        }
389    }
390    Ok(len)
391}
392
393/// Percent-decoded bytes of already validated text.
394#[derive(Clone)]
395pub(crate) struct PctBytes<'a> {
396    pub(crate) rest: core::slice::Iter<'a, u8>,
397}
398
399impl Iterator for PctBytes<'_> {
400    type Item = u8;
401
402    fn next(&mut self) -> Option<u8> {
403        let b = *self.rest.next()?;
404        if b != b'%' {
405            return Some(b);
406        }
407        let hi = self.rest.next().copied().and_then(class::hex_value);
408        let lo = self.rest.next().copied().and_then(class::hex_value);
409        match (hi, lo) {
410            (Some(h), Some(l)) => Some((h << 4) | l),
411            _ => Some(b),
412        }
413    }
414}
415
416/// Writes `UTF-8''` and `text` with every byte that is not an `attr-char`
417/// as `%XX` (uppercase hex). ISO-8859-1 is never written (D-010).
418///
419/// # Errors
420///
421/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
422/// [`WriteError::Full`] (nothing written when the sink reports its room).
423///
424/// # Examples
425///
426/// ```
427/// use parse_net::{ext_value::write_ext_value, SliceSink};
428///
429/// let mut buf = [0u8; 64];
430/// let mut out = SliceSink::new(&mut buf);
431/// write_ext_value("\u{20ac} rates.txt", &mut out).expect("room");
432/// assert_eq!(out.written(), b"UTF-8''%E2%82%AC%20rates.txt");
433/// ```
434pub fn write_ext_value(text: &str, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
435    for (i, c) in text.char_indices() {
436        if is_control_point(u32::from(c)) {
437            return Err(WriteError::forbidden(i));
438        }
439    }
440    let set = EncodeSet::ATTR_CHAR_COMPLEMENT;
441    reserve(
442        out,
443        7usize.saturating_add(crate::pct::encoded_len(text.as_bytes(), &set)),
444    )?;
445    out.put(b"UTF-8''")?;
446    crate::pct::encode(text.as_bytes(), &set, HexCase::Upper, out)
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    fn decode(v: &[u8]) -> Result<std::string::String> {
454        let e = ExtValue::parse(v)?;
455        let mut buf = [0u8; 64];
456        e.decode_into(&mut buf).map(std::string::String::from)
457    }
458
459    #[test]
460    fn test_ext_value_charset_matrix() {
461        assert_eq!(decode(b"UTF-8''").unwrap(), "");
462        assert_eq!(decode(b"utf-8''a").unwrap(), "a");
463        assert_eq!(decode(b"UTF-8'en'a").unwrap(), "a");
464        assert_eq!(decode(b"ISO-8859-1''%E9").unwrap(), "\u{e9}");
465        assert_eq!(decode(b"iso-8859-1''caf%e9").unwrap(), "caf\u{e9}");
466        assert_eq!(decode(b"us-ascii'en'a%20b").unwrap(), "a b");
467        assert_eq!(
468            decode(b"US-ASCII''caf%e9").unwrap_err().kind(),
469            ErrorKind::InvalidByte
470        );
471        assert_eq!(decode(b"UTF-8''%C3%A9").unwrap(), "\u{e9}");
472        // The same bytes read under the other charset are refused or differ.
473        assert_eq!(
474            decode(b"UTF-8''%E9").unwrap_err().kind(),
475            ErrorKind::InvalidUtf8
476        );
477        assert_eq!(decode(b"ISO-8859-1''%C3%A9").unwrap(), "\u{c3}\u{a9}");
478        assert_eq!(
479            ExtValue::parse(b"UTF-16''a").unwrap_err().kind(),
480            ErrorKind::UnsupportedCharset
481        );
482        assert_eq!(
483            ExtValue::parse(b"''a").unwrap_err().kind(),
484            ErrorKind::InvalidCharset
485        );
486        assert_eq!(
487            ExtValue::parse(b"UTF-8'a").unwrap_err().kind(),
488            ErrorKind::InvalidCharset
489        );
490        assert_eq!(
491            ExtValue::parse(b"UTF 8''a").unwrap_err().kind(),
492            ErrorKind::InvalidCharset
493        );
494        assert_eq!(
495            ExtValue::parse(b"UTF-8'e n'a").unwrap_err().kind(),
496            ErrorKind::InvalidCharset
497        );
498    }
499
500    #[test]
501    fn test_ext_value_percent_crlf_refused() {
502        assert_eq!(
503            ExtValue::parse(b"UTF-8''%").unwrap_err().kind(),
504            ErrorKind::InvalidPercentEncoding
505        );
506        assert_eq!(
507            ExtValue::parse(b"UTF-8''%G0").unwrap_err().kind(),
508            ErrorKind::InvalidPercentEncoding
509        );
510        assert_eq!(
511            decode(b"UTF-8''%C3").unwrap_err().kind(),
512            ErrorKind::InvalidUtf8
513        );
514        assert_eq!(
515            decode(b"UTF-8''%C0%AF").unwrap_err().kind(),
516            ErrorKind::InvalidUtf8
517        );
518        assert_eq!(
519            decode(b"UTF-8''%00").unwrap_err().kind(),
520            ErrorKind::ControlInDecoded
521        );
522        assert_eq!(
523            decode(b"UTF-8''a%0D%0Ab").unwrap_err().kind(),
524            ErrorKind::ControlInDecoded
525        );
526        assert_eq!(
527            decode(b"UTF-8''%C2%85").unwrap_err().kind(),
528            ErrorKind::ControlInDecoded
529        );
530        assert_eq!(
531            decode(b"ISO-8859-1''%85").unwrap_err().kind(),
532            ErrorKind::ControlInDecoded
533        );
534        assert_eq!(decode(b"UTF-8''a%2Fb").unwrap(), "a/b");
535        assert_eq!(
536            ExtValue::parse(b"UTF-8''a b").unwrap_err().kind(),
537            ErrorKind::InvalidByte
538        );
539        assert_eq!(
540            ExtValue::parse(b"UTF-8''a\rb").unwrap_err().kind(),
541            ErrorKind::BareCr
542        );
543    }
544
545    #[test]
546    fn test_ext_value_buffer_and_lengths() {
547        let e = ExtValue::parse(b"ISO-8859-1''%E9%E9").unwrap();
548        assert_eq!(e.max_decoded_len(), 4);
549        let mut short = [0xaau8; 3];
550        assert_eq!(
551            e.decode_into(&mut short).unwrap_err().kind(),
552            ErrorKind::BufferTooSmall
553        );
554        assert_eq!(short, [0xaa; 3]);
555        let u = ExtValue::parse(b"UTF-8''%E2%82%AC").unwrap();
556        assert_eq!(u.max_decoded_len(), 3);
557    }
558
559    #[test]
560    fn test_write_ext_value_round_trip_and_refusals() {
561        let mut buf = [0u8; 128];
562        let mut out = crate::SliceSink::new(&mut buf);
563        assert_eq!(
564            write_ext_value("a\r\nb", &mut out),
565            Err(WriteError::Forbidden { at: 1 })
566        );
567        assert_eq!(
568            write_ext_value("x\u{85}", &mut out),
569            Err(WriteError::Forbidden { at: 1 })
570        );
571        write_ext_value("caf\u{e9} \u{1f600}/", &mut out).unwrap();
572        let back = ExtValue::parse(out.written()).unwrap();
573        let mut dec = [0u8; 64];
574        assert_eq!(back.decode_into(&mut dec).unwrap(), "caf\u{e9} \u{1f600}/");
575    }
576}