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 proptest::prelude::*;
158    use regex::Regex;
159    use std::sync::LazyLock;
160    use url::Url;
161
162    mod path {
163        use cm_types::{MAX_NAME_LENGTH, MAX_PATH_LENGTH};
164        use proptest::prelude::*;
165        use regex::Regex;
166        use std::sync::LazyLock;
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        static ROUGH_PATH_REGEX_STR: LazyLock<String> =
191            LazyLock::new(|| format!("(/{})+", super::NAME_REGEX_STR));
192        static ROUGH_PATH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
193            Regex::new(&("^".to_string() + &ROUGH_PATH_REGEX_STR as &str + "$")).unwrap()
194        });
195    }
196
197    const NAME_REGEX_STR: &str = r"[0-9a-zA-Z_][0-9a-zA-Z_\-\.]*";
198
199    static NAME_REGEX: LazyLock<Regex> =
200        LazyLock::new(|| Regex::new(&("^".to_string() + NAME_REGEX_STR + "$")).unwrap());
201    static A_BASE_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("relative:///").unwrap());
202
203    proptest! {
204        #[test]
205        fn check_path_matches_regex(s in path::path_strategy()) {
206            let mut errors = vec![];
207            prop_assert!(check_path(Some(&s), DeclType::Child, "", &mut errors));
208            prop_assert!(errors.is_empty());
209        }
210        #[test]
211        fn check_name_matches_regex(s in NAME_REGEX_STR) {
212            if s.len() <= MAX_NAME_LENGTH {
213                let mut errors = vec![];
214                prop_assert!(check_name(Some(&s), DeclType::Child, "", &mut errors));
215                prop_assert!(errors.is_empty());
216            }
217        }
218        #[test]
219        fn check_path_fails_invalid_input(s in ".*") {
220            if !path::is_path_valid(&s) {
221                let mut errors = vec![];
222                prop_assert!(!check_path(Some(&s), DeclType::Child, "", &mut errors));
223                prop_assert!(!errors.is_empty());
224            }
225        }
226        #[test]
227        fn check_name_fails_invalid_input(s in ".*") {
228            if !NAME_REGEX.is_match(&s) {
229                let mut errors = vec![];
230                prop_assert!(!check_name(Some(&s), DeclType::Child, "", &mut errors));
231                prop_assert!(!errors.is_empty());
232            }
233        }
234        // NOTE: The Url crate's parser is used to validate legal URLs. Testing
235        // random strings against component URL validation is redundant, so
236        // a `check_url_fails_invalid_input` is not necessary (and would be
237        // non-trivial to do using just a regular expression).
238
239
240    }
241
242    fn check_test<F>(check_fn: F, input: &str, expected_res: Result<(), ErrorList>)
243    where
244        F: FnOnce(Option<&String>, DeclType, &str, &mut Vec<Error>) -> bool,
245    {
246        let mut errors = vec![];
247        let res: Result<(), ErrorList> =
248            match check_fn(Some(&input.to_string()), DeclType::Child, "foo", &mut errors) {
249                true => Ok(()),
250                false => Err(ErrorList::new(errors)),
251            };
252        assert_eq!(
253            format!("{:?}", res),
254            format!("{:?}", expected_res),
255            "Unexpected result for input: '{}'\n{}",
256            input,
257            {
258                match Url::parse(input).or_else(|err| {
259                    if err == url::ParseError::RelativeUrlWithoutBase {
260                        A_BASE_URL.join(input)
261                    } else {
262                        Err(err)
263                    }
264                }) {
265                    Ok(url) => format!(
266                        "scheme={}, host={:?}, path={}, fragment={:?}",
267                        url.scheme(),
268                        url.host_str(),
269                        url.path(),
270                        url.fragment()
271                    ),
272                    Err(_) => "".to_string(),
273                }
274            }
275        );
276    }
277
278    macro_rules! test_string_checks {
279        (
280            $(
281                $test_name:ident => {
282                    check_fn = $check_fn:expr,
283                    input = $input:expr,
284                    result = $result:expr,
285                },
286            )+
287        ) => {
288            $(
289                #[test]
290                fn $test_name() {
291                    check_test($check_fn, $input, $result);
292                }
293            )+
294        }
295    }
296
297    test_string_checks! {
298        // path
299        test_identifier_path_valid => {
300            check_fn = check_path,
301            input = "/foo/bar",
302            result = Ok(()),
303        },
304        test_identifier_path_invalid_empty => {
305            check_fn = check_path,
306            input = "",
307            result = Err(ErrorList::new(vec![Error::empty_field(DeclType::Child, "foo")])),
308        },
309        test_identifier_path_invalid_root => {
310            check_fn = check_path,
311            input = "/",
312            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
313        },
314        test_identifier_path_invalid_relative => {
315            check_fn = check_path,
316            input = "foo/bar",
317            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
318        },
319        test_identifier_path_invalid_trailing => {
320            check_fn = check_path,
321            input = "/foo/bar/",
322            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
323        },
324        test_identifier_path_segment_invalid => {
325            check_fn = check_path,
326            input = "/.",
327            result = Err(ErrorList::new(vec![Error::field_invalid_segment(DeclType::Child, "foo")])),
328        },
329        test_identifier_path_segment_too_long => {
330            check_fn = check_path,
331            input = &format!("/{}", "a".repeat(256)),
332            result = Err(ErrorList::new(vec![Error::field_invalid_segment(DeclType::Child, "foo")])),
333        },
334        test_identifier_path_too_long => {
335            check_fn = check_path,
336            // 2048 * 2 characters per repeat = 4096
337            input = &"/a".repeat(2048),
338            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
339        },
340
341        // name
342        test_identifier_dynamic_name_valid => {
343            check_fn = check_dynamic_name,
344            input = &format!("{}", "a".repeat(MAX_LONG_NAME_LENGTH)),
345            result = Ok(()),
346        },
347        test_identifier_name_valid => {
348            check_fn = check_name,
349            input = "abcdefghijklmnopqrstuvwxyz0123456789_-.",
350            result = Ok(()),
351        },
352        test_identifier_name_invalid => {
353            check_fn = check_name,
354            input = "^bad",
355            result = Err(ErrorList::new(vec![Error::invalid_field(DeclType::Child, "foo")])),
356        },
357        test_identifier_name_too_long => {
358            check_fn = check_name,
359            input = &format!("{}", "a".repeat(MAX_NAME_LENGTH + 1)),
360            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
361        },
362        test_identifier_dynamic_name_too_long => {
363            check_fn = check_dynamic_name,
364            input = &format!("{}", "a".repeat(MAX_LONG_NAME_LENGTH + 1)),
365            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
366        },
367
368        // url
369        test_identifier_url_valid => {
370            check_fn = check_url,
371            input = "my+awesome-scheme.2://abc123!@$%.com",
372            result = Ok(()),
373        },
374        test_host_path_url_valid => {
375            check_fn = check_url,
376            input = "some-scheme://host/path/segments",
377            result = Ok(()),
378        },
379        test_host_path_resource_url_valid => {
380            check_fn = check_url,
381            input = "some-scheme://host/path/segments#meta/comp.cm",
382            result = Ok(()),
383        },
384        test_nohost_path_resource_url_valid => {
385            check_fn = check_url,
386            input = "some-scheme:///path/segments#meta/comp.cm",
387            result = Ok(()),
388        },
389        test_relative_path_resource_url_valid => {
390            check_fn = check_url,
391            input = "path/segments#meta/comp.cm",
392            result = Ok(()),
393        },
394        test_relative_resource_url_valid => {
395            check_fn = check_url,
396            input = "path/segments#meta/comp.cm",
397            result = Ok(()),
398        },
399        test_identifier_url_host_pound_invalid => {
400            check_fn = check_url,
401            input = "my+awesome-scheme.2://abc123!@#$%.com",
402            result = Err(ErrorList::new(vec![Error::invalid_url(DeclType::Child, "foo", "\"my+awesome-scheme.2://abc123!@#$%.com\": Malformed URL: EmptyHost.")])),
403        },
404        test_identifier_url_invalid => {
405            check_fn = check_url,
406            input = "fuchsia-pkg://",
407            result = Err(ErrorList::new(vec![Error::invalid_url(DeclType::Child, "foo","\"fuchsia-pkg://\": URL is missing either `host`, `path`, and/or `resource`.")])),
408        },
409        test_url_invalid_port => {
410            check_fn = check_url,
411            input = "scheme://invalid-port:99999999/path#frag",
412            result = Err(ErrorList::new(vec![
413                Error::invalid_url(DeclType::Child, "foo", "\"scheme://invalid-port:99999999/path#frag\": Malformed URL: InvalidPort."),
414            ])),
415        },
416        test_identifier_url_too_long => {
417            check_fn = check_url,
418            input = &format!("fuchsia-pkg://{}", "a".repeat(4083)),
419            result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
420        },
421    }
422}