use std::{
fmt::{Debug, Display},
path::{Path, PathBuf},
sync::Arc,
};
use casper_types::{
crypto,
file_utils::{read_file, ReadFileError},
};
use datasize::DataSize;
#[cfg(test)]
use once_cell::sync::Lazy;
use openssl::{
pkey::{PKey, Private},
x509::X509,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use casper_types::SecretKey;
use crate::tls::{self, LoadCertError, LoadSecretKeyError};
#[cfg(test)]
pub static RESOURCES_PATH: Lazy<PathBuf> =
Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../resources"));
#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(untagged)]
pub enum External {
Path(PathBuf),
#[serde(skip)]
#[default]
Missing,
}
impl External {
pub fn load<T, P>(self, root: P) -> Result<T, LoadError<T::Error>>
where
T: Loadable,
P: AsRef<Path>,
{
match self {
External::Path(path) => {
let full_path = if path.is_relative() {
root.as_ref().join(&path)
} else {
path
};
T::from_path(&full_path).map_err(move |error| LoadError::Failed {
error,
path: full_path.canonicalize().unwrap_or(full_path),
})
}
External::Missing => Err(LoadError::Missing),
}
}
}
pub trait Loadable: Sized {
type Error: Debug + Display;
fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>;
#[cfg(test)]
fn from_resources<P: AsRef<Path>>(rel_path: P) -> Self {
Self::from_path(RESOURCES_PATH.join(rel_path.as_ref())).unwrap_or_else(|error| {
panic!(
"could not load resources from {}: {}",
rel_path.as_ref().display(),
error
)
})
}
}
fn display_res_path<E>(result: &Result<PathBuf, E>) -> String {
result
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| String::new())
}
#[derive(Debug, Error)]
pub enum LoadError<E: Debug + Display> {
#[error("could not load from {}: {error}", display_res_path(&.path.canonicalize()))]
Failed {
path: PathBuf,
error: E,
},
#[error("value is missing (default requested)")]
Missing,
}
impl Loadable for X509 {
type Error = anyhow::Error;
fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
let error = match tls::load_cert(path) {
Ok(cert) => return Ok(cert),
Err(LoadCertError::ReadFile(error)) => {
anyhow::Error::new(error).context("failed to load certificate")
}
Err(LoadCertError::X509CertFromPem(error)) => {
anyhow::Error::new(error).context("parsing certificate")
}
};
Err(error)
}
}
impl Loadable for PKey<Private> {
type Error = anyhow::Error;
fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
let error = match tls::load_secret_key(path) {
Ok(secret_key) => return Ok(secret_key),
Err(LoadSecretKeyError::ReadFile(error)) => {
anyhow::Error::new(error).context("failed to load private key")
}
Err(LoadSecretKeyError::PrivateKeyFromPem(error)) => {
anyhow::Error::new(error).context("parsing private key")
}
};
Err(error)
}
}
impl Loadable for Arc<SecretKey> {
type Error = crypto::ErrorExt;
fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
Ok(Arc::new(SecretKey::from_file(path)?))
}
}
impl Loadable for Vec<u8> {
type Error = ReadFileError;
fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
read_file(path)
}
}
#[cfg(test)]
mod tests {
use super::External;
#[test]
fn test_to_string() {
let val: External = External::Path("foo/bar.toml".into());
assert_eq!(
"\"foo/bar.toml\"",
serde_json::to_string(&val).expect("serialization error")
);
}
#[test]
fn test_load_from_string() {
let input = "\"foo/bar.toml\"";
let val: External = serde_json::from_str(input).expect("deserialization failed");
assert_eq!(External::Path("foo/bar.toml".into()), val);
}
}