1use 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#[inline(always)]
19fn ows(input: &str) -> IResult<&str, &str> {
20 space0(input)
21}
22
23#[inline]
25fn vchar(input: &str) -> IResult<&str, char> {
26 verify(anychar, |c| c.is_ascii_graphic())(input)
27}
28
29#[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#[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#[inline]
54fn quoted_pair(input: &str) -> IResult<&str, char> {
55 preceded(nchar('\\'), alt((tab, nchar(' '), vchar, obs_text)))(input)
56}
57
58#[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#[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#[inline]
78fn token(input: &str) -> IResult<&str, &str> {
79 recognize(many1(tchar))(input)
80}
81
82fn 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
97fn 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}