1use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5#[derive(Debug, Serialize)]
7#[serde(tag = "command", rename_all = "kebab-case")]
8pub enum Control {
9 Init {
11 version: u32,
13 host: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
22 superuser: Option<Value>,
23 },
24 Open {
26 channel: String,
28 payload: String,
30 #[serde(flatten)]
32 options: Map<String, Value>,
33 },
34 Done {
36 channel: String,
38 },
39 Close {
41 channel: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 problem: Option<String>,
46 },
47}
48
49impl Control {
50 pub fn open(channel: &str, payload: &str) -> Control {
52 Control::Open {
53 channel: channel.into(),
54 payload: payload.into(),
55 options: Map::new(),
56 }
57 }
58 pub fn opt(mut self, key: &str, value: Value) -> Control {
60 if let Control::Open {
61 ref mut options, ..
62 } = self
63 {
64 options.insert(key.to_string(), value);
65 }
66 self
67 }
68 pub fn to_json(&self) -> Vec<u8> {
73 serde_json::to_vec(self).unwrap_or_else(|_| {
74 serde_json::json!({"command":"close","channel":"","problem":"internal-error"})
75 .to_string()
76 .into_bytes()
77 })
78 }
79}
80
81#[derive(Debug)]
83pub struct DbusCall {
84 pub channel: String,
86 pub id: String,
88 pub body: DbusCallBody,
90}
91
92#[derive(Debug, Serialize)]
94pub struct DbusCallBody {
95 pub call: (String, String, String, Value),
97 pub id: String,
99}
100
101impl DbusCall {
102 pub fn new(channel: &str, path: &str, iface: &str, method: &str, args: Value) -> DbusCall {
105 let id = format!("{}", next_cookie());
107 DbusCall {
108 channel: channel.into(),
109 id: id.clone(),
110 body: DbusCallBody {
111 call: (path.into(), iface.into(), method.into(), args),
112 id,
113 },
114 }
115 }
116 pub fn to_json(&self) -> Vec<u8> {
121 serde_json::to_vec(&self.body).unwrap_or_else(|_| {
122 serde_json::json!({
123 "call": ["", "", "", []],
124 "id": "serialize-error"
125 })
126 .to_string()
127 .into_bytes()
128 })
129 }
130}
131
132fn next_cookie() -> u64 {
133 use std::sync::atomic::{AtomicU64, Ordering};
134 static N: AtomicU64 = AtomicU64::new(1);
135 N.fetch_add(1, Ordering::Relaxed)
136}
137
138#[derive(Debug, Deserialize)]
140pub struct DbusResponse {
141 #[serde(default)]
143 pub reply: Option<Vec<Value>>,
144 #[serde(default)]
146 pub error: Option<Vec<Value>>,
147 #[serde(default)]
149 pub id: Option<String>,
150}
151
152impl DbusResponse {
153 pub fn out_args(&self) -> Option<&Value> {
155 self.reply.as_ref().and_then(|r| r.first())
156 }
157 pub fn dbus_error_name(&self) -> Option<&str> {
159 self.error
160 .as_ref()
161 .and_then(|e| e.first())
162 .and_then(|v| v.as_str())
163 }
164 pub fn dbus_error_message(&self) -> Option<String> {
166 self.error
167 .as_ref()
168 .and_then(|e| e.get(1))
169 .and_then(|v| v.as_array())
170 .and_then(|a| a.first())
171 .and_then(|v| v.as_str())
172 .map(|s| s.to_string())
173 }
174}
175
176#[derive(Debug, Deserialize)]
178pub struct IncomingControl {
179 pub command: String,
181 #[serde(default)]
183 pub channel: Option<String>,
184 #[serde(default)]
186 pub problem: Option<String>,
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193
194 #[test]
195 fn serializes_init_without_superuser() {
196 let v = serde_json::to_value(Control::Init {
197 version: 1,
198 host: "localhost".into(),
199 superuser: None,
200 })
201 .unwrap();
202 assert_eq!(v, json!({"command":"init","version":1,"host":"localhost"}));
204 }
205
206 #[test]
207 fn serializes_init_with_superuser() {
208 let v = serde_json::to_value(Control::Init {
209 version: 1,
210 host: "localhost".into(),
211 superuser: Some(json!({"id": "sudo"})),
212 })
213 .unwrap();
214 assert_eq!(
215 v,
216 json!({
217 "command":"init","version":1,"host":"localhost",
218 "superuser":{"id":"sudo"}
219 })
220 );
221 }
222
223 #[test]
224 fn serializes_open_with_options() {
225 let open = Control::open("ch1", "dbus-json3")
226 .opt("bus", json!("system"))
227 .opt("name", json!("org.freedesktop.systemd1"));
228 let v = serde_json::to_value(open).unwrap();
229 assert_eq!(
230 v,
231 json!({
232 "command":"open","channel":"ch1","payload":"dbus-json3",
233 "bus":"system","name":"org.freedesktop.systemd1"
234 })
235 );
236 }
237
238 #[test]
239 fn serializes_dbus_call() {
240 let call = DbusCall::new(
241 "ch1",
242 "/org/freedesktop/systemd1",
243 "org.freedesktop.systemd1.Manager",
244 "ListUnits",
245 json!([]),
246 );
247 let v = serde_json::to_value(&call.body).unwrap();
248 assert_eq!(
249 v,
250 json!({
251 "call":["/org/freedesktop/systemd1","org.freedesktop.systemd1.Manager","ListUnits",[]],
252 "id": call.id
253 })
254 );
255 }
256
257 #[test]
258 fn parses_dbus_reply() {
259 let msg: DbusResponse = serde_json::from_value(json!({
260 "reply":[[ [["sshd.service","OpenSSH","loaded","active","running"]] ]],
261 "id":"7"
262 }))
263 .unwrap();
264 assert_eq!(msg.id.as_deref(), Some("7"));
265 assert!(msg.error.is_none());
266 assert!(msg.reply.is_some());
267 }
268
269 #[test]
270 fn parses_dbus_error() {
271 let msg: DbusResponse = serde_json::from_value(json!({
272 "error":["org.freedesktop.DBus.Error.UnknownMethod",["nope"]],
273 "id":"7"
274 }))
275 .unwrap();
276 assert_eq!(
277 msg.dbus_error_name(),
278 Some("org.freedesktop.DBus.Error.UnknownMethod")
279 );
280 }
281
282 #[test]
283 fn parses_incoming_control() {
284 let c: IncomingControl = serde_json::from_value(json!({
285 "command":"close","channel":"ch1","problem":"not-found"
286 }))
287 .unwrap();
288 assert_eq!(c.command, "close");
289 assert_eq!(c.channel.as_deref(), Some("ch1"));
290 assert_eq!(c.problem.as_deref(), Some("not-found"));
291 }
292
293 #[test]
294 fn serializes_done_control() {
295 let v = serde_json::to_value(Control::Done {
296 channel: "ch1".into(),
297 })
298 .unwrap();
299 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
300 }
301
302 #[test]
303 fn serializes_close_control_with_and_without_problem() {
304 let with = serde_json::to_value(Control::Close {
305 channel: "ch1".into(),
306 problem: Some("not-found".into()),
307 })
308 .unwrap();
309 assert_eq!(
310 with,
311 json!({"command":"close","channel":"ch1","problem":"not-found"})
312 );
313
314 let without = serde_json::to_value(Control::Close {
315 channel: "ch2".into(),
316 problem: None,
317 })
318 .unwrap();
319 assert_eq!(without, json!({"command":"close","channel":"ch2"}));
320 }
321
322 #[test]
323 fn control_to_json_round_trips() {
324 let bytes = Control::Done {
325 channel: "ch1".into(),
326 }
327 .to_json();
328 let v: Value = serde_json::from_slice(&bytes).unwrap();
329 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
330 }
331
332 #[test]
333 fn dbus_response_out_args_none_when_empty() {
334 let resp: DbusResponse = serde_json::from_value(json!({"id":"1"})).unwrap();
335 assert!(resp.out_args().is_none());
336 assert!(resp.dbus_error_name().is_none());
337 assert!(resp.dbus_error_message().is_none());
338 }
339}