Skip to main content

serde_vars/source/
file.rs

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