Skip to main content

rite_runtime/
output_config.rs

1//! Configuration for ceremony output directory structure.
2
3use chrono::Utc;
4use std::path::PathBuf;
5
6/// Configuration for ceremony output directory structure.
7///
8/// All ceremony outputs (artifacts and transcript) are written to a structured
9/// directory layout:
10///
11/// ```text
12/// ceremony-name-20251229T143022/
13/// ├── transcript.jsonl
14/// └── artifacts/
15///     ├── wrapped_key.bin
16///     ├── public_key.pem
17///     └── share_1.txt
18/// ```
19#[derive(Debug, Clone)]
20pub struct OutputConfig {
21    /// Base directory for all outputs (e.g., "./runs/ceremony-20251229T143022/")
22    base_dir: PathBuf,
23}
24
25impl OutputConfig {
26    /// Create a new output configuration with the given base directory.
27    #[must_use]
28    pub fn new(base_dir: PathBuf) -> Self {
29        Self { base_dir }
30    }
31
32    /// Create output configuration for a ceremony run.
33    ///
34    /// Generates a timestamped directory name from the ceremony name:
35    /// `<parent>/<ceremony-slug>-<YYYYMMDDTHHMMSS>/`
36    ///
37    /// # Arguments
38    /// * `parent` - Parent directory (defaults to current directory if None)
39    /// * `ceremony_name` - Human-readable ceremony name to slugify
40    #[must_use]
41    pub fn for_ceremony(parent: Option<PathBuf>, ceremony_name: &str) -> Self {
42        let parent = parent.unwrap_or_else(|| PathBuf::from("."));
43        let timestamp = Utc::now().format("%Y%m%dT%H%M%S");
44        let slug = slugify(ceremony_name);
45        Self {
46            base_dir: parent.join(format!("{slug}-{timestamp}")),
47        }
48    }
49
50    /// Returns the base output directory.
51    pub fn base_dir(&self) -> &std::path::Path {
52        &self.base_dir
53    }
54
55    /// Returns the path to the artifacts subdirectory.
56    pub fn artifacts_dir(&self) -> PathBuf {
57        self.base_dir.join("artifacts")
58    }
59
60    /// Returns the path to the transcript file.
61    ///
62    /// Uses JSONL format (`transcript.jsonl`) for hash-chained event log.
63    pub fn transcript_path(&self) -> PathBuf {
64        self.base_dir.join("transcript.jsonl")
65    }
66
67    /// Returns the path to the evidence subdirectory.
68    pub fn evidence_dir(&self) -> PathBuf {
69        self.base_dir.join("evidence")
70    }
71
72    /// Returns the path for an artifact with the given ID and extension.
73    pub fn artifact_path(&self, id: &str, extension: &str) -> PathBuf {
74        self.artifacts_dir().join(format!("{id}.{extension}"))
75    }
76}
77
78/// Convert a ceremony name to a filesystem-safe slug.
79fn slugify(name: &str) -> String {
80    name.to_lowercase()
81        .chars()
82        .map(|c| if c.is_alphanumeric() { c } else { '-' })
83        .collect::<String>()
84        .split('-')
85        .filter(|s| !s.is_empty())
86        .collect::<Vec<_>>()
87        .join("-")
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn output_config_paths() {
96        let config = OutputConfig::new(PathBuf::from("/tmp/ceremony-20251229T143022"));
97
98        assert_eq!(
99            config.base_dir(),
100            std::path::Path::new("/tmp/ceremony-20251229T143022")
101        );
102        assert_eq!(
103            config.artifacts_dir(),
104            PathBuf::from("/tmp/ceremony-20251229T143022/artifacts")
105        );
106        assert_eq!(
107            config.transcript_path(),
108            PathBuf::from("/tmp/ceremony-20251229T143022/transcript.jsonl")
109        );
110        assert_eq!(
111            config.artifact_path("wrapped_key", "bin"),
112            PathBuf::from("/tmp/ceremony-20251229T143022/artifacts/wrapped_key.bin")
113        );
114    }
115
116    #[test]
117    fn test_slugify() {
118        assert_eq!(slugify("Simple Name"), "simple-name");
119        assert_eq!(slugify("HSM Bootstrap"), "hsm-bootstrap");
120        assert_eq!(slugify("Root CA  Signing"), "root-ca-signing");
121        assert_eq!(slugify("Test_Ceremony-2024"), "test-ceremony-2024");
122        assert_eq!(slugify("  Leading/Trailing  "), "leading-trailing");
123    }
124}