use std::path::{Path, PathBuf};
use super::CoreError;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Manifest {
pub project_id: String,
#[serde(default = "default_rotation_reminder_days")]
pub rotation_reminder_days: u32,
}
fn default_rotation_reminder_days() -> u32 {
90
}
pub fn find_manifest(start_dir: &Path) -> Result<(Manifest, PathBuf), CoreError> {
let mut dir = start_dir.to_path_buf();
loop {
let candidate = dir.join("envy.toml");
match std::fs::read_to_string(&candidate) {
Ok(content) => {
let manifest = toml::from_str::<Manifest>(&content)
.map_err(|e| CoreError::ManifestInvalid(e.to_string()))?;
return Ok((manifest, dir));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(CoreError::ManifestIo(e.to_string())),
}
match dir.parent() {
Some(parent) => dir = parent.to_path_buf(),
None => return Err(CoreError::ManifestNotFound),
}
}
}
pub fn create_manifest(target_dir: &Path, project_id: &str) -> Result<(), CoreError> {
use std::io::Write as _;
let path = target_dir.join("envy.toml");
let content = format!(
"# Created by `envy init`. Do not delete — this file links the directory to its vault.\nproject_id = \"{}\"\n\n# Days a secret can go unmodified before `envy status` flags it for rotation.\n# Uncomment to override the default (90).\n# rotation_reminder_days = 90\n",
project_id
);
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|e| CoreError::ManifestIo(e.to_string()))?;
file.write_all(content.as_bytes())
.map_err(|e| CoreError::ManifestIo(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_manifest_in_current_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let project_id = "550e8400-e29b-41d4-a716-446655440000";
std::fs::write(
tmp.path().join("envy.toml"),
format!("project_id = \"{}\"\n", project_id),
)
.expect("write envy.toml");
let (manifest, found_dir) = find_manifest(tmp.path()).expect("find_manifest must succeed");
assert_eq!(manifest.project_id, project_id);
assert_eq!(found_dir, tmp.path());
}
#[test]
fn find_manifest_in_parent_dir() {
let parent = tempfile::tempdir().expect("tempdir");
let grandchild = parent.path().join("child").join("grandchild");
std::fs::create_dir_all(&grandchild).expect("create subdirectories");
let project_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
std::fs::write(
parent.path().join("envy.toml"),
format!("project_id = \"{}\"\n", project_id),
)
.expect("write envy.toml");
let (manifest, found_dir) = find_manifest(&grandchild).expect("find_manifest must succeed");
assert_eq!(manifest.project_id, project_id);
assert_eq!(found_dir, parent.path());
}
#[test]
fn find_manifest_not_found() {
let tmp = tempfile::tempdir().expect("tempdir");
let result = find_manifest(tmp.path());
assert!(
matches!(result, Err(CoreError::ManifestNotFound)),
"expected ManifestNotFound, got: {:?}",
result
);
}
#[test]
fn create_and_read_manifest() {
let tmp = tempfile::tempdir().expect("tempdir");
let project_id = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
create_manifest(tmp.path(), project_id).expect("create_manifest must succeed");
assert!(
tmp.path().join("envy.toml").exists(),
"envy.toml must be created"
);
let content = std::fs::read_to_string(tmp.path().join("envy.toml")).expect("read file");
let parsed: Manifest = toml::from_str(&content).expect("parse TOML");
assert_eq!(parsed.project_id, project_id);
let (found, _dir) = find_manifest(tmp.path()).expect("find_manifest must succeed");
assert_eq!(found.project_id, project_id);
}
}