zbus_lockstep/lib.rs
1//! # zbus-lockstep
2//!
3//! Is a collection of helpers for retrieving `DBus` type signatures from XML descriptions.
4//! Useful for comparing these with your types' signatures to ensure that they are compatible.
5//!
6//! It offers functions that retrieve the signature of a method's argument type, of a method's
7//! return type, pf a signal's body type or of a property's type from `DBus` XML.
8//!
9//! These functions require that you provide the file path to the XML file, the interface name,
10//! and the interface member wherein the signature resides.
11//!
12//! Corresponding to each of these functions, macros are provided which do not
13//! require you to exactly point out where the signature is found. These will just search
14//! by interface member name.
15//!
16//! The macros assume that the file path to the XML files is either:
17//!
18//! - `xml` or `XML`, the default path for `DBus` XML files - or is set by the
19//! - `LOCKSTEP_XML_PATH`, the env variable that overrides the default.
20#![doc(html_root_url = "https://docs.rs/zbus-lockstep/0.7.0")]
21#![allow(clippy::missing_errors_doc)]
22
23mod error;
24mod macros;
25
26use std::{fmt::Write, io::Read, str::FromStr};
27
28use LockstepError::{ArgumentNotFound, InterfaceNotFound, MemberNotFound, PropertyNotFound};
29pub use error::LockstepError;
30pub use macros::resolve_xml_path;
31#[cfg(feature = "macros")]
32pub use zbus_lockstep_macros::validate;
33#[doc(hidden)]
34pub use zbus_xml;
35use zbus_xml::ArgDirection::{In, Out};
36use zvariant::Signature;
37
38type Result<T> = std::result::Result<T, LockstepError>;
39
40#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
41pub enum MsgType {
42 Method,
43 Signal,
44 Property,
45}
46
47/// Retrieve a signal's body type signature from `DBus` XML.
48///
49/// If you provide an argument name, then the signature of that argument is returned.
50/// If you do not provide an argument name, then the signature of all arguments is returned.
51///
52/// # Examples
53///
54/// ```rust
55/// # use std::fs::File;
56/// # use std::io::{Seek, SeekFrom, Write};
57/// # use tempfile::tempfile;
58/// use zvariant::{Signature, Type, OwnedObjectPath};
59/// use zbus_lockstep::get_signal_body_type;
60///
61/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
62/// <node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
63/// <interface name="org.freedesktop.bolt1.Manager">
64/// <signal name="DeviceAdded">
65/// <arg name="device" type="o"/>
66/// </signal>
67/// </interface>
68/// </node>
69/// "#;
70///
71/// let mut xml_file: File = tempfile().unwrap();
72/// xml_file.write_all(xml.as_bytes()).unwrap();
73/// xml_file.seek(SeekFrom::Start(0)).unwrap();
74///
75/// #[derive(Debug, PartialEq, Type)]
76/// #[zvariant(signature = "o")]
77/// struct DeviceEvent {
78/// device: OwnedObjectPath,
79/// }
80///
81/// let interface_name = "org.freedesktop.bolt1.Manager";
82/// let member_name = "DeviceAdded";
83///
84/// let signature = get_signal_body_type(xml_file, interface_name, member_name, None).unwrap();
85///
86/// assert_eq!(&signature, DeviceEvent::SIGNATURE);
87/// ```
88pub fn get_signal_body_type(
89 mut xml: impl Read,
90 interface_name: &str,
91 member_name: &str,
92 arg_name: Option<&str>,
93) -> Result<Signature> {
94 let node = zbus_xml::Node::from_reader(&mut xml)?;
95
96 let interfaces = node.interfaces();
97 let interface = interfaces
98 .iter()
99 .find(|iface| iface.name() == interface_name)
100 .ok_or(InterfaceNotFound(interface_name.to_owned()))?;
101
102 let signals = interface.signals();
103 let signal = signals
104 .iter()
105 .find(|signal| signal.name() == member_name)
106 .ok_or(MemberNotFound(member_name.to_owned()))?;
107
108 let signature: Signature = {
109 if let Some(needle_arg_name) = arg_name {
110 signal
111 .args()
112 .iter()
113 .find(|signal_arg| signal_arg.name() == Some(needle_arg_name))
114 .ok_or(ArgumentNotFound(needle_arg_name.to_owned()))?
115 .ty()
116 .inner()
117 .clone()
118 } else {
119 let mut combined_sig = String::new();
120 for signal_arg in signal.args() {
121 write!(combined_sig, "{}", signal_arg.ty().inner())?;
122 }
123 Signature::from_str(&combined_sig)?
124 }
125 };
126
127 Ok(signature)
128}
129
130/// Retrieve the signature of a property's type from XML.
131///
132/// # Examples
133///
134/// ```rust
135/// use std::fs::File;
136/// use std::io::{Seek, SeekFrom, Write};
137/// use tempfile::tempfile;
138/// use zvariant::Type;
139/// use zbus_lockstep::get_property_type;
140///
141/// #[derive(Debug, PartialEq, Type)]
142/// struct InUse(bool);
143///
144/// let xml = String::from(r#"
145/// <node>
146/// <interface name="org.freedesktop.GeoClue2.Manager">
147/// <property type="b" name="InUse" access="read"/>
148/// </interface>
149/// </node>
150/// "#);
151///
152/// let mut xml_file: File = tempfile().unwrap();
153/// xml_file.write_all(xml.as_bytes()).unwrap();
154/// xml_file.seek(SeekFrom::Start(0)).unwrap();
155///
156/// let interface_name = "org.freedesktop.GeoClue2.Manager";
157/// let property_name = "InUse";
158///
159/// let signature = get_property_type(xml_file, interface_name, property_name).unwrap();
160/// assert_eq!(signature, *InUse::SIGNATURE);
161/// ```
162pub fn get_property_type(
163 mut xml: impl Read,
164 interface_name: &str,
165 property_name: &str,
166) -> Result<Signature> {
167 let node = zbus_xml::Node::from_reader(&mut xml)?;
168
169 let interfaces = node.interfaces();
170 let interface = interfaces
171 .iter()
172 .find(|iface| iface.name() == interface_name)
173 .ok_or(InterfaceNotFound(interface_name.to_string()))?;
174
175 let properties = interface.properties();
176 let property = properties
177 .iter()
178 .find(|property| property.name() == property_name)
179 .ok_or(PropertyNotFound(property_name.to_owned()))?;
180
181 let signature = property.ty().to_string();
182 Ok(Signature::from_str(&signature)?)
183}
184
185/// Retrieve the signature of a method's return type from XML.
186///
187/// If you provide an argument name, then the signature of that argument is returned.
188/// If you do not provide an argument name, then the signature of all arguments is returned.
189///
190///
191/// # Examples
192///
193/// ```rust
194/// use std::fs::File;
195/// use std::io::{Seek, SeekFrom, Write};
196/// use tempfile::tempfile;
197/// use zvariant::Type;
198/// use zbus_lockstep::get_method_return_type;
199///
200/// #[derive(Debug, PartialEq, Type)]
201/// #[repr(u32)]
202/// enum Role {
203/// Invalid,
204/// TitleBar,
205/// MenuBar,
206/// ScrollBar,
207/// }
208///
209/// let xml = String::from(r#"
210/// <node>
211/// <interface name="org.a11y.atspi.Accessible">
212/// <method name="GetRole">
213/// <arg name="role" type="u" direction="out"/>
214/// </method>
215/// </interface>
216/// </node>
217/// "#);
218///
219/// let mut xml_file: File = tempfile().unwrap();
220/// xml_file.write_all(xml.as_bytes()).unwrap();
221/// xml_file.seek(SeekFrom::Start(0)).unwrap();
222///
223/// let interface_name = "org.a11y.atspi.Accessible";
224/// let member_name = "GetRole";
225///
226/// let signature = get_method_return_type(xml_file, interface_name, member_name, None).unwrap();
227/// assert_eq!(signature, *Role::SIGNATURE);
228/// ```
229///
230/// ## Argument name collisions
231///
232/// If multiple arguments share the same name within the same direction,
233/// the first matching argument's signature is returned.
234pub fn get_method_return_type(
235 mut xml: impl Read,
236 interface_name: &str,
237 member_name: &str,
238 arg_name: Option<&str>,
239) -> Result<Signature> {
240 let node = zbus_xml::Node::from_reader(&mut xml)?;
241
242 let interfaces = node.interfaces();
243 let interface = interfaces
244 .iter()
245 .find(|iface| iface.name() == interface_name)
246 .ok_or(InterfaceNotFound(interface_name.to_string()))?;
247
248 let methods = interface.methods();
249 let method = methods
250 .iter()
251 .find(|method| method.name() == member_name)
252 .ok_or(MemberNotFound(member_name.to_string()))?;
253
254 let args = method.args();
255
256 let signature: Signature = {
257 if let Some(needle_arg_name) = arg_name {
258 args.iter()
259 .find(|arg| arg.name() == Some(needle_arg_name) && arg.direction() == Some(Out))
260 .ok_or(ArgumentNotFound(needle_arg_name.to_string()))?
261 .ty()
262 .inner()
263 .clone()
264 } else {
265 let mut combined_sig = String::new();
266 for arg in args.iter().filter(|arg| arg.direction() == Some(Out)) {
267 write!(combined_sig, "{}", arg.ty().inner())?;
268 }
269 Signature::from_str(&combined_sig)?
270 }
271 };
272
273 Ok(signature)
274}
275
276/// Retrieve the signature of a method's argument type from XML.
277///
278/// Useful when one or more arguments, used to call a method, outline a useful type.
279///
280/// If you provide an argument name, then the signature of that argument is returned.
281/// If you do not provide an argument name, then the signature of all arguments to the call is
282/// returned.
283///
284/// # Examples
285///
286/// ```rust
287/// use std::fs::File;
288/// use std::collections::HashMap;
289/// use std::io::{Seek, SeekFrom, Write};
290/// use tempfile::tempfile;
291/// use zvariant::{Type, Value};
292/// use zbus_lockstep::get_method_args_type;
293///
294/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
295/// <node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
296/// <interface name="org.freedesktop.Notifications">
297/// <method name="Notify">
298/// <arg type="s" name="app_name" direction="in"/>
299/// <arg type="u" name="replaces_id" direction="in"/>
300/// <arg type="s" name="app_icon" direction="in"/>
301/// <arg type="s" name="summary" direction="in"/>
302/// <arg type="s" name="body" direction="in"/>
303/// <arg type="as" name="actions" direction="in"/>
304/// <arg type="a{sv}" name="hints" direction="in"/>
305/// <arg type="i" name="expire_timeout" direction="in"/>
306/// <arg type="u" name="id" direction="out"/>
307/// </method>
308/// </interface>
309/// </node>
310/// "#;
311///
312/// #[derive(Debug, PartialEq, Type)]
313/// struct Notification<'a> {
314/// app_name: String,
315/// replaces_id: u32,
316/// app_icon: String,
317/// summary: String,
318/// body: String,
319/// actions: Vec<String>,
320/// hints: HashMap<String, Value<'a>>,
321/// expire_timeout: i32,
322/// }
323///
324/// let mut xml_file = tempfile().unwrap();
325/// xml_file.write_all(xml.as_bytes()).unwrap();
326/// xml_file.seek(SeekFrom::Start(0)).unwrap();
327///
328/// let interface_name = "org.freedesktop.Notifications";
329/// let member_name = "Notify";
330///
331/// let signature = get_method_args_type(xml_file, interface_name, member_name, None).unwrap();
332/// assert_eq!(&signature, Notification::SIGNATURE);
333/// ```
334///
335/// ## Argument name collisions
336/// If multiple arguments share the same name within the same direction,
337/// the first matching argument's signature is returned.
338pub fn get_method_args_type(
339 mut xml: impl Read,
340 interface_name: &str,
341 member_name: &str,
342 arg_name: Option<&str>,
343) -> Result<Signature> {
344 let node = zbus_xml::Node::from_reader(&mut xml)?;
345
346 let interfaces = node.interfaces();
347 let interface = interfaces
348 .iter()
349 .find(|iface| iface.name() == interface_name)
350 .ok_or(InterfaceNotFound(interface_name.to_owned()))?;
351
352 let methods = interface.methods();
353 let method = methods
354 .iter()
355 .find(|method| method.name() == member_name)
356 .ok_or(MemberNotFound(member_name.to_owned()))?;
357
358 let args = method.args();
359
360 let signature: Signature = if let Some(needle_arg_name) = arg_name {
361 args.iter()
362 .find(|method_arg| {
363 method_arg.name() == Some(needle_arg_name) && method_arg.direction() == Some(In)
364 })
365 .ok_or(ArgumentNotFound(needle_arg_name.to_string()))?
366 .ty()
367 .inner()
368 .clone()
369 } else {
370 let mut combined_sig = String::new();
371 for arg in args.iter().filter(|arg| arg.direction() == Some(In)) {
372 write!(combined_sig, "{}", arg.ty().inner())?;
373 }
374 Signature::from_str(&combined_sig)?
375 };
376
377 Ok(signature)
378}
379
380#[cfg(test)]
381mod test {
382 use std::io::{Seek, SeekFrom, Write};
383
384 use tempfile::tempfile;
385 use zvariant::{OwnedObjectPath, Type};
386
387 use crate::{get_method_args_type, get_method_return_type, get_signal_body_type};
388
389 // Introspection format provides no guarantees that argument names are unique.
390 // Even same name, same direction could occur but this would be poor D-Bus API design.
391 // zbus-lockstep will filter by appropriate direction and pick the first match.
392 //
393 // https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format
394 #[test]
395 fn test_overlapping_names_return_type() {
396 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
397 <node>
398 <interface name="org.example.Calculator">
399 <method name="Calculate">
400 <arg name="data" type="s" direction="in"/>
401 <arg name="data" type="ai" direction="out"/>
402 </method>
403 </interface>
404 </node>
405 "#;
406
407 let mut xml_file = tempfile().unwrap();
408 xml_file.write_all(xml.as_bytes()).unwrap();
409 xml_file.seek(SeekFrom::Start(0)).unwrap();
410
411 let sig = get_method_return_type(
412 xml_file,
413 "org.example.Calculator",
414 "Calculate",
415 Some("data"),
416 )
417 .unwrap();
418
419 assert_eq!(&sig.to_string(), "ai");
420 }
421
422 #[test]
423 fn test_overlapping_names_args_type_swapped() {
424 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
425 <node>
426 <interface name="org.example.Calculator">
427 <method name="Calculate">
428 <arg name="data" type="ai" direction="out"/>
429 <arg name="data" type="s" direction="in"/>
430 </method>
431 </interface>
432 </node>
433 "#;
434
435 let mut xml_file = tempfile().unwrap();
436 xml_file.write_all(xml.as_bytes()).unwrap();
437 xml_file.seek(SeekFrom::Start(0)).unwrap();
438
439 let sig = get_method_args_type(
440 xml_file,
441 "org.example.Calculator",
442 "Calculate",
443 Some("data"),
444 )
445 .unwrap();
446
447 assert_eq!(&sig.to_string(), "s");
448 }
449
450 #[test]
451 fn test_get_signature_of_cache_add_accessible() {
452 #[derive(Debug, PartialEq, Type)]
453 struct Accessible {
454 name: String,
455 path: OwnedObjectPath,
456 }
457
458 #[derive(Debug, PartialEq, Type)]
459 struct CacheItem {
460 obj: Accessible,
461 application: Accessible,
462 parent: Accessible,
463 index_in_parent: i32,
464 child_count: i32,
465 interfaces: Vec<String>,
466 name: String,
467 role: u32,
468 description: String,
469 state_set: Vec<u32>,
470 }
471
472 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
473 <node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
474 <interface name="org.a11y.atspi.Cache">
475 <signal name="AddAccessible">
476 <arg name="nodeAdded" type="((so)(so)(so)iiassusau)"/>
477 <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QSpiAccessibleCacheItem"/>
478 </signal>
479 </interface>
480 </node>
481 "#;
482
483 let mut xml_file = tempfile().unwrap();
484 xml_file.write_all(xml.as_bytes()).unwrap();
485 xml_file.seek(SeekFrom::Start(0)).unwrap();
486
487 let interface_name = "org.a11y.atspi.Cache";
488 let member_name = "AddAccessible";
489
490 let signature = get_signal_body_type(xml_file, interface_name, member_name, None).unwrap();
491 assert_eq!(signature, *CacheItem::SIGNATURE);
492 }
493}