1use crate::ast::{Directive, Policy, PolicyList};
10
11pub fn parse_policy_list(input: &str) -> PolicyList {
20 PolicyList {
21 policies: input.split(',').map(parse_policy).collect(),
22 }
23}
24
25fn parse_policy(segment: &str) -> Policy {
26 let directives = segment
27 .trim_ascii()
28 .split(';')
29 .map(str::trim_ascii)
30 .filter(|token| !token.is_empty())
31 .map(parse_directive)
32 .collect();
33 Policy { directives }
34}
35
36fn parse_directive(token: &str) -> Directive {
37 match token.find(|c: char| c.is_ascii_whitespace()) {
38 Some(split_at) => {
39 let name = &token[..split_at];
40 let rest = token[split_at..].trim_ascii_start();
41 let raw_value = if rest.is_empty() {
42 None
43 } else {
44 Some(rest.to_string())
45 };
46 Directive {
47 name: name.to_string(),
48 raw_value,
49 }
50 }
51 None => Directive {
52 name: token.to_string(),
53 raw_value: None,
54 },
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 fn directive(name: &str, raw_value: Option<&str>) -> Directive {
63 Directive {
64 name: name.to_string(),
65 raw_value: raw_value.map(str::to_string),
66 }
67 }
68
69 #[test]
70 fn single_policy_multiple_directives() {
71 let result = parse_policy_list(
72 "default-src 'self'; img-src *; object-src 'none'; script-src example.com",
73 );
74 assert_eq!(
75 result,
76 PolicyList {
77 policies: vec![Policy {
78 directives: vec![
79 directive("default-src", Some("'self'")),
80 directive("img-src", Some("*")),
81 directive("object-src", Some("'none'")),
82 directive("script-src", Some("example.com")),
83 ],
84 }],
85 }
86 );
87 }
88
89 #[test]
90 fn comma_separated_policy_list() {
91 let result = parse_policy_list("default-src 'self', script-src 'none'");
92 assert_eq!(result.policies.len(), 2);
93 assert_eq!(
94 result.policies[0].directives,
95 vec![directive("default-src", Some("'self'"))]
96 );
97 assert_eq!(
98 result.policies[1].directives,
99 vec![directive("script-src", Some("'none'"))]
100 );
101 }
102
103 #[test]
104 fn empty_string_yields_single_empty_policy() {
105 let result = parse_policy_list("");
106 assert_eq!(
107 result,
108 PolicyList {
109 policies: vec![Policy { directives: vec![] }],
110 }
111 );
112 }
113
114 #[test]
115 fn whitespace_only_yields_empty_policy() {
116 let result = parse_policy_list(" \t ");
117 assert_eq!(result.policies, vec![Policy { directives: vec![] }]);
118 }
119
120 #[test]
121 fn semicolons_only_yield_empty_policy() {
122 let result = parse_policy_list(";;;");
123 assert_eq!(result.policies, vec![Policy { directives: vec![] }]);
124 }
125
126 #[test]
127 fn empty_segments_between_commas_yield_empty_policies() {
128 let result = parse_policy_list("default-src 'self',,report-to endpoint");
129 assert_eq!(result.policies.len(), 3);
130 assert_eq!(
131 result.policies[0].directives,
132 vec![directive("default-src", Some("'self'"))]
133 );
134 assert_eq!(result.policies[1].directives, vec![]);
135 assert_eq!(
136 result.policies[2].directives,
137 vec![directive("report-to", Some("endpoint"))]
138 );
139 }
140
141 #[test]
142 fn multiple_whitespace_between_name_and_value_is_stripped() {
143 let result = parse_policy_list("script-src 'self'");
144 assert_eq!(
145 result.policies[0].directives,
146 vec![directive("script-src", Some("'self'"))]
147 );
148 }
149
150 #[test]
151 fn valueless_directive() {
152 let result = parse_policy_list("upgrade-insecure-requests");
153 assert_eq!(
154 result.policies[0].directives,
155 vec![directive("upgrade-insecure-requests", None)]
156 );
157 }
158
159 #[test]
160 fn directive_name_with_digits_and_hyphen() {
161 let result = parse_policy_list("script-src-elem 'self'");
162 assert_eq!(
163 result.policies[0].directives,
164 vec![directive("script-src-elem", Some("'self'"))]
165 );
166 }
167
168 #[test]
169 fn leading_and_trailing_whitespace_around_directive_is_trimmed() {
170 let result = parse_policy_list(" script-src 'self' ; img-src * ");
171 assert_eq!(
172 result.policies[0].directives,
173 vec![
174 directive("script-src", Some("'self'")),
175 directive("img-src", Some("*")),
176 ]
177 );
178 }
179
180 #[test]
181 fn fault_tolerance_malformed_directive_next_to_valid_ones() {
182 let result = parse_policy_list("default-src 'self'; script-src 'self' \u{1}bad");
187 let directives = &result.policies[0].directives;
188 assert_eq!(directives.len(), 2);
189 assert!(directives[0].name_is_valid());
190 assert!(directives[0].value_is_valid());
191 assert!(directives[1].name_is_valid());
192 assert!(!directives[1].value_is_valid());
193 }
194
195 #[test]
196 fn invalid_directive_name_is_still_parsed_but_flagged() {
197 let result = parse_policy_list("bad_name 'self'");
198 let directive = &result.policies[0].directives[0];
199 assert_eq!(directive.name, "bad_name");
200 assert!(!directive.name_is_valid());
201 }
202
203 #[test]
204 fn duplicate_directive_names_are_both_kept() {
205 let result = parse_policy_list("default-src 'self'; default-src 'none'");
208 assert_eq!(
209 result.policies[0].directives,
210 vec![
211 directive("default-src", Some("'self'")),
212 directive("default-src", Some("'none'")),
213 ]
214 );
215 }
216}