serde_vars/source/
file.rs

1use std::{
2    borrow::Cow,
3    path::{Path, PathBuf},
4};
5
6use serde::de;
7
8use crate::source::{utils, Any, Expansion, Source};
9
10// Possible future improvements:
11//  - A file-system abstraction
12//  - Abstract into a byte-source
13//  - Allow modifications to conversions
14//  - More validations (e.g. base-path)
15//  - A way to specify base path for relative paths
16
17/// A [`Source`] which provides values by reading them from the file-system.
18///
19/// For string and byte types, the source will simply attempt to open the file and load its
20/// contents.
21///
22/// If, during de-serialization, the target type is known, the source will attempt to load the file
23/// as a string parse the value into the target type using [`std::str::FromStr`].
24///
25/// When de-serializing self-describing formats, like JSON or YAML into dynamic containers,
26/// like for example:
27///
28/// ```
29/// #[derive(serde::Deserialize)]
30/// #[serde(untagged)]
31/// enum StringOrInt {
32///     String(String),
33///     Int(u64),
34/// }
35/// ```
36///
37/// The target type is inferred from the loaded file contents. The source parses the file contents
38/// in following order:
39///
40/// - `true`, `false` -> `bool`
41/// - `123`, `42` -> `u64`
42/// - `-123`, `-42` -> `i64`
43/// - `-123.0`, `42.12` -> `f64`
44/// - any valid UTF-8 string -> `String`
45/// - -> `Vec<u8>`
46///
47/// # Warning:
48///
49/// This source must not be used with untrusted user input, it provides unfiltered access to the
50/// filesystem.
51pub struct FileSource {
52    base_path: PathBuf,
53    variable: utils::Variable,
54}
55
56impl FileSource {
57    /// Creates a [`FileSource`].
58    ///
59    /// By default the created source uses `${` and `}` as variable specifiers.
60    /// These can be changed using [`Self::with_variable_prefix`] and [`Self::with_variable_suffix`].
61    ///
62    /// # Examples:
63    ///
64    /// ```
65    /// # let temp = tempfile::tempdir().unwrap();
66    /// # std::fs::write(temp.path().join("my_file.txt"), "some secret value").unwrap();
67    /// #
68    /// use serde_vars::FileSource;
69    ///
70    /// let mut source = FileSource::new();
71    /// # let mut source = source.with_base_path(temp.path());
72    ///
73    /// let mut de = serde_json::Deserializer::from_str(r#""${my_file.txt}""#);
74    /// let r: String = serde_vars::deserialize(&mut de, &mut source).unwrap();
75    /// assert_eq!(r, "some secret value");
76    /// ```
77    pub fn new() -> Self {
78        Self {
79            base_path: PathBuf::new(),
80            variable: Default::default(),
81        }
82    }
83
84    /// Configures the base path to use for relative paths.
85    ///
86    /// The configured path is joined with relative paths. To be independent of the
87    /// current working directory it is recommended to configure an absolute path.
88    ///
89    /// Note: There is no validation that a final path must be within that base directory.
90    pub fn with_base_path<P>(mut self, path: P) -> Self
91    where
92        P: Into<PathBuf>,
93    {
94        self.base_path = path.into();
95        self
96    }
97
98    /// Changes the variable prefix.
99    ///
100    /// # Examples:
101    ///
102    /// ```
103    /// # let temp = tempfile::tempdir().unwrap();
104    /// # std::fs::write(temp.path().join("my_file.txt"), "some secret value").unwrap();
105    /// #
106    /// use serde_vars::FileSource;
107    ///
108    /// let mut source = FileSource::new().with_variable_prefix("${file:");
109    /// # let mut source = source.with_base_path(temp.path());
110    ///
111    /// let mut de = serde_json::Deserializer::from_str(r#""${file:my_file.txt}""#);
112    /// let r: String = serde_vars::deserialize(&mut de, &mut source).unwrap();
113    /// assert_eq!(r, "some secret value");
114    /// ```
115    pub fn with_variable_prefix(mut self, prefix: impl Into<String>) -> Self {
116        self.variable.prefix = prefix.into();
117        self
118    }
119
120    /// Changes the variable suffix.
121    pub fn with_variable_suffix(mut self, suffix: impl Into<String>) -> Self {
122        self.variable.suffix = suffix.into();
123        self
124    }
125}
126
127impl FileSource {
128    fn resolve_path<'a>(&self, path: &'a Path) -> Cow<'a, Path> {
129        match path.is_absolute() {
130            true => Cow::Borrowed(path),
131            false => Cow::Owned(self.base_path.join(path)),
132        }
133    }
134
135    fn io_error<E>(&self, path: &Path, v: &Path, error: std::io::Error) -> E
136    where
137        E: de::Error,
138    {
139        let path = path.display();
140        let var = self.variable.fmt(v.display());
141        E::custom(format!(
142            "failed to read file `{path}` from variable `{var}`: {error}"
143        ))
144    }
145
146    fn mismatched_type<E>(&self, var: &str, unexpected: de::Unexpected<'_>, expected: &str) -> E
147    where
148        E: de::Error,
149    {
150        let var = self.variable.fmt(var);
151        E::invalid_value(
152            unexpected,
153            &format!("file contents of variable `{var}` to be {expected}").as_str(),
154        )
155    }
156
157    fn parsed<V, E>(&mut self, v: &str, expected: &str) -> Result<Option<V>, E>
158    where
159        V: std::str::FromStr,
160        V::Err: std::fmt::Display,
161        E: de::Error,
162    {
163        let Some(var) = self.variable.parse_str(v) else {
164            return Ok(None);
165        };
166
167        let path = self.resolve_path(var.as_ref());
168        let value = std::fs::read_to_string(&path)
169            .map_err(|error| self.io_error(&path, var.as_ref(), error))?;
170
171        value
172            .parse()
173            .map(Some)
174            .map_err(|_| self.mismatched_type(var, de::Unexpected::Str(&value), expected))
175    }
176}
177
178impl Source for FileSource {
179    fn expand_str<'a, E>(&mut self, v: Cow<'a, str>) -> Result<Expansion<Cow<'a, str>>, E>
180    where
181        E: serde::de::Error,
182    {
183        let Some(var) = self.variable.parse_str(&v) else {
184            return Ok(Expansion::Original(v));
185        };
186
187        let path = self.resolve_path(var.as_ref());
188        let value = std::fs::read_to_string(&path)
189            .map_err(|error| self.io_error(&path, var.as_ref(), error))?;
190
191        match utils::parse(Cow::Owned(value)) {
192            Any::Str(value) => Ok(Expansion::Expanded(value)),
193            other => Err(self.mismatched_type(var, other.unexpected(), "a string")),
194        }
195    }
196
197    fn expand_bytes<'a, E>(&mut self, v: Cow<'a, [u8]>) -> Result<Expansion<Cow<'a, [u8]>>, E>
198    where
199        E: serde::de::Error,
200    {
201        let Some(var) = self.variable.parse_bytes(&v) else {
202            return Ok(Expansion::Original(v));
203        };
204
205        #[cfg(unix)]
206        let path = {
207            use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::Path};
208            Path::new(OsStr::from_bytes(var))
209        };
210        // Technically `wasi` also provides an `OsStrExt` which allows conversion from bytes, but
211        // since that seems to also be conditional on `target_env` for the sake of simplicity it's
212        // omitted here and should be added on demand.
213        #[cfg(not(unix))]
214        let path = std::str::from_utf8(var).map(Path::new).map_err(E::custom)?;
215
216        let full_path = self.resolve_path(path);
217        let value =
218            std::fs::read(&full_path).map_err(|error| self.io_error(&full_path, path, error))?;
219
220        Ok(Expansion::Expanded(Cow::Owned(value)))
221    }
222
223    fn expand_bool<E>(&mut self, v: &str) -> Result<Option<bool>, E>
224    where
225        E: de::Error,
226    {
227        self.parsed(v, "a boolean")
228    }
229
230    fn expand_i8<E>(&mut self, v: &str) -> Result<Option<i8>, E>
231    where
232        E: de::Error,
233    {
234        self.parsed(v, "a signed integer (i8)")
235    }
236
237    fn expand_i16<E>(&mut self, v: &str) -> Result<Option<i16>, E>
238    where
239        E: de::Error,
240    {
241        self.parsed(v, "a signed integer (i16)")
242    }
243
244    fn expand_i32<E>(&mut self, v: &str) -> Result<Option<i32>, E>
245    where
246        E: de::Error,
247    {
248        self.parsed(v, "a signed integer (i32)")
249    }
250
251    fn expand_i64<E>(&mut self, v: &str) -> Result<Option<i64>, E>
252    where
253        E: de::Error,
254    {
255        self.parsed(v, "a signed integer (i64)")
256    }
257
258    fn expand_u8<E>(&mut self, v: &str) -> Result<Option<u8>, E>
259    where
260        E: de::Error,
261    {
262        self.parsed(v, "an unsigned integer (i8)")
263    }
264
265    fn expand_u16<E>(&mut self, v: &str) -> Result<Option<u16>, E>
266    where
267        E: de::Error,
268    {
269        self.parsed(v, "an unsigned integer (i16)")
270    }
271
272    fn expand_u32<E>(&mut self, v: &str) -> Result<Option<u32>, E>
273    where
274        E: de::Error,
275    {
276        self.parsed(v, "an unsigned integer (i32)")
277    }
278
279    fn expand_u64<E>(&mut self, v: &str) -> Result<Option<u64>, E>
280    where
281        E: de::Error,
282    {
283        self.parsed(v, "an unsigned integer (i64)")
284    }
285
286    fn expand_f32<E>(&mut self, v: &str) -> Result<Option<f32>, E>
287    where
288        E: de::Error,
289    {
290        self.parsed(v, "a floating point")
291    }
292
293    fn expand_f64<E>(&mut self, v: &str) -> Result<Option<f64>, E>
294    where
295        E: de::Error,
296    {
297        self.parsed(v, "a floating point")
298    }
299
300    fn expand_any<'a, E>(&mut self, v: Cow<'a, str>) -> Result<Expansion<Any<'a>, Cow<'a, str>>, E>
301    where
302        E: de::Error,
303    {
304        let Some(var) = self.variable.parse_str(&v) else {
305            return Ok(Expansion::Original(v));
306        };
307
308        let path = self.resolve_path(var.as_ref());
309        let value =
310            std::fs::read(&path).map_err(|error| self.io_error(&path, var.as_ref(), error))?;
311
312        let value = String::from_utf8(value)
313            .map(Cow::Owned)
314            .map(utils::parse)
315            .unwrap_or_else(|err| Any::Bytes(Cow::Owned(err.into_bytes())));
316        Ok(Expansion::Expanded(value))
317    }
318}
319
320impl Default for FileSource {
321    fn default() -> Self {
322        Self::new()
323    }
324}