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}