use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Serialize)]
#[serde(tag = "command", rename_all = "kebab-case")]
pub enum Control {
Init {
version: u32,
host: String,
#[serde(skip_serializing_if = "Option::is_none")]
superuser: Option<Value>,
},
Open {
channel: String,
payload: String,
#[serde(flatten)]
options: Map<String, Value>,
},
Done {
channel: String,
},
Close {
channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
problem: Option<String>,
},
}
impl Control {
pub fn open(channel: &str, payload: &str) -> Control {
Control::Open {
channel: channel.into(),
payload: payload.into(),
options: Map::new(),
}
}
pub fn opt(mut self, key: &str, value: Value) -> Control {
if let Control::Open {
ref mut options, ..
} = self
{
options.insert(key.to_string(), value);
}
self
}
pub fn to_json(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap_or_else(|_| {
serde_json::json!({"command":"close","channel":"","problem":"internal-error"})
.to_string()
.into_bytes()
})
}
}
#[derive(Debug)]
pub struct DbusCall {
pub channel: String,
pub id: String,
pub body: DbusCallBody,
}
#[derive(Debug, Serialize)]
pub struct DbusCallBody {
pub call: (String, String, String, Value),
pub id: String,
}
impl DbusCall {
pub fn new(channel: &str, path: &str, iface: &str, method: &str, args: Value) -> DbusCall {
let id = format!("{}", next_cookie());
DbusCall {
channel: channel.into(),
id: id.clone(),
body: DbusCallBody {
call: (path.into(), iface.into(), method.into(), args),
id,
},
}
}
pub fn to_json(&self) -> Vec<u8> {
serde_json::to_vec(&self.body).unwrap_or_else(|_| {
serde_json::json!({
"call": ["", "", "", []],
"id": "serialize-error"
})
.to_string()
.into_bytes()
})
}
}
fn next_cookie() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(1);
N.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Deserialize)]
pub struct DbusResponse {
#[serde(default)]
pub reply: Option<Vec<Value>>,
#[serde(default)]
pub error: Option<Vec<Value>>,
#[serde(default)]
pub id: Option<String>,
}
impl DbusResponse {
pub fn out_args(&self) -> Option<&Value> {
self.reply.as_ref().and_then(|r| r.first())
}
pub fn dbus_error_name(&self) -> Option<&str> {
self.error
.as_ref()
.and_then(|e| e.first())
.and_then(|v| v.as_str())
}
pub fn dbus_error_message(&self) -> Option<String> {
self.error
.as_ref()
.and_then(|e| e.get(1))
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
}
#[derive(Debug, Deserialize)]
pub struct DbusSignal {
#[serde(default)]
pub signal: Option<Vec<Value>>,
}
impl DbusSignal {
pub fn member(&self) -> Option<&str> {
self.signal
.as_ref()
.and_then(|s| s.get(2))
.and_then(|v| v.as_str())
}
pub fn path(&self) -> Option<&str> {
self.signal
.as_ref()
.and_then(|s| s.first())
.and_then(|v| v.as_str())
}
pub fn args(&self) -> Option<&Vec<Value>> {
self.signal
.as_ref()
.and_then(|s| s.get(3))
.and_then(|v| v.as_array())
}
}
#[derive(Debug, Deserialize)]
pub struct IncomingControl {
pub command: String,
#[serde(default)]
pub channel: Option<String>,
#[serde(default)]
pub problem: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_dbus_signal() {
let v = serde_json::json!({
"signal": [
"/1234_abcd",
"org.freedesktop.PackageKit.Transaction",
"Package",
[8, "htop;3.4.1-3.fc44;x86_64;fedora", "Interactive process viewer"]
]
});
let sig: DbusSignal = serde_json::from_value(v).unwrap();
assert_eq!(sig.member(), Some("Package"));
assert_eq!(sig.path(), Some("/1234_abcd"));
let args = sig.args().unwrap();
assert_eq!(args[0].as_u64(), Some(8));
assert_eq!(args[1].as_str(), Some("htop;3.4.1-3.fc44;x86_64;fedora"));
}
#[test]
fn dbus_reply_is_not_a_signal() {
let v = serde_json::json!({ "reply": [[]], "id": "7" });
let sig: DbusSignal = serde_json::from_value(v).unwrap();
assert!(sig.member().is_none());
}
#[test]
fn serializes_init_without_superuser() {
let v = serde_json::to_value(Control::Init {
version: 1,
host: "localhost".into(),
superuser: None,
})
.unwrap();
assert_eq!(v, json!({"command":"init","version":1,"host":"localhost"}));
}
#[test]
fn serializes_init_superuser_none() {
let v = serde_json::to_value(Control::Init {
version: 1,
host: "localhost".into(),
superuser: Some(json!("none")),
})
.unwrap();
assert_eq!(
v,
json!({
"command":"init","version":1,"host":"localhost",
"superuser":"none"
})
);
}
#[test]
fn serializes_open_with_options() {
let open = Control::open("ch1", "dbus-json3")
.opt("bus", json!("system"))
.opt("name", json!("org.freedesktop.systemd1"));
let v = serde_json::to_value(open).unwrap();
assert_eq!(
v,
json!({
"command":"open","channel":"ch1","payload":"dbus-json3",
"bus":"system","name":"org.freedesktop.systemd1"
})
);
}
#[test]
fn serializes_internal_bus_open() {
let open = Control::open("ch9", "dbus-json3").opt("bus", json!("internal"));
let v = serde_json::to_value(open).unwrap();
assert_eq!(
v,
json!({
"command":"open","channel":"ch9","payload":"dbus-json3",
"bus":"internal"
})
);
let obj = v.as_object().unwrap();
assert!(!obj.contains_key("name"));
assert!(!obj.contains_key("superuser"));
}
#[test]
fn serializes_dbus_call() {
let call = DbusCall::new(
"ch1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"ListUnits",
json!([]),
);
let v = serde_json::to_value(&call.body).unwrap();
assert_eq!(
v,
json!({
"call":["/org/freedesktop/systemd1","org.freedesktop.systemd1.Manager","ListUnits",[]],
"id": call.id
})
);
}
#[test]
fn parses_dbus_reply() {
let msg: DbusResponse = serde_json::from_value(json!({
"reply":[[ [["sshd.service","OpenSSH","loaded","active","running"]] ]],
"id":"7"
}))
.unwrap();
assert_eq!(msg.id.as_deref(), Some("7"));
assert!(msg.error.is_none());
assert!(msg.reply.is_some());
}
#[test]
fn parses_dbus_error() {
let msg: DbusResponse = serde_json::from_value(json!({
"error":["org.freedesktop.DBus.Error.UnknownMethod",["nope"]],
"id":"7"
}))
.unwrap();
assert_eq!(
msg.dbus_error_name(),
Some("org.freedesktop.DBus.Error.UnknownMethod")
);
}
#[test]
fn parses_incoming_control() {
let c: IncomingControl = serde_json::from_value(json!({
"command":"close","channel":"ch1","problem":"not-found"
}))
.unwrap();
assert_eq!(c.command, "close");
assert_eq!(c.channel.as_deref(), Some("ch1"));
assert_eq!(c.problem.as_deref(), Some("not-found"));
}
#[test]
fn serializes_done_control() {
let v = serde_json::to_value(Control::Done {
channel: "ch1".into(),
})
.unwrap();
assert_eq!(v, json!({"command":"done","channel":"ch1"}));
}
#[test]
fn serializes_close_control_with_and_without_problem() {
let with = serde_json::to_value(Control::Close {
channel: "ch1".into(),
problem: Some("not-found".into()),
})
.unwrap();
assert_eq!(
with,
json!({"command":"close","channel":"ch1","problem":"not-found"})
);
let without = serde_json::to_value(Control::Close {
channel: "ch2".into(),
problem: None,
})
.unwrap();
assert_eq!(without, json!({"command":"close","channel":"ch2"}));
}
#[test]
fn control_to_json_round_trips() {
let bytes = Control::Done {
channel: "ch1".into(),
}
.to_json();
let v: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v, json!({"command":"done","channel":"ch1"}));
}
#[test]
fn dbus_response_out_args_none_when_empty() {
let resp: DbusResponse = serde_json::from_value(json!({"id":"1"})).unwrap();
assert!(resp.out_args().is_none());
assert!(resp.dbus_error_name().is_none());
assert!(resp.dbus_error_message().is_none());
}
}