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)]
177pub struct IncomingControl {
178 pub command: String,
180 #[serde(default)]
182 pub channel: Option<String>,
183 #[serde(default)]
185 pub problem: Option<String>,
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use serde_json::json;
192
193 #[test]
194 fn serializes_init_without_superuser() {
195 let v = serde_json::to_value(Control::Init {
196 version: 1,
197 host: "localhost".into(),
198 superuser: None,
199 })
200 .unwrap();
201 assert_eq!(v, json!({"command":"init","version":1,"host":"localhost"}));
203 }
204
205 #[test]
206 fn serializes_init_superuser_none() {
207 let v = serde_json::to_value(Control::Init {
210 version: 1,
211 host: "localhost".into(),
212 superuser: Some(json!("none")),
213 })
214 .unwrap();
215 assert_eq!(
216 v,
217 json!({
218 "command":"init","version":1,"host":"localhost",
219 "superuser":"none"
220 })
221 );
222 }
223
224 #[test]
225 fn serializes_open_with_options() {
226 let open = Control::open("ch1", "dbus-json3")
227 .opt("bus", json!("system"))
228 .opt("name", json!("org.freedesktop.systemd1"));
229 let v = serde_json::to_value(open).unwrap();
230 assert_eq!(
231 v,
232 json!({
233 "command":"open","channel":"ch1","payload":"dbus-json3",
234 "bus":"system","name":"org.freedesktop.systemd1"
235 })
236 );
237 }
238
239 #[test]
240 fn serializes_internal_bus_open() {
241 let open = Control::open("ch9", "dbus-json3").opt("bus", json!("internal"));
244 let v = serde_json::to_value(open).unwrap();
245 assert_eq!(
246 v,
247 json!({
248 "command":"open","channel":"ch9","payload":"dbus-json3",
249 "bus":"internal"
250 })
251 );
252 let obj = v.as_object().unwrap();
253 assert!(!obj.contains_key("name"));
254 assert!(!obj.contains_key("superuser"));
255 }
256
257 #[test]
258 fn serializes_dbus_call() {
259 let call = DbusCall::new(
260 "ch1",
261 "/org/freedesktop/systemd1",
262 "org.freedesktop.systemd1.Manager",
263 "ListUnits",
264 json!([]),
265 );
266 let v = serde_json::to_value(&call.body).unwrap();
267 assert_eq!(
268 v,
269 json!({
270 "call":["/org/freedesktop/systemd1","org.freedesktop.systemd1.Manager","ListUnits",[]],
271 "id": call.id
272 })
273 );
274 }
275
276 #[test]
277 fn parses_dbus_reply() {
278 let msg: DbusResponse = serde_json::from_value(json!({
279 "reply":[[ [["sshd.service","OpenSSH","loaded","active","running"]] ]],
280 "id":"7"
281 }))
282 .unwrap();
283 assert_eq!(msg.id.as_deref(), Some("7"));
284 assert!(msg.error.is_none());
285 assert!(msg.reply.is_some());
286 }
287
288 #[test]
289 fn parses_dbus_error() {
290 let msg: DbusResponse = serde_json::from_value(json!({
291 "error":["org.freedesktop.DBus.Error.UnknownMethod",["nope"]],
292 "id":"7"
293 }))
294 .unwrap();
295 assert_eq!(
296 msg.dbus_error_name(),
297 Some("org.freedesktop.DBus.Error.UnknownMethod")
298 );
299 }
300
301 #[test]
302 fn parses_incoming_control() {
303 let c: IncomingControl = serde_json::from_value(json!({
304 "command":"close","channel":"ch1","problem":"not-found"
305 }))
306 .unwrap();
307 assert_eq!(c.command, "close");
308 assert_eq!(c.channel.as_deref(), Some("ch1"));
309 assert_eq!(c.problem.as_deref(), Some("not-found"));
310 }
311
312 #[test]
313 fn serializes_done_control() {
314 let v = serde_json::to_value(Control::Done {
315 channel: "ch1".into(),
316 })
317 .unwrap();
318 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
319 }
320
321 #[test]
322 fn serializes_close_control_with_and_without_problem() {
323 let with = serde_json::to_value(Control::Close {
324 channel: "ch1".into(),
325 problem: Some("not-found".into()),
326 })
327 .unwrap();
328 assert_eq!(
329 with,
330 json!({"command":"close","channel":"ch1","problem":"not-found"})
331 );
332
333 let without = serde_json::to_value(Control::Close {
334 channel: "ch2".into(),
335 problem: None,
336 })
337 .unwrap();
338 assert_eq!(without, json!({"command":"close","channel":"ch2"}));
339 }
340
341 #[test]
342 fn control_to_json_round_trips() {
343 let bytes = Control::Done {
344 channel: "ch1".into(),
345 }
346 .to_json();
347 let v: Value = serde_json::from_slice(&bytes).unwrap();
348 assert_eq!(v, json!({"command":"done","channel":"ch1"}));
349 }
350
351 #[test]
352 fn dbus_response_out_args_none_when_empty() {
353 let resp: DbusResponse = serde_json::from_value(json!({"id":"1"})).unwrap();
354 assert!(resp.out_args().is_none());
355 assert!(resp.dbus_error_name().is_none());
356 assert!(resp.dbus_error_message().is_none());
357 }
358}