use {
crate::error::DictionaryConfigError,
std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::Arc,
},
};
#[derive(Clone, Debug)]
pub enum Dictionary {
InlineToml {
contents: Arc<HashMap<String, String>>,
},
Json {
file: PathBuf,
},
}
impl Dictionary {
pub fn is_json(&self) -> bool {
matches!(self, Self::Json { .. })
}
pub fn file_path(&self) -> Option<&Path> {
match self {
Self::InlineToml { .. } => None,
Self::Json { file, .. } => Some(file.as_path()),
}
}
fn read_json_contents(file: &Path) -> Result<HashMap<String, String>, DictionaryConfigError> {
let data = fs::read_to_string(file).map_err(DictionaryConfigError::IoError)?;
let json = match serde_json::from_str(&data)
.map_err(|_| DictionaryConfigError::DictionaryFileWrongFormat)?
{
serde_json::Value::Object(obj) => obj,
_ => {
return Err(DictionaryConfigError::DictionaryFileWrongFormat);
}
};
let mut contents = HashMap::with_capacity(json.len());
for (key, value) in json {
let value = value
.as_str()
.ok_or_else(|| DictionaryConfigError::DictionaryItemValueWrongFormat {
key: key.clone(),
})?
.to_owned();
contents.insert(key, value);
}
deserialization::validate_dictionary_contents(&contents)?;
Ok(contents)
}
pub fn load(&self) -> Result<LoadedDictionary, DictionaryConfigError> {
let contents = match self {
Dictionary::InlineToml { contents } => Arc::clone(contents),
Dictionary::Json { file } => {
let contents = Self::read_json_contents(file)?;
Arc::new(contents)
}
};
Ok(LoadedDictionary { contents })
}
}
#[derive(Clone)]
pub struct LoadedDictionary {
pub contents: Arc<HashMap<String, String>>,
}
#[derive(Clone, Debug, Default)]
pub struct DictionariesConfig(pub HashMap<String, Dictionary>);
mod deserialization {
use {
super::{DictionariesConfig, Dictionary},
crate::{
config::limits::{DICTIONARY_ITEM_KEY_MAX_LEN, DICTIONARY_ITEM_VALUE_MAX_LEN},
error::{DictionaryConfigError, FastlyConfigError},
},
std::{collections::HashMap, path::PathBuf, sync::Arc},
toml::value::{Table, Value},
tracing::info,
};
fn check_for_unrecognized_keys(table: &Table) -> Result<(), DictionaryConfigError> {
if let Some(key) = table.keys().next() {
Err(DictionaryConfigError::UnrecognizedKey(key.to_owned()))
} else {
Ok(())
}
}
impl TryFrom<Table> for DictionariesConfig {
type Error = FastlyConfigError;
fn try_from(toml: Table) -> Result<Self, Self::Error> {
fn process_entry(
name: &str,
entry: Value,
) -> Result<(String, Dictionary), DictionaryConfigError> {
let mut toml = match entry {
Value::Table(table) => table,
_ => return Err(DictionaryConfigError::InvalidEntryType),
};
let format = toml
.remove("format")
.ok_or(DictionaryConfigError::MissingFormat)
.and_then(|format| match format {
Value::String(format) => Ok(format),
_ => Err(DictionaryConfigError::InvalidFormatEntry),
})?;
let dictionary = match format.as_str() {
"inline-toml" => process_inline_toml_dictionary(&mut toml)?,
"json" => process_json_dictionary(&mut toml)?,
"" => return Err(DictionaryConfigError::EmptyFormatEntry),
_ => {
return Err(DictionaryConfigError::InvalidDictionaryFormat(
format.to_owned(),
));
}
};
check_for_unrecognized_keys(&toml)?;
Ok((name.to_string(), dictionary))
}
toml.into_iter()
.map(|(name, defs)| {
process_entry(&name, defs)
.map_err(|err| FastlyConfigError::InvalidDictionaryDefinition { name, err })
})
.collect::<Result<_, _>>()
.map(Self)
}
}
fn process_inline_toml_dictionary(
toml: &mut Table,
) -> Result<Dictionary, DictionaryConfigError> {
let toml = match toml
.remove("contents")
.ok_or(DictionaryConfigError::MissingContents)?
{
Value::Table(table) => table,
_ => return Err(DictionaryConfigError::InvalidContentsType),
};
let mut contents = HashMap::with_capacity(toml.len());
for (key, value) in toml {
let value = value
.as_str()
.ok_or(DictionaryConfigError::InvalidInlineEntryType)?
.to_owned();
contents.insert(key, value);
}
validate_dictionary_contents(&contents)?;
Ok(Dictionary::InlineToml {
contents: Arc::new(contents),
})
}
fn process_json_dictionary(toml: &mut Table) -> Result<Dictionary, DictionaryConfigError> {
let file: PathBuf = match toml
.remove("file")
.ok_or(DictionaryConfigError::MissingFile)?
{
Value::String(file) => {
if file.is_empty() {
return Err(DictionaryConfigError::EmptyFileEntry);
} else {
file.into()
}
}
_ => return Err(DictionaryConfigError::InvalidFileEntry),
};
Dictionary::read_json_contents(&file)?;
Ok(Dictionary::Json { file })
}
pub(super) fn validate_dictionary_contents(
dict: &HashMap<String, String>,
) -> Result<(), DictionaryConfigError> {
info!("checking if dictionary adheres to Fastly's API",);
for (key, value) in dict.iter() {
if key.chars().count() > DICTIONARY_ITEM_KEY_MAX_LEN {
return Err(DictionaryConfigError::DictionaryItemKeyTooLong {
key: key.clone(),
size: DICTIONARY_ITEM_KEY_MAX_LEN.try_into().unwrap(),
});
}
if value.chars().count() > DICTIONARY_ITEM_VALUE_MAX_LEN {
return Err(DictionaryConfigError::DictionaryItemValueTooLong {
key: key.clone(),
size: DICTIONARY_ITEM_VALUE_MAX_LEN.try_into().unwrap(),
});
}
}
Ok(())
}
}