1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use std::time::Duration;

use super::Metric;
use crate::{ensure, format_err, StatusCode};

/// Parse multiple entries from a single header.
///
/// Each entry is comma-delimited.
pub(super) fn parse_header(s: &str, entries: &mut Vec<Metric>) -> crate::Result<()> {
    for part in s.trim().split(',') {
        let entry = parse_entry(part).map_err(|mut e| {
            e.set_status(StatusCode::BadRequest);
            e
        })?;
        entries.push(entry);
    }
    Ok(())
}

/// Create an entry from a string. Parsing rules in ABNF are:
//
/// ```txt
/// Server-Timing             = #server-timing-metric
/// server-timing-metric      = metric-name *( OWS ";" OWS server-timing-param )
/// metric-name               = token
/// server-timing-param       = server-timing-param-name OWS "=" OWS server-timing-param-value
/// server-timing-param-name  = token
/// server-timing-param-value = token / quoted-string
/// ```
//
/// Source: https://w3c.github.io/server-timing/#the-server-timing-header-field
fn parse_entry(s: &str) -> crate::Result<Metric> {
    let mut parts = s.trim().split(';');

    // Get the name. This is non-optional.
    let name = parts
        .next()
        .ok_or_else(|| format_err!("Server timing headers must include a name"))?
        .trim_end();

    // We must extract these values from the k-v pairs that follow.
    let mut dur = None;
    let mut desc = None;

    for mut part in parts {
        ensure!(
            !part.is_empty(),
            "Server timing params cannot end with a trailing `;`"
        );

        part = part.trim_start();

        let mut params = part.split('=');
        let name = params
            .next()
            .ok_or_else(|| format_err!("Server timing params must have a name"))?
            .trim_end();
        let mut value = params
            .next()
            .ok_or_else(|| format_err!("Server timing params must have a value"))?
            .trim_start();

        match name {
            "dur" => {
                let millis: f64 = value.parse().map_err(|_| {
                        format_err!("Server timing duration params must be a valid double-precision floating-point number.")
                    })?;
                dur = Some(Duration::from_secs_f64(millis / 1000.0));
            }
            "desc" => {
                // Ensure quotes line up, and strip them from the resulting output
                if value.starts_with('"') {
                    value = &value[1..value.len()];
                    ensure!(
                        value.ends_with('"'),
                        "Server timing description params must use matching quotes"
                    );
                    value = &value[0..value.len() - 1];
                } else {
                    ensure!(
                        !value.ends_with('"'),
                        "Server timing description params must use matching quotes"
                    );
                }
                desc = Some(value.to_string());
            }
            _ => continue,
        }
    }

    Ok(Metric {
        name: name.to_string(),
        dur,
        desc,
    })
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn decode_header() -> crate::Result<()> {
        // Metric name only.
        assert_entry("Server", "Server", None, None)?;
        assert_entry("Server ", "Server", None, None)?;
        assert_entry_err(
            "Server ;",
            "Server timing params cannot end with a trailing `;`",
        );
        assert_entry_err(
            "Server; ",
            "Server timing params cannot end with a trailing `;`",
        );

        // Metric name + param
        assert_entry("Server; dur=1000", "Server", Some(1000), None)?;
        assert_entry("Server; dur =1000", "Server", Some(1000), None)?;
        assert_entry("Server; dur= 1000", "Server", Some(1000), None)?;
        assert_entry("Server; dur = 1000", "Server", Some(1000), None)?;
        assert_entry_err(
            "Server; dur=1000;",
            "Server timing params cannot end with a trailing `;`",
        );

        // Metric name + desc
        assert_entry(r#"DB; desc="a db""#, "DB", None, Some("a db"))?;
        assert_entry(r#"DB; desc ="a db""#, "DB", None, Some("a db"))?;
        assert_entry(r#"DB; desc= "a db""#, "DB", None, Some("a db"))?;
        assert_entry(r#"DB; desc = "a db""#, "DB", None, Some("a db"))?;
        assert_entry(r#"DB; desc=a_db"#, "DB", None, Some("a_db"))?;
        assert_entry_err(
            r#"DB; desc="db"#,
            "Server timing description params must use matching quotes",
        );
        assert_entry_err(
            "Server; desc=a_db;",
            "Server timing params cannot end with a trailing `;`",
        );

        // Metric name + dur + desc
        assert_entry(
            r#"Server; dur=1000; desc="a server""#,
            "Server",
            Some(1000),
            Some("a server"),
        )?;
        assert_entry_err(
            r#"Server; dur=1000; desc="a server";"#,
            "Server timing params cannot end with a trailing `;`",
        );
        Ok(())
    }

    #[test]
    fn decode_headers() -> crate::Result<()> {
        // Example from MDN.
        let mut entries = vec![];
        parse_header("db;dur=53, app;dur=47.2", &mut entries)?;
        let e = &entries[0];
        assert_eq!(e.name(), "db");
        assert_eq!(e.duration(), Some(Duration::from_millis(53)));
        let e = &entries[1];
        assert_eq!(e.name(), "app");
        assert_eq!(e.duration(), Some(Duration::from_micros(47200)));
        Ok(())
    }

    fn assert_entry_err(s: &str, msg: &str) {
        let err = parse_entry(s).unwrap_err();
        assert_eq!(format!("{}", err), msg);
    }

    /// Assert an entry and all of its fields.
    fn assert_entry(s: &str, n: &str, du: Option<u64>, de: Option<&str>) -> crate::Result<()> {
        let e = parse_entry(s)?;
        assert_eq!(e.name(), n);
        assert_eq!(e.duration(), du.map(Duration::from_millis));
        assert_eq!(e.description(), de);
        Ok(())
    }
}