use std::fmt;
use microsandbox_protocol::control::{CpuState, MemoryState, RuntimeCapabilities};
use microsandbox_protocol_client::{ClientError, ErrorKind};
use zeroize::Zeroizing;
use crate::{ControlClientError, ControlClientResult, ControlMode, JsonValue};
pub struct JsonReply {
raw: Zeroizing<Vec<u8>>,
value: JsonValue,
}
impl JsonReply {
pub fn parse(raw: Vec<u8>) -> ControlClientResult<Self> {
let raw = Zeroizing::new(raw);
let text =
std::str::from_utf8(&raw).map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
let value = JsonValue::parse(text.trim().as_bytes())
.map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
if value.as_object().is_none() {
return Err(ClientError::new(ErrorKind::InvalidData).into());
}
Ok(Self { raw, value })
}
pub fn raw(&self) -> &[u8] {
&self.raw
}
pub fn value(&self) -> &JsonValue {
&self.value
}
pub fn discovery_mode(&self) -> ControlClientResult<ControlMode> {
if self.value.get("ok").and_then(JsonValue::as_bool) != Some(true)
|| self
.value
.get("error")
.is_some_and(|value| !matches!(value, JsonValue::Null))
|| self
.value
.get("capabilities")
.and_then(capabilities)
.is_none()
{
return Err(ClientError::new(ErrorKind::InvalidData).into());
}
let Some(advertisement) = self.value.get("control_protocols") else {
return Ok(ControlMode::Json);
};
let protocols = advertisement
.as_array()
.ok_or_else(|| ClientError::new(ErrorKind::InvalidData))?;
let names = protocols
.iter()
.map(JsonValue::as_str)
.collect::<Option<Vec<_>>>()
.ok_or_else(|| ClientError::new(ErrorKind::InvalidData))?;
if names.contains(&"cbor") {
Ok(ControlMode::Framed)
} else if names.contains(&"json") {
Ok(ControlMode::Json)
} else {
Err(ClientError::new(ErrorKind::UnsupportedOperation).into())
}
}
pub(crate) fn checked<T>(
self,
decode: impl FnOnce(&JsonValue) -> Option<T>,
) -> ControlClientResult<T> {
match self.value.get("ok").and_then(JsonValue::as_bool) {
Some(false) => Err(ControlClientError::LegacyRemote {
reply: Box::new(self),
}),
Some(true) => match decode(&self.value) {
Some(value) => Ok(value),
None => Err(ControlClientError::InvalidJsonResponse {
reply: Box::new(self),
}),
},
None => Err(ControlClientError::InvalidJsonResponse {
reply: Box::new(self),
}),
}
}
}
impl fmt::Debug for JsonReply {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("JsonReply")
.field("bytes", &self.raw.len())
.finish_non_exhaustive()
}
}
pub(crate) fn capabilities(value: &JsonValue) -> Option<RuntimeCapabilities> {
let optional = |name| {
value
.get(name)
.map(JsonValue::as_bool)
.unwrap_or(Some(false))
};
Some(RuntimeCapabilities {
root_disk_grow: optional("root_disk_grow")?,
guest_flush_policy: optional("guest_flush_policy")?,
optional_disk_integrity: optional("optional_disk_integrity")?,
branch_create: optional("branch_create")?,
branch_memfd: optional("branch_memfd")?,
pause_resume: optional("pause_resume")?,
disk_compact: optional("disk_compact")?,
disk_compact_owned: optional("disk_compact_owned")?,
cpu_resize: value.get("cpu_resize")?.as_bool()?,
memory_resize: value.get("memory_resize")?.as_bool()?,
secrets_update: value.get("secrets_update")?.as_bool()?,
checkpoint_create: optional("checkpoint_create")?,
disk_checkpoint_create: optional("disk_checkpoint_create")?,
})
}
pub(crate) fn memory(value: &JsonValue) -> Option<MemoryState> {
Some(MemoryState {
boot_mib: value.get("boot_mib")?.as_u64()?,
target_mib: value.get("target_mib")?.as_u64()?,
current_mib: value.get("current_mib")?.as_u64()?,
max_mib: value.get("max_mib")?.as_u64()?,
})
}
pub(crate) fn cpu(value: &JsonValue) -> Option<CpuState> {
Some(CpuState {
possible: value.get("possible")?.as_u64()?.try_into().ok()?,
requested_online: value.get("requested_online")?.as_u64()?.try_into().ok()?,
actual_online: value.get("actual_online")?.as_u64()?.try_into().ok()?,
enforced: value.get("enforced")?.as_u64()?.try_into().ok()?,
})
}