Skip to main content

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}