1use 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 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 (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 _ => (),
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 }
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 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 input = &"/a".repeat(2048),
338 result = Err(ErrorList::new(vec![Error::field_too_long(DeclType::Child, "foo")])),
339 },
340
341 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 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}