openbim_ifcx/layers/
fs.rs

1use std::fmt;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use super::LayerResolver;
6
7/// Loads layers from the local filesystem. Requires the `fs` feature.
8///
9/// An import `uri` is a file path. A relative path resolves against the
10/// directory of the importing layer, and the main layer's against the current
11/// directory. Keys are canonical paths where the file exists, so a layer
12/// reached by different relative paths loads once.
13///
14/// URIs with a scheme (`scheme://...`) are rejected with
15/// [`FsError::UnsupportedUri`] unless [`map_prefix`](Self::map_prefix) maps
16/// them to a local directory, for example an offline mirror of `ifcx.dev`:
17///
18/// ```no_run
19/// use openbim_ifcx::layers::{FsResolver, LayerStackBuilder};
20///
21/// let resolver = FsResolver::new().map_prefix("https://ifcx.dev/", "mirror/ifcx.dev");
22/// let stack = LayerStackBuilder::new(resolver).build("model.ifcx")?;
23/// # Ok::<(), Box<dyn std::error::Error>>(())
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct FsResolver {
27    prefixes: Vec<(String, PathBuf)>,
28}
29
30impl FsResolver {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Resolves URIs starting with `prefix` to the rest of the URI below
36    /// `dir`. The first matching prefix in insertion order applies. The rest
37    /// is used as written; percent-escapes are not decoded.
38    pub fn map_prefix(mut self, prefix: impl Into<String>, dir: impl Into<PathBuf>) -> Self {
39        self.prefixes.push((prefix.into(), dir.into()));
40        self
41    }
42
43    fn path(&self, uri: &str, importer: Option<&str>) -> Result<PathBuf, FsError> {
44        for (prefix, dir) in &self.prefixes {
45            if let Some(rest) = uri.strip_prefix(prefix.as_str()) {
46                return Ok(dir.join(rest.trim_start_matches('/')));
47            }
48        }
49        if uri.contains("://") {
50            return Err(FsError::UnsupportedUri(uri.to_owned()));
51        }
52        let path = Path::new(uri);
53        Ok(match importer.and_then(|key| Path::new(key).parent()) {
54            Some(dir) if path.is_relative() => dir.join(path),
55            _ => path.to_path_buf(),
56        })
57    }
58}
59
60impl LayerResolver for FsResolver {
61    type Error = FsError;
62
63    fn key(&mut self, uri: &str, importer: Option<&str>) -> Result<String, FsError> {
64        let path = self.path(uri, importer)?;
65        // A missing file keeps its joined path; `load` then reports it missing.
66        let path = std::fs::canonicalize(&path).unwrap_or(path);
67        path.into_os_string()
68            .into_string()
69            .map_err(|path| FsError::NonUtf8Path(path.into()))
70    }
71
72    fn load(&mut self, key: &str) -> Result<Option<Vec<u8>>, FsError> {
73        match std::fs::read(key) {
74            Ok(bytes) => Ok(Some(bytes)),
75            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
76            Err(source) => Err(FsError::Io {
77                path: key.into(),
78                source,
79            }),
80        }
81    }
82}
83
84/// Why [`FsResolver`] could not load a layer.
85#[derive(Debug)]
86pub enum FsError {
87    /// The URI has a scheme and no [`FsResolver::map_prefix`] mapping.
88    UnsupportedUri(String),
89    /// Layer keys are strings, and this path is not valid UTF-8.
90    NonUtf8Path(PathBuf),
91    Io {
92        path: PathBuf,
93        source: io::Error,
94    },
95}
96
97impl fmt::Display for FsError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::UnsupportedUri(uri) => write!(f, "no local mapping for URI {uri:?}"),
101            Self::NonUtf8Path(path) => write!(f, "path {} is not valid UTF-8", path.display()),
102            Self::Io { path, source } => write!(f, "reading {}: {source}", path.display()),
103        }
104    }
105}
106
107impl std::error::Error for FsError {
108    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
109        match self {
110            Self::Io { source, .. } => Some(source),
111            Self::UnsupportedUri(_) | Self::NonUtf8Path(_) => None,
112        }
113    }
114}