Skip to main content

kopiur_kopia/
session.rs

1//! The closed command surface of a browse session.
2//!
3//! A `kubectl kopiur browse` session pod connects to its repository
4//! **read-only** ([`crate::KopiaClient::repository_connect_readonly`]) and then
5//! only ever executes commands rendered by [`SessionCmd`]. The enum is closed
6//! and every transport (the CLI's pod-exec path) builds argv exclusively
7//! through [`SessionCmd::argv`], so a mutating kopia verb is *structurally*
8//! impossible — the type system, not a denylist, is the guarantee (ADR §5.5).
9
10/// The ONLY kopia invocations a browse session may issue. Closed enum — the
11/// CLI/exec transport renders argv exclusively through this, so a mutating
12/// verb is structurally impossible. A new variant cannot compile until
13/// [`SessionCmd::argv`] (and its read-only test) accounts for it.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum SessionCmd {
16    /// `kopia snapshot list --json --all`: the repository's full snapshot
17    /// catalog (every user@host identity), parsed as
18    /// [`Vec<SnapshotListEntry>`](crate::SnapshotListEntry).
19    SnapshotListJson,
20    /// `kopia show <oid>`: a directory object's manifest
21    /// ([`DirManifest`](crate::DirManifest) JSON) or a file object's raw bytes.
22    ShowObject {
23        /// The kopia object id to show (a directory's manifest or a file's
24        /// content stream).
25        oid: String,
26    },
27}
28
29impl SessionCmd {
30    /// Render the full argv (binary first) for this command. Exhaustive
31    /// `match`: a new session command must decide its argv here to compile.
32    pub fn argv(&self, kopia_bin: &str) -> Vec<String> {
33        match self {
34            SessionCmd::SnapshotListJson => vec![
35                kopia_bin.to_string(),
36                "snapshot".to_string(),
37                "list".to_string(),
38                "--json".to_string(),
39                "--all".to_string(),
40            ],
41            SessionCmd::ShowObject { oid } => {
42                vec![kopia_bin.to_string(), "show".to_string(), oid.clone()]
43            }
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    /// One representative of every variant, so the read-only sweep below cannot
53    /// silently skip a new command.
54    fn all_commands() -> Vec<SessionCmd> {
55        vec![
56            SessionCmd::SnapshotListJson,
57            SessionCmd::ShowObject {
58                oid: "kdeadbeef".into(),
59            },
60        ]
61    }
62
63    #[test]
64    fn snapshot_list_renders_the_exact_argv() {
65        assert_eq!(
66            SessionCmd::SnapshotListJson.argv("/usr/local/bin/kopia"),
67            vec![
68                "/usr/local/bin/kopia",
69                "snapshot",
70                "list",
71                "--json",
72                "--all"
73            ]
74        );
75    }
76
77    #[test]
78    fn show_object_renders_the_exact_argv() {
79        let cmd = SessionCmd::ShowObject {
80            oid: "k9c0ffee".into(),
81        };
82        assert_eq!(
83            cmd.argv("/usr/local/bin/kopia"),
84            vec!["/usr/local/bin/kopia", "show", "k9c0ffee"]
85        );
86    }
87
88    #[test]
89    fn no_variant_renders_a_mutating_verb() {
90        // The structural guarantee, asserted: no session command's argv may
91        // ever contain a kopia verb that can change the repository.
92        const MUTATING: &[&str] = &[
93            "delete",
94            "create",
95            "set",
96            "policy",
97            "maintenance",
98            "restore",
99        ];
100        for cmd in all_commands() {
101            let argv = cmd.argv("kopia");
102            for verb in MUTATING {
103                assert!(
104                    !argv.iter().any(|a| a == verb),
105                    "{cmd:?} renders mutating verb {verb}: {argv:?}"
106                );
107            }
108        }
109    }
110}