Skip to main content

confium_publish/
cli.rs

1// Clap-derived argument definitions for the `confium-publish` command.
2//
3// The single `PublishArgs` struct holds every flag the publishing flow
4// needs. There are no subcommands — `confium-publish` is one-shot: load
5// artifact, query FFI, emit a manifest tree. New flags are added by
6// appending a field here (Open/Closed Principle — no dispatch table to
7// touch).
8//
9
10use std::path::PathBuf;
11
12use clap::Parser;
13
14/// Author a registry-ready plugin release.
15///
16/// Loads the built artifact, queries it via the FFI contract, computes
17/// its SHA-256, generates `manifest.toml`, signs it with the publisher's
18/// PGP key, and writes a directory tree ready to drop into
19/// `github.com/confium/registry/plugins/`.
20#[derive(Parser, Debug)]
21#[command(
22    name = "confium-publish",
23    about = "Author a signed registry release for a Confium plugin",
24    long_about = "Confium plugin publishing tool — load, manifest, sign, output.",
25    // The spec's `--version <semver>` is the *plugin* version, which would
26    // collide with clap's auto-generated tool `--version`. Disable the
27    // auto flag so the plugin-version arg owns the name unambiguously.
28    disable_version_flag = true,
29)]
30pub struct PublishArgs {
31    /// Path to the built plugin artifact (`.so` / `.dylib` / `.dll`).
32    pub artifact: PathBuf,
33
34    /// Plugin name (e.g. `botan`). Overrides FFI metadata when present.
35    #[arg(long)]
36    pub name: Option<String>,
37
38    /// Plugin version as SemVer (e.g. `3.2.0`). Overrides FFI metadata.
39    #[arg(long)]
40    pub version: Option<String>,
41
42    /// Publisher identity whose key will sign the release.
43    #[arg(long)]
44    pub publisher: String,
45
46    /// Path to the publisher's PGP secret-key file (`.asc`).
47    #[arg(long)]
48    pub signing_key: PathBuf,
49
50    /// Registry git URL the output is destined for.
51    #[arg(long, default_value = "git@github.com:confium/registry.git")]
52    pub registry: String,
53
54    /// URL prefix where the artifact will be hosted. The artifact
55    /// basename is appended to form `[artifact].url`.
56    #[arg(long)]
57    pub artifact_base: Option<String>,
58
59    /// Override the `[interfaces]` map (e.g. `hash:0,rng:0`). Skips the
60    /// FFI query when present.
61    #[arg(long, value_delimiter = ',')]
62    pub interfaces: Option<Vec<String>>,
63
64    /// Override the `[algorithms]` map (e.g. `hash:SHA-256;SHA-512`).
65    #[arg(long, value_delimiter = ',')]
66    pub algorithms: Option<Vec<String>>,
67
68    /// Print the planned actions and output tree without writing to disk
69    /// or invoking `gpg`.
70    #[arg(long, default_value_t = false)]
71    pub dry_run: bool,
72}
73
74/// Parse `--interfaces name:ver,name:ver` into an ordered list of
75/// `(name, version)` pairs. Used by `load` when the FFI query is
76/// bypassed.
77pub fn parse_interface_overrides(raw: &[String]) -> Result<Vec<(String, u8)>, String> {
78    let mut out = Vec::with_capacity(raw.len());
79    for entry in raw {
80        let (name, ver) = entry
81            .split_once(':')
82            .ok_or_else(|| format!("interface '{entry}' missing ':version'"))?;
83        let version: u8 = ver
84            .parse()
85            .map_err(|_| format!("interface '{name}' version '{ver}' not a u8"))?;
86        out.push((name.to_string(), version));
87    }
88    Ok(out)
89}
90
91/// Parse `--algorithms iface:a1;a2,iface:b1` into an ordered list of
92/// `(interface, [algorithm, ...])` pairs.
93pub fn parse_algorithm_overrides(raw: &[String]) -> Result<Vec<(String, Vec<String>)>, String> {
94    let mut out = Vec::with_capacity(raw.len());
95    for entry in raw {
96        let (iface, algos) = entry
97            .split_once(':')
98            .ok_or_else(|| format!("algorithm '{entry}' missing ':list'"))?;
99        let list: Vec<String> = algos.split(';').map(str::to_string).collect();
100        out.push((iface.to_string(), list));
101    }
102    Ok(out)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn parse_interface_overrides_splits_name_version() {
111        let raw = vec!["hash:0".to_string(), "aead:1".to_string()];
112        let got = parse_interface_overrides(&raw).unwrap();
113        assert_eq!(got, vec![("hash".into(), 0), ("aead".into(), 1)]);
114    }
115
116    #[test]
117    fn parse_interface_overrides_rejects_missing_colon() {
118        let raw = vec!["hash".to_string()];
119        assert!(parse_interface_overrides(&raw).is_err());
120    }
121
122    #[test]
123    fn parse_algorithm_overrides_splits_semicolons() {
124        let raw = vec!["hash:SHA-256;SHA-512".to_string()];
125        let got = parse_algorithm_overrides(&raw).unwrap();
126        assert_eq!(
127            got,
128            vec![("hash".into(), vec!["SHA-256".into(), "SHA-512".into()])]
129        );
130    }
131}