casper_node/utils/
external.rs

1//! External resource handling
2//!
3//! The `External` type abstracts away the loading of external resources. See the type documentation
4//! for details.
5
6use std::{
7    fmt::{Debug, Display},
8    path::{Path, PathBuf},
9    sync::Arc,
10};
11
12use casper_types::{
13    crypto,
14    file_utils::{read_file, ReadFileError},
15};
16use datasize::DataSize;
17#[cfg(test)]
18use once_cell::sync::Lazy;
19use openssl::{
20    pkey::{PKey, Private},
21    x509::X509,
22};
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26use casper_types::SecretKey;
27
28use crate::tls::{self, LoadCertError, LoadSecretKeyError};
29
30/// Path to bundled resources.
31#[cfg(test)]
32pub static RESOURCES_PATH: Lazy<PathBuf> =
33    Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../resources"));
34
35/// External resource.
36///
37/// An `External` resource can be given in two ways: Either as an immediate value, or through a
38/// path, provided the value implements `Loadable`.
39///
40/// Serializing and deserializing an `External` value is only possible if it is in path form. This
41/// is especially useful when writing structure configurations.
42///
43/// An `External` also always provides a default, which will always result in an error when `load`
44/// is called. Should the underlying type `T` implement `Default`, the `with_default` can be
45/// used instead.
46#[derive(Clone, DataSize, Eq, Debug, Deserialize, PartialEq, Serialize, Default)]
47#[serde(untagged)]
48pub enum External {
49    /// Value that should be loaded from an external path.
50    Path(PathBuf),
51    /// The value has not been specified, but a default has been requested.
52    #[serde(skip)]
53    #[default]
54    Missing,
55}
56
57impl External {
58    /// Loads the value if not loaded already, resolving relative paths from `root` or returns
59    /// available value. If the value is `Missing`, returns an error.
60    pub fn load<T, P>(self, root: P) -> Result<T, LoadError<T::Error>>
61    where
62        T: Loadable,
63        P: AsRef<Path>,
64    {
65        match self {
66            External::Path(path) => {
67                let full_path = if path.is_relative() {
68                    root.as_ref().join(&path)
69                } else {
70                    path
71                };
72
73                T::from_path(&full_path).map_err(move |error| LoadError::Failed {
74                    error,
75                    // We canonicalize `full_path` here, with `ReadFileError` we get extra
76                    // information about the absolute path this way if the latter is relative. It
77                    // will still be relative if the current path does not exist.
78                    path: full_path.canonicalize().unwrap_or(full_path),
79                })
80            }
81            External::Missing => Err(LoadError::Missing),
82        }
83    }
84}
85
86/// A value that can be loaded from a file.
87pub trait Loadable: Sized {
88    /// Error that can occur when attempting to load.
89    type Error: Debug + Display;
90
91    /// Loads a value from the given input path.
92    fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>;
93
94    /// Load a test-only instance from the local path.
95    #[cfg(test)]
96    fn from_resources<P: AsRef<Path>>(rel_path: P) -> Self {
97        Self::from_path(RESOURCES_PATH.join(rel_path.as_ref())).unwrap_or_else(|error| {
98            panic!(
99                "could not load resources from {}: {}",
100                rel_path.as_ref().display(),
101                error
102            )
103        })
104    }
105}
106
107fn display_res_path<E>(result: &Result<PathBuf, E>) -> String {
108    result
109        .as_ref()
110        .map(|p| p.display().to_string())
111        .unwrap_or_else(|_| String::new())
112}
113
114/// Error loading external value.
115#[derive(Debug, Error)]
116pub enum LoadError<E: Debug + Display> {
117    /// Failed to load from path.
118    #[error("could not load from {}: {error}", display_res_path(&.path.canonicalize()))]
119    Failed {
120        /// Path that failed to load.
121        path: PathBuf,
122        /// Error load failed with.
123        error: E,
124    },
125    /// A value was missing.
126    #[error("value is missing (default requested)")]
127    Missing,
128}
129
130// We supply a few useful implementations for external types.
131impl Loadable for X509 {
132    type Error = anyhow::Error;
133
134    fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
135        let error = match tls::load_cert(path) {
136            Ok(cert) => return Ok(cert),
137            Err(LoadCertError::ReadFile(error)) => {
138                anyhow::Error::new(error).context("failed to load certificate")
139            }
140            Err(LoadCertError::X509CertFromPem(error)) => {
141                anyhow::Error::new(error).context("parsing certificate")
142            }
143        };
144        Err(error)
145    }
146}
147
148impl Loadable for PKey<Private> {
149    type Error = anyhow::Error;
150
151    fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
152        let error = match tls::load_secret_key(path) {
153            Ok(secret_key) => return Ok(secret_key),
154            Err(LoadSecretKeyError::ReadFile(error)) => {
155                anyhow::Error::new(error).context("failed to load private key")
156            }
157            Err(LoadSecretKeyError::PrivateKeyFromPem(error)) => {
158                anyhow::Error::new(error).context("parsing private key")
159            }
160        };
161
162        Err(error)
163    }
164}
165
166impl Loadable for Arc<SecretKey> {
167    type Error = crypto::ErrorExt;
168
169    fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
170        Ok(Arc::new(SecretKey::from_file(path)?))
171    }
172}
173
174impl Loadable for Vec<u8> {
175    type Error = ReadFileError;
176
177    fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error> {
178        read_file(path)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::External;
185
186    #[test]
187    fn test_to_string() {
188        let val: External = External::Path("foo/bar.toml".into());
189        assert_eq!(
190            "\"foo/bar.toml\"",
191            serde_json::to_string(&val).expect("serialization error")
192        );
193    }
194
195    #[test]
196    fn test_load_from_string() {
197        let input = "\"foo/bar.toml\"";
198
199        let val: External = serde_json::from_str(input).expect("deserialization failed");
200
201        assert_eq!(External::Path("foo/bar.toml".into()), val);
202    }
203}