| // Copyright 2019 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::errors::{PackageNameError, PackageVariantError, ResourcePathError}; |
| use lazy_static::lazy_static; |
| use regex::Regex; |
| |
| pub const MAX_OBJECT_BYTES: usize = 255; |
| pub const MAX_PACKAGE_NAME_BYTES: usize = 100; |
| pub const MAX_PACKAGE_VARIANT_BYTES: usize = 100; |
| // FIXME(PKG-757): '_' is not valid in package names, but many Fuchsia packages currently use that |
| // character. |
| pub const PACKAGE_NAME_REGEX: &str = r"^[-_0-9a-z\.]{1, 100}$"; |
| pub const PACKAGE_VARIANT_REGEX: &str = r"^[-0-9a-z\.]{1, 100}$"; |
| |
| /// Checks if `input` is a valid path for a file in a Fuchsia package. |
| /// Fuchsia package resource paths are Fuchsia object relative paths without |
| /// the limit on maximum path length. |
| /// Passes the input through if it is valid. |
| pub fn check_resource_path(input: &str) -> Result<&str, ResourcePathError> { |
| if input.is_empty() { |
| return Err(ResourcePathError::PathIsEmpty); |
| } |
| if input.starts_with('/') { |
| return Err(ResourcePathError::PathStartsWithSlash); |
| } |
| if input.ends_with('/') { |
| return Err(ResourcePathError::PathEndsWithSlash); |
| } |
| for segment in input.split('/') { |
| if segment.contains('\0') { |
| return Err(ResourcePathError::NameContainsNull); |
| } |
| if segment == "." { |
| return Err(ResourcePathError::NameIsDot); |
| } |
| if segment == ".." { |
| return Err(ResourcePathError::NameIsDotDot); |
| } |
| if segment.is_empty() { |
| return Err(ResourcePathError::NameEmpty); |
| } |
| if segment.len() > MAX_OBJECT_BYTES { |
| return Err(ResourcePathError::NameTooLong); |
| } |
| // TODO(PKG-597) allow newline once meta/contents supports it in blob paths |
| if segment.contains('\n') { |
| return Err(ResourcePathError::NameContainsNewline); |
| } |
| } |
| Ok(input) |
| } |
| |
| /// Checks if `input` is a valid Fuchsia package name. |
| /// Passes `input` through if valid. |
| pub fn check_package_name(input: &str) -> Result<&str, PackageNameError> { |
| if input.len() > MAX_PACKAGE_NAME_BYTES { |
| return Err(PackageNameError::TooLong { invalid_name: input.to_string() }); |
| } |
| if input.is_empty() { |
| return Err(PackageNameError::Empty); |
| } |
| lazy_static! { |
| static ref RE: Regex = Regex::new(PACKAGE_NAME_REGEX).unwrap(); |
| } |
| if !RE.is_match(input) { |
| return Err(PackageNameError::InvalidCharacter { invalid_name: input.to_string() }); |
| } |
| Ok(input) |
| } |
| |
| /// Checks if `input` is a valid Fuchsia package variant. |
| /// Passes `input` through if valid. |
| pub fn check_package_variant(input: &str) -> Result<&str, PackageVariantError> { |
| if input.len() > MAX_PACKAGE_VARIANT_BYTES { |
| return Err(PackageVariantError::TooLong { invalid_variant: input.to_string() }); |
| } |
| if input.is_empty() { |
| return Err(PackageVariantError::Empty); |
| } |
| lazy_static! { |
| static ref RE: Regex = Regex::new(PACKAGE_VARIANT_REGEX).unwrap(); |
| } |
| if !RE.is_match(input) { |
| return Err(PackageVariantError::InvalidCharacter { invalid_variant: input.to_string() }); |
| } |
| Ok(input) |
| } |
| |
| #[cfg(test)] |
| mod check_resource_path_tests { |
| use super::*; |
| use crate::test::*; |
| use proptest::prelude::*; |
| |
| // Tests for invalid paths |
| #[test] |
| fn test_empty_string() { |
| assert_eq!(check_resource_path(""), Err(ResourcePathError::PathIsEmpty)); |
| } |
| |
| proptest! { |
| #[test] |
| fn test_reject_empty_object_name( |
| ref s in random_resource_path_with_regex_segment_str(5, "")) { |
| prop_assume!(!s.starts_with('/') && !s.ends_with('/')); |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameEmpty)); |
| } |
| |
| #[test] |
| fn test_reject_long_object_name( |
| ref s in random_resource_path_with_regex_segment_str(5, r"[[[:ascii:]]--\.--/--\x00]{256}")) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameTooLong)); |
| } |
| |
| #[test] |
| fn test_reject_contains_null( |
| ref s in random_resource_path_with_regex_segment_string( |
| 5, format!(r"{}{{0,3}}\x00{}{{0,3}}", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE, |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameContainsNull)); |
| } |
| |
| #[test] |
| fn test_reject_name_is_dot( |
| ref s in random_resource_path_with_regex_segment_str(5, r"\.")) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameIsDot)); |
| } |
| |
| #[test] |
| fn test_reject_name_is_dot_dot( |
| ref s in random_resource_path_with_regex_segment_str(5, r"\.\.")) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameIsDotDot)); |
| } |
| |
| #[test] |
| fn test_reject_starts_with_slash( |
| ref s in format!( |
| "/{}{{1,5}}", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE).as_str()) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::PathStartsWithSlash)); |
| } |
| |
| #[test] |
| fn test_reject_ends_with_slash( |
| ref s in format!( |
| "{}{{1,5}}/", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE).as_str()) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::PathEndsWithSlash)); |
| } |
| |
| #[test] |
| fn test_reject_contains_newline( |
| ref s in random_resource_path_with_regex_segment_string( |
| 5, format!(r"{}{{0,3}}\x0a{}{{0,3}}", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE, |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) { |
| prop_assert_eq!(check_resource_path(s), Err(ResourcePathError::NameContainsNewline)); |
| } |
| } |
| |
| // Tests for valid paths |
| proptest! { |
| #[test] |
| fn test_name_contains_dot( |
| ref s in random_resource_path_with_regex_segment_string( |
| 5, format!(r"{}{{1,4}}\.{}{{1,4}}", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE, |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) |
| { |
| prop_assert_eq!(check_resource_path(s), Ok(s.as_str())); |
| } |
| |
| #[test] |
| fn test_name_contains_dot_dot( |
| ref s in random_resource_path_with_regex_segment_string( |
| 5, format!(r"{}{{1,4}}\.\.{}{{1,4}}", |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE, |
| ANY_UNICODE_EXCEPT_SLASH_NULL_DOT_OR_NEWLINE))) |
| { |
| prop_assert_eq!(check_resource_path(s), Ok(s.as_str())); |
| } |
| |
| #[test] |
| fn test_single_segment(ref s in always_valid_resource_path_chars(1, 4)) { |
| prop_assert_eq!(check_resource_path(s), Ok(s.as_str())); |
| } |
| |
| #[test] |
| fn test_multi_segment( |
| ref s in prop::collection::vec(always_valid_resource_path_chars(1, 4), 1..5)) |
| { |
| let path = s.join("/"); |
| prop_assert_eq!(check_resource_path(&path), Ok(path.as_str())); |
| } |
| |
| #[test] |
| fn test_long_name( |
| ref s in random_resource_path_with_regex_segment_str( |
| 5, "[[[:ascii:]]--\0--/--\n]{255}")) // TODO(PKG-597) allow newline once meta/contents supports it in blob paths |
| { |
| prop_assert_eq!(check_resource_path(s), Ok(s.as_str())); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod check_package_name_tests { |
| use super::*; |
| use crate::test::random_package_name; |
| use proptest::prelude::*; |
| |
| #[test] |
| fn test_reject_empty_name() { |
| assert_eq!(check_package_name(""), Err(PackageNameError::Empty)); |
| } |
| |
| proptest! { |
| #[test] |
| fn test_reject_name_too_long(ref s in r"[-0-9a-z\.]{101, 200}") |
| { |
| prop_assert_eq!( |
| check_package_name(s), |
| Err(PackageNameError::TooLong{invalid_name: s.to_string()})); |
| } |
| |
| // FIXME(PKG-757): '_' should be considered to be an invalid character. |
| #[test] |
| fn test_reject_invalid_character(ref s in r"[-0-9a-z\.]{0, 48}[^-_0-9a-z\.][-0-9a-z\.]{0, 48}") |
| { |
| prop_assert_eq!( |
| check_package_name(s), |
| Err(PackageNameError::InvalidCharacter{invalid_name: s.to_string()})); |
| } |
| |
| #[test] |
| fn test_pass_through_valid_name(ref s in random_package_name()) |
| { |
| prop_assert_eq!( |
| check_package_name(s), |
| Ok(s.as_str())); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod check_package_variant_tests { |
| use super::*; |
| use crate::test::random_package_variant; |
| use proptest::prelude::*; |
| |
| #[test] |
| fn test_reject_empty_variant() { |
| assert_eq!(check_package_variant(""), Err(PackageVariantError::Empty)); |
| } |
| |
| proptest! { |
| #[test] |
| fn test_reject_variant_too_long(ref s in r"[-0-9a-z\.]{101, 200}") |
| { |
| prop_assert_eq!( |
| check_package_variant(s), |
| Err(PackageVariantError::TooLong{invalid_variant: s.to_string()})); |
| } |
| |
| #[test] |
| fn test_reject_invalid_character(ref s in r"[-0-9a-z\.]{0, 48}[^-0-9a-z\.][-0-9a-z\.]{0, 48}") |
| { |
| prop_assert_eq!( |
| check_package_variant(s), |
| Err(PackageVariantError::InvalidCharacter{invalid_variant: s.to_string()})); |
| } |
| |
| #[test] |
| fn test_pass_through_valid_variant(ref s in random_package_variant()) |
| { |
| prop_assert_eq!( |
| check_package_variant(s), |
| Ok(s.as_str())); |
| } |
| } |
| } |