#![allow(unsafe_code)]
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
unsafe extern "C" {
fn geteuid() -> u32;
}
#[derive(Debug)]
pub struct DBus {
stream: UnixStream,
serial: u32,
unique_name: String,
}
impl DBus {
pub fn connect_session() -> Result<DBus, String> {
let addr = std::env::var("DBUS_SESSION_BUS_ADDRESS")
.map_err(|_| "DBUS_SESSION_BUS_ADDRESS not set".to_string())?;
let path =
parse_unix_path(&addr).ok_or_else(|| format!("unsupported bus address: {addr}"))?;
let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
Self::handshake(stream)
}
pub fn connect_address(addr: &str) -> Result<DBus, String> {
let path =
parse_unix_path(addr).ok_or_else(|| format!("unsupported bus address: {addr}"))?;
let stream = UnixStream::connect(&path).map_err(|e| format!("connect {path}: {e}"))?;
Self::handshake(stream)
}
fn handshake(mut stream: UnixStream) -> Result<DBus, String> {
let uid = unsafe { geteuid() };
let uid_hex: String = uid
.to_string()
.bytes()
.map(|b| format!("{b:02x}"))
.collect();
stream.write_all(&[0]).map_err(io)?;
stream
.write_all(format!("AUTH EXTERNAL {uid_hex}\r\n").as_bytes())
.map_err(io)?;
let line = read_line(&mut stream)?;
if !line.starts_with("OK ") {
return Err(format!("SASL auth failed: {line:?}"));
}
stream.write_all(b"BEGIN\r\n").map_err(io)?;
let mut bus = DBus {
stream,
serial: 0,
unique_name: String::new(),
};
let reply = bus.call(
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"Hello",
)?;
bus.unique_name = read_string(&reply, &mut 0)?;
Ok(bus)
}
pub fn unique_name(&self) -> &str {
&self.unique_name
}
pub fn a11y_bus_address(&mut self) -> Result<String, String> {
let reply = self.call(
"org.a11y.Bus",
"/org/a11y/bus",
"org.a11y.Bus",
"GetAddress",
)?;
read_string(&reply, &mut 0)
}
pub fn connect_a11y() -> Result<DBus, String> {
let mut session = Self::connect_session()?;
let addr = session.a11y_bus_address()?;
Self::connect_address(&addr)
}
pub fn request_name(&mut self, name: &str) -> Result<u32, String> {
self.serial += 1;
let serial = self.serial;
let mut fields = Vec::new();
put_field(&mut fields, 1, b'o', "/org/freedesktop/DBus");
put_field(&mut fields, 6, b's', "org.freedesktop.DBus");
put_field(&mut fields, 2, b's', "org.freedesktop.DBus");
put_field(&mut fields, 3, b's', "RequestName");
put_field_sig(&mut fields, 8, "su");
let mut body = Vec::new();
put_string(&mut body, name);
align(&mut body, 4);
body.extend_from_slice(&0u32.to_le_bytes()); let msg = build_message(1, serial, &fields, &body);
self.stream.write_all(&msg).map_err(io)?;
loop {
let m = self.read_message()?;
match m.mtype {
2 if m.reply_serial == serial => return read_u32(&m.body, &mut 0),
3 if m.reply_serial == serial => return Err("RequestName error".into()),
_ => continue,
}
}
}
fn call_with_body(
&mut self,
destination: &str,
path: &str,
interface: &str,
member: &str,
signature: &str,
body: &[u8],
) -> Result<Vec<u8>, String> {
self.serial += 1;
let serial = self.serial;
let mut fields = Vec::new();
put_field(&mut fields, 1, b'o', path);
put_field(&mut fields, 6, b's', destination);
put_field(&mut fields, 2, b's', interface);
put_field(&mut fields, 3, b's', member);
if !signature.is_empty() {
put_field_sig(&mut fields, 8, signature);
}
let msg = build_message(1, serial, &fields, body);
self.stream.write_all(&msg).map_err(io)?;
loop {
let m = self.read_message()?;
match m.mtype {
2 if m.reply_serial == serial => return Ok(m.body),
3 if m.reply_serial == serial => return Err("D-Bus error reply".into()),
_ => continue,
}
}
}
fn add_match(&mut self, rule: &str) -> Result<(), String> {
let mut body = Vec::new();
put_string(&mut body, rule);
self.call_with_body(
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"AddMatch",
"s",
&body,
)?;
Ok(())
}
fn emit_signal(
&mut self,
path: &str,
interface: &str,
member: &str,
signature: &str,
body: &[u8],
) -> Result<(), String> {
self.serial += 1;
let mut fields = Vec::new();
put_field(&mut fields, 1, b'o', path);
put_field(&mut fields, 2, b's', interface);
put_field(&mut fields, 3, b's', member);
if !signature.is_empty() {
put_field_sig(&mut fields, 8, signature);
}
let msg = build_message(4, self.serial, &fields, body);
self.stream.write_all(&msg).map_err(io)
}
pub fn portal_file_chooser(
&mut self,
member: &str,
title: &str,
directory: bool,
) -> Result<Option<PathBuf>, String> {
self.add_match(
"type='signal',interface='org.freedesktop.portal.Request',member='Response'",
)?;
let mut body = Vec::new();
put_string(&mut body, ""); put_string(&mut body, title);
let mut opts: Vec<(&str, Dv)> = vec![("handle_token", Dv::Str("forma1"))];
if directory {
opts.push(("directory", Dv::Bool(true)));
}
put_dict_sv(&mut body, &opts);
let reply = self.call_with_body(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser",
member,
"ssa{sv}",
&body,
)?;
let handle = read_string(&reply, &mut 0)?;
loop {
let m = self.read_message()?;
if m.mtype == 4 && m.member == "Response" && m.path == handle {
return parse_portal_response(&m.body);
}
}
}
pub fn serve(&mut self, introspect_xml: &str) -> Result<(), String> {
loop {
let m = self.read_message()?;
if m.mtype != 1 {
continue; }
match (m.interface.as_str(), m.member.as_str()) {
("org.freedesktop.DBus.Peer", "Ping") => self.send_return(&m, "", &[])?,
("org.freedesktop.DBus.Peer", "GetMachineId") => {
let mut body = Vec::new();
put_string(&mut body, "00000000000000000000000000000000");
self.send_return(&m, "s", &body)?;
}
("org.freedesktop.DBus.Introspectable", "Introspect") => {
let mut body = Vec::new();
put_string(&mut body, introspect_xml);
self.send_return(&m, "s", &body)?;
}
_ => self.send_error(&m, "org.freedesktop.DBus.Error.UnknownMethod")?,
}
}
}
pub fn serve_atspi_tree(
&mut self,
tree: &AtspiTree,
introspect_xml: &str,
) -> Result<(), String> {
let bus = self.unique_name.clone();
loop {
let m = self.read_message()?;
if m.mtype != 1 {
continue;
}
let idx = node_index_for_path(&m.path);
match (m.interface.as_str(), m.member.as_str()) {
("org.freedesktop.DBus.Peer", "Ping") => self.send_return(&m, "", &[])?,
("org.freedesktop.DBus.Introspectable", "Introspect") => {
let mut body = Vec::new();
put_string(&mut body, introspect_xml);
self.send_return(&m, "s", &body)?;
}
("org.a11y.atspi.Accessible", member) => {
let Some(i) = idx.filter(|&i| i < tree.nodes.len()) else {
self.send_error(&m, "org.freedesktop.DBus.Error.UnknownObject")?;
continue;
};
let node = &tree.nodes[i];
match member {
"GetRole" => self.send_return(&m, "u", &node.role.to_le_bytes())?,
"GetRoleName" | "GetLocalizedRoleName" => {
let mut body = Vec::new();
put_string(&mut body, role_name(node.role));
self.send_return(&m, "s", &body)?;
}
"GetIndexInParent" => {
self.send_return(&m, "i", &node.index_in_parent.to_le_bytes())?
}
"GetChildAtIndex" => {
let mut off = 0;
let ci = read_u32(&m.body, &mut off).unwrap_or(0) as i32;
let mut body = Vec::new();
match usize::try_from(ci).ok().and_then(|c| node.children.get(c)) {
Some(&c) => put_object_ref(&mut body, &bus, &path_for_index(c)),
None => put_object_ref(&mut body, "", ATSPI_NULL_PATH),
}
self.send_return(&m, "(so)", &body)?;
}
"GetChildren" => {
let refs: Vec<(String, String)> = node
.children
.iter()
.map(|&c| (bus.clone(), path_for_index(c)))
.collect();
let mut body = Vec::new();
put_object_ref_array(&mut body, &refs);
self.send_return(&m, "a(so)", &body)?;
}
_ => self.send_error(&m, "org.freedesktop.DBus.Error.UnknownMethod")?,
}
}
("org.freedesktop.DBus.Properties", "Get") => {
let mut off = 0;
let _iface = read_string(&m.body, &mut off).unwrap_or_default();
let prop = read_string(&m.body, &mut off).unwrap_or_default();
let Some(i) = idx.filter(|&i| i < tree.nodes.len()) else {
self.send_error(&m, "org.freedesktop.DBus.Error.UnknownObject")?;
continue;
};
let node = &tree.nodes[i];
let mut body = Vec::new();
match prop.as_str() {
"Name" => put_variant_string(&mut body, &node.name),
"Description" => put_variant_string(&mut body, ""),
"ChildCount" => put_variant_i32(&mut body, node.children.len() as i32),
"Parent" => match node.parent {
Some(p) => put_variant_object_ref(&mut body, &bus, &path_for_index(p)),
None => put_variant_object_ref(&mut body, "", ATSPI_NULL_PATH),
},
_ => {
self.send_error(&m, "org.freedesktop.DBus.Error.InvalidArgs")?;
continue;
}
}
self.send_return(&m, "v", &body)?;
}
_ => self.send_error(&m, "org.freedesktop.DBus.Error.UnknownMethod")?,
}
}
}
fn send_return(&mut self, req: &Message, signature: &str, body: &[u8]) -> Result<(), String> {
self.serial += 1;
let mut fields = Vec::new();
put_field_u32(&mut fields, 5, req.serial); if !req.sender.is_empty() {
put_field(&mut fields, 6, b's', &req.sender); }
if !signature.is_empty() {
put_field_sig(&mut fields, 8, signature);
}
let msg = build_message(2, self.serial, &fields, body);
self.stream.write_all(&msg).map_err(io)
}
fn send_error(&mut self, req: &Message, error_name: &str) -> Result<(), String> {
self.serial += 1;
let mut fields = Vec::new();
put_field_u32(&mut fields, 5, req.serial); put_field(&mut fields, 4, b's', error_name); if !req.sender.is_empty() {
put_field(&mut fields, 6, b's', &req.sender); }
let msg = build_message(3, self.serial, &fields, &[]);
self.stream.write_all(&msg).map_err(io)
}
fn call(
&mut self,
destination: &str,
path: &str,
interface: &str,
member: &str,
) -> Result<Vec<u8>, String> {
self.serial += 1;
let serial = self.serial;
let msg = marshal_method_call(serial, destination, path, interface, member);
self.stream.write_all(&msg).map_err(io)?;
loop {
let msg = self.read_message()?;
match msg.mtype {
2 if msg.reply_serial == serial => return Ok(msg.body), 3 if msg.reply_serial == serial => return Err("D-Bus error reply".into()), _ => continue,
}
}
}
fn read_message(&mut self) -> Result<Message, String> {
let mut fixed = [0u8; 16];
self.stream.read_exact(&mut fixed).map_err(io)?;
if fixed[0] != b'l' {
return Err("only little-endian D-Bus is supported".into());
}
let mtype = fixed[1];
let body_len = u32::from_le_bytes([fixed[4], fixed[5], fixed[6], fixed[7]]) as usize;
let serial = u32::from_le_bytes([fixed[8], fixed[9], fixed[10], fixed[11]]);
let fields_len = u32::from_le_bytes([fixed[12], fixed[13], fixed[14], fixed[15]]) as usize;
let mut fields = vec![0u8; fields_len];
self.stream.read_exact(&mut fields).map_err(io)?;
let header_len = 16 + fields_len;
let pad = (8 - (header_len % 8)) % 8;
if pad > 0 {
let mut p = vec![0u8; pad];
self.stream.read_exact(&mut p).map_err(io)?;
}
let mut body = vec![0u8; body_len];
self.stream.read_exact(&mut body).map_err(io)?;
let mut msg = parse_fields(&fields);
msg.mtype = mtype;
msg.serial = serial;
msg.body = body;
Ok(msg)
}
}
pub fn run_mock_file_portal(canned_uri: &str) -> Result<(), String> {
let mut bus = DBus::connect_session()?;
bus.request_name("org.freedesktop.portal.Desktop")?;
loop {
let m = bus.read_message()?;
if m.mtype != 1 {
continue; }
match (m.interface.as_str(), m.member.as_str()) {
("org.freedesktop.portal.FileChooser", "OpenFile" | "SaveFile") => {
let handle = "/org/freedesktop/portal/desktop/request/stipple/1";
let mut rbody = Vec::new();
put_string(&mut rbody, handle); bus.send_return(&m, "o", &rbody)?;
let mut sbody = Vec::new();
put_response_body(&mut sbody, 0, canned_uri);
bus.emit_signal(
handle,
"org.freedesktop.portal.Request",
"Response",
"ua{sv}",
&sbody,
)?;
return Ok(());
}
("org.freedesktop.DBus.Peer", "Ping") => bus.send_return(&m, "", &[])?,
("org.freedesktop.DBus.Introspectable", "Introspect") => {
let mut body = Vec::new();
put_string(&mut body, "<node/>");
bus.send_return(&m, "s", &body)?;
}
_ => bus.send_error(&m, "org.freedesktop.DBus.Error.UnknownMethod")?,
}
}
}
pub const ATSPI_ROOT_PATH: &str = "/org/stippleui/a11y";
const ATSPI_NULL_PATH: &str = "/org/a11y/atspi/null";
#[derive(Debug, Clone)]
pub struct AtspiTreeNode {
pub role: u32,
pub name: String,
pub parent: Option<usize>,
pub children: Vec<usize>,
pub index_in_parent: i32,
}
#[derive(Debug, Clone, Default)]
pub struct AtspiTree {
pub nodes: Vec<AtspiTreeNode>,
}
impl AtspiTree {
pub fn new() -> AtspiTree {
AtspiTree::default()
}
pub fn push(&mut self, role: u32, name: &str, parent: Option<usize>) -> usize {
let idx = self.nodes.len();
let index_in_parent = match parent {
Some(p) => {
let n = self.nodes[p].children.len() as i32;
self.nodes[p].children.push(idx);
n
}
None => -1,
};
self.nodes.push(AtspiTreeNode {
role,
name: name.to_string(),
parent,
children: Vec::new(),
index_in_parent,
});
idx
}
}
fn path_for_index(i: usize) -> String {
if i == 0 {
ATSPI_ROOT_PATH.to_string()
} else {
format!("{ATSPI_ROOT_PATH}/{i}")
}
}
fn node_index_for_path(path: &str) -> Option<usize> {
if path == ATSPI_ROOT_PATH {
return Some(0);
}
path.strip_prefix(ATSPI_ROOT_PATH)?
.strip_prefix('/')?
.parse()
.ok()
}
fn role_name(role: u32) -> &'static str {
match role {
27 => "frame",
54 => "panel",
44 => "push button",
80 => "entry",
29 => "label",
_ => "unknown",
}
}
#[derive(Debug, Default)]
struct Message {
mtype: u8,
serial: u32,
reply_serial: u32,
path: String,
interface: String,
member: String,
sender: String,
body: Vec<u8>,
}
fn parse_unix_path(addr: &str) -> Option<String> {
let addr = addr.split(';').next().unwrap_or(addr);
for kv in addr.trim_start_matches("unix:").split(',') {
if let Some(p) = kv.strip_prefix("path=") {
return Some(p.to_string());
}
if let Some(p) = kv.strip_prefix("abstract=") {
return Some(format!("\0{p}"));
}
}
None
}
fn io(e: std::io::Error) -> String {
format!("dbus i/o: {e}")
}
fn read_line(stream: &mut UnixStream) -> Result<String, String> {
let mut out = Vec::new();
let mut b = [0u8; 1];
loop {
stream.read_exact(&mut b).map_err(io)?;
if b[0] == b'\n' {
if out.last() == Some(&b'\r') {
out.pop();
}
break;
}
out.push(b[0]);
if out.len() > 4096 {
return Err("SASL line too long".into());
}
}
String::from_utf8(out).map_err(|_| "SASL line not UTF-8".to_string())
}
fn align(buf: &mut Vec<u8>, n: usize) {
while !buf.len().is_multiple_of(n) {
buf.push(0);
}
}
fn put_string(buf: &mut Vec<u8>, s: &str) {
align(buf, 4);
buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
buf.extend_from_slice(s.as_bytes());
buf.push(0);
}
fn put_field(buf: &mut Vec<u8>, code: u8, sig: u8, value: &str) {
align(buf, 8);
buf.push(code);
buf.push(1);
buf.push(sig);
buf.push(0);
put_string(buf, value);
}
fn put_field_u32(buf: &mut Vec<u8>, code: u8, value: u32) {
align(buf, 8);
buf.push(code);
buf.push(1);
buf.push(b'u');
buf.push(0);
align(buf, 4);
buf.extend_from_slice(&value.to_le_bytes());
}
fn put_field_sig(buf: &mut Vec<u8>, code: u8, sig: &str) {
align(buf, 8);
buf.push(code);
buf.push(1);
buf.push(b'g');
buf.push(0);
buf.push(sig.len() as u8);
buf.extend_from_slice(sig.as_bytes());
buf.push(0);
}
fn build_message(mtype: u8, serial: u32, fields: &[u8], body: &[u8]) -> Vec<u8> {
let mut msg = vec![b'l', mtype, 0, 1];
msg.extend_from_slice(&(body.len() as u32).to_le_bytes());
msg.extend_from_slice(&serial.to_le_bytes());
msg.extend_from_slice(&(fields.len() as u32).to_le_bytes());
msg.extend_from_slice(fields);
align(&mut msg, 8);
msg.extend_from_slice(body);
msg
}
fn marshal_method_call(
serial: u32,
destination: &str,
path: &str,
interface: &str,
member: &str,
) -> Vec<u8> {
let mut fields = Vec::new();
put_field(&mut fields, 1, b'o', path);
put_field(&mut fields, 6, b's', destination);
put_field(&mut fields, 2, b's', interface);
put_field(&mut fields, 3, b's', member);
build_message(1, serial, &fields, &[])
}
fn put_variant_string(buf: &mut Vec<u8>, s: &str) {
buf.push(1);
buf.push(b's');
buf.push(0);
put_string(buf, s);
}
fn put_object_ref(buf: &mut Vec<u8>, bus: &str, path: &str) {
align(buf, 8);
put_string(buf, bus);
put_string(buf, path);
}
fn put_object_ref_array(buf: &mut Vec<u8>, refs: &[(String, String)]) {
align(buf, 4);
let len_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); align(buf, 8); let start = buf.len();
for (bus, path) in refs {
put_object_ref(buf, bus, path);
}
let len = (buf.len() - start) as u32;
buf[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
}
fn put_variant_object_ref(buf: &mut Vec<u8>, bus: &str, path: &str) {
buf.push(4);
buf.extend_from_slice(b"(so)");
buf.push(0);
put_object_ref(buf, bus, path);
}
enum Dv<'a> {
Str(&'a str),
Bool(bool),
}
fn put_dict_sv(buf: &mut Vec<u8>, entries: &[(&str, Dv)]) {
align(buf, 4);
let len_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); align(buf, 8); let start = buf.len();
for (key, val) in entries {
align(buf, 8);
put_string(buf, key);
match val {
Dv::Str(s) => put_variant_string(buf, s),
Dv::Bool(b) => {
buf.push(1);
buf.push(b'b');
buf.push(0);
align(buf, 4);
buf.extend_from_slice(&(*b as u32).to_le_bytes());
}
}
}
let len = (buf.len() - start) as u32;
buf[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
}
fn put_response_body(buf: &mut Vec<u8>, response: u32, uri: &str) {
buf.extend_from_slice(&response.to_le_bytes()); align(buf, 4);
let len_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); align(buf, 8);
let start = buf.len();
align(buf, 8);
put_string(buf, "uris");
buf.push(2);
buf.push(b'a');
buf.push(b's');
buf.push(0);
align(buf, 4);
let as_len_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); align(buf, 4);
let as_start = buf.len();
put_string(buf, uri);
let as_len = (buf.len() - as_start) as u32;
buf[as_len_pos..as_len_pos + 4].copy_from_slice(&as_len.to_le_bytes());
let len = (buf.len() - start) as u32;
buf[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
}
fn parse_portal_response(body: &[u8]) -> Result<Option<PathBuf>, String> {
let mut off = 0;
let response = read_u32(body, &mut off)?;
if response != 0 {
return Ok(None);
}
Ok(read_results_uris(body, &mut off)?.map(|u| uri_to_path(&u)))
}
fn read_results_uris(buf: &[u8], off: &mut usize) -> Result<Option<String>, String> {
align_to(off, 4);
let arr_len = read_u32(buf, off)? as usize;
align_to(off, 8);
let end = (*off + arr_len).min(buf.len());
let mut found = None;
while *off < end {
align_to(off, 8);
if *off >= end {
break;
}
let key = read_string(buf, off)?;
let sig_len = *buf.get(*off).ok_or("truncated variant sig")? as usize;
*off += 1;
let sig = buf
.get(*off..*off + sig_len)
.ok_or("truncated variant sig")?
.to_vec();
*off += sig_len + 1; if key == "uris" && sig == b"as" {
align_to(off, 4);
let as_len = read_u32(buf, off)? as usize;
let as_end = (*off + as_len).min(buf.len());
if found.is_none() && *off < as_end {
found = Some(read_string(buf, off)?);
}
*off = as_end;
} else {
let mut si = 0;
skip_value(buf, off, &sig, &mut si)?;
}
}
Ok(found)
}
fn align_to(off: &mut usize, n: usize) {
while !off.is_multiple_of(n) {
*off += 1;
}
}
fn type_align(c: u8) -> usize {
match c {
b'n' | b'q' => 2,
b'b' | b'i' | b'u' | b'h' | b'a' | b's' | b'o' => 4,
b'x' | b't' | b'd' | b'(' | b'{' => 8,
_ => 1, }
}
fn skip_value(buf: &[u8], off: &mut usize, sig: &[u8], si: &mut usize) -> Result<(), String> {
let t = *sig.get(*si).ok_or("signature underrun")?;
*si += 1;
match t {
b'y' => *off += 1,
b'n' | b'q' => {
align_to(off, 2);
*off += 2;
}
b'b' | b'i' | b'u' | b'h' => {
align_to(off, 4);
*off += 4;
}
b'x' | b't' | b'd' => {
align_to(off, 8);
*off += 8;
}
b's' | b'o' => {
read_string(buf, off)?;
}
b'g' => {
let l = *buf.get(*off).ok_or("truncated signature")? as usize;
*off += 1 + l + 1;
}
b'v' => {
let l = *buf.get(*off).ok_or("truncated variant")? as usize;
*off += 1;
let vsig = buf.get(*off..*off + l).ok_or("truncated variant")?.to_vec();
*off += l + 1;
let mut vi = 0;
skip_value(buf, off, &vsig, &mut vi)?;
}
b'a' => {
align_to(off, 4);
let len = read_u32(buf, off)? as usize;
let elem = *sig.get(*si).ok_or("array element type")?;
align_to(off, type_align(elem));
let target = (*off + len).min(buf.len());
skip_type_sig(sig, si)?;
*off = target;
}
b'(' => {
align_to(off, 8);
while sig.get(*si).copied() != Some(b')') {
skip_value(buf, off, sig, si)?;
}
*si += 1; }
b'{' => {
align_to(off, 8);
skip_value(buf, off, sig, si)?; skip_value(buf, off, sig, si)?; *si += 1; }
other => return Err(format!("unsupported D-Bus type '{}'", other as char)),
}
Ok(())
}
fn skip_type_sig(sig: &[u8], si: &mut usize) -> Result<(), String> {
let t = *sig.get(*si).ok_or("signature underrun")?;
*si += 1;
match t {
b'a' => skip_type_sig(sig, si)?,
b'(' => {
while sig.get(*si).copied() != Some(b')') {
skip_type_sig(sig, si)?;
}
*si += 1;
}
b'{' => {
skip_type_sig(sig, si)?;
skip_type_sig(sig, si)?;
*si += 1;
}
_ => {}
}
Ok(())
}
fn uri_to_path(uri: &str) -> PathBuf {
let rest = uri.strip_prefix("file://").unwrap_or(uri);
PathBuf::from(percent_decode(rest))
}
fn percent_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'%'
&& i + 2 < b.len()
&& let (Some(h), Some(l)) = (hex_digit(b[i + 1]), hex_digit(b[i + 2]))
{
out.push(h * 16 + l);
i += 3;
continue;
}
out.push(b[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn put_variant_i32(buf: &mut Vec<u8>, v: i32) {
buf.push(1);
buf.push(b'i');
buf.push(0);
align(buf, 4);
buf.extend_from_slice(&v.to_le_bytes());
}
fn read_u32(buf: &[u8], off: &mut usize) -> Result<u32, String> {
while !off.is_multiple_of(4) {
*off += 1;
}
if *off + 4 > buf.len() {
return Err("truncated u32".into());
}
let v = u32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
*off += 4;
Ok(v)
}
fn read_string(buf: &[u8], off: &mut usize) -> Result<String, String> {
while !off.is_multiple_of(4) {
*off += 1;
}
if *off + 4 > buf.len() {
return Err("truncated string length".into());
}
let len = u32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]) as usize;
*off += 4;
if *off + len > buf.len() {
return Err("truncated string body".into());
}
let s = String::from_utf8_lossy(&buf[*off..*off + len]).into_owned();
*off += len + 1; Ok(s)
}
fn parse_fields(fields: &[u8]) -> Message {
let mut msg = Message::default();
let mut off = 0usize;
while off < fields.len() {
while !off.is_multiple_of(8) && off < fields.len() {
off += 1;
}
if off + 2 > fields.len() {
break;
}
let code = fields[off];
off += 1;
let sig_len = fields[off] as usize;
off += 1;
let sig = fields.get(off..off + sig_len).unwrap_or(&[]).to_vec();
off += sig_len + 1; match sig.first().copied() {
Some(b'u') | Some(b'i') => {
while !off.is_multiple_of(4) && off < fields.len() {
off += 1;
}
if off + 4 > fields.len() {
break;
}
let v = u32::from_le_bytes([
fields[off],
fields[off + 1],
fields[off + 2],
fields[off + 3],
]);
off += 4;
if code == 5 {
msg.reply_serial = v;
}
}
Some(b's') | Some(b'o') | Some(b'g') => {
let value = if sig.first() == Some(&b'g') {
let l = *fields.get(off).unwrap_or(&0) as usize;
let v =
String::from_utf8_lossy(fields.get(off + 1..off + 1 + l).unwrap_or(&[]))
.into_owned();
off += 1 + l + 1;
v
} else {
match read_string(fields, &mut off) {
Ok(v) => v,
Err(_) => break,
}
};
match code {
1 => msg.path = value,
2 => msg.interface = value,
3 => msg.member = value,
7 => msg.sender = value,
_ => {}
}
}
_ => break, }
}
msg
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atspi_path_index_round_trip() {
for i in [0usize, 1, 5, 42] {
assert_eq!(node_index_for_path(&path_for_index(i)), Some(i));
}
assert_eq!(node_index_for_path(ATSPI_ROOT_PATH), Some(0));
assert_eq!(node_index_for_path("/some/other/path"), None);
}
#[test]
fn atspi_tree_links_parent_and_index() {
let mut t = AtspiTree::new();
let root = t.push(27, "Win", None);
let a = t.push(29, "Hello", Some(root));
let g = t.push(54, "", Some(root));
let b = t.push(29, "OK", Some(g));
assert_eq!(t.nodes[root].children, vec![a, g]);
assert_eq!(t.nodes[root].index_in_parent, -1);
assert_eq!(t.nodes[g].index_in_parent, 1); assert_eq!(t.nodes[b].parent, Some(g));
assert_eq!(t.nodes[b].index_in_parent, 0);
}
#[test]
fn object_ref_array_round_trips() {
let refs = vec![
(":1.5".to_string(), "/org/stippleui/a11y/1".to_string()),
(String::new(), "/org/stippleui/a11y/2".to_string()),
];
let mut buf = Vec::new();
put_object_ref_array(&mut buf, &refs);
let mut off = 0;
let alen = read_u32(&buf, &mut off).unwrap() as usize;
align_to(&mut off, 8);
let end = off + alen;
let mut got = Vec::new();
while off < end {
align_to(&mut off, 8);
let bus = read_string(&buf, &mut off).unwrap();
let path = read_string(&buf, &mut off).unwrap();
got.push((bus, path));
}
assert_eq!(got, refs);
assert_eq!(off, end);
}
#[test]
fn portal_response_round_trips_a_uri_to_a_path() {
let mut body = Vec::new();
put_response_body(&mut body, 0, "file:///tmp/stipple%20pick.txt");
let path = parse_portal_response(&body).unwrap();
assert_eq!(path, Some(PathBuf::from("/tmp/stipple pick.txt")));
}
#[test]
fn portal_response_cancelled_is_none() {
let mut body = Vec::new();
put_response_body(&mut body, 1, "file:///tmp/x.txt");
assert_eq!(parse_portal_response(&body).unwrap(), None);
}
#[test]
fn skips_unknown_result_keys_before_uris() {
let mut body = Vec::new();
body.extend_from_slice(&0u32.to_le_bytes()); align(&mut body, 4);
let len_pos = body.len();
body.extend_from_slice(&0u32.to_le_bytes());
align(&mut body, 8);
let start = body.len();
align(&mut body, 8);
put_string(&mut body, "writable");
body.push(1);
body.push(b'b');
body.push(0);
align(&mut body, 4);
body.extend_from_slice(&1u32.to_le_bytes());
align(&mut body, 8);
put_string(&mut body, "uris");
body.push(2);
body.push(b'a');
body.push(b's');
body.push(0);
align(&mut body, 4);
let as_len_pos = body.len();
body.extend_from_slice(&0u32.to_le_bytes());
align(&mut body, 4);
let as_start = body.len();
put_string(&mut body, "file:///tmp/y.txt");
let as_len = (body.len() - as_start) as u32;
body[as_len_pos..as_len_pos + 4].copy_from_slice(&as_len.to_le_bytes());
let len = (body.len() - start) as u32;
body[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
let path = parse_portal_response(&body).unwrap();
assert_eq!(path, Some(PathBuf::from("/tmp/y.txt")));
}
}