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