1mod ast;
23mod check;
24pub mod coverage;
25mod diag;
26pub mod eval;
27mod lexer;
28mod parser;
29pub mod paths;
30
31pub use ast::*;
32pub use diag::{Issue, Position, Severity};
33
34#[derive(Debug, Clone, Default)]
36pub struct SourceFile {
37 pub name: Option<String>,
38 pub content: String,
39}
40
41#[derive(Debug, Clone)]
43pub struct Compilation {
44 pub ruleset: Option<Ruleset>,
46 pub issues: Vec<Issue>,
47}
48
49impl Compilation {
50 pub fn has_errors(&self) -> bool {
51 self.issues.iter().any(|i| i.severity == Severity::Error)
52 }
53
54 pub fn error_message(&self) -> String {
57 let mut message = "Error compiling rules:".to_owned();
58 for issue in &self.issues {
59 message.push('\n');
60 message.push_str(&issue.line());
61 }
62 message
63 }
64}
65
66pub fn compile(files: &[SourceFile]) -> Compilation {
72 std::thread::scope(|scope| {
73 let thread = std::thread::Builder::new()
74 .name("hidane-rules".into())
75 .stack_size(COMPILE_STACK)
76 .spawn_scoped(scope, || compile_here(files));
77 match thread {
78 Ok(thread) => thread
79 .join()
80 .unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
81 Err(_) => compile_here(files),
83 }
84 })
85}
86
87const COMPILE_STACK: usize = 256 * 1024 * 1024;
89
90fn compile_here(files: &[SourceFile]) -> Compilation {
91 if files.is_empty() || files.iter().any(|f| f.content.is_empty()) {
92 return Compilation {
93 ruleset: None,
94 issues: vec![Issue {
95 file_name: None,
96 position: None,
97 description: "Rules content empty. Compilation aborted.".to_owned(),
98 severity: Severity::Error,
99 }],
100 };
101 }
102 let mut parsed: Vec<parser::Parsed> = files.iter().map(|f| parser::parse(&f.content)).collect();
103 let finish = |parsed: Vec<parser::Parsed>, ruleset| {
104 let mut issues = Vec::new();
105 for (file, p) in files.iter().zip(parsed) {
106 let mut diags = p.diags;
107 diag::sort(&mut diags);
108 let offsets = diag::Offsets::new(&file.content);
109 issues.extend(
110 diags
111 .into_iter()
112 .map(|d| d.into_issue(&offsets, file.name.as_deref())),
113 );
114 }
115 Compilation { ruleset, issues }
116 };
117 if parsed.iter().any(|p| p.syntax_error) {
118 return finish(parsed, None);
119 }
120 let mut supported = true;
121 let mut versions = Vec::new();
122 for p in &mut parsed {
123 let version = check::version(&p.file, &mut p.diags);
124 let too_deep = check::structure(&p.file, version, &mut p.diags);
125 supported &= version.is_some() && !too_deep;
126 versions.push(version);
127 }
128 if !supported {
129 return finish(parsed, None);
130 }
131 for p in &mut parsed {
132 check::packages(&p.file, &mut p.diags);
133 check::numbers(&p.file, &mut p.diags);
134 check::complexity(&p.file, &mut p.diags);
135 }
136 let has_errors = |parsed: &[parser::Parsed]| {
137 parsed
138 .iter()
139 .flat_map(|p| &p.diags)
140 .any(|d| d.severity == Severity::Error)
141 };
142 if has_errors(&parsed) {
143 return finish(parsed, None);
144 }
145 let version = versions[0].unwrap_or(Version::V1);
146 let first = &mut parsed[0];
147 check::semantics(&first.file, version, &mut first.diags);
148 if has_errors(&parsed) {
149 return finish(parsed, None);
150 }
151 let first = std::mem::take(&mut parsed[0].file);
152 let ruleset = Ruleset {
153 version,
154 file_name: files[0].name.clone(),
155 functions: first.functions,
156 services: first.services,
157 parens: first.parens,
158 };
159 finish(parsed, Some(ruleset))
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn file(name: &str, content: &str) -> SourceFile {
167 SourceFile {
168 name: Some(name.to_owned()),
169 content: content.to_owned(),
170 }
171 }
172
173 fn rules(condition: &str) -> String {
174 format!(
175 "rules_version = '2';\nservice cloud.firestore {{\n match /databases/{{database}}/documents {{\n match /a/{{x}} {{\n allow read: if {condition};\n }}\n }}\n}}\n"
176 )
177 }
178
179 #[test]
180 fn a_ruleset_with_warnings() {
181 let c = compile(&[file("f.rules", &rules("'😀' != '' && foo() && x != null"))]);
182 assert!(!c.has_errors());
183 let ruleset = c.ruleset.expect("compiled");
184 assert_eq!(ruleset.version, Version::V2);
185 assert!(ruleset.service("cloud.firestore").is_some());
186 let issues: Vec<_> = c
187 .issues
188 .iter()
189 .map(|i| {
190 let p = i.position.unwrap();
191 (
192 p.line,
193 p.column,
194 p.current_offset,
195 p.end_offset,
196 i.description.as_str(),
197 )
198 })
199 .collect();
200 assert_eq!(
202 issues,
203 vec![
204 (5, 35, 142, 144, "Invalid function name: foo."),
205 (
206 5,
207 46,
208 153,
209 154,
210 "The sub-expressions are not comparable, so this comparison will always return true."
211 ),
212 ]
213 );
214 assert_eq!(c.issues[0].file_name.as_deref(), Some("f.rules"));
215 }
216
217 #[test]
218 fn errors_come_with_the_warnings_found_with_them() {
219 let src = "service cloud.firestore {\n function f() { let a = 1; return a == 1; }\n match /a { allow read: if f(); }\n}\n";
220 let c = compile(&[file("f.rules", src)]);
221 assert!(c.has_errors() && c.ruleset.is_none());
222 assert_eq!(
223 c.error_message(),
224 "Error compiling rules:\n\
225 L0:0 Ruleset uses old version (version [1]). Please update to the latest version (version [2]).\n\
226 L2:22 Let bindings are not allowed in rules_version=\"1\";"
227 );
228 }
229
230 #[test]
231 fn only_the_first_file_is_checked_in_full() {
232 let c = compile(&[
233 file("a.rules", &rules("true")),
234 file("b.rules", &rules("foo()")),
235 ]);
236 assert!(c.issues.is_empty() && c.ruleset.is_some());
237 let c = compile(&[
238 file("a.rules", &rules("true")),
239 file("b.rules", &rules("true &&")),
240 ]);
241 assert!(c.has_errors());
242 assert_eq!(c.issues[0].file_name.as_deref(), Some("b.rules"));
243 let c = compile(&[file("a.rules", &rules("true")), file("b.rules", "")]);
244 assert_eq!(
245 c.error_message(),
246 "Error compiling rules:\nL0:0 Rules content empty. Compilation aborted."
247 );
248 }
249
250 #[test]
251 fn stages() {
252 let bad_path = |condition: &str| {
254 format!(
255 "rules_version = '2';\nservice s {{\n match /{{a}}/{{a}} {{\n allow read: {condition};\n }}\n}}\n"
256 )
257 };
258 let c = compile(&[file("f", &bad_path("if true &&"))]);
259 assert_eq!(c.issues.len(), 1);
260 let c = compile(&[file("f", &bad_path("true"))]);
261 let lines: Vec<_> = c.issues.iter().map(Issue::line).collect();
262 assert_eq!(
263 lines,
264 vec![
265 "L3:15 Variable a already exists in the PathTemplate.",
266 "L4:17 Missing 'if' before conditional expression.",
267 ]
268 );
269 let c = compile(&[file(
271 "f",
272 "rules_version = '3';\nservice s { match /a { allow read: if math == 1; } }",
273 )]);
274 assert_eq!(
275 c.error_message(),
276 "Error compiling rules:\nL1:1 Unsupported rules version 3."
277 );
278 }
279
280 #[test]
281 fn int_literals_beyond_64_bits() {
282 for (condition, error) in [
283 ("-9223372036854775808 == 1", None),
284 ("- 9223372036854775808 == 1", None),
285 (
286 "-9223372036854775809 == 1",
287 Some("L5:23 Invalid number literal 9223372036854775809."),
288 ),
289 (
290 "-(9223372036854775808) == 1",
291 Some("L5:24 Invalid number literal 9223372036854775808."),
292 ),
293 (
294 "1 - 9223372036854775808 == 1",
295 Some("L5:26 Invalid number literal 9223372036854775808."),
296 ),
297 (
298 "[9223372036854775808] == 1",
299 Some("L5:23 Invalid number literal 9223372036854775808."),
300 ),
301 ] {
302 let c = compile(&[file("f", &rules(condition))]);
303 let lines: Vec<String> = c.issues.iter().map(Issue::line).collect();
304 assert_eq!(lines, error.into_iter().collect::<Vec<_>>(), "{condition}");
305 }
306 }
307
308 #[test]
310 fn deep_input() {
311 let service = |body: String| format!("rules_version = '2';\nservice s {{\n{body}}}\n");
312 for (src, first) in [
313 (
314 rules(&format!("{}true{}", "(".repeat(50_000), ")".repeat(50_000))),
315 "L5:121 Expression is too complex to evaluate safely.",
316 ),
317 (
318 rules(&format!("{}true", "!".repeat(50_000))),
319 "L5:121 Expression is too complex to evaluate safely.",
320 ),
321 (
322 rules(&vec!["true"; 200_000].join(" && ")),
323 "L5:22 Expression is too complex to evaluate safely.",
324 ),
325 (
326 rules(&format!("request{} == 1", ".a".repeat(100_000))),
327 "L5:4029 Expression is too complex to evaluate safely.",
328 ),
329 (
330 service(format!(
331 "{}allow read;\n{}",
332 "match /a {\n".repeat(5_000),
333 "}\n".repeat(5_000)
334 )),
335 "L66:1 Rule hierarchy is too complex to evaluate safely.",
336 ),
337 ] {
338 let c = compile(&[file("f", &src)]);
339 assert!(c.has_errors());
340 assert_eq!(c.issues[0].line(), first);
341 }
342 }
343}