Skip to main content

microsandbox_control_client/
compat_message.rs

1//! Explicit native translation and optional checked JSON normalization.
2
3use microsandbox_protocol::control::{ControlRequest, SecretChange, SecretValue, SecretsResult};
4use microsandbox_protocol_client::{
5    ClientError, EncodedMessage, ErrorKind, IntoOutboundMessage, Request, TypedMessage,
6};
7use serde::Serialize;
8use zeroize::Zeroizing;
9
10use crate::{
11    ControlClientError, ControlClientResult, ControlProtocol, GetCapabilities, GetCpuState,
12    GetMemoryState, JsonReply, JsonValue, SetCpuTarget, SetMemoryTarget, UpdateSecrets, json_reply,
13};
14
15//--------------------------------------------------------------------------------------------------
16// Types
17//--------------------------------------------------------------------------------------------------
18
19/// Named messages that can choose a real framed or legacy representation.
20pub trait IntoControlMessage: IntoOutboundMessage<ControlProtocol> {
21    /// Translate a known native request. Encoded payloads fail locally because
22    /// parsing them into a JSON substitute would discard the caller's wire form.
23    fn into_json(self) -> ControlClientResult<ControlRequest>;
24}
25
26/// Checked control requests sharing operation records across both formats.
27pub trait CheckedControlRequest: Request<ControlProtocol, Error = ControlClientError> {
28    /// Prepare the actual legacy operation, before any connection or write.
29    fn json_request(&self) -> ControlClientResult<ControlRequest>;
30    /// Normalize a real JSON response, preserving its original bytes on failure.
31    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response>;
32}
33
34/// A checked request that can select framed CBOR or the historical JSON representation.
35///
36/// Existing [`CheckedControlRequest`] implementations receive this behavior through a blanket
37/// implementation. Generation-two requests implement it directly without expanding the released
38/// generation-one [`ControlRequest`] enum.
39pub trait CompatibleControlRequest: Request<ControlProtocol, Error = ControlClientError> {
40    /// First framed-control generation that defines this operation.
41    fn min_generation(&self) -> u8;
42    /// Encode the exact historical JSON request without a trailing newline.
43    fn compatibility_json_bytes(&self) -> ControlClientResult<Zeroizing<Vec<u8>>>;
44    /// Decode the actual historical JSON response.
45    fn decode_compatibility_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response>;
46}
47
48//--------------------------------------------------------------------------------------------------
49// Trait Implementations
50//--------------------------------------------------------------------------------------------------
51
52impl<T: Serialize> IntoControlMessage for TypedMessage<T> {
53    fn into_json(self) -> ControlClientResult<ControlRequest> {
54        // Do not apply the framed four-MiB ceiling to a native legacy request.
55        // Parsing its actual JSON tokens also catches duplicate keys emitted by
56        // a custom Serialize implementation before a map could erase them.
57        let bytes = Zeroizing::new(
58            serde_json::to_vec(&self.payload).map_err(|_| ClientError::new(ErrorKind::Encode))?,
59        );
60        let value =
61            JsonValue::parse(&bytes).map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
62        if value.as_object().is_none() {
63            return invalid();
64        }
65        Ok(match self.message_type.as_str() {
66            "control.capabilities" => ControlRequest::Capabilities,
67            "control.memory.state" => ControlRequest::MemoryState,
68            "control.cpu.state" => ControlRequest::CpuState,
69            "control.memory.target" => ControlRequest::MemoryTarget {
70                total_mib: value
71                    .get("total_mib")
72                    .and_then(JsonValue::as_u64)
73                    .ok_or_else(invalid_error)?,
74            },
75            "control.cpu.target" => ControlRequest::CpuTarget {
76                online: value
77                    .get("online")
78                    .and_then(JsonValue::as_u64)
79                    .and_then(|number| number.try_into().ok())
80                    .ok_or_else(invalid_error)?,
81            },
82            "control.secrets.update" => ControlRequest::SecretsUpdate {
83                changes: value
84                    .get("changes")
85                    .and_then(JsonValue::as_array)
86                    .ok_or_else(invalid_error)?
87                    .iter()
88                    .map(secret_change)
89                    .collect::<ControlClientResult<_>>()?,
90            },
91            _ => return Err(ControlClientError::UnsupportedMode),
92        })
93    }
94}
95
96impl<T: CheckedControlRequest> CompatibleControlRequest for T {
97    fn min_generation(&self) -> u8 {
98        1
99    }
100
101    fn compatibility_json_bytes(&self) -> ControlClientResult<Zeroizing<Vec<u8>>> {
102        Ok(Zeroizing::new(
103            serde_json::to_vec(&self.json_request()?)
104                .map_err(|_| ClientError::new(ErrorKind::Encode))?,
105        ))
106    }
107
108    fn decode_compatibility_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
109        CheckedControlRequest::decode_json(self, reply)
110    }
111}
112
113impl IntoControlMessage for EncodedMessage {
114    fn into_json(self) -> ControlClientResult<ControlRequest> {
115        Err(ControlClientError::UnsupportedMode)
116    }
117}
118
119impl CheckedControlRequest for GetCapabilities {
120    fn json_request(&self) -> ControlClientResult<ControlRequest> {
121        Ok(ControlRequest::Capabilities)
122    }
123    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
124        reply.checked(|value| {
125            json_reply::capabilities(value.get("capabilities")?)
126                .map(microsandbox_protocol::control::RuntimeCapabilities::generation_one)
127        })
128    }
129}
130
131impl CheckedControlRequest for GetMemoryState {
132    fn json_request(&self) -> ControlClientResult<ControlRequest> {
133        Ok(ControlRequest::MemoryState)
134    }
135    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
136        reply.checked(|value| json_reply::memory(value.get("memory")?))
137    }
138}
139
140impl CheckedControlRequest for SetMemoryTarget {
141    fn json_request(&self) -> ControlClientResult<ControlRequest> {
142        Ok(ControlRequest::MemoryTarget {
143            total_mib: self.total_mib,
144        })
145    }
146    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
147        CheckedControlRequest::decode_json(&GetMemoryState, reply)
148    }
149}
150
151impl CheckedControlRequest for GetCpuState {
152    fn json_request(&self) -> ControlClientResult<ControlRequest> {
153        Ok(ControlRequest::CpuState)
154    }
155    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
156        reply.checked(|value| json_reply::cpu(value.get("cpu")?))
157    }
158}
159
160impl CheckedControlRequest for SetCpuTarget {
161    fn json_request(&self) -> ControlClientResult<ControlRequest> {
162        Ok(ControlRequest::CpuTarget {
163            online: self.online,
164        })
165    }
166    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
167        CheckedControlRequest::decode_json(&GetCpuState, reply)
168    }
169}
170
171impl CheckedControlRequest for UpdateSecrets {
172    fn json_request(&self) -> ControlClientResult<ControlRequest> {
173        Ok(ControlRequest::SecretsUpdate {
174            changes: self.changes.clone(),
175        })
176    }
177    fn decode_json(&self, reply: JsonReply) -> ControlClientResult<Self::Response> {
178        reply.checked(|_| {
179            Some(SecretsResult::Complete {
180                applied_count: self.changes.len().try_into().ok()?,
181            })
182        })
183    }
184}
185
186//--------------------------------------------------------------------------------------------------
187// Functions
188//--------------------------------------------------------------------------------------------------
189
190fn invalid_error() -> ControlClientError {
191    ClientError::new(ErrorKind::InvalidData).into()
192}
193
194fn invalid<T>() -> ControlClientResult<T> {
195    Err(invalid_error())
196}
197
198fn secret_change(value: &JsonValue) -> ControlClientResult<SecretChange> {
199    let name = value
200        .get("name")
201        .and_then(JsonValue::as_str)
202        .ok_or_else(invalid_error)?
203        .to_owned();
204    Ok(match value.get("change").and_then(JsonValue::as_str) {
205        Some("rotate") => SecretChange::Rotate {
206            name,
207            value: SecretValue(
208                value
209                    .get("value")
210                    .and_then(JsonValue::as_str)
211                    .ok_or_else(invalid_error)?
212                    .to_owned(),
213            ),
214        },
215        Some("remove") => SecretChange::Remove { name },
216        Some("set_allowed_hosts") => SecretChange::SetAllowedHosts {
217            name,
218            hosts: value
219                .get("hosts")
220                .and_then(JsonValue::as_array)
221                .ok_or_else(invalid_error)?
222                .iter()
223                .map(|host| host.as_str().map(str::to_owned).ok_or_else(invalid_error))
224                .collect::<ControlClientResult<_>>()?,
225        },
226        _ => return invalid(),
227    })
228}