cm_fidl_validator/
util.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::*;
6use cm_types::{LongName, Name, ParseError, Path, RelativePath, Url, UrlScheme};
7use fidl_fuchsia_component_decl as fdecl;
8use std::collections::HashMap;
9
10#[derive(Clone, Copy, PartialEq)]
11pub(crate) enum AllowableIds {
12    One,
13    Many,
14}
15
16#[derive(Clone, Copy, PartialEq, Eq)]
17pub(crate) enum CollectionSource {
18    Allow,
19    Deny,
20}
21
22#[derive(Debug, PartialEq, Eq, Hash)]
23pub(crate) enum TargetId<'a> {
24    Component(&'a str, Option<&'a str>),
25    Collection(&'a str),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) enum OfferType {
30    Static,
31    Dynamic,
32}
33
34pub(crate) type IdMap<'a> = HashMap<TargetId<'a>, HashMap<&'a str, AllowableIds>>;
35
36pub(crate) fn check_path(
37    prop: Option<&String>,
38    decl_type: DeclType,
39    keyword: &str,
40    errors: &mut Vec<Error>,
41) -> bool {
42    let conversion_ctor = |s: &String| Path::new(s).map(|_| ());
43    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
44}
45
46pub(crate) fn check_relative_path(
47    prop: Option<&String>,
48    decl_type: DeclType,
49    keyword: &str,
50    errors: &mut Vec<Error>,
51) -> bool {
52    let conversion_ctor = |s: &String| RelativePath::new(s).map(|_| ());
53    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
54}
55
56pub(crate) fn check_name(
57    prop: Option<&String>,
58    decl_type: DeclType,
59    keyword: &str,
60    errors: &mut Vec<Error>,
61) -> bool {
62    let conversion_ctor = |s: &String| Name::new(s).map(|_| ());
63    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
64}
65
66pub(crate) fn check_dynamic_name(
67    prop: Option<&String>,
68    decl_type: DeclType,
69    keyword: &str,
70    errors: &mut Vec<Error>,
71) -> bool {
72    let conversion_ctor = |s: &String| LongName::new(s).map(|_| ());
73    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
74}
75
76fn check_identifier(
77    conversion_ctor: impl Fn(&String) -> Result<(), ParseError>,
78    prop: Option<&String>,
79    decl_type: DeclType,
80    keyword: &str,
81    errors: &mut Vec<Error>,
82) -> bool {
83    let Some(prop) = prop else {
84        errors.push(Error::missing_field(decl_type, keyword));
85        return false;
86    };
87    if let Err(e) = conversion_ctor(prop) {
88        errors.push(Error::from_parse_error(e, prop, decl_type, keyword));
89        return false;
90    }
91    true
92}
93
94pub(crate) fn check_use_availability(
95    decl_type: DeclType,
96    availability: Option<&fdecl::Availability>,
97    errors: &mut Vec<Error>,
98) {
99    match availability {
100        Some(fdecl::Availability::Required)
101        | Some(fdecl::Availability::Optional)
102        | Some(fdecl::Availability::Transitional) => {}
103        Some(fdecl::Availability::SameAsTarget) => {
104            errors.push(Error::invalid_field(decl_type, "availability"))
105        }
106        // TODO(dgonyeo): we need to soft migrate the requirement for this field to be set
107        //None => errors.push(Error::missing_field(decl_type, "availability")),
108        None => (),
109    }
110}
111
112pub(crate) fn check_route_availability(
113    decl: DeclType,
114    availability: Option<&fdecl::Availability>,
115    source: Option<&fdecl::Ref>,
116    source_name: Option<&String>,
117    errors: &mut Vec<Error>,
118) {
119    match (source, availability) {
120        // The availability must be optional or transitional when the source is void.
121        (Some(fdecl::Ref::VoidType(_)), Some(fdecl::Availability::Optional))
122        | (Some(fdecl::Ref::VoidType(_)), Some(fdecl::Availability::Transitional)) => (),
123        (
124            Some(fdecl::Ref::VoidType(_)),
125            Some(fdecl::Availability::Required | fdecl::Availability::SameAsTarget),
126        ) => errors.push(Error::availability_must_be_optional(decl, "availability", source_name)),
127        // All other cases are valid
128        _ => (),
129    }
130}
131
132pub fn check_url(
133    prop: Option<&String>,
134    decl_type: DeclType,
135    keyword: &str,
136    errors: &mut Vec<Error>,
137) -> bool {
138    let conversion_ctor = |s: &String| Url::new(s).map(|_| ());
139    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
140}
141
142pub(crate) fn check_url_scheme(
143    prop: Option<&String>,
144    decl_type: DeclType,
145    keyword: &str,
146    errors: &mut Vec<Error>,
147) -> bool {
148    let conversion_ctor = |s: &String| UrlScheme::new(s).map(|_| ());
149    check_identifier(conversion_ctor, prop, decl_type, keyword, errors)
150}
151
152#[cfg(test)]
153mod tests {
154
155    use super::*;
156    use cm_types::{MAX_LONG_NAME_LENGTH, MAX_NAME_LENGTH};
157    use lazy_static::lazy_static;
158    use proptest::prelude::*;
159    use regex::Regex;
160    use url::Url;
161
162    mod path {
163        use cm_types::{MAX_NAME_LENGTH, MAX_PATH_LENGTH};
164        use lazy_static::lazy_static;
165        use proptest::prelude::*;
166        use regex::Regex;
167
168        pub fn is_path_valid(s: &str) -> bool {
169            if !ROUGH_PATH_REGEX.is_match(s) {
170                return false;
171            }
172            check_segment_and_length(s)
173        }
174
175        pub fn path_strategy() -> SBoxedStrategy<String> {
176            ROUGH_PATH_REGEX_STR
177                .as_str()
178                .prop_filter("Length and segment must be valid", check_segment_and_length)
179                .sboxed()
180        }
181
182        fn check_segment_and_length(s: &(impl AsRef<str> + ?Sized)) -> bool {
183            let s: &str = s.as_ref();
184            if s.len() > MAX_PATH_LENGTH {
185                return false;
186            }
187            s.split("/").all(|v| v != "." && v != ".." && v.len() <= MAX_NAME_LENGTH)
188        }
189
190        lazy_static! {
191            static ref ROUGH_PATH_REGEX_STR: String = format!("(/{})+", super::NAME_REGEX_STR);
192            static ref ROUGH_PATH_REGEX: Regex =
193                Regex::new(&("^".to_string() + &ROUGH_PATH_REGEX_STR + "$")).unwrap();
194        }
195    }
196
197    const NAME_REGEX_STR: &str = r"[0-9a-zA-Z_][0-9a-zA-Z_\-\.]*";
198
199    lazy_static! {
200        static ref NAME_REGEX: Regex =
201            Regex::new(&("^".to_string() + NAME_REGEX_STR + "$")).unwrap();
202        static ref A_BASE_URL: Url = Url::parse("relative:///").unwrap();
203    }
204
205    proptest! {
206        #[test]
207        fn check_path_matches_regex(s in path::path_strategy()) {
208            let mut errors = vec![];
209            prop_assert!(check_path(Some(&s), DeclType::Child, "", &mut errors));
210            prop_assert!(errors.is_empty());
211        }
212        #[test]
213        fn check_name_matches_regex(s in NAME_REGEX_STR) {
214            if s.len() <= MAX_NAME_LENGTH {
215                let mut errors = vec![];
216                prop_assert!(check_name(Some(&s), DeclType::Child, "", &mut errors));
217                prop_assert!(errors.is_empty());
218            }
219        }
220        #[test]
221        fn check_path_fails_invalid_input(s in ".*") {
222            if !path::is_path_valid(&s) {
223                let mut errors = vec![];
224                prop_assert!(!check_path(Some(&s), DeclType::Child, "", &mut errors));
225                prop_assert!(!errors.is_empty());
226            }
227        }
228        #[test]
229        fn check_name_fails_invalid_input(s in ".*") {
230            if !NAME_REGEX.is_match(&s) {
231                let mut errors = vec![];
232                prop_assert!(!check_name(Some(&s), DeclType::Child, "", &mut errors));
233                prop_assert!(!errors.is_empty());
234            }
235        }
236        // NOTE: The Url crate's parser is used to validate legal URLs. Testing
237        // random strings against component URL validation is redundant, so
238        // a `check_url_fails_invalid_input` is not necessary (and would be
239        // non-trivial to do using just a regular expression).
240
241
242    }
243
244    fn check_test<F>(check_fn: F, input: &str, expected_res: Result<(), ErrorList>)
245    where
246        F: FnOnce(Option<&String>, DeclType, &str, &mut Vec<Error>) -> bool,
247    {
248        let mut errors = vec![];
249        let res: Result<(), ErrorList> =
250            match check_fn(Some(&input.to_string()), DeclType::Child, "foo", &mut errors) {
251                true => Ok(()),
252                false => Err(ErrorList::new(errors)),
253            };
254        assert_eq!(
255            format!("{:?}", res),
256            format!("{:?}", expected_res),
257            "Unexpected result for input: '{}'\n{}",
258            input,
259            {
260                match Url::parse(input).or_else(|err| {
261                    if err == url::ParseError::RelativeUrlWithoutBase {
262                        A_BASE_URL.join(input)
263                    } else {
264                        Err(err)
265                    }
266                }) {
267                    Ok(url) => format!(
268                        "scheme={}, host={:?}, path={}, fragment={:?}",
269                        url.scheme(),
270                        url.host_str(),
271                        url.path(),
272                        url.fragment()
273                    ),
274                    Err(_) => "".to_string(),
275                }
276            }
277        );
278    }
279
280    macro_rules! test_string_checks {
281        (
282            $(
283                $test_name:ident => {
284                    check_fn = $check_fn:expr,
285                    input = $input:expr,
286                    result = $result:expr,
287                },
288            )+
289        ) => {
290            $(
291                #[test]
292                fn $test_name() {
293                    check_test($check_fn, $input, $result);
294                }
295            )+
296        }
297    }
298
299    test_string_checks! {
300        // path
301        test_identifier_path_valid => {
302            check_fn = check_path,
303            input = "/foo/bar",
304            result = Ok(()),
305        },
306        test_identifier_path_invalid_empty => {
307            check_fn = check_path,
308            input = "",
309            result = Err(ErrorList::new(vec![Error::empty_field(DeclType::Child, "foo")])),
310        },
311        test_identifier_path_invalid_root => {
312            check_fn = check_path,
313            input = "/",
314            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
315        },
316        test_identifier_path_invalid_relative => {
317            check_fn = check_path,
318            input = "foo/bar",
319            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
320        },
321        test_identifier_path_invalid_trailing => {
322            check_fn = check_path,
323            input = "/foo/bar/",
324            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
325        },
326        test_identifier_path_segment_invalid => {
327            check_fn = check_path,
328            input = "/.",
329            result = Err(ErrorList::new(vec![Error::field_invalid_segment(DeclType::Child, "foo")])),
330        },
331        test_identifier_path_segment_too_long => {
332            check_fn = check_path,
333            input = &format!("/{}", "a".repeat(256)),
334            result = Err(ErrorList::new(vec![Error::field_invalid_segment(DeclType::Child, "foo")])),
335        },
336        test_identifier_path_too_long => {
337            check_fn = check_path,
338            // 2048 * 2 characters per repeat = 4096
339            input = &"/a".repeat(2048),
340            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
341        },
342
343        // name
344        test_identifier_dynamic_name_valid => {
345            check_fn = check_dynamic_name,
346            input = &format!("{}", "a".repeat(MAX_LONG_NAME_LENGTH)),
347            result = Ok(()),
348        },
349        test_identifier_name_valid => {
350            check_fn = check_name,
351            input = "abcdefghijklmnopqrstuvwxyz0123456789_-.",
352            result = Ok(()),
353        },
354        test_identifier_name_invalid => {
355            check_fn = check_name,
356            input = "^bad",
357            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
358        },
359        test_identifier_name_too_long => {
360            check_fn = check_name,
361            input = &format!("{}", "a".repeat(MAX_NAME_LENGTH + 1)),
362            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
363        },
364        test_identifier_dynamic_name_too_long => {
365            check_fn = check_dynamic_name,
366            input = &format!("{}", "a".repeat(MAX_LONG_NAME_LENGTH + 1)),
367            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
368        },
369
370        // url
371        test_identifier_url_valid => {
372            check_fn = check_url,
373            input = "my+awesome-scheme.2://abc123!@$%.com",
374            result = Ok(()),
375        },
376        test_host_path_url_valid => {
377            check_fn = check_url,
378            input = "some-scheme://host/path/segments",
379            result = Ok(()),
380        },
381        test_host_path_resource_url_valid => {
382            check_fn = check_url,
383            input = "some-scheme://host/path/segments#meta/comp.cm",
384            result = Ok(()),
385        },
386        test_nohost_path_resource_url_valid => {
387            check_fn = check_url,
388            input = "some-scheme:///path/segments#meta/comp.cm",
389            result = Ok(()),
390        },
391        test_relative_path_resource_url_valid => {
392            check_fn = check_url,
393            input = "path/segments#meta/comp.cm",
394            result = Ok(()),
395        },
396        test_relative_resource_url_valid => {
397            check_fn = check_url,
398            input = "path/segments#meta/comp.cm",
399            result = Ok(()),
400        },
401        test_identifier_url_host_pound_invalid => {
402            check_fn = check_url,
403            input = "my+awesome-scheme.2://abc123!@#$%.com",
404            result = Err(ErrorList::new(vec![Error::invalid_url(DeclType::Child, "foo", "\"my+awesome-scheme.2://abc123!@#$%.com\": Malformed URL: EmptyHost.")])),
405        },
406        test_identifier_url_invalid => {
407            check_fn = check_url,
408            input = "fuchsia-pkg://",
409            result = Err(ErrorList::new(vec![Error::invalid_url(DeclType::Child, "foo","\"fuchsia-pkg://\": URL is missing either `host`, `path`, and/or `resource`.")])),
410        },
411        test_url_invalid_port => {
412            check_fn = check_url,
413            input = "scheme://invalid-port:99999999/path#frag",
414            result = Err(ErrorList::new(vec![
415                Error::invalid_url(DeclType::Child, "foo", "\"scheme://invalid-port:99999999/path#frag\": Malformed URL: InvalidPort."),
416            ])),
417        },
418        test_identifier_url_too_long => {
419            check_fn = check_url,
420            input = &format!("fuchsia-pkg://{}", "a".repeat(4083)),
421            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
422        },
423    }
424}