ruma_common/
http_headers.rs

1//! Helpers for HTTP headers.
2
3use std::borrow::Cow;
4
5mod content_disposition;
6mod rfc8187;
7
8use http::HeaderValue;
9
10pub use self::content_disposition::{
11    ContentDisposition, ContentDispositionParseError, ContentDispositionType, TokenString,
12    TokenStringParseError,
13};
14
15/// The `application/json` media type as a [`HeaderValue`].
16pub const APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
17
18/// The `application/octet-stream` media type as a [`HeaderValue`].
19pub const APPLICATION_OCTET_STREAM: HeaderValue =
20    HeaderValue::from_static("application/octet-stream");
21
22/// Whether the given byte is a [`token` char].
23///
24/// [`token` char]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
25pub const fn is_tchar(b: u8) -> bool {
26    b.is_ascii_alphanumeric()
27        || matches!(
28            b,
29            b'!' | b'#'
30                | b'$'
31                | b'%'
32                | b'&'
33                | b'\''
34                | b'*'
35                | b'+'
36                | b'-'
37                | b'.'
38                | b'^'
39                | b'_'
40                | b'`'
41                | b'|'
42                | b'~'
43        )
44}
45
46/// Whether the given bytes slice is a [`token`].
47///
48/// [`token`]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
49pub fn is_token(bytes: &[u8]) -> bool {
50    bytes.iter().all(|b| is_tchar(*b))
51}
52
53/// Whether the given string is a [`token`].
54///
55/// [`token`]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
56pub fn is_token_string(s: &str) -> bool {
57    is_token(s.as_bytes())
58}
59
60/// Whether the given char is a [visible US-ASCII char].
61///
62/// [visible US-ASCII char]: https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1
63pub const fn is_vchar(c: char) -> bool {
64    matches!(c, '\x21'..='\x7E')
65}
66
67/// Whether the given char is in the US-ASCII character set and allowed inside a [quoted string].
68///
69/// Contrary to the definition of quoted strings, this doesn't allow `obs-text` characters, i.e.
70/// non-US-ASCII characters, as we usually deal with UTF-8 strings rather than ISO-8859-1 strings.
71///
72/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
73pub const fn is_ascii_string_quotable(c: char) -> bool {
74    is_vchar(c) || matches!(c, '\x09' | '\x20')
75}
76
77/// Remove characters that do not pass [`is_ascii_string_quotable()`] from the given string.
78///
79/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
80pub fn sanitize_for_ascii_quoted_string(value: &str) -> Cow<'_, str> {
81    if value.chars().all(is_ascii_string_quotable) {
82        return Cow::Borrowed(value);
83    }
84
85    Cow::Owned(value.chars().filter(|c| is_ascii_string_quotable(*c)).collect())
86}
87
88/// If the US-ASCII field value does not contain only token chars, convert it to a [quoted string].
89///
90/// The string should be sanitized with [`sanitize_for_ascii_quoted_string()`] or should only
91/// contain characters that pass [`is_ascii_string_quotable()`].
92///
93/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
94pub fn quote_ascii_string_if_required(value: &str) -> Cow<'_, str> {
95    if !value.is_empty() && is_token_string(value) {
96        return Cow::Borrowed(value);
97    }
98
99    let value = value.replace('\\', r#"\\"#).replace('"', r#"\""#);
100    Cow::Owned(format!("\"{value}\""))
101}
102
103/// Removes the escape backslashes in the given string.
104pub fn unescape_string(s: &str) -> String {
105    let mut is_escaped = false;
106
107    s.chars()
108        .filter(|c| {
109            is_escaped = *c == '\\' && !is_escaped;
110            !is_escaped
111        })
112        .collect()
113}