blob: b8316da764c9ba40f9b84f097c0fcf76ee3d8b79 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {crate::error::*, fidl_fuchsia_component_decl as fdecl, std::collections::HashMap};
const MAX_PATH_LENGTH: usize = 1024;
const MAX_URL_LENGTH: usize = 4096;
pub const MAX_NAME_LENGTH: usize = 100;
pub const MAX_DYNAMIC_NAME_LENGTH: usize = 1024;
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum AllowableIds {
One,
Many,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum CollectionSource {
Allow,
Deny,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub(crate) enum TargetId<'a> {
Component(&'a str),
Collection(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OfferType {
Static,
Dynamic,
}
pub(crate) type IdMap<'a> = HashMap<TargetId<'a>, HashMap<&'a str, AllowableIds>>;
pub(crate) fn check_presence_and_length(
max_len: usize,
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) {
match prop {
Some(prop) if prop.len() == 0 => errors.push(Error::empty_field(decl_type, keyword)),
Some(prop) if prop.len() > max_len => {
errors.push(Error::field_too_long_with_max(decl_type, keyword, max_len))
}
Some(_) => (),
None => errors.push(Error::missing_field(decl_type, keyword)),
}
}
pub(crate) fn check_path(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
let start_err_len = errors.len();
check_presence_and_length(MAX_PATH_LENGTH, prop, decl_type, keyword, errors);
if let Some(path) = prop {
// Paths must be more than 1 character long
if path.len() < 2 {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Paths must start with `/`
if !path.starts_with('/') {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Paths cannot have two `/`s in a row
if path.contains("//") {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Paths cannot end with `/`
if path.ends_with('/') {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
}
start_err_len == errors.len()
}
pub(crate) fn check_relative_path(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
let start_err_len = errors.len();
check_presence_and_length(MAX_PATH_LENGTH, prop, decl_type, keyword, errors);
if let Some(path) = prop {
// Relative paths must be nonempty
if path.is_empty() {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Relative paths cannot start with `/`
if path.starts_with('/') {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Relative paths cannot have two `/`s in a row
if path.contains("//") {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Relative paths cannot end with `/`
if path.ends_with('/') {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
}
start_err_len == errors.len()
}
pub(crate) fn check_dynamic_name(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
check_name_impl(prop, decl_type, keyword, MAX_DYNAMIC_NAME_LENGTH, errors)
}
pub(crate) fn check_name(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
check_name_impl(prop, decl_type, keyword, MAX_NAME_LENGTH, errors)
}
fn check_name_impl(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
max_len: usize,
errors: &mut Vec<Error>,
) -> bool {
let start_err_len = errors.len();
check_presence_and_length(max_len, prop, decl_type, keyword, errors);
let mut invalid_field = false;
if let Some(name) = prop {
let mut char_iter = name.chars();
if let Some(first_char) = char_iter.next() {
if !first_char.is_ascii_alphanumeric() && first_char != '_' {
invalid_field = true;
}
}
for c in char_iter {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
// Ok
} else {
invalid_field = true;
}
}
}
if invalid_field {
errors.push(Error::invalid_field(decl_type, keyword));
}
start_err_len == errors.len()
}
pub(crate) fn check_use_availability(
decl_type: &str,
availability: Option<&fdecl::Availability>,
errors: &mut Vec<Error>,
) {
match availability {
Some(fdecl::Availability::Required) | Some(fdecl::Availability::Optional) => {}
Some(fdecl::Availability::SameAsTarget) => {
errors.push(Error::invalid_field(decl_type, "availability"))
}
// TODO(dgonyeo): we need to soft migrate the requirement for this field to be set
//None => errors.push(Error::missing_field(decl_type, "availability")),
None => (),
}
}
pub(crate) fn check_offer_availability(
decl: &str,
availability: Option<&fdecl::Availability>,
source: Option<&fdecl::Ref>,
source_name: Option<&String>,
errors: &mut Vec<Error>,
) {
match (source, availability) {
// The availability can be anything when the source is parent.
(Some(fdecl::Ref::Parent(_)), _) => (),
// The availability must be optional when the source is void.
(Some(fdecl::Ref::VoidType(_)), Some(fdecl::Availability::Optional)) => (),
(
Some(fdecl::Ref::VoidType(_)),
Some(fdecl::Availability::Required | fdecl::Availability::SameAsTarget),
) => errors.push(Error::availability_must_be_optional(decl, "availability", source_name)),
// In all other sources the availability must be set to `required` (it's always
// available if it comes from something like `self` or `framework`, and there's no
// optional exposes so it can't be optional coming from a child).
(
Some(
fdecl::Ref::Self_(_)
| fdecl::Ref::Child(_)
| fdecl::Ref::Collection(_)
| fdecl::Ref::Framework(_)
| fdecl::Ref::Capability(_)
| fdecl::Ref::Debug(_)
| fdecl::RefUnknown!(),
)
| None,
Some(fdecl::Availability::Optional | fdecl::Availability::SameAsTarget),
) => errors.push(Error::availability_must_be_required(decl, "availability", source_name)),
// TODO(dgonyeo): we need to soft migrate the requirement for this field to be set
(_, Some(fdecl::Availability::Required) | None) => (),
}
}
// TODO: This should probably be checking with the `url` crate
pub(crate) fn check_url(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
let start_err_len = errors.len();
check_presence_and_length(MAX_URL_LENGTH, prop, decl_type, keyword, errors);
if let Some(url) = prop {
let mut chars_iter = url.chars();
let mut first_char = true;
while let Some(c) = chars_iter.next() {
match c {
'0'..='9' | 'a'..='z' | '+' | '-' | '.' => first_char = false,
':' => {
if first_char {
// There must be at least one character in the schema
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
// Once a `:` character is found, it must be followed by two `/` characters and
// then at least one more character. Note that these sequential calls to
// `.next()` without checking the result won't panic because `Chars` implements
// `FusedIterator`.
match (chars_iter.next(), chars_iter.next(), chars_iter.next()) {
(Some('/'), Some('/'), Some(_)) => return start_err_len == errors.len(),
_ => {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
}
}
// If the first character is # then it's a relative URL.
// It must have at least one more character.
'#' => {
if first_char && chars_iter.next().is_some() {
return start_err_len == errors.len();
}
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
_ => {
errors.push(Error::invalid_field(decl_type, keyword));
return false;
}
}
}
// If we've reached here then the string terminated unexpectedly
errors.push(Error::invalid_field(decl_type, keyword));
}
start_err_len == errors.len()
}
pub(crate) fn check_url_scheme(
prop: Option<&String>,
decl_type: &str,
keyword: &str,
errors: &mut Vec<Error>,
) -> bool {
if let Some(scheme) = prop {
if let Err(err) = cm_types::UrlScheme::validate(scheme) {
errors.push(match err {
cm_types::ParseError::InvalidLength => {
if scheme.is_empty() {
Error::empty_field(decl_type, keyword)
} else {
Error::field_too_long(decl_type, keyword)
}
}
cm_types::ParseError::InvalidValue => Error::invalid_field(decl_type, keyword),
e => {
panic!("unexpected parse error: {:?}", e);
}
});
return false;
}
} else {
errors.push(Error::missing_field(decl_type, keyword));
return false;
}
true
}
#[cfg(test)]
mod tests {
use {super::*, lazy_static::lazy_static, proptest::prelude::*, regex::Regex};
const PATH_REGEX_STR: &str = r"(/[^/]+)+";
const NAME_REGEX_STR: &str = r"[0-9a-zA-Z_][0-9a-zA-Z_\-\.]*";
const URL_REGEX_STR: &str = r"([0-9a-z\+\-\.]+://.+|#.+)";
lazy_static! {
static ref PATH_REGEX: Regex =
Regex::new(&("^".to_string() + PATH_REGEX_STR + "$")).unwrap();
static ref NAME_REGEX: Regex =
Regex::new(&("^".to_string() + NAME_REGEX_STR + "$")).unwrap();
static ref URL_REGEX: Regex = Regex::new(&("^".to_string() + URL_REGEX_STR + "$")).unwrap();
}
proptest! {
#[test]
fn check_path_matches_regex(s in PATH_REGEX_STR) {
if s.len() < MAX_PATH_LENGTH {
let mut errors = vec![];
prop_assert!(check_path(Some(&s), "", "", &mut errors));
prop_assert!(errors.is_empty());
}
}
#[test]
fn check_name_matches_regex(s in NAME_REGEX_STR) {
if s.len() < MAX_NAME_LENGTH {
let mut errors = vec![];
prop_assert!(check_name(Some(&s), "", "", &mut errors));
prop_assert!(errors.is_empty());
}
}
#[test]
fn check_url_matches_regex(s in URL_REGEX_STR) {
if s.len() < MAX_URL_LENGTH {
let mut errors = vec![];
prop_assert!(check_url(Some(&s), "", "", &mut errors));
prop_assert!(errors.is_empty());
}
}
#[test]
fn check_path_fails_invalid_input(s in ".*") {
if !PATH_REGEX.is_match(&s) {
let mut errors = vec![];
prop_assert!(!check_path(Some(&s), "", "", &mut errors));
prop_assert!(!errors.is_empty());
}
}
#[test]
fn check_name_fails_invalid_input(s in ".*") {
if !NAME_REGEX.is_match(&s) {
let mut errors = vec![];
prop_assert!(!check_name(Some(&s), "", "", &mut errors));
prop_assert!(!errors.is_empty());
}
}
#[test]
fn check_url_fails_invalid_input(s in ".*") {
if !URL_REGEX.is_match(&s) {
let mut errors = vec![];
prop_assert!(!check_url(Some(&s), "", "", &mut errors));
prop_assert!(!errors.is_empty());
}
}
}
fn check_test<F>(check_fn: F, input: &str, expected_res: Result<(), ErrorList>)
where
F: FnOnce(Option<&String>, &str, &str, &mut Vec<Error>) -> bool,
{
let mut errors = vec![];
let res: Result<(), ErrorList> =
match check_fn(Some(&input.to_string()), "FooDecl", "foo", &mut errors) {
true => Ok(()),
false => Err(ErrorList::new(errors)),
};
assert_eq!(format!("{:?}", res), format!("{:?}", expected_res));
}
macro_rules! test_string_checks {
(
$(
$test_name:ident => {
check_fn = $check_fn:expr,
input = $input:expr,
result = $result:expr,
},
)+
) => {
$(
#[test]
fn $test_name() {
check_test($check_fn, $input, $result);
}
)+
}
}
test_string_checks! {
// path
test_identifier_path_valid => {
check_fn = check_path,
input = "/foo/bar",
result = Ok(()),
},
test_identifier_path_invalid_empty => {
check_fn = check_path,
input = "",
result = Err(ErrorList::new(vec![
Error::empty_field("FooDecl", "foo"),
Error::invalid_field("FooDecl", "foo"),
])),
},
test_identifier_path_invalid_root => {
check_fn = check_path,
input = "/",
result = Err(ErrorList::new(vec![Error::invalid_field("FooDecl", "foo")])),
},
test_identifier_path_invalid_relative => {
check_fn = check_path,
input = "foo/bar",
result = Err(ErrorList::new(vec![Error::invalid_field("FooDecl", "foo")])),
},
test_identifier_path_invalid_trailing => {
check_fn = check_path,
input = "/foo/bar/",
result = Err(ErrorList::new(vec![Error::invalid_field("FooDecl", "foo")])),
},
test_identifier_path_too_long => {
check_fn = check_path,
input = &format!("/{}", "a".repeat(1024)),
result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", /*max=*/1024usize)])),
},
// name
test_identifier_dynamic_name_valid => {
check_fn = check_dynamic_name,
input = &format!("{}", "a".repeat(MAX_DYNAMIC_NAME_LENGTH)),
result = Ok(()),
},
test_identifier_name_valid => {
check_fn = check_name,
input = "abcdefghijklmnopqrstuvwxyz0123456789_-.",
result = Ok(()),
},
test_identifier_name_invalid => {
check_fn = check_name,
input = "^bad",
result = Err(ErrorList::new(vec![Error::invalid_field("FooDecl", "foo")])),
},
test_identifier_name_too_long => {
check_fn = check_name,
input = &format!("{}", "a".repeat(MAX_NAME_LENGTH + 1)),
result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", MAX_NAME_LENGTH)])),
},
test_identifier_dynamic_name_too_long => {
check_fn = check_dynamic_name,
input = &format!("{}", "a".repeat(MAX_DYNAMIC_NAME_LENGTH + 1)),
result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", MAX_DYNAMIC_NAME_LENGTH)])),
},
// url
test_identifier_url_valid => {
check_fn = check_url,
input = "my+awesome-scheme.2://abc123!@#$%.com",
result = Ok(()),
},
test_identifier_url_invalid => {
check_fn = check_url,
input = "fuchsia-pkg://",
result = Err(ErrorList::new(vec![Error::invalid_field("FooDecl", "foo")])),
},
test_identifier_url_too_long => {
check_fn = check_url,
input = &format!("fuchsia-pkg://{}", "a".repeat(4083)),
result = Err(ErrorList::new(vec![Error::field_too_long_with_max("FooDecl", "foo", 4096)])),
},
}
}