gatehouse_runtime_protocol/lifecycle_v2/
profile.rs1use serde::{Deserialize, Serialize};
3
4pub const PROTOCOL: &str = "gate-runtime/lifecycle-v2";
5pub const PRODUCT: &str = "podman-lifecycle";
6pub const VERSION: &str = "0.1.0";
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct Profile {
13 pub protocol: String,
14 pub product: String,
15 pub version: String,
16 pub engine: String,
17}
18
19impl Profile {
20 pub fn validate(&self) -> Result<(), &'static str> {
21 if self.protocol != PROTOCOL
22 || self.product != PRODUCT
23 || self.version != VERSION
24 || self.engine != "podman"
25 {
26 return Err("unsupported Podman lifecycle profile");
27 }
28 Ok(())
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct SlotIdentity {
37 pub profile: Profile,
38 pub installation_sha256: String,
39 pub config_sha256: String,
40 pub executable_sha256: String,
41}
42
43impl SlotIdentity {
44 pub fn validate(&self) -> Result<(), &'static str> {
45 self.profile.validate()?;
46 for hash in [
47 &self.installation_sha256,
48 &self.config_sha256,
49 &self.executable_sha256,
50 ] {
51 if hash.len() != 64
52 || !hash
53 .bytes()
54 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
55 {
56 return Err("invalid Podman slot identity digest");
57 }
58 }
59 Ok(())
60 }
61
62 pub fn require_same(&self, recorded: &Self) -> Result<(), &'static str> {
65 self.validate()?;
66 recorded.validate()?;
67 if self != recorded {
68 return Err("Podman slot profile or installation changed");
69 }
70 Ok(())
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn identity() -> SlotIdentity {
79 SlotIdentity {
80 profile: Profile {
81 protocol: PROTOCOL.into(),
82 product: PRODUCT.into(),
83 version: VERSION.into(),
84 engine: "podman".into(),
85 },
86 installation_sha256: "a".repeat(64),
87 config_sha256: "b".repeat(64),
88 executable_sha256: "c".repeat(64),
89 }
90 }
91
92 #[test]
93 fn exact_profile_and_identity_roundtrip() {
94 let original = identity();
95 let decoded: SlotIdentity =
96 serde_json::from_slice(&serde_json::to_vec(&original).unwrap()).unwrap();
97 original.require_same(&decoded).unwrap();
98 }
99
100 #[test]
101 fn docker_and_mixed_profiles_are_rejected() {
102 for field in ["protocol", "product", "version", "engine"] {
103 let mut value = serde_json::to_value(identity()).unwrap();
104 value["profile"][field] = serde_json::json!(match field {
105 "protocol" => "gate-runtime/lifecycle-v1",
106 "product" => "docker-lifecycle",
107 "version" => "0.2.0",
108 _ => "docker",
109 });
110 let changed: SlotIdentity = serde_json::from_value(value).unwrap();
111 assert!(changed.validate().is_err(), "{field}");
112 }
113 }
114
115 #[test]
116 fn changed_installation_config_or_executable_cannot_adopt_slot() {
117 for field in ["installation_sha256", "config_sha256", "executable_sha256"] {
118 let original = identity();
119 let mut value = serde_json::to_value(&original).unwrap();
120 value[field] = serde_json::json!("d".repeat(64));
121 let changed: SlotIdentity = serde_json::from_value(value).unwrap();
122 assert!(original.require_same(&changed).is_err());
123 }
124 }
125
126 #[test]
127 fn missing_profile_and_unknown_authority_fields_are_rejected() {
128 for value in [serde_json::json!({"released":true}), {
129 let mut value = serde_json::to_value(identity()).unwrap();
130 value["authority"] = serde_json::json!("adopt-docker");
131 value
132 }] {
133 assert!(serde_json::from_value::<SlotIdentity>(value).is_err());
134 }
135 }
136
137 #[test]
138 fn noncanonical_hashes_are_rejected() {
139 for hash in ["A".repeat(64), "a".repeat(63), "g".repeat(64)] {
140 let mut value = identity();
141 value.config_sha256 = hash;
142 assert!(value.validate().is_err());
143 }
144 }
145}