Skip to main content

microsandbox_control_client/
json_reply.rs

1//! Original JSON replies and checked field access, without fabricated frames.
2
3use std::fmt;
4
5use microsandbox_protocol::control::{Capabilities, CpuState, MemoryState};
6use microsandbox_protocol_client::{ClientError, ErrorKind};
7use zeroize::Zeroizing;
8
9use crate::{ControlClientError, ControlClientResult, ControlMode, JsonValue};
10
11//--------------------------------------------------------------------------------------------------
12// Types
13//--------------------------------------------------------------------------------------------------
14
15/// One actual response line, retaining unknown fields and lossless numbers.
16pub struct JsonReply {
17    raw: Zeroizing<Vec<u8>>,
18    value: JsonValue,
19}
20
21//--------------------------------------------------------------------------------------------------
22// Methods
23//--------------------------------------------------------------------------------------------------
24
25impl JsonReply {
26    /// Decode a complete original line (or a nonempty EOF-delimited reply).
27    pub fn parse(raw: Vec<u8>) -> ControlClientResult<Self> {
28        let raw = Zeroizing::new(raw);
29        let text =
30            std::str::from_utf8(&raw).map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
31        let value = JsonValue::parse(text.trim().as_bytes())
32            .map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
33        if value.as_object().is_none() {
34            return Err(ClientError::new(ErrorKind::InvalidData).into());
35        }
36        Ok(Self { raw, value })
37    }
38
39    /// Exact original line, including received whitespace and delimiter.
40    pub fn raw(&self) -> &[u8] {
41        &self.raw
42    }
43
44    /// Inspect unknown fields without losing numeric precision.
45    pub fn value(&self) -> &JsonValue {
46        &self.value
47    }
48
49    /// Validate an affirmative capabilities reply and select a supported format.
50    /// No error strings or unexpected EOFs are interpreted as legacy support.
51    pub fn discovery_mode(&self) -> ControlClientResult<ControlMode> {
52        if self.value.get("ok").and_then(JsonValue::as_bool) != Some(true)
53            || self
54                .value
55                .get("error")
56                .is_some_and(|value| !matches!(value, JsonValue::Null))
57            || self
58                .value
59                .get("capabilities")
60                .and_then(capabilities)
61                .is_none()
62        {
63            return Err(ClientError::new(ErrorKind::InvalidData).into());
64        }
65        let Some(advertisement) = self.value.get("control_protocols") else {
66            return Ok(ControlMode::Json);
67        };
68        let protocols = advertisement
69            .as_array()
70            .ok_or_else(|| ClientError::new(ErrorKind::InvalidData))?;
71        let names = protocols
72            .iter()
73            .map(JsonValue::as_str)
74            .collect::<Option<Vec<_>>>()
75            .ok_or_else(|| ClientError::new(ErrorKind::InvalidData))?;
76        if names.contains(&"cbor") {
77            Ok(ControlMode::Framed)
78        } else if names.contains(&"json") {
79            Ok(ControlMode::Json)
80        } else {
81            Err(ClientError::new(ErrorKind::UnsupportedOperation).into())
82        }
83    }
84
85    pub(crate) fn checked<T>(
86        self,
87        decode: impl FnOnce(&JsonValue) -> Option<T>,
88    ) -> ControlClientResult<T> {
89        match self.value.get("ok").and_then(JsonValue::as_bool) {
90            Some(false) => Err(ControlClientError::LegacyRemote {
91                reply: Box::new(self),
92            }),
93            Some(true) => match decode(&self.value) {
94                Some(value) => Ok(value),
95                None => Err(ControlClientError::InvalidJsonResponse {
96                    reply: Box::new(self),
97                }),
98            },
99            None => Err(ControlClientError::InvalidJsonResponse {
100                reply: Box::new(self),
101            }),
102        }
103    }
104}
105
106//--------------------------------------------------------------------------------------------------
107// Trait Implementations
108//--------------------------------------------------------------------------------------------------
109
110impl fmt::Debug for JsonReply {
111    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112        formatter
113            .debug_struct("JsonReply")
114            .field("bytes", &self.raw.len())
115            .finish_non_exhaustive()
116    }
117}
118
119//--------------------------------------------------------------------------------------------------
120// Functions
121//--------------------------------------------------------------------------------------------------
122
123pub(crate) fn capabilities(value: &JsonValue) -> Option<Capabilities> {
124    Some(Capabilities {
125        root_disk_grow: value
126            .get("root_disk_grow")
127            .map(|v| v.as_bool())
128            .unwrap_or(Some(false))?,
129        cpu_resize: value.get("cpu_resize")?.as_bool()?,
130        memory_resize: value.get("memory_resize")?.as_bool()?,
131        secrets_update: value.get("secrets_update")?.as_bool()?,
132    })
133}
134
135pub(crate) fn memory(value: &JsonValue) -> Option<MemoryState> {
136    Some(MemoryState {
137        boot_mib: value.get("boot_mib")?.as_u64()?,
138        target_mib: value.get("target_mib")?.as_u64()?,
139        current_mib: value.get("current_mib")?.as_u64()?,
140        max_mib: value.get("max_mib")?.as_u64()?,
141    })
142}
143
144pub(crate) fn cpu(value: &JsonValue) -> Option<CpuState> {
145    Some(CpuState {
146        possible: value.get("possible")?.as_u64()?.try_into().ok()?,
147        requested_online: value.get("requested_online")?.as_u64()?.try_into().ok()?,
148        actual_online: value.get("actual_online")?.as_u64()?.try_into().ok()?,
149        enforced: value.get("enforced")?.as_u64()?.try_into().ok()?,
150    })
151}