use std::path::{Path, PathBuf};
#[cfg(any(feature = "wasm", test))]
pub(crate) use in_memory::InMemoryConfigFiles;
pub(crate) trait ConfigFileSource {
fn exists(&self, path: &Path) -> bool;
fn read_to_string(&self, path: &Path) -> std::io::Result<String>;
fn canonicalize(&self, path: &Path) -> PathBuf;
fn env_var(&self, name: &str) -> Option<String>;
fn home_dir(&self) -> Option<PathBuf>;
}
pub(crate) struct FsConfigFiles;
impl ConfigFileSource for FsConfigFiles {
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
fn canonicalize(&self, path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn env_var(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn home_dir(&self) -> Option<PathBuf> {
#[cfg(feature = "native")]
{
use etcetera::{BaseStrategy, choose_base_strategy};
choose_base_strategy().ok().map(|s| s.home_dir().to_path_buf())
}
#[cfg(not(feature = "native"))]
{
None
}
}
}
#[cfg(any(feature = "wasm", test))]
mod in_memory {
use super::ConfigFileSource;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
pub(crate) struct InMemoryConfigFiles {
files: HashMap<PathBuf, Option<String>>,
env: HashMap<String, String>,
home: Option<PathBuf>,
needed: RefCell<Option<PathBuf>>,
}
impl InMemoryConfigFiles {
pub(crate) fn new(
files: impl IntoIterator<Item = (String, Option<String>)>,
env: HashMap<String, String>,
home: Option<PathBuf>,
) -> Self {
Self {
files: files
.into_iter()
.map(|(path, content)| (lexically_normalized(Path::new(&path)), content))
.collect(),
env,
home,
needed: RefCell::new(None),
}
}
pub(crate) fn needed(&self) -> Option<PathBuf> {
self.needed.borrow().clone()
}
fn lookup(&self, path: &Path) -> Option<&Option<String>> {
let key = lexically_normalized(path);
let found = self.files.get(&key);
if found.is_none() {
let mut needed = self.needed.borrow_mut();
if needed.is_none() {
*needed = Some(key);
}
}
found
}
}
impl ConfigFileSource for InMemoryConfigFiles {
fn exists(&self, path: &Path) -> bool {
matches!(self.lookup(path), Some(Some(_)))
}
fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
match self.lookup(path) {
Some(Some(content)) => Ok(content.clone()),
_ => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{} was not provided", path.display()),
)),
}
}
fn canonicalize(&self, path: &Path) -> PathBuf {
lexically_normalized(path)
}
fn env_var(&self, name: &str) -> Option<String> {
self.env.get(name).cloned()
}
fn home_dir(&self) -> Option<PathBuf> {
self.home.clone()
}
}
fn lexically_normalized(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
let climbable = matches!(out.components().next_back(), Some(Component::Normal(_)));
if climbable {
out.pop();
} else {
out.push(component);
}
}
other => out.push(other),
}
}
if out.as_os_str().is_empty() {
out.push(".");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_dot_segments_lexically() {
assert_eq!(
lexically_normalized(Path::new("docs/../base.toml")),
PathBuf::from("base.toml")
);
assert_eq!(
lexically_normalized(Path::new("./a/./b.toml")),
PathBuf::from("a/b.toml")
);
assert_eq!(
lexically_normalized(Path::new("a/b/../../c.toml")),
PathBuf::from("c.toml")
);
assert_eq!(
lexically_normalized(Path::new("/x/../y.toml")),
PathBuf::from("/y.toml")
);
}
#[test]
fn keeps_unclimbable_parent_segments() {
assert_eq!(
lexically_normalized(Path::new("../base.toml")),
PathBuf::from("../base.toml")
);
assert_eq!(
lexically_normalized(Path::new("../../base.toml")),
PathBuf::from("../../base.toml")
);
assert_eq!(
lexically_normalized(Path::new("a/../../base.toml")),
PathBuf::from("../base.toml")
);
assert_eq!(lexically_normalized(Path::new(".")), PathBuf::from("."));
}
#[test]
fn in_memory_reports_the_first_unsupplied_path() {
let files =
InMemoryConfigFiles::new([(".rumdl.toml".to_string(), Some(String::new()))], HashMap::new(), None);
assert!(files.exists(Path::new("./.rumdl.toml")));
assert_eq!(files.needed(), None);
assert!(!files.exists(Path::new("docs/../base/.rumdl.toml")));
assert!(files.read_to_string(Path::new("other.toml")).is_err());
assert_eq!(
files.needed(),
Some(PathBuf::from("base/.rumdl.toml")),
"the first unsupplied path is reported, in its normalized form"
);
}
#[test]
fn in_memory_distinguishes_missing_from_unsupplied() {
let files = InMemoryConfigFiles::new([("gone.toml".to_string(), None)], HashMap::new(), None);
assert!(!files.exists(Path::new("gone.toml")));
assert!(files.read_to_string(Path::new("gone.toml")).is_err());
assert_eq!(
files.needed(),
None,
"a file the embedder reported missing is not a request for it"
);
}
}
}