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#[non_exhaustive]
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum Charset {
29    /// UTF-8, the charset producers must use (RFC 8187 section 3.2.1).
30    Utf8,
31    /// ISO-8859-1, decoded only (D-010).
32    Iso8859_1,
33}
34
35/// A validated extended value.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37pub struct ExtValue<'a> {
38    charset: Charset,
39    language: &'a [u8],
40    encoded: &'a [u8],
41}
42
43/// Whether `b` may appear in a `mime-charset` (RFC 8187 section 3.2.1).
44const fn is_mime_charset_char(b: u8) -> bool {
45    b.is_ascii_alphanumeric()
46        || matches!(
47            b,
48            b'!' | b'#'
49                | b'$'
50                | b'%'
51                | b'&'
52                | b'+'
53                | b'-'
54                | b'^'
55                | b'_'
56                | b'`'
57                | b'{'
58                | b'}'
59                | b'~'
60        )
61}
62
63/// Whether a decoded code point is a control: C0, DEL, or C1.
64const fn is_control_point(c: u32) -> bool {
65    c < 0x20 || (c >= 0x7f && c <= 0x9f)
66}
67
68impl<'a> ExtValue<'a> {
69    /// Parses an extended value.
70    ///
71    /// The charset is `UTF-8` or `ISO-8859-1` (case-insensitive); the
72    /// language is empty or letters, digits, and `-`; the value is
73    /// `attr-char` bytes and `%XX` triples.
74    ///
75    /// # Errors
76    ///
77    /// [`ErrorKind::InvalidCharset`] (no `'`, an empty or malformed charset,
78    /// a malformed language); [`ErrorKind::UnsupportedCharset`];
79    /// [`ErrorKind::InvalidPercentEncoding`]; [`ErrorKind::InvalidByte`], or a
80    /// control kind, for a byte outside the value grammar.
81    pub fn parse(value: &'a [u8]) -> Result<Self> {
82        let Some(q1) = value.iter().position(|&b| b == b'\'') else {
83            return err(ErrorKind::InvalidCharset, value.len());
84        };
85        let charset_text = value.get(..q1).unwrap_or(&[]);
86        if charset_text.is_empty() || !charset_text.iter().all(|&b| is_mime_charset_char(b)) {
87            return err(ErrorKind::InvalidCharset, 0);
88        }
89        let charset = if charset_text.eq_ignore_ascii_case(b"UTF-8") {
90            Charset::Utf8
91        } else if charset_text.eq_ignore_ascii_case(b"ISO-8859-1") {
92            Charset::Iso8859_1
93        } else {
94            return err(ErrorKind::UnsupportedCharset, 0);
95        };
96        let after = value.get(q1 + 1..).unwrap_or(&[]);
97        let Some(q2) = after.iter().position(|&b| b == b'\'') else {
98            return err(ErrorKind::InvalidCharset, value.len());
99        };
100        let language = after.get(..q2).unwrap_or(&[]);
101        if let Some(i) = language
102            .iter()
103            .position(|&b| !(b.is_ascii_alphanumeric() || b == b'-'))
104        {
105            return err(ErrorKind::InvalidCharset, q1 + 1 + i);
106        }
107        let encoded_at = q1 + 1 + q2 + 1;
108        let encoded = after.get(q2 + 1..).unwrap_or(&[]);
109        let mut i = 0usize;
110        while let Some(&b) = encoded.get(i) {
111            if b == b'%' {
112                match (encoded.get(i + 1), encoded.get(i + 2)) {
113                    (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
114                    _ => return err(ErrorKind::InvalidPercentEncoding, encoded_at + i),
115                }
116            } else if class::is_attr_char(b) {
117                i += 1;
118            } else if crate::scan::is_control(b) {
119                return err(control_kind(b), encoded_at + i);
120            } else {
121                return err(ErrorKind::InvalidByte, encoded_at + i);
122            }
123        }
124        Ok(Self {
125            charset,
126            language,
127            encoded,
128        })
129    }
130
131    /// The declared charset.
132    #[must_use]
133    pub const fn charset(&self) -> Charset {
134        self.charset
135    }
136
137    /// The language tag, possibly empty, as sent.
138    #[must_use]
139    pub const fn language(&self) -> &'a [u8] {
140        self.language
141    }
142
143    /// The `value-chars`, still percent-encoded.
144    #[must_use]
145    pub const fn encoded(&self) -> &'a [u8] {
146        self.encoded
147    }
148
149    /// The decoded bytes, before charset handling.
150    pub(crate) fn raw_bytes(&self) -> PctBytes<'a> {
151        PctBytes {
152            rest: self.encoded.iter(),
153        }
154    }
155
156    /// An upper bound on the length [`decode_into`](Self::decode_into)
157    /// writes: exact for UTF-8, doubled for ISO-8859-1.
158    #[must_use]
159    pub fn max_decoded_len(&self) -> usize {
160        let n = self.raw_bytes().count();
161        match self.charset {
162            Charset::Utf8 => n,
163            Charset::Iso8859_1 => n.saturating_mul(2),
164        }
165    }
166
167    /// Checks that the value decodes: valid UTF-8 (for UTF-8) and no
168    /// control character.
169    pub(crate) fn check_text(&self) -> Result<()> {
170        decoded_text_len(self.raw_bytes(), self.charset).map(|_| ())
171    }
172
173    /// Decodes into `out` and returns the text: UTF-8 is validated
174    /// (overlong forms and surrogates refused), ISO-8859-1 is transcoded.
175    /// A decoded control character (C0, DEL, or C1, such as `%0D%0A` or
176    /// `%00`) is refused: never copy such a value into another header or a
177    /// log.
178    ///
179    /// # Errors
180    ///
181    /// [`ErrorKind::InvalidUtf8`], [`ErrorKind::ControlInDecoded`],
182    /// [`ErrorKind::BufferTooSmall`]. Nothing is written on error.
183    pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
184        decode_checked(self.raw_bytes(), self.charset, out)
185    }
186}
187
188/// Decodes `bytes` in `charset` into `out`, refusing controls, after a first
189/// pass that validates and sizes the output so nothing is written on error.
190pub(crate) fn decode_checked(
191    bytes: impl Iterator<Item = u8> + Clone,
192    charset: Charset,
193    out: &mut [u8],
194) -> Result<&str> {
195    let len = decoded_text_len(bytes.clone(), charset)?;
196    let Some(dst) = out.get_mut(..len) else {
197        return err(ErrorKind::BufferTooSmall, 0);
198    };
199    let mut slots = dst.iter_mut();
200    for b in bytes {
201        match charset {
202            Charset::Utf8 => {
203                if let Some(s) = slots.next() {
204                    *s = b;
205                }
206            }
207            Charset::Iso8859_1 => {
208                if b < 0x80 {
209                    if let Some(s) = slots.next() {
210                        *s = b;
211                    }
212                } else {
213                    // U+0080 to U+00FF in UTF-8: 0xC2 or 0xC3, then 0x80 | low six bits.
214                    if let Some(s) = slots.next() {
215                        *s = 0xc0 | (b >> 6);
216                    }
217                    if let Some(s) = slots.next() {
218                        *s = 0x80 | (b & 0x3f);
219                    }
220                }
221            }
222        }
223    }
224    core::str::from_utf8(dst)
225        .map_err(|e| crate::ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
226}
227
228/// Validates `bytes` as text in `charset` with no control character, and
229/// returns its UTF-8 length.
230pub(crate) fn decoded_text_len(bytes: impl Iterator<Item = u8>, charset: Charset) -> Result<usize> {
231    let mut len = 0usize;
232    match charset {
233        Charset::Utf8 => {
234            let mut v = Utf8::new();
235            let mut point = 0u32;
236            for (i, b) in bytes.enumerate() {
237                if !v.push(b) {
238                    return err(ErrorKind::InvalidUtf8, i);
239                }
240                // Accumulate the code point; `v` has already refused any
241                // byte out of place, so the masks below see valid UTF-8.
242                point = match b {
243                    0x00..=0x7f => u32::from(b),
244                    0x80..=0xbf => (point << 6) | u32::from(b & 0x3f),
245                    0xc0..=0xdf => u32::from(b & 0x1f),
246                    0xe0..=0xef => u32::from(b & 0x0f),
247                    _ => u32::from(b & 0x07),
248                };
249                if v.is_complete() && is_control_point(point) {
250                    return err(ErrorKind::ControlInDecoded, i);
251                }
252                len += 1;
253            }
254            if !v.is_complete() {
255                return err(ErrorKind::InvalidUtf8, len);
256            }
257        }
258        Charset::Iso8859_1 => {
259            for (i, b) in bytes.enumerate() {
260                if is_control_point(u32::from(b)) {
261                    return err(ErrorKind::ControlInDecoded, i);
262                }
263                len += if b < 0x80 { 1 } else { 2 };
264            }
265        }
266    }
267    Ok(len)
268}
269
270/// Percent-decoded bytes of already validated text.
271#[derive(Clone)]
272pub(crate) struct PctBytes<'a> {
273    pub(crate) rest: core::slice::Iter<'a, u8>,
274}
275
276impl Iterator for PctBytes<'_> {
277    type Item = u8;
278
279    fn next(&mut self) -> Option<u8> {
280        let b = *self.rest.next()?;
281        if b != b'%' {
282            return Some(b);
283        }
284        let hi = self.rest.next().copied().and_then(class::hex_value);
285        let lo = self.rest.next().copied().and_then(class::hex_value);
286        match (hi, lo) {
287            (Some(h), Some(l)) => Some((h << 4) | l),
288            _ => Some(b),
289        }
290    }
291}
292
293/// Writes `UTF-8''` and `text` with every byte that is not an `attr-char`
294/// as `%XX` (uppercase hex). ISO-8859-1 is never written (D-010).
295///
296/// # Errors
297///
298/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
299/// [`WriteError::Full`] (nothing written when the sink reports its room).
300///
301/// # Examples
302///
303/// ```
304/// use parse_net::{ext_value::write_ext_value, SliceSink};
305///
306/// let mut buf = [0u8; 64];
307/// let mut out = SliceSink::new(&mut buf);
308/// write_ext_value("\u{20ac} rates.txt", &mut out).expect("room");
309/// assert_eq!(out.written(), b"UTF-8''%E2%82%AC%20rates.txt");
310/// ```
311pub fn write_ext_value(text: &str, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
312    for (i, c) in text.char_indices() {
313        if is_control_point(u32::from(c)) {
314            return Err(WriteError::forbidden(i));
315        }
316    }
317    let set = EncodeSet::ATTR_CHAR_COMPLEMENT;
318    reserve(
319        out,
320        7usize.saturating_add(crate::pct::encoded_len(text.as_bytes(), &set)),
321    )?;
322    out.put(b"UTF-8''")?;
323    crate::pct::encode(text.as_bytes(), &set, HexCase::Upper, out)
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn decode(v: &[u8]) -> Result<std::string::String> {
331        let e = ExtValue::parse(v)?;
332        let mut buf = [0u8; 64];
333        e.decode_into(&mut buf).map(std::string::String::from)
334    }
335
336    #[test]
337    fn test_ext_value_charset_matrix() {
338        assert_eq!(decode(b"UTF-8''").unwrap(), "");
339        assert_eq!(decode(b"utf-8''a").unwrap(), "a");
340        assert_eq!(decode(b"UTF-8'en'a").unwrap(), "a");
341        assert_eq!(decode(b"ISO-8859-1''%E9").unwrap(), "\u{e9}");
342        assert_eq!(decode(b"iso-8859-1''caf%e9").unwrap(), "caf\u{e9}");
343        assert_eq!(decode(b"UTF-8''%C3%A9").unwrap(), "\u{e9}");
344        // The same bytes read under the other charset are refused or differ.
345        assert_eq!(
346            decode(b"UTF-8''%E9").unwrap_err().kind(),
347            ErrorKind::InvalidUtf8
348        );
349        assert_eq!(decode(b"ISO-8859-1''%C3%A9").unwrap(), "\u{c3}\u{a9}");
350        assert_eq!(
351            ExtValue::parse(b"UTF-16''a").unwrap_err().kind(),
352            ErrorKind::UnsupportedCharset
353        );
354        assert_eq!(
355            ExtValue::parse(b"''a").unwrap_err().kind(),
356            ErrorKind::InvalidCharset
357        );
358        assert_eq!(
359            ExtValue::parse(b"UTF-8'a").unwrap_err().kind(),
360            ErrorKind::InvalidCharset
361        );
362        assert_eq!(
363            ExtValue::parse(b"UTF 8''a").unwrap_err().kind(),
364            ErrorKind::InvalidCharset
365        );
366        assert_eq!(
367            ExtValue::parse(b"UTF-8'e n'a").unwrap_err().kind(),
368            ErrorKind::InvalidCharset
369        );
370    }
371
372    #[test]
373    fn test_ext_value_percent_crlf_refused() {
374        assert_eq!(
375            ExtValue::parse(b"UTF-8''%").unwrap_err().kind(),
376            ErrorKind::InvalidPercentEncoding
377        );
378        assert_eq!(
379            ExtValue::parse(b"UTF-8''%G0").unwrap_err().kind(),
380            ErrorKind::InvalidPercentEncoding
381        );
382        assert_eq!(
383            decode(b"UTF-8''%C3").unwrap_err().kind(),
384            ErrorKind::InvalidUtf8
385        );
386        assert_eq!(
387            decode(b"UTF-8''%C0%AF").unwrap_err().kind(),
388            ErrorKind::InvalidUtf8
389        );
390        assert_eq!(
391            decode(b"UTF-8''%00").unwrap_err().kind(),
392            ErrorKind::ControlInDecoded
393        );
394        assert_eq!(
395            decode(b"UTF-8''a%0D%0Ab").unwrap_err().kind(),
396            ErrorKind::ControlInDecoded
397        );
398        assert_eq!(
399            decode(b"UTF-8''%C2%85").unwrap_err().kind(),
400            ErrorKind::ControlInDecoded
401        );
402        assert_eq!(
403            decode(b"ISO-8859-1''%85").unwrap_err().kind(),
404            ErrorKind::ControlInDecoded
405        );
406        assert_eq!(decode(b"UTF-8''a%2Fb").unwrap(), "a/b");
407        assert_eq!(
408            ExtValue::parse(b"UTF-8''a b").unwrap_err().kind(),
409            ErrorKind::InvalidByte
410        );
411        assert_eq!(
412            ExtValue::parse(b"UTF-8''a\rb").unwrap_err().kind(),
413            ErrorKind::BareCr
414        );
415    }
416
417    #[test]
418    fn test_ext_value_buffer_and_lengths() {
419        let e = ExtValue::parse(b"ISO-8859-1''%E9%E9").unwrap();
420        assert_eq!(e.max_decoded_len(), 4);
421        let mut short = [0xaau8; 3];
422        assert_eq!(
423            e.decode_into(&mut short).unwrap_err().kind(),
424            ErrorKind::BufferTooSmall
425        );
426        assert_eq!(short, [0xaa; 3]);
427        let u = ExtValue::parse(b"UTF-8''%E2%82%AC").unwrap();
428        assert_eq!(u.max_decoded_len(), 3);
429    }
430
431    #[test]
432    fn test_write_ext_value_round_trip_and_refusals() {
433        let mut buf = [0u8; 128];
434        let mut out = crate::SliceSink::new(&mut buf);
435        assert_eq!(
436            write_ext_value("a\r\nb", &mut out),
437            Err(WriteError::Forbidden { at: 1 })
438        );
439        assert_eq!(
440            write_ext_value("x\u{85}", &mut out),
441            Err(WriteError::Forbidden { at: 1 })
442        );
443        write_ext_value("caf\u{e9} \u{1f600}/", &mut out).unwrap();
444        let back = ExtValue::parse(out.written()).unwrap();
445        let mut dec = [0u8; 64];
446        assert_eq!(back.decode_into(&mut dec).unwrap(), "caf\u{e9} \u{1f600}/");
447    }
448}