1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! External resource handling
//!
//! The `External` type abstracts away the loading of external resources. See the type documentation
//! for details.

use std::{
    fmt::{Debug, Display},
    path::{Path, PathBuf},
};

use datasize::DataSize;
use openssl::{
    pkey::{PKey, Private},
    x509::X509,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::{read_file, ReadFileError};
use crate::{
    crypto::{self, asymmetric_key::SecretKey},
    tls,
};

#[cfg(test)]
lazy_static::lazy_static! {
    /// Path to bundled resources.
    pub static ref RESOURCES_PATH: PathBuf =
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../resources");
}

/// External resource.
///
/// An `External` resource can be given in two ways: Either as an immediate value, or through a
/// path, provided the value implements `Loadable`.
///
/// Serializing and deserializing an `External` value is only possible if it is in path form. This
/// is especially useful when writing structure configurations.
///
/// An `External` also always provides a default, which will always result in an error when `load`
/// is called. Should the underlying type `T` implement `Default`, the `with_default` can be
/// used instead.
#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum External<T> {
    /// Value that should be loaded from an external path.
    Path(PathBuf),
    /// Loaded or immediate value.
    #[serde(skip)]
    Loaded(T),
    /// The value has not been specified, but a default has been requested.
    #[serde(skip)]
    Missing,
}

impl<T> External<T> {
    /// Creates an external from a value.
    pub fn value(value: T) -> Self {
        External::Loaded(value)
    }

    /// Creates an external referencing a path.
    pub fn path<P: AsRef<Path>>(path: P) -> Self {
        External::Path(path.as_ref().to_owned())
    }
}

impl<T> External<T>
where
    T: Loadable,
{
    /// Loads the value if not loaded already, resolving relative paths from `root` or returns
    /// available value. If the value is `Missing`, returns an error.
    pub fn load<P: AsRef<Path>>(self, root: P) -> Result<T, LoadError<T::Error>> {
        match self {
            External::Path(path) => {
                let full_path = if path.is_relative() {
                    root.as_ref().join(&path)
                } else {
                    path
                };

                T::from_file(&full_path).map_err(move |error| LoadError::Failed {
                    error,
                    // We canonicalize `full_path` here, with `ReadFileError` we get extra
                    // information about the absolute path this way if the latter is relative. It
                    // will still be relative if the current path does not exist.
                    path: full_path.canonicalize().unwrap_or(full_path),
                })
            }
            External::Loaded(value) => Ok(value),
            External::Missing => Err(LoadError::Missing),
        }
    }
}

impl<T> External<T>
where
    T: Loadable + Default,
{
    /// Insert a default value if missing.
    pub fn with_default(self) -> Self {
        match self {
            External::Missing => External::Loaded(Default::default()),
            _ => self,
        }
    }
}

/// A value that can be loaded from a file.
pub trait Loadable: Sized {
    /// Error that can occur when attempting to load.
    type Error: Debug + Display;

    /// Loads a value from the given input path.
    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>;

    /// Load a test-only instance from the local path.
    #[cfg(test)]
    fn from_resources<P: AsRef<Path>>(rel_path: P) -> Self {
        Self::from_file(RESOURCES_PATH.join(rel_path)).expect("could not load resources from local")
    }
}

impl<T> Default for External<T> {
    fn default() -> Self {
        External::Missing
    }
}

fn display_res_path<E>(result: &Result<PathBuf, E>) -> String {
    result
        .as_ref()
        .map(|p| p.display().to_string())
        .unwrap_or_else(|_| String::new())
}

/// Error loading external value.
#[derive(Debug, Error)]
pub enum LoadError<E: Debug + Display> {
    /// Failed to load from path.
    #[error("could not load from {}: {error}", display_res_path(&.path.canonicalize()))]
    Failed {
        /// Path that failed to load.
        path: PathBuf,
        /// Error load failed with.
        error: E,
    },
    /// A value was missing.
    #[error("value is missing (default requested)")]
    Missing,
}

// We supply a few useful implementations for external types.
impl Loadable for X509 {
    type Error = anyhow::Error;

    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
        tls::load_cert(path)
    }
}

impl Loadable for PKey<Private> {
    type Error = anyhow::Error;

    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
        tls::load_private_key(path)
    }
}

impl Loadable for SecretKey {
    type Error = crypto::Error;

    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
        SecretKey::from_file(path)
    }
}

impl Loadable for Vec<u8> {
    type Error = ReadFileError;

    fn from_file<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);
    }
}