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")]
21 superuser: Option<Value>,
22 },
23 Open {
25 channel: String,
27 payload: String,
29 #[serde(flatten)]
31 options: Map<String, Value>,
32 },
33 Done {
35 channel: String,
37 },
38 Close {
40 channel: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 problem: Option<String>,
45 },
46}
47
48impl Control {
49 pub fn open(channel: &str, payload: &str) -> Control {
51 Control::Open {
52 channel: channel.into(),
53 payload: payload.into(),
54 options: Map::new(),
55 }
56 }
57 pub fn opt(mut self, key: &str, value: Value) -> Control {
59 if let Control::Open {
60 ref mut options, ..
61 } = self
62 {
63 options.insert(key.to_string(), value);
64 }
65 self
66 }
67 pub fn to_json(&self) -> Vec<u8> {
72 serde_json::to_vec(self).unwrap_or_else(|_| {
73 serde_json::json!({"command":"close","channel":"","problem":"internal-error"})
74 .to_string()
75 .into_bytes()
76 })
77 }
78}
79
80#[derive(Debug)]
82pub struct DbusCall {
83 pub channel: String,
85 pub id: String,
87 pub body: DbusCallBody,
89}
90
91#[derive(Debug, Serialize)]
93pub struct DbusCallBody {
94 pub call: (String, String, String, Value),
96 pub id: String,
98}
99
100impl DbusCall {
101 pub fn new(channel: &str, path: &str, iface: &str, method: &str, args: Value) -> DbusCall {
104 let id = format!("{}", next_cookie());
106 DbusCall {
107 channel: channel.into(),
108 id: id.clone(),
109 body: DbusCallBody {
110 call: (path.into(), iface.into(), method.into(), args),
111 id,
112 },
113 }
114 }
115 pub fn to_json(&self) -> Vec<u8> {
120 serde_json::to_vec(&self.body).unwrap_or_else(|_| {
121 serde_json::json!({
122 "call": ["", "", "", []],
123 "id": "serialize-error"
124 })
125 .to_string()
126 .into_bytes()
127 })
128 }
129}
130
131fn next_cookie() -> u64 {
132 use std::sync::atomic::{AtomicU64, Ordering};
133 static N: AtomicU64 = AtomicU64::new(1);
134 N.fetch_add(1, Ordering::Relaxed)
135}
136
137#[derive(Debug, Deserialize)]
139pub struct DbusResponse {
140 #[serde(default)]
142 pub reply: Option<Vec<Value>>,
143 #[serde(default)]
145 pub error: Option<Vec<Value>>,
146 #[serde(default)]
148 pub id: Option<String>,
149}
150
151impl DbusResponse {
152 pub fn out_args(&self) -> Option<&Value> {
154 self.reply.as_ref().and_then(|r| r.first())
155 }
156 pub fn dbus_error_name(&self) -> Option<&str> {
158 self.error
159 .as_ref()
160 .and_then(|e| e.first())
161 .and_then(|v| v.as_str())
162 }
163 pub fn dbus_error_message(&self) -> Option<String> {
165 self.error
166 .as_ref()
167 .and_then(|e| e.get(1))
168 .and_then(|v| v.as_array())
169 .and_then(|a| a.first())
170 .and_then(|v| v.as_str())
171 .map(|s| s.to_string())
172 }
173}
174
175#[derive(Debug, Deserialize)]
184pub struct DbusSignal {
185 #[serde(default)]
187 pub signal: Option<Vec<Value>>,
188}
189
190impl DbusSignal {
191 pub fn member(&self) -> Option<&str> {
193 self.signal
194 .as_ref()
195 .and_then(|s| s.get(2))
196 .and_then(|v| v.as_str())
197 }
198 pub fn path(&self) -> Option<&str> {
200 self.signal
201 .as_ref()
202 .and_then(|s| s.first())
203 .and_then(|v| v.as_str())
204 }
205 pub fn args(&self) -> Option<&Vec<Value>> {
207 self.signal
208 .as_ref()
209 .and_then(|s| s.get(3))
210 .and_then(|v| v.as_array())
211 }
212}
213
214#[derive(Debug, Deserialize)]
216pub struct IncomingControl {
217 pub command: String,
219 #[serde(default)]
221 pub channel: Option<String>,
222 #[serde(default)]
224 pub problem: Option<String>,
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use serde_json::json;
231
232 #[test]
233 fn parses_dbus_signal() {
234 let v = serde_json::json!({
235 "signal": [
236 "/1234_abcd",
237 "org.freedesktop.PackageKit.Transaction",
238 "Package",
239 [8, "htop;3.4.1-3.fc44;x86_64;fedora", "Interactive process viewer"]
240 ]
241 });
242 let sig: DbusSignal = serde_json::from_value(v).unwrap();
243 assert_eq!(sig.member(), Some("Package"));
244 assert_eq!(sig.path(), Some("/1234_abcd"));
245 let args = sig.args().unwrap();
246 assert_eq!(args[0].as_u64(), Some(8));
247 assert_eq!(args[1].as_str(), Some("htop;3.4.1-3.fc44;x86_64;fedora"));
248 }
249
250 #[test]
251 fn dbus_reply_is_not_a_signal() {
252 let v = serde_json::json!({ "reply": [[]], "id": "7" });
254 let sig: DbusSignal = serde_json::from_value(v).unwrap();
255 assert!(sig.member().is_none());
256 }
257
258 #[test]
259 fn serializes_init_without_superuser() {
260 let v = serde_json::to_value(Control::Init {
261 version: 1,
262 host: "localhost".into(),
263 superuser: None,
264 })
265 .unwrap();
266 assert_eq!(v, json!({"command":"init","version":1,"host":"localhost"}));
268 }
269
270 #[test]
271 fn serializes_init_superuser_none() {
272 let v = serde_json::to_value(Control::Init {
275 version: 1,
276 host: "localhost".into(),
277 superuser: Some(json!("none")),
278 })
279 .unwrap();
280 assert_eq!(
281 v,
282 json!({
283 "command":"init","version":1,"host":"localhost",
284 "superuser":"none"
285 })
286 );
287 }
288
289 #[test]
290 fn serializes_open_with_options() {
291 let open = Control::open("ch1", "dbus-json3")
292 .opt("bus", json!("system"))
293 .opt("name", json!("org.freedesktop.systemd1"));
294 let v = serde_json::to_value(open).unwrap();
295 assert_eq!(
296 v,
297 json!({
298 "command":"open","channel":"ch1","payload":"dbus-json3",
299 "bus":"system","name":"org.freedesktop.systemd1"
300 })
301 );
302 }
303
304 #[test]
305 fn serializes_internal_bus_open() {
306 let open = Control::open("ch9", "dbus-json3").opt("bus", json!("internal"));
309 let v = serde_json::to_value(open).unwrap();
310 assert_eq!(
311 v,
312 json!({
313 "command":"open","channel":"ch9","payload":"dbus-json3",
314 "bus":"internal"
315 })
316 );
317 let obj = v.as_object().unwrap();
318 assert!(!obj.contains_key("name"));
319 assert!(!obj.contains_key("superuser"));
320 }
321
322 #[test]
323 fn serializes_dbus_call() {
324 let call = DbusCall::new(
325 "ch1",
326 "/org/freedesktop/systemd1",
327 "org.freedesktop.systemd1.Manager",
328 "ListUnits",
329 json!([]),
330 );
331 let v = serde_json::to_value(&call.body).unwrap();
332 assert_eq!(
333 v,
334 json!({
335 "call":["/org/freedesktop/systemd1","org.freedesktop.systemd1.Manager","ListUnits",[]],
336 "id": call.id
337 })
338 );
339 }
340
341 #[test]
342 fn parses_dbus_reply() {
343 let msg: DbusResponse = serde_json::from_value(json!({
344 "reply":[[ [["sshd.service","OpenSSH","loaded","active","running"]] ]],
345 "id":"7"
346 }))
347 .unwrap();
348 assert_eq!(msg.id.as_deref(), Some("7"));
349 assert!(msg.error.is_none());
350 assert!(msg.reply.is_some());
351 }
352
353 #[test]
354 fn parses_dbus_error() {
355 let msg: DbusResponse = serde_json::from_value(json!({
356 "error":["org.freedesktop.DBus.Error.UnknownMethod",["nope"]],
357 "id":"7"
358 }))
359 .unwrap();
360 assert_eq!(
361 msg.dbus_error_name(),
362 Some("org.freedesktop.DBus.Error.UnknownMethod")
363 );
364 }
365
366 #[test]
367 fn parses_incoming_control() {
368 let c: IncomingControl = serde_json::from_value(json!({
369 "command":"close","channel":"ch1","problem":"not-found"
370 }))
371 .unwrap();
372 assert_eq!(c.command, "close");
373 assert_eq!(c.channel.as_deref(), Some("ch1"));
374 assert_eq!(c.problem.as_deref(), Some("not-found"));
375 }
376
377 #[test]
378 fn serializes_done_control() {
379 let v = serde_json::to_value(Control::Done {
380 channel: "ch1".into(),
381 })
382 .unwrap();
383 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
384 }
385
386 #[test]
387 fn serializes_close_control_with_and_without_problem() {
388 let with = serde_json::to_value(Control::Close {
389 channel: "ch1".into(),
390 problem: Some("not-found".into()),
391 })
392 .unwrap();
393 assert_eq!(
394 with,
395 json!({"command":"close","channel":"ch1","problem":"not-found"})
396 );
397
398 let without = serde_json::to_value(Control::Close {
399 channel: "ch2".into(),
400 problem: None,
401 })
402 .unwrap();
403 assert_eq!(without, json!({"command":"close","channel":"ch2"}));
404 }
405
406 #[test]
407 fn control_to_json_round_trips() {
408 let bytes = Control::Done {
409 channel: "ch1".into(),
410 }
411 .to_json();
412 let v: Value = serde_json::from_slice(&bytes).unwrap();
413 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
414 }
415
416 #[test]
417 fn dbus_response_out_args_none_when_empty() {
418 let resp: DbusResponse = serde_json::from_value(json!({"id":"1"})).unwrap();
419 assert!(resp.out_args().is_none());
420 assert!(resp.dbus_error_name().is_none());
421 assert!(resp.dbus_error_message().is_none());
422 }
423}