blob: b7bdcc55fe7fa3ba26c8d8674646959c0551a800 [file] [log] [blame]
// 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::config::ValidatorFileArgsMap;
use crate::validators::{Validator, ValidatorError};
use anyhow::{format_err, Error};
use serde_json::map::Map;
use serde_json::value::Value;
use std::collections::HashMap;
/// A constraint for the size of the contents of a file.
///
/// Either min or max may be `None` but not both. Both min and max may be the same value, but max
/// should never be less than min if it exists.
#[derive(Debug)]
struct SizeConstraint {
pub min: Option<u64>,
pub max: Option<u64>,
}
impl SizeConstraint {
/// Creates a `SizeConstraint` based on the `size` value.
///
/// Sets min and max to `size`.
pub fn from_size(size: u64) -> Self {
Self { min: Some(size), max: Some(size) }
}
/// Creates a `SizeConstraint` from the given `map` value.
///
/// `map` is expected to have one or both of the following entries:
/// * min - Minimum size
/// * max - Maximum size
/// All values are expected to be convertable to `u64` values.
pub fn from_map(map: &Map<String, Value>) -> Result<Self, Error> {
let max = map.get("max").and_then(|val| val.as_u64());
let min = map.get("min").and_then(|val| val.as_u64());
match (min, max) {
(None, None) => Err(format_err!("max or min must be an unsigned 64-bit integer")),
(Some(min), Some(max)) if max < min => Err(format_err!("max cannot be less than min")),
(min, max) => Ok(Self { min, max }),
}
}
/// Tests whether the given `size` satisfies this `SizeConstraint`.
pub fn test(&self, size: u64) -> Result<(), Error> {
if let Some(min_size) = self.min {
if size < min_size {
return Err(format_err!(
"Size of contents is {}, must be at least {}",
size,
min_size,
));
}
}
if let Some(max_size) = self.max {
if size > max_size {
return Err(format_err!(
"Size of contents is {}, must be at most {}",
size,
max_size,
));
}
}
Ok(())
}
}
/// A validator that tests whether a set of files satisfies a specific size requirement.
///
/// Files may be validated for a combination of their minimum and maximum size or exact size.
/// Any file passed to this validator without an associated size constraint will result in a
/// validation failure.
#[derive(Debug)]
pub struct SizeValidator {
file_sizes: HashMap<String, SizeConstraint>,
}
impl SizeValidator {
/// Creates a `SizeValidator` based on the given `args_map`.
///
/// The `args_map` map is expected to contain entries with the following format:
/// filename : size | size_constraint
/// where
/// * filename - a string with the file's name
/// * size - u64 integer of the file's exact size
/// * size_constraint is an object with one or both of the following properties:
/// * min - Minimum size of the file in bytes
/// * max - Maximum size of the file in bytes
pub fn from_file_args_map(args_map: ValidatorFileArgsMap) -> Result<Self, ValidatorError> {
if args_map.is_empty() {
return Err(ValidatorError::InvalidValidatorArgs {
cause: format_err!("At least one file with size must be given in args"),
});
}
let mut file_sizes = HashMap::new();
for (file_name, size_value) in args_map.into_iter() {
if size_value.is_null() {
return Err(ValidatorError::InvalidValidatorArgs {
cause: format_err!("Size or min/max missing for {} file", &file_name),
});
}
let size = size_value
.as_u64()
.map_or_else(
|| match size_value.as_object() {
Some(obj) => SizeConstraint::from_map(obj),
None => Err(format_err!(
"Expected size value for {} to be a JSON object",
&file_name
)),
},
|v| Ok(SizeConstraint::from_size(v)),
)
.map_err(|cause| ValidatorError::InvalidValidatorArgs { cause })?;
file_sizes.insert(file_name, size);
}
Ok(Self { file_sizes })
}
}
impl Validator for SizeValidator {
/// Validates a file based on its size.
///
/// Any file passed to this validator whose name is not associated with a size constraint will
/// result in a `ValidatorError::FailedToValidate` error.
fn validate(&self, file_name: &str, contents: &[u8]) -> Result<(), ValidatorError> {
tracing::info!("Validating size of {}", file_name);
let size_constraint =
self.file_sizes.get(file_name).ok_or_else(|| ValidatorError::FailedToValidate {
cause: format_err!("{} not found in size file list", file_name),
file_name: file_name.to_string(),
})?;
let actual_size = contents.len() as u64;
size_constraint.test(actual_size).map_err(|err| ValidatorError::FailedToValidate {
cause: err,
file_name: file_name.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const TO_VALIDATE1: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const TO_VALIDATE2: &str = "Здравствуйте";
#[test]
fn create_from_file_args_map_missing_args_fails() {
let args = ValidatorFileArgsMap::new();
SizeValidator::from_file_args_map(args).unwrap_err();
}
#[test]
fn create_from_file_args_map_invalid_arg_value_fails() {
let mut args = ValidatorFileArgsMap::new();
args.insert("bad/value".to_string(), json!("abcdefgh"));
SizeValidator::from_file_args_map(args).unwrap_err();
}
#[test]
fn create_from_file_args_map_missing_target_file_fails() {
let mut args = ValidatorFileArgsMap::new();
args.insert("unvalidated/file".to_string(), json!(0));
let file_name_to_validate = "non/existent/file";
let validator = SizeValidator::from_file_args_map(args).unwrap();
match validator.validate(file_name_to_validate, &[]).unwrap_err() {
ValidatorError::FailedToValidate { cause: _cause, file_name } => {
assert_eq!(file_name, file_name_to_validate);
}
_ => panic!("Unexpected error returned"),
}
}
#[test]
fn create_from_file_args_map_succeeds() {
let mut args = ValidatorFileArgsMap::new();
args.insert("file_to_validate1".to_string(), json!(TO_VALIDATE1.len()));
args.insert("file_to_validate2".to_string(), json!(TO_VALIDATE2.len()));
SizeValidator::from_file_args_map(args).unwrap();
}
#[test]
fn validate_with_zero_and_empty_contents_succeeds() {
let mut args = ValidatorFileArgsMap::new();
args.insert("file_to_validate1".to_string(), json!(0));
let validator = SizeValidator::from_file_args_map(args).unwrap();
validator.validate("file_to_validate1", &[]).unwrap();
}
#[test]
fn validate_with_numbers_succeeds() {
let mut args = ValidatorFileArgsMap::new();
args.insert("file_to_validate1".to_string(), json!(TO_VALIDATE1.len()));
args.insert("file_to_validate2".to_string(), json!(TO_VALIDATE2.len()));
let validator = SizeValidator::from_file_args_map(args).unwrap();
validator.validate("file_to_validate1", TO_VALIDATE1.as_bytes()).unwrap();
validator.validate("file_to_validate2", TO_VALIDATE2.as_bytes()).unwrap();
}
#[test]
fn validate_with_objects_succeeds() {
let mut args = ValidatorFileArgsMap::new();
// Set min
args.insert("file_to_validate1".to_string(), json!({"min": TO_VALIDATE1.len()}));
args.insert("file_to_validate2".to_string(), json!({"min": TO_VALIDATE2.len()}));
// Set max
args.insert("file_to_validate3".to_string(), json!({"max": TO_VALIDATE1.len()}));
args.insert("file_to_validate4".to_string(), json!({"max": TO_VALIDATE2.len()}));
// Set both min and max with same value.
args.insert(
"file_to_validate5".to_string(),
json!({"min": TO_VALIDATE1.len(), "max": TO_VALIDATE1.len()}),
);
args.insert(
"file_to_validate6".to_string(),
json!({"min": TO_VALIDATE2.len(), "max": TO_VALIDATE2.len()}),
);
// Set both min and max with different values.
args.insert(
"file_to_validate7".to_string(),
json!({"min": TO_VALIDATE1.len()-1, "max": TO_VALIDATE1.len()+1}),
);
args.insert(
"file_to_validate8".to_string(),
json!({"min": TO_VALIDATE2.len()-1, "max": TO_VALIDATE2.len()+1}),
);
let validator = SizeValidator::from_file_args_map(args).unwrap();
validator.validate("file_to_validate1", TO_VALIDATE1.as_bytes()).unwrap();
validator.validate("file_to_validate2", TO_VALIDATE2.as_bytes()).unwrap();
validator.validate("file_to_validate3", TO_VALIDATE1.as_bytes()).unwrap();
validator.validate("file_to_validate4", TO_VALIDATE2.as_bytes()).unwrap();
validator.validate("file_to_validate5", TO_VALIDATE1.as_bytes()).unwrap();
validator.validate("file_to_validate6", TO_VALIDATE2.as_bytes()).unwrap();
validator.validate("file_to_validate7", TO_VALIDATE1.as_bytes()).unwrap();
validator.validate("file_to_validate8", TO_VALIDATE2.as_bytes()).unwrap();
}
#[test]
fn validate_with_bad_sizes_fails() {
let mut args = ValidatorFileArgsMap::new();
args.insert("file_to_validate1".to_string(), json!({"min": 0, "max": 2}));
args.insert("file_to_validate2".to_string(), json!({"min": 3, "max": 6}));
let validator = SizeValidator::from_file_args_map(args).unwrap();
validator.validate("file_to_validate1", TO_VALIDATE1.as_bytes()).unwrap_err();
validator.validate("file_to_validate2", TO_VALIDATE2.as_bytes()).unwrap_err();
}
}