server_timing/
parser.rs

1//! Follows the W3C Server Timing spec and RFC 7230 for definitions of the Server Timing header and its grammar.
2//! https://w3c.github.io/server-timing/#the-server-timing-header-field
3//! https://httpwg.org/specs/rfc7230.html
4
5use std::borrow::Cow;
6
7use nom::{
8    branch::alt,
9    bytes::complete::tag,
10    character::complete::{anychar, char as nchar, one_of, space0, tab},
11    combinator::{recognize, verify},
12    multi::{many0, many1, separated_list1},
13    sequence::{delimited, pair, preceded, separated_pair},
14    IResult, Parser as _,
15};
16
17/// optional whitespace
18#[inline(always)]
19fn ows(input: &str) -> IResult<&str, &str> {
20    space0(input)
21}
22
23/// Any visible USASCII character
24#[inline]
25fn vchar(input: &str) -> IResult<&str, char> {
26    verify(anychar, |c| c.is_ascii_graphic())(input)
27}
28
29/// A string of text is parsed as a single value if it is quoted using double-quote marks.
30#[inline]
31fn quoted_string(input: &str) -> IResult<&str, String> {
32    let (input, chars) = delimited(tag("\""), many0(alt((qdtext, quoted_pair))), tag("\""))(input)?;
33    Ok((input, chars.into_iter().collect()))
34}
35
36/// HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
37#[inline]
38fn qdtext(input: &str) -> IResult<&str, char> {
39    alt((
40        tab,
41        nchar(' '),
42        verify(anychar, |c| {
43            let c = *c as u32;
44            c == 0x21 || (c >= 0x23 && c <= 0x5B) || (c >= 0x5D && c <= 0x7E)
45        }),
46        obs_text,
47    ))(input)
48}
49
50/// The backslash octet ("\") can be used as a single-octet quoting mechanism within quoted-string and comment
51/// constructs. Recipients that process the value of a quoted-string MUST handle a quoted-pair as if it were replaced by
52/// the octet following the backslash.
53#[inline]
54fn quoted_pair(input: &str) -> IResult<&str, char> {
55    preceded(nchar('\\'), alt((tab, nchar(' '), vchar, obs_text)))(input)
56}
57
58/// %x80-FF (obsolete)
59#[inline]
60fn obs_text(input: &str) -> IResult<&str, char> {
61    verify(anychar, |c| {
62        let c = *c as u32;
63        c >= 0x80 && c <= 0xFF
64    })(input)
65}
66
67/// any VCHAR, except delimiters
68#[inline]
69fn tchar(input: &str) -> IResult<&str, char> {
70    alt((
71        (one_of("!#$'*+-.^_`|~")),
72        verify(anychar, |c| c.is_ascii_alphanumeric()),
73    ))(input)
74}
75
76/// 1*tchar
77#[inline]
78fn token(input: &str) -> IResult<&str, &str> {
79    recognize(many1(tchar))(input)
80}
81
82///  server-timing-param       = server-timing-param-name OWS "=" OWS server-timing-param-value
83///  server-timing-param-name  = token
84///  server-timing-param-value = token / quoted-string
85fn server_timing_param(input: &str) -> IResult<&str, (&str, Cow<str>)> {
86    let server_timing_param_name = token;
87    let server_timing_param_value = alt((token.map(Cow::Borrowed), quoted_string.map(Cow::Owned)));
88    separated_pair(
89        server_timing_param_name,
90        delimited(ows, tag("="), ows),
91        server_timing_param_value,
92    )(input)
93}
94
95pub type Metric<'a> = (&'a str, Vec<(&'a str, Cow<'a, str>)>);
96
97///  Server-Timing             = #server-timing-metric
98///  server-timing-metric      = metric-name *( OWS ";" OWS server-timing-param )
99///  metric-name               = token
100fn server_timing_metric(input: &str) -> IResult<&str, Metric> {
101    let metric_name = token;
102    let params = many0(preceded(delimited(ows, tag(";"), ows), server_timing_param));
103    pair(metric_name, params)(input)
104}
105
106pub fn server_timing(input: &str) -> IResult<&str, Vec<Metric>> {
107    separated_list1(delimited(ows, tag(","), ows), server_timing_metric)(input)
108}
109
110#[cfg(test)]
111mod test {
112    use super::{server_timing, server_timing_metric, server_timing_param};
113
114    #[test]
115    fn test_server_timing_metric_param_order() {
116        let input = "foo;dur=12.3;desc=bar";
117        let (rest, (name, params)) = server_timing_metric(input).unwrap();
118        assert_eq!(rest, "");
119        assert_eq!(name, "foo");
120        assert_eq!(params, vec![("dur", "12.3".into()), ("desc", "bar".into())]);
121    }
122
123    #[test]
124    fn test_server_timing_param_bare_string() {
125        let input = "desc=bar;dur=12.3";
126        let (rest, (key, value)) = server_timing_param(input).unwrap();
127        assert_eq!(rest, ";dur=12.3");
128        assert_eq!(key, "desc");
129        assert_eq!(value, "bar");
130    }
131
132    #[test]
133    fn test_server_timing_param_bare_string_spaces() {
134        let input = "desc=bar baz;dur=12.3";
135        let (rest, (key, value)) = server_timing_param(input).unwrap();
136        assert_eq!(rest, " baz;dur=12.3");
137        assert_eq!(key, "desc");
138        assert_eq!(value, "bar");
139    }
140
141    #[test]
142    fn test_server_timing_param_quoted_string() {
143        let input = "desc=\"hello=world;description\";dur=12.3";
144        let (rest, (key, value)) = server_timing_param(input).unwrap();
145        assert_eq!(rest, ";dur=12.3");
146        assert_eq!(key, "desc");
147        assert_eq!(value, "hello=world;description");
148    }
149
150    #[test]
151    fn test_server_timing_param_quoted_string_empty() {
152        let input = "desc=\"\";dur=12.3";
153        let (rest, (key, value)) = server_timing_param(input).unwrap();
154        assert_eq!(rest, ";dur=12.3");
155        assert_eq!(key, "desc");
156        assert_eq!(value, "");
157    }
158
159    #[test]
160    fn test_server_timing_param_quoted_string_spaces() {
161        let input = "desc=\"bar baz\";dur=12.3";
162        let (rest, (key, value)) = server_timing_param(input).unwrap();
163        assert_eq!(rest, ";dur=12.3");
164        assert_eq!(key, "desc");
165        assert_eq!(value, "bar baz");
166    }
167
168    #[test]
169    fn test_server_timing_param_quoted_string_escaped() {
170        let input = "desc=\"\\\"\";dur=12.3";
171        let (rest, (key, value)) = server_timing_param(input).unwrap();
172        assert_eq!(rest, ";dur=12.3");
173        assert_eq!(key, "desc");
174        assert_eq!(value, "\"");
175    }
176
177    #[test]
178    fn test_server_timing_metric() {
179        let input = "foo;desc=bar;dur=12.3";
180        let (rest, (name, params)) = server_timing_metric(input).unwrap();
181        assert_eq!(rest, "");
182        assert_eq!(name, "foo");
183        assert_eq!(params, vec![("desc", "bar".into()), ("dur", "12.3".into())]);
184    }
185
186    #[test]
187    fn test_server_timing_metric_param_unknown() {
188        let input = "foo;bar=baz";
189        let (rest, (name, params)) = server_timing_metric(input).unwrap();
190        assert_eq!(rest, "");
191        assert_eq!(name, "foo");
192        assert_eq!(params, vec![("bar", "baz".into())]);
193    }
194
195    #[test]
196    fn test_server_timing() {
197        let input = "foo;bar=baz, hello, db;dur=12.3;desc=mysql";
198        let (rest, metrics) = server_timing(input).unwrap();
199        let expected = vec![
200            ("foo", vec![("bar", "baz".into())]),
201            ("hello", vec![]),
202            ("db", vec![("dur", "12.3".into()), ("desc", "mysql".into())]),
203        ];
204        assert_eq!(rest, "");
205        assert_eq!(metrics, expected);
206    }
207
208    #[test]
209    fn test_comma_in_quoted_string() {
210        let input = "foo;desc=\"bar, baz\", hello";
211        let (rest, metrics) = server_timing(input).unwrap();
212        assert_eq!(rest, "");
213        assert_eq!(
214            metrics,
215            vec![
216                ("foo", vec![("desc", "bar, baz".into())]),
217                ("hello", vec![])
218            ]
219        );
220    }
221}