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