use anyhow::{Context, bail};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Environment {
pub path: String,
pub name: String,
}
fn resolve_project_symlink(env_dir: &Path, id: &str) -> Option<PathBuf> {
let meta = enwiro_daemon::meta::load_env_meta(env_dir);
if let Some(main_folder) = meta.main_folder
&& is_plain_component(&main_folder)
{
let candidate = env_dir.join(main_folder);
if candidate.is_symlink() {
return Some(candidate);
}
}
let default = env_dir.join(id);
default.is_symlink().then_some(default)
}
fn is_plain_component(name: &str) -> bool {
!name.is_empty() && !name.contains('/') && name != "." && name != ".."
}
impl Environment {
pub fn get_all(source_directory: &str) -> anyhow::Result<HashMap<String, Environment>> {
let mut results: HashMap<String, Environment> = HashMap::new();
let directory_entries = fs::read_dir(source_directory)?;
for directory_entry in directory_entries {
let entry = directory_entry.context("Failed to read directory entry")?;
let path = entry.path();
let id = path
.file_name()
.context("Failed to get file name")?
.to_str()
.context("Failed to convert file name to string")?
.to_string();
let metadata = match fs::symlink_metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
if metadata.file_type().is_symlink() && path.is_dir() {
let new_environment = Environment {
path: path
.to_str()
.context("Failed to convert path to string")?
.to_string(),
name: id.clone(),
};
results.insert(id, new_environment);
} else if metadata.file_type().is_dir() {
if let Some(inner_path) = resolve_project_symlink(&path, &id) {
let new_environment = Environment {
path: inner_path
.to_str()
.context("Failed to convert inner path to string")?
.to_string(),
name: id.clone(),
};
results.insert(id, new_environment);
}
}
}
Ok(results)
}
pub fn get_one(source_directory: &str, name: &str) -> anyhow::Result<Environment> {
let mut environments = Self::get_all(source_directory)?;
match environments.remove(name) {
Some(x) => Ok(x),
None => bail!("Environment \"{}\" does not exist", name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use enwiro_daemon::meta::EnvStats;
fn new_format_env(root: &Path, name: &str) -> PathBuf {
let env_dir = root.join(name);
fs::create_dir_all(&env_dir).unwrap();
env_dir
}
fn symlink_to_fresh_dir(env_dir: &Path, link_name: &str) -> PathBuf {
let target = env_dir.join(format!("{link_name}-target"));
fs::create_dir(&target).unwrap();
let link = env_dir.join(link_name);
std::os::unix::fs::symlink(&target, &link).unwrap();
target
}
fn write_main_folder(env_dir: &Path, main_folder: &str) {
let meta = EnvStats {
main_folder: Some(main_folder.to_string()),
..Default::default()
};
fs::write(
env_dir.join("meta.json"),
serde_json::to_string(&meta).unwrap(),
)
.unwrap();
}
#[test]
fn get_all_defaults_to_the_same_named_symlink_without_main_folder() {
let root = tempfile::tempdir().unwrap();
let env_dir = new_format_env(root.path(), "my-env");
symlink_to_fresh_dir(&env_dir, "my-env");
let environments = Environment::get_all(root.path().to_str().unwrap()).unwrap();
assert_eq!(
environments["my-env"].path,
env_dir.join("my-env").to_str().unwrap()
);
}
#[test]
fn get_all_uses_main_folder_when_present() {
let root = tempfile::tempdir().unwrap();
let env_dir = new_format_env(root.path(), "my-env");
symlink_to_fresh_dir(&env_dir, "my-env");
symlink_to_fresh_dir(&env_dir, "sub");
write_main_folder(&env_dir, "sub");
let environments = Environment::get_all(root.path().to_str().unwrap()).unwrap();
assert_eq!(
environments["my-env"].path,
env_dir.join("sub").to_str().unwrap()
);
}
#[test]
fn get_all_falls_back_to_default_when_main_folder_does_not_resolve() {
let root = tempfile::tempdir().unwrap();
let env_dir = new_format_env(root.path(), "my-env");
symlink_to_fresh_dir(&env_dir, "my-env");
write_main_folder(&env_dir, "does-not-exist");
let environments = Environment::get_all(root.path().to_str().unwrap()).unwrap();
assert_eq!(
environments["my-env"].path,
env_dir.join("my-env").to_str().unwrap()
);
}
#[test]
fn get_all_falls_back_to_default_when_main_folder_attempts_traversal() {
let root = tempfile::tempdir().unwrap();
let env_dir = new_format_env(root.path(), "my-env");
symlink_to_fresh_dir(&env_dir, "my-env");
symlink_to_fresh_dir(root.path(), "escaped");
write_main_folder(&env_dir, "../escaped");
let environments = Environment::get_all(root.path().to_str().unwrap()).unwrap();
assert_eq!(
environments["my-env"].path,
env_dir.join("my-env").to_str().unwrap()
);
}
}