use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize, Serializer};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnyInString(pub serde_json::Value);
impl AnyInString {
#[must_use]
pub const fn new(value: serde_json::Value) -> Self {
Self(value)
}
#[must_use]
pub const fn value(&self) -> &serde_json::Value {
&self.0
}
#[must_use]
pub fn into_value(self) -> serde_json::Value {
self.0
}
}
impl From<serde_json::Value> for AnyInString {
fn from(value: serde_json::Value) -> Self {
Self(value)
}
}
impl Serialize for AnyInString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let inner =
serde_json::to_string(&self.0).map_err(<S::Error as serde::ser::Error>::custom)?;
serializer.serialize_str(&inner)
}
}
impl<'de> Deserialize<'de> for AnyInString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let value = serde_json::from_str(&s).map_err(de::Error::custom)?;
Ok(Self(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RequestBase {
pub activity_id: Uuid,
pub container_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ResponseBase {
#[serde(default)]
pub activity_id: Uuid,
#[serde(default)]
pub result: i32,
#[serde(default)]
pub error_message: String,
#[serde(
default,
rename = "ErrorRecords",
skip_serializing_if = "serde_json::Value::is_null"
)]
pub error_records: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NegotiateProtocolRequest {
#[serde(flatten)]
pub base: RequestBase,
pub minimum_version: u32,
pub maximum_version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct NegotiateProtocolResponse {
#[serde(flatten)]
pub base: ResponseBase,
pub version: u32,
pub capabilities: ProtocolSupport,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProtocolSupport {
#[serde(default)]
pub protocol_version: u32,
#[serde(default)]
pub send_host_create_message: bool,
#[serde(default)]
pub send_host_start_message: bool,
#[serde(default)]
pub hv_socket_config_on_startup: bool,
#[serde(default)]
pub send_lifecycle_notifications: bool,
#[serde(default)]
pub modify_service_settings_supported: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CreateRequest {
#[serde(flatten)]
pub base: RequestBase,
#[serde(rename = "ContainerConfig")]
pub container_config: AnyInString,
}
pub type CreateResponse = ResponseBase;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StartRequest {
#[serde(flatten)]
pub base: RequestBase,
}
pub type StartResponse = ResponseBase;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ShutdownRequest {
#[serde(flatten)]
pub base: RequestBase,
}
pub type ShutdownResponse = ResponseBase;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ModifySettingsRequest {
#[serde(flatten)]
pub base: RequestBase,
pub request: serde_json::Value,
}
pub type ModifySettingsResponse = ResponseBase;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ExecuteProcessRequest {
#[serde(flatten)]
pub base: RequestBase,
pub settings: ExecuteProcessSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ExecuteProcessSettings {
pub process_parameters: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stdio_relay_settings: Option<StdioRelaySettings>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct StdioRelaySettings {
#[serde(rename = "StdIn", default, skip_serializing_if = "Option::is_none")]
pub stdin_pipe: Option<Uuid>,
#[serde(rename = "StdOut", default, skip_serializing_if = "Option::is_none")]
pub stdout_pipe: Option<Uuid>,
#[serde(rename = "StdErr", default, skip_serializing_if = "Option::is_none")]
pub stderr_pipe: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ExecuteProcessResponse {
#[serde(flatten)]
pub base: ResponseBase,
#[serde(default)]
pub process_id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct WaitForProcessRequest {
#[serde(flatten)]
pub base: RequestBase,
pub process_id: u32,
pub timeout_in_ms: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct WaitForProcessResponse {
#[serde(flatten)]
pub base: ResponseBase,
#[serde(default)]
pub exit_code: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SignalProcessRequest {
#[serde(flatten)]
pub base: RequestBase,
pub process_id: u32,
pub options: serde_json::Value,
}
pub type SignalProcessResponse = ResponseBase;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn negotiate_request_round_trip_pascal_case() {
let req = NegotiateProtocolRequest {
base: RequestBase {
activity_id: Uuid::nil(),
container_id: "abc".into(),
},
minimum_version: 1,
maximum_version: 4,
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains("\"MinimumVersion\":1"));
assert!(s.contains("\"MaximumVersion\":4"));
assert!(s.contains("\"ContainerId\":\"abc\""));
assert!(s.contains("\"ActivityId\""));
let _back: NegotiateProtocolRequest = serde_json::from_str(&s).unwrap();
}
#[test]
fn response_default_carries_zero_hresult() {
let resp = NegotiateProtocolResponse::default();
assert_eq!(resp.base.result, 0);
assert!(resp.base.error_message.is_empty());
let s = serde_json::to_string(&resp).unwrap();
let back: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(back["Result"], json!(0));
}
#[test]
fn any_in_string_double_encodes_as_json_string() {
let v = AnyInString::new(json!({"SystemType": "Container"}));
let s = serde_json::to_string(&v).unwrap();
assert_eq!(s, "\"{\\\"SystemType\\\":\\\"Container\\\"}\"");
let back: AnyInString = serde_json::from_str(&s).unwrap();
assert_eq!(back, v);
}
#[test]
fn create_request_container_config_is_escaped_string() {
let req = CreateRequest {
base: RequestBase {
activity_id: Uuid::nil(),
container_id: "00000000-0000-0000-0000-000000000000".into(),
},
container_config: AnyInString::new(json!({
"SystemType": "Container",
})),
};
let s = serde_json::to_string(&req).unwrap();
assert!(
s.contains("\"ContainerConfig\":\"{\\\"SystemType\\\":\\\"Container\\\"}\""),
"expected double-encoded ContainerConfig string, got: {s}"
);
assert!(
!s.contains("\"Settings\""),
"must not emit a Settings field"
);
assert!(s.contains("\"ContainerId\":\"00000000-0000-0000-0000-000000000000\""));
assert!(s.contains("\"ActivityId\""));
let back: CreateRequest = serde_json::from_str(&s).unwrap();
assert_eq!(back.base.container_id, req.base.container_id);
assert_eq!(back.container_config, req.container_config);
}
#[test]
fn response_base_parses_error_records_array() {
let wire = r#"{
"Result": -1070137077,
"ActivityId": "00000000-0000-0000-0000-000000000000",
"ErrorRecords": [
{
"Result": -1070137077,
"Message": "Message Type 270532865 unknown (215 byte)",
"ModuleName": "vmcomputeagent.exe"
}
]
}"#;
let base: ResponseBase = serde_json::from_str(wire).unwrap();
assert_eq!(base.result, -1_070_137_077);
assert!(
base.error_records.is_array(),
"error_records must be an array"
);
assert_eq!(
base.error_records[0]["Message"],
json!("Message Type 270532865 unknown (215 byte)")
);
let empty: ResponseBase = serde_json::from_str(r#"{"Result":0}"#).unwrap();
assert!(empty.error_records.is_null());
let s = serde_json::to_string(&empty).unwrap();
assert!(!s.contains("ErrorRecords"), "null records must be skipped");
}
#[test]
fn protocol_support_parses_modify_service_settings_supported() {
let with: ProtocolSupport =
serde_json::from_str(r#"{"ModifyServiceSettingsSupported": true}"#).unwrap();
assert!(with.modify_service_settings_supported);
let without: ProtocolSupport = serde_json::from_str(r"{}").unwrap();
assert!(!without.modify_service_settings_supported);
}
#[test]
fn execute_process_settings_optional_stdio_relay() {
let s = ExecuteProcessSettings {
process_parameters: json!({"CommandLine": "cmd /c ver"}),
stdio_relay_settings: None,
};
let json_s = serde_json::to_string(&s).unwrap();
assert!(!json_s.contains("StdioRelaySettings"), "absent when None");
}
}