1use crate::error::FMLError;
6use crate::intermediate_representation::{FeatureDef, FeatureManifest, TypeFinder, TypeRef};
7use crate::{
8 error::Result,
9 intermediate_representation::{EnumDef, ObjectDef},
10};
11use regex::Regex;
12use std::collections::{BTreeMap, HashSet};
13
14const DISALLOWED_PREFS: &[(&str, &str)] = &[
15 (
16 r#"^app\.shield\.optoutstudies\.enabled$"#,
17 "disabling Nimbus causes immediate unenrollment",
18 ),
19 (
20 r#"^datareporting\.healthreport\.uploadEnabled$"#,
21 "disabling telemetry causes immediate unenrollment",
22 ),
23 (
24 r#"^services\.settings\.server$"#,
25 "changing the Remote Settings endpoint will break clients",
26 ),
27 (r#"^nimbus\.debug$"#, "internal Nimbus preference for QA"),
28 (
29 r#"^security\.turn_off_all_security_so_that_viruses_can_take_over_this_computer$"#,
30 "this pref is automation-only and is unsafe to enable outside tests",
31 ),
32];
33
34pub(crate) struct SchemaValidator<'a> {
35 enum_defs: &'a BTreeMap<String, EnumDef>,
36 object_defs: &'a BTreeMap<String, ObjectDef>,
37}
38
39impl<'a> SchemaValidator<'a> {
40 pub(crate) fn new(
41 enums: &'a BTreeMap<String, EnumDef>,
42 objs: &'a BTreeMap<String, ObjectDef>,
43 ) -> Self {
44 Self {
45 enum_defs: enums,
46 object_defs: objs,
47 }
48 }
49
50 fn _get_enum(&self, nm: &str) -> Option<&EnumDef> {
51 self.enum_defs.get(nm)
52 }
53
54 fn get_object(&self, nm: &str) -> Option<&ObjectDef> {
55 self.object_defs.get(nm)
56 }
57
58 pub(crate) fn validate_object_def(&self, object_def: &ObjectDef) -> Result<()> {
59 let obj_nm = &object_def.name;
60 for prop in &object_def.props {
61 let prop_nm = &prop.name;
62
63 let path = format!("objects/{obj_nm}/{prop_nm}");
65 self.validate_type_ref(&path, &prop.typ)?;
66 }
67
68 Ok(())
69 }
70
71 pub(crate) fn validate_feature_def(&self, feature_def: &FeatureDef) -> Result<()> {
72 let feat_nm = &feature_def.name;
73 let mut string_aliases: HashSet<_> = Default::default();
74
75 for prop in &feature_def.props {
76 let prop_nm = &prop.name;
77 let prop_t = &prop.typ;
78
79 let path = format!("features/{feat_nm}/{prop_nm}");
80
81 self.validate_type_ref(&path, prop_t)?;
83
84 if let Some(pref) = &prop.gecko_pref {
86 for (pref_str, error) in DISALLOWED_PREFS {
87 let regex = Regex::new(pref_str)?;
88 if regex.is_match(&pref.pref()) {
89 return Err(FMLError::ValidationError(
90 path,
91 format!(
92 "Cannot use pref `{}` in experiments, reason: {}",
93 pref.pref(),
94 error
95 ),
96 ));
97 }
98 }
99 }
100
101 if prop.gecko_pref.is_some() && !prop.typ.supports_prefs() {
103 return Err(FMLError::ValidationError(
104 path,
105 "Pref keys can only be used with Boolean, String, Int and Text variables"
106 .to_string(),
107 ));
108 }
109
110 if let Some(sa) = &prop.string_alias {
112 if !string_aliases.insert(sa) {
114 return Err(FMLError::ValidationError(
115 path,
116 format!("The string-alias {sa} should only be declared once per feature"),
117 ));
118 }
119
120 let types = prop_t.all_types();
122 if !types.contains(sa) {
123 return Err(FMLError::ValidationError(
124 path,
125 format!(
126 "The string-alias {sa} must be part of the {} type declaration",
127 prop_nm
128 ),
129 ));
130 }
131 }
132 }
133
134 let types = feature_def.all_types();
137 self.validate_string_alias_declarations(
138 &format!("features/{feat_nm}"),
139 feat_nm,
140 &types,
141 &string_aliases,
142 )?;
143
144 Ok(())
145 }
146
147 pub(crate) fn validate_prefs(&self, feature_manifest: &FeatureManifest) -> Result<()> {
148 let prefs = feature_manifest
149 .iter_gecko_prefs()
150 .map(|p| p.pref())
151 .collect::<Vec<String>>();
152 for pref in prefs.clone() {
153 if prefs
154 .iter()
155 .map(|p| if p == &pref { 1 } else { 0 })
156 .sum::<i32>()
157 > 1
158 {
159 let path = format!(r#"prefs/"{}""#, pref);
160 return Err(FMLError::ValidationError(
161 path,
162 "Prefs can only be include once per feature manifest".into(),
163 ));
164 }
165 }
166
167 Ok(())
168 }
169
170 fn validate_string_alias_declarations(
171 &self,
172 path: &str,
173 feature: &str,
174 types: &HashSet<TypeRef>,
175 string_aliases: &HashSet<&TypeRef>,
176 ) -> Result<()> {
177 let unaccounted: Vec<_> = types
178 .iter()
179 .filter(|t| matches!(t, TypeRef::StringAlias(_)))
180 .filter(|t| !string_aliases.contains(t))
181 .collect();
182
183 if !unaccounted.is_empty() {
184 let t = unaccounted.first().unwrap();
185 return Err(FMLError::ValidationError(
186 path.to_string(),
187 format!("A string-alias {t} is used by– but has not been defined in– the {feature} feature"),
188 ));
189 }
190 for t in types {
191 if let TypeRef::Object(nm) = t {
192 if let Some(obj) = self.get_object(nm) {
193 let types = obj.all_types();
194 self.validate_string_alias_declarations(
195 &format!("objects/{nm}"),
196 feature,
197 &types,
198 string_aliases,
199 )?;
200 }
201 }
202 }
203 Ok(())
204 }
205
206 fn validate_type_ref(&self, path: &str, type_ref: &TypeRef) -> Result<()> {
207 match type_ref {
208 TypeRef::Enum(name) => {
209 if !self.enum_defs.contains_key(name) {
210 return Err(FMLError::ValidationError(
211 path.to_string(),
212 format!("Found enum reference with name: {name}, but no definition"),
213 ));
214 }
215 }
216 TypeRef::Object(name) => {
217 if !self.object_defs.contains_key(name) {
218 return Err(FMLError::ValidationError(
219 path.to_string(),
220 format!("Found object reference with name: {name}, but no definition"),
221 ));
222 }
223 }
224 TypeRef::EnumMap(key_type, value_type) => match key_type.as_ref() {
225 TypeRef::Enum(_) | TypeRef::String | TypeRef::StringAlias(_) => {
226 self.validate_type_ref(path, key_type)?;
227 self.validate_type_ref(path, value_type)?;
228 }
229 _ => {
230 return Err(FMLError::ValidationError(
231 path.to_string(),
232 format!(
233 "Map key must be a String, string-alias or enum, found: {key_type:?}",
234 ),
235 ))
236 }
237 },
238 TypeRef::List(list_type) => self.validate_type_ref(path, list_type)?,
239 TypeRef::StringMap(value_type) => self.validate_type_ref(path, value_type)?,
240 TypeRef::Option(option_type) => {
241 if let TypeRef::Option(_) = option_type.as_ref() {
242 return Err(FMLError::ValidationError(
243 path.to_string(),
244 "Found nested optional types".into(),
245 ));
246 } else {
247 self.validate_type_ref(path, option_type)?
248 }
249 }
250 _ => (),
251 };
252 Ok(())
253 }
254}
255
256#[cfg(test)]
257mod manifest_schema {
258 use serde_json::json;
259
260 use super::*;
261 use crate::error::Result;
262 use crate::intermediate_representation::{PrefBranch, PropDef};
263
264 #[test]
265 fn validate_enum_type_ref_doesnt_match_def() -> Result<()> {
266 let enums = Default::default();
267 let objs = Default::default();
268 let validator = SchemaValidator::new(&enums, &objs);
269 let fm = FeatureDef::new(
270 "some_def",
271 "test doc",
272 vec![PropDef::new(
273 "prop name",
274 &TypeRef::Enum("EnumDoesntExist".into()),
275 &json!(null),
276 )],
277 false,
278 );
279 validator.validate_feature_def(&fm).expect_err(
280 "Should fail since EnumDoesntExist isn't a an enum defined in the manifest",
281 );
282 Ok(())
283 }
284
285 #[test]
286 fn validate_obj_type_ref_doesnt_match_def() -> Result<()> {
287 let enums = Default::default();
288 let objs = Default::default();
289 let validator = SchemaValidator::new(&enums, &objs);
290 let fm = FeatureDef::new(
291 "some_def",
292 "test doc",
293 vec![PropDef::new(
294 "prop name",
295 &TypeRef::Object("ObjDoesntExist".into()),
296 &json!(null),
297 )],
298 false,
299 );
300 validator.validate_feature_def(&fm).expect_err(
301 "Should fail since ObjDoesntExist isn't a an Object defined in the manifest",
302 );
303 Ok(())
304 }
305
306 #[test]
307 fn validate_enum_map_with_non_enum_key() -> Result<()> {
308 let enums = Default::default();
309 let objs = Default::default();
310 let validator = SchemaValidator::new(&enums, &objs);
311 let fm = FeatureDef::new(
312 "some_def",
313 "test doc",
314 vec![PropDef::new(
315 "prop_name",
316 &TypeRef::EnumMap(Box::new(TypeRef::Int), Box::new(TypeRef::String)),
317 &json!(null),
318 )],
319 false,
320 );
321 validator
322 .validate_feature_def(&fm)
323 .expect_err("Should fail since the key on an EnumMap must be an Enum");
324 Ok(())
325 }
326
327 #[test]
328 fn validate_list_with_enum_with_no_def() -> Result<()> {
329 let enums = Default::default();
330 let objs = Default::default();
331 let validator = SchemaValidator::new(&enums, &objs);
332 let fm = FeatureDef::new(
333 "some_def",
334 "test doc",
335 vec![PropDef::new(
336 "prop name",
337 &TypeRef::List(Box::new(TypeRef::Enum("EnumDoesntExist".into()))),
338 &json!(null),
339 )],
340 false,
341 );
342 validator
343 .validate_feature_def(&fm)
344 .expect_err("Should fail EnumDoesntExist isn't a an enum defined in the manifest");
345 Ok(())
346 }
347
348 #[test]
349 fn validate_enum_map_with_enum_with_no_def() -> Result<()> {
350 let enums = Default::default();
351 let objs = Default::default();
352 let validator = SchemaValidator::new(&enums, &objs);
353 let fm = FeatureDef::new(
354 "some_def",
355 "test doc",
356 vec![PropDef::new(
357 "prop name",
358 &TypeRef::EnumMap(
359 Box::new(TypeRef::Enum("EnumDoesntExist".into())),
360 Box::new(TypeRef::String),
361 ),
362 &json!(null),
363 )],
364 false,
365 );
366 validator.validate_feature_def(&fm).expect_err(
367 "Should fail since EnumDoesntExist isn't a an enum defined in the manifest",
368 );
369 Ok(())
370 }
371
372 #[test]
373 fn validate_enum_map_with_obj_value_no_def() -> Result<()> {
374 let enums = Default::default();
375 let objs = Default::default();
376 let validator = SchemaValidator::new(&enums, &objs);
377 let fm = FeatureDef::new(
378 "some_def",
379 "test doc",
380 vec![PropDef::new(
381 "prop name",
382 &TypeRef::EnumMap(
383 Box::new(TypeRef::String),
384 Box::new(TypeRef::Object("ObjDoesntExist".into())),
385 ),
386 &json!(null),
387 )],
388 false,
389 );
390 validator
391 .validate_feature_def(&fm)
392 .expect_err("Should fail since ObjDoesntExist isn't an Object defined in the manifest");
393 Ok(())
394 }
395
396 #[test]
397 fn validate_string_map_with_enum_value_no_def() -> Result<()> {
398 let enums = Default::default();
399 let objs = Default::default();
400 let validator = SchemaValidator::new(&enums, &objs);
401 let fm = FeatureDef::new(
402 "some_def",
403 "test doc",
404 vec![PropDef::new(
405 "prop name",
406 &TypeRef::StringMap(Box::new(TypeRef::Enum("EnumDoesntExist".into()))),
407 &json!(null),
408 )],
409 false,
410 );
411 validator
412 .validate_feature_def(&fm)
413 .expect_err("Should fail since ObjDoesntExist isn't an Object defined in the manifest");
414 Ok(())
415 }
416
417 #[test]
418 fn validate_nested_optionals_fail() -> Result<()> {
419 let enums = Default::default();
420 let objs = Default::default();
421 let validator = SchemaValidator::new(&enums, &objs);
422 let fm = FeatureDef::new(
423 "some_def",
424 "test doc",
425 vec![PropDef::new(
426 "prop name",
427 &TypeRef::Option(Box::new(TypeRef::Option(Box::new(TypeRef::String)))),
428 &json!(null),
429 )],
430 false,
431 );
432 validator
433 .validate_feature_def(&fm)
434 .expect_err("Should fail since we can't have nested optionals");
435 Ok(())
436 }
437
438 #[test]
439 fn validate_disallowed_pref_fails() -> Result<()> {
440 let enums = Default::default();
441 let objs = Default::default();
442 let validator = SchemaValidator::new(&enums, &objs);
443 let fm = FeatureDef::new(
444 "some_def",
445 "test doc",
446 vec![PropDef::new_with_gecko_pref(
447 "prop name",
448 &TypeRef::String,
449 &json!(null),
450 "app.shield.optoutstudies.enabled",
451 PrefBranch::User,
452 )],
453 false,
454 );
455 validator
456 .validate_feature_def(&fm)
457 .expect_err("Should fail since we can't use that pref for experimentation");
458 Ok(())
459 }
460}
461
462#[cfg(test)]
463mod string_aliases {
464 use serde_json::json;
465
466 use crate::intermediate_representation::PropDef;
467
468 use super::*;
469
470 fn with_objects(objects: &[ObjectDef]) -> BTreeMap<String, ObjectDef> {
471 let mut obj_defs: BTreeMap<_, _> = Default::default();
472 for o in objects {
473 obj_defs.insert(o.name(), o.clone());
474 }
475 obj_defs
476 }
477
478 fn with_feature(props: &[PropDef]) -> FeatureDef {
479 FeatureDef::new("test-feature", "", props.into(), false)
480 }
481
482 #[test]
483 fn test_validate_feature_schema() -> Result<()> {
484 let name = TypeRef::StringAlias("PersonName".to_string());
485 let all_names = {
486 let t = TypeRef::List(Box::new(name.clone()));
487 let v = json!(["Alice", "Bonnie", "Charlie", "Denise", "Elise", "Frankie"]);
488 PropDef::with_string_alias("all-names", &t, &v, &name)
489 };
490
491 let all_names2 = {
492 let t = TypeRef::List(Box::new(name.clone()));
493 let v = json!(["Alice", "Bonnie"]);
494 PropDef::with_string_alias("all-names-duplicate", &t, &v, &name)
495 };
496
497 let enums = Default::default();
498 let objects = Default::default();
499 let validator = SchemaValidator::new(&enums, &objects);
500
501 let fm = with_feature(&[all_names.clone(), all_names2.clone()]);
503 assert!(validator.validate_feature_def(&fm).is_err());
504
505 let newest_member = {
506 let t = &name;
507 let v = json!("Alice"); PropDef::new("newest-member", t, &v)
509 };
510
511 let fm = with_feature(&[all_names.clone(), newest_member.clone()]);
515 validator.validate_feature_def(&fm)?;
516
517 let fm = with_feature(&[newest_member.clone()]);
521 assert!(validator.validate_feature_def(&fm).is_err());
522
523 let team_def = ObjectDef::new("Team", &[newest_member.clone()]);
526 let team = {
527 let t = TypeRef::Object("Team".to_string());
528 let v = json!({ "newest-member": "Alice" });
529
530 PropDef::new("team", &t, &v)
531 };
532
533 let fm = with_feature(&[all_names.clone(), team.clone()]);
535 let objs = with_objects(&[team_def.clone()]);
536 let validator = SchemaValidator::new(&enums, &objs);
537 validator.validate_feature_def(&fm)?;
538
539 let fm = with_feature(&[team.clone()]);
541 let objs = with_objects(&[team_def.clone()]);
542 let validator = SchemaValidator::new(&enums, &objs);
543 assert!(validator.validate_feature_def(&fm).is_err());
544
545 let match_def = ObjectDef::new("Match", &[team.clone()]);
549 let match_ = {
550 let t = TypeRef::Object("Match".to_string());
551 let v = json!({ "team": { "newest-member": "Alice" }});
552
553 PropDef::new("match", &t, &v)
554 };
555
556 let fm = with_feature(&[all_names.clone(), match_.clone()]);
558 let objs = with_objects(&[team_def.clone(), match_def.clone()]);
559 let validator = SchemaValidator::new(&enums, &objs);
560 validator.validate_feature_def(&fm)?;
561
562 let fm = with_feature(&[match_.clone()]);
564 let validator = SchemaValidator::new(&enums, &objs);
565 assert!(validator.validate_feature_def(&fm).is_err());
566
567 Ok(())
568 }
569}