#![allow(dead_code)]
use std::env;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
pub const DEFAULT_PROFILE: &str = "main";
pub const PROFILE_ENV_VAR: &str = "CCD_PROFILE";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileName(String);
impl ProfileName {
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
validate_profile_name(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ProfileName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ProfileName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub fn resolve(explicit: Option<&str>) -> Result<ProfileName> {
let env_value = env::var_os(PROFILE_ENV_VAR).map(PathBuf::from);
resolve_with_env(explicit, env_value.as_deref())
}
pub fn resolve_with_env(explicit: Option<&str>, env_value: Option<&Path>) -> Result<ProfileName> {
if let Some(profile) = explicit {
return ProfileName::new(profile)
.with_context(|| format!("invalid explicit profile value `{profile}`"));
}
if let Some(profile) = env_value {
let profile = profile.to_string_lossy();
return ProfileName::new(profile.as_ref())
.with_context(|| format!("invalid {PROFILE_ENV_VAR} value `{profile}`"));
}
ProfileName::new(DEFAULT_PROFILE)
}
pub fn ensure_profile_dir(ccd_root: &Path, profile: &ProfileName) -> Result<PathBuf> {
let profile_root = ccd_root.join("profiles").join(profile.as_str());
let overlay_root = profile_root.join("repos");
fs::create_dir_all(&overlay_root)
.with_context(|| format!("failed to create directory {}", overlay_root.display()))?;
Ok(profile_root)
}
fn validate_profile_name(value: &str) -> Result<()> {
if value.is_empty() {
bail!("profile name cannot be empty");
}
if value == "." || value == ".." {
bail!("profile name cannot be `.` or `..`");
}
if value.contains('/') || value.contains('\\') {
bail!("profile name cannot contain path separators");
}
if value.as_bytes().contains(&0) {
bail!("profile name cannot contain NUL bytes");
}
Ok(())
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn resolve_prefers_explicit_profile() {
let profile =
resolve_with_env(Some("work"), Some(Path::new("personal"))).expect("profile resolves");
assert_eq!(profile.as_str(), "work");
}
#[test]
fn resolve_uses_environment_when_explicit_is_missing() {
let profile =
resolve_with_env(None, Some(Path::new("personal"))).expect("profile resolves");
assert_eq!(profile.as_str(), "personal");
}
#[test]
fn resolve_falls_back_to_main() {
let profile = resolve_with_env(None, None).expect("profile resolves");
assert_eq!(profile.as_str(), DEFAULT_PROFILE);
}
#[test]
fn resolve_rejects_invalid_profile_names() {
let error = resolve_with_env(Some("../bad"), None).expect_err("profile should fail");
assert!(error.to_string().contains("invalid explicit profile value"));
}
#[test]
fn ensure_profile_dir_creates_profile_kernel_skeleton() {
let temp = tempdir().expect("tempdir");
let ccd_root = temp.path().join(".ccd");
let profile = ProfileName::new("main").expect("profile");
let profile_root = ensure_profile_dir(&ccd_root, &profile).expect("profile dir");
assert_eq!(profile_root, ccd_root.join("profiles/main"));
assert!(profile_root.is_dir());
assert!(profile_root.join("repos").is_dir());
}
}