1use serde::de::{self, Deserializer};
11use serde::{Deserialize, Serialize, Serializer};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct AnyInString(pub serde_json::Value);
30
31impl AnyInString {
32 #[must_use]
34 pub const fn new(value: serde_json::Value) -> Self {
35 Self(value)
36 }
37
38 #[must_use]
40 pub const fn value(&self) -> &serde_json::Value {
41 &self.0
42 }
43
44 #[must_use]
46 pub fn into_value(self) -> serde_json::Value {
47 self.0
48 }
49}
50
51impl From<serde_json::Value> for AnyInString {
52 fn from(value: serde_json::Value) -> Self {
53 Self(value)
54 }
55}
56
57impl Serialize for AnyInString {
58 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
59 where
60 S: Serializer,
61 {
62 let inner =
66 serde_json::to_string(&self.0).map_err(<S::Error as serde::ser::Error>::custom)?;
67 serializer.serialize_str(&inner)
68 }
69}
70
71impl<'de> Deserialize<'de> for AnyInString {
72 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73 where
74 D: Deserializer<'de>,
75 {
76 let s = String::deserialize(deserializer)?;
79 let value = serde_json::from_str(&s).map_err(de::Error::custom)?;
80 Ok(Self(value))
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(rename_all = "PascalCase")]
87pub struct RequestBase {
88 pub activity_id: Uuid,
89 pub container_id: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
94#[serde(rename_all = "PascalCase")]
95pub struct ResponseBase {
96 #[serde(default)]
97 pub activity_id: Uuid,
98 #[serde(default)]
101 pub result: i32,
102 #[serde(default)]
103 pub error_message: String,
104 #[serde(
111 default,
112 rename = "ErrorRecords",
113 skip_serializing_if = "serde_json::Value::is_null"
114 )]
115 pub error_records: serde_json::Value,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "PascalCase")]
123pub struct NegotiateProtocolRequest {
124 #[serde(flatten)]
125 pub base: RequestBase,
126 pub minimum_version: u32,
127 pub maximum_version: u32,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, Default)]
132#[serde(rename_all = "PascalCase")]
133pub struct NegotiateProtocolResponse {
134 #[serde(flatten)]
135 pub base: ResponseBase,
136 pub version: u32,
137 pub capabilities: ProtocolSupport,
138}
139
140#[allow(clippy::struct_excessive_bools)]
145#[derive(Debug, Clone, Serialize, Deserialize, Default)]
146#[serde(rename_all = "PascalCase")]
147pub struct ProtocolSupport {
148 #[serde(default)]
149 pub protocol_version: u32,
150 #[serde(default)]
151 pub send_host_create_message: bool,
152 #[serde(default)]
153 pub send_host_start_message: bool,
154 #[serde(default)]
155 pub hv_socket_config_on_startup: bool,
156 #[serde(default)]
157 pub send_lifecycle_notifications: bool,
158 #[serde(default)]
164 pub modify_service_settings_supported: bool,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "PascalCase")]
179pub struct CreateRequest {
180 #[serde(flatten)]
181 pub base: RequestBase,
182 #[serde(rename = "ContainerConfig")]
184 pub container_config: AnyInString,
185}
186
187pub type CreateResponse = ResponseBase;
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "PascalCase")]
192pub struct StartRequest {
193 #[serde(flatten)]
194 pub base: RequestBase,
195}
196
197pub type StartResponse = ResponseBase;
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(rename_all = "PascalCase")]
202pub struct ShutdownRequest {
203 #[serde(flatten)]
204 pub base: RequestBase,
205}
206
207pub type ShutdownResponse = ResponseBase;
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "PascalCase")]
213pub struct ModifySettingsRequest {
214 #[serde(flatten)]
215 pub base: RequestBase,
216 pub request: serde_json::Value,
219}
220
221pub type ModifySettingsResponse = ResponseBase;
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "PascalCase")]
226pub struct ExecuteProcessRequest {
227 #[serde(flatten)]
228 pub base: RequestBase,
229 pub settings: ExecuteProcessSettings,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "PascalCase")]
236pub struct ExecuteProcessSettings {
237 pub process_parameters: serde_json::Value,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub stdio_relay_settings: Option<StdioRelaySettings>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "PascalCase")]
246pub struct StdioRelaySettings {
247 #[serde(rename = "StdIn", default, skip_serializing_if = "Option::is_none")]
253 pub stdin_pipe: Option<Uuid>,
254 #[serde(rename = "StdOut", default, skip_serializing_if = "Option::is_none")]
255 pub stdout_pipe: Option<Uuid>,
256 #[serde(rename = "StdErr", default, skip_serializing_if = "Option::is_none")]
257 pub stderr_pipe: Option<Uuid>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, Default)]
261#[serde(rename_all = "PascalCase")]
262pub struct ExecuteProcessResponse {
263 #[serde(flatten)]
264 pub base: ResponseBase,
265 #[serde(default)]
267 pub process_id: u32,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "PascalCase")]
273pub struct WaitForProcessRequest {
274 #[serde(flatten)]
275 pub base: RequestBase,
276 pub process_id: u32,
277 pub timeout_in_ms: u32,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, Default)]
282#[serde(rename_all = "PascalCase")]
283pub struct WaitForProcessResponse {
284 #[serde(flatten)]
285 pub base: ResponseBase,
286 #[serde(default)]
287 pub exit_code: u32,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(rename_all = "PascalCase")]
293pub struct SignalProcessRequest {
294 #[serde(flatten)]
295 pub base: RequestBase,
296 pub process_id: u32,
297 pub options: serde_json::Value,
298}
299
300pub type SignalProcessResponse = ResponseBase;
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use serde_json::json;
306
307 #[test]
308 fn negotiate_request_round_trip_pascal_case() {
309 let req = NegotiateProtocolRequest {
310 base: RequestBase {
311 activity_id: Uuid::nil(),
312 container_id: "abc".into(),
313 },
314 minimum_version: 1,
315 maximum_version: 4,
316 };
317 let s = serde_json::to_string(&req).unwrap();
318 assert!(s.contains("\"MinimumVersion\":1"));
319 assert!(s.contains("\"MaximumVersion\":4"));
320 assert!(s.contains("\"ContainerId\":\"abc\""));
321 assert!(s.contains("\"ActivityId\""));
322 let _back: NegotiateProtocolRequest = serde_json::from_str(&s).unwrap();
323 }
324
325 #[test]
326 fn response_default_carries_zero_hresult() {
327 let resp = NegotiateProtocolResponse::default();
328 assert_eq!(resp.base.result, 0);
329 assert!(resp.base.error_message.is_empty());
330 let s = serde_json::to_string(&resp).unwrap();
331 let back: serde_json::Value = serde_json::from_str(&s).unwrap();
332 assert_eq!(back["Result"], json!(0));
333 }
334
335 #[test]
336 fn any_in_string_double_encodes_as_json_string() {
337 let v = AnyInString::new(json!({"SystemType": "Container"}));
338 let s = serde_json::to_string(&v).unwrap();
339 assert_eq!(s, "\"{\\\"SystemType\\\":\\\"Container\\\"}\"");
342 let back: AnyInString = serde_json::from_str(&s).unwrap();
344 assert_eq!(back, v);
345 }
346
347 #[test]
348 fn create_request_container_config_is_escaped_string() {
349 let req = CreateRequest {
350 base: RequestBase {
351 activity_id: Uuid::nil(),
352 container_id: "00000000-0000-0000-0000-000000000000".into(),
353 },
354 container_config: AnyInString::new(json!({
355 "SystemType": "Container",
356 })),
357 };
358 let s = serde_json::to_string(&req).unwrap();
359 assert!(
362 s.contains("\"ContainerConfig\":\"{\\\"SystemType\\\":\\\"Container\\\"}\""),
363 "expected double-encoded ContainerConfig string, got: {s}"
364 );
365 assert!(
366 !s.contains("\"Settings\""),
367 "must not emit a Settings field"
368 );
369 assert!(s.contains("\"ContainerId\":\"00000000-0000-0000-0000-000000000000\""));
370 assert!(s.contains("\"ActivityId\""));
371
372 let back: CreateRequest = serde_json::from_str(&s).unwrap();
374 assert_eq!(back.base.container_id, req.base.container_id);
375 assert_eq!(back.container_config, req.container_config);
376 }
377
378 #[test]
379 fn response_base_parses_error_records_array() {
380 let wire = r#"{
384 "Result": -1070137077,
385 "ActivityId": "00000000-0000-0000-0000-000000000000",
386 "ErrorRecords": [
387 {
388 "Result": -1070137077,
389 "Message": "Message Type 270532865 unknown (215 byte)",
390 "ModuleName": "vmcomputeagent.exe"
391 }
392 ]
393 }"#;
394 let base: ResponseBase = serde_json::from_str(wire).unwrap();
395 assert_eq!(base.result, -1_070_137_077);
396 assert!(
397 base.error_records.is_array(),
398 "error_records must be an array"
399 );
400 assert_eq!(
401 base.error_records[0]["Message"],
402 json!("Message Type 270532865 unknown (215 byte)")
403 );
404
405 let empty: ResponseBase = serde_json::from_str(r#"{"Result":0}"#).unwrap();
408 assert!(empty.error_records.is_null());
409 let s = serde_json::to_string(&empty).unwrap();
410 assert!(!s.contains("ErrorRecords"), "null records must be skipped");
411 }
412
413 #[test]
414 fn protocol_support_parses_modify_service_settings_supported() {
415 let with: ProtocolSupport =
417 serde_json::from_str(r#"{"ModifyServiceSettingsSupported": true}"#).unwrap();
418 assert!(with.modify_service_settings_supported);
419 let without: ProtocolSupport = serde_json::from_str(r"{}").unwrap();
421 assert!(!without.modify_service_settings_supported);
422 }
423
424 #[test]
425 fn execute_process_settings_optional_stdio_relay() {
426 let s = ExecuteProcessSettings {
427 process_parameters: json!({"CommandLine": "cmd /c ver"}),
428 stdio_relay_settings: None,
429 };
430 let json_s = serde_json::to_string(&s).unwrap();
431 assert!(!json_s.contains("StdioRelaySettings"), "absent when None");
432 }
433}