Skip to main content

ashpd/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![deny(rustdoc::broken_intra_doc_links)]
3#![deny(missing_docs)]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/bilelmoussaoui/ashpd/main/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/bilelmoussaoui/ashpd/main/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo-symbolic.svg"
7)]
8#![doc = include_str!("../README.md")]
9#[cfg(all(all(feature = "tokio", feature = "async-io"), not(doc)))]
10compile_error!("You can't enable both async-io & tokio features at once");
11#[cfg(all(not(feature = "tokio"), not(feature = "async-io"), not(doc)))]
12compile_error!("Either the `async-io` or the `tokio` feature has to be enabled");
13
14/// Alias for a [`Result`] with the error type `ashpd::Error`.
15pub type Result<T> = std::result::Result<T, Error>;
16
17static IS_SANDBOXED: OnceLock<bool> = OnceLock::new();
18
19mod activation_token;
20/// Interact with the user's desktop such as taking a screenshot, setting a
21/// background or querying the user's location.
22pub mod desktop;
23/// Interact with the documents store or transfer files across apps.
24#[cfg(feature = "documents")]
25#[cfg_attr(docsrs, doc(cfg(feature = "documents")))]
26pub mod documents;
27mod error;
28mod window_identifier;
29
30pub use self::{activation_token::ActivationToken, window_identifier::WindowIdentifier};
31mod app_id;
32mod registry;
33mod uri;
34pub use self::{
35    app_id::AppID,
36    registry::{register_host_app, register_host_app_with_connection},
37    uri::Uri,
38};
39mod file_path;
40pub use self::file_path::FilePath;
41
42mod proxy;
43
44pub use self::window_identifier::WindowIdentifierType;
45#[cfg(feature = "backend")]
46#[cfg_attr(docsrs, doc(cfg(feature = "backend")))]
47#[allow(missing_docs)]
48/// Build your custom portals backend.
49pub mod backend;
50/// Spawn commands outside the sandbox or monitor if the running application has
51/// received an update & install it.
52#[cfg(feature = "flatpak")]
53#[cfg_attr(docsrs, doc(cfg(feature = "flatpak")))]
54pub mod flatpak;
55mod helpers;
56use std::sync::OnceLock;
57
58#[cfg(feature = "backend")]
59#[cfg_attr(docsrs, doc(cfg(feature = "backend")))]
60pub use async_trait;
61pub use enumflags2;
62pub use zbus::{self, zvariant};
63
64/// Check whether the application is running inside a sandbox.
65///
66/// The function checks whether the file `/.flatpak-info` exists, or if the app
67/// is running as a snap, or if the environment variable `GTK_USE_PORTAL` is set
68/// to `1`. As the return value of this function will not change during the
69/// runtime of a program; it is cached for future calls.
70pub fn is_sandboxed() -> bool {
71    if let Some(cached_value) = IS_SANDBOXED.get() {
72        return *cached_value;
73    }
74    let new_value = crate::helpers::is_flatpak() || crate::helpers::is_snap();
75
76    *IS_SANDBOXED.get_or_init(|| new_value)
77}
78
79pub use self::error::{Error, PortalError};
80
81mod sealed {
82    /// Use as a supertrait for public traits that users should not be able to
83    /// implement
84    pub trait Sealed {}
85}
86
87pub(crate) use sealed::Sealed;
88
89/// Process ID.
90///
91/// Matches the type used in std.
92pub type Pid = u32;
93
94#[cfg(test)]
95mod tests {
96    use std::{collections::HashMap, fs, path::PathBuf};
97
98    use quick_xml::{Reader, events::Event};
99
100    // Helper to convert PascalCase to snake_case
101    fn pascal_to_snake_case(s: &str) -> String {
102        let mut result = String::new();
103        for (i, c) in s.chars().enumerate() {
104            if c.is_ascii_uppercase() {
105                if i > 0 {
106                    result.push('_');
107                }
108                result.push(c.to_ascii_lowercase());
109            } else {
110                result.push(c);
111            }
112        }
113        result
114    }
115
116    fn extract_names_from_xml(xml_content: &str) -> HashMap<String, Vec<String>> {
117        let mut interfaces = HashMap::new();
118        let mut reader = Reader::from_str(xml_content);
119        reader.config_mut().trim_text(true);
120        let mut buf = Vec::new();
121        let mut current_interface_name = String::new();
122        let mut current_names = Vec::new();
123
124        loop {
125            match reader.read_event_into(&mut buf) {
126                Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
127                Ok(Event::Eof) => break,
128                Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
129                    // Handle both Start and Empty events
130                    if let Some(Ok(attr)) = e
131                        .attributes()
132                        .find(|a| a.as_ref().is_ok_and(|a| a.key.as_ref() == b"name"))
133                        && let Ok(value) = attr.decode_and_unescape_value(reader.decoder())
134                    {
135                        match e.name().as_ref() {
136                            b"interface" => {
137                                current_interface_name = value.to_string();
138                                current_names.clear();
139                            }
140                            b"method" | b"property" | b"signal" => {
141                                if value != "version" {
142                                    current_names.push(value.to_string());
143                                }
144                            }
145                            _ => (),
146                        }
147                    }
148                }
149                Ok(Event::End(e)) => {
150                    if e.name().as_ref() == b"interface" {
151                        interfaces.insert(current_interface_name.clone(), current_names.clone());
152                    }
153                }
154                _ => (),
155            }
156            buf.clear();
157        }
158        interfaces
159    }
160
161    struct TestConfig {
162        interfaces_dir: PathBuf,
163        rust_src_prefix: &'static str,
164        rust_file_mappings: HashMap<&'static str, &'static str>,
165        ignored_interfaces: &'static [&'static str],
166        interface_prefix: &'static str,
167    }
168
169    fn check_doc_aliases_for_config(config: TestConfig) {
170        assert!(
171            config.interfaces_dir.exists(),
172            "Interfaces directory not found at {}",
173            config.interfaces_dir.display()
174        );
175
176        let entries =
177            fs::read_dir(&config.interfaces_dir).expect("Failed to read interfaces directory");
178
179        for entry in entries.filter_map(|e| e.ok()) {
180            let path = entry.path();
181            if path.is_file() && path.extension().is_some_and(|ext| ext == "xml") {
182                println!("Checking XML file: {}", path.display());
183
184                let xml_content = fs::read_to_string(&path).unwrap();
185                let interfaces = extract_names_from_xml(&xml_content);
186
187                for (interface_name, names_to_check) in interfaces {
188                    // Map the D-Bus interface name to the corresponding Rust file path
189                    let interface_name_suffix = interface_name
190                        .strip_prefix(config.interface_prefix)
191                        .expect("Interface name does not have the expected prefix.");
192
193                    if config.ignored_interfaces.contains(&interface_name_suffix) {
194                        continue;
195                    }
196
197                    let rust_path = if let Some(mapped_path) =
198                        config.rust_file_mappings.get(interface_name.as_str())
199                    {
200                        PathBuf::from(mapped_path)
201                    } else {
202                        let rust_file_name_snake = pascal_to_snake_case(interface_name_suffix);
203                        PathBuf::from(format!(
204                            "{}{}.rs",
205                            config.rust_src_prefix, rust_file_name_snake
206                        ))
207                    };
208
209                    // Check if the Rust file exists
210                    assert!(
211                        rust_path.exists(),
212                        "Corresponding Rust file not found for interface '{}' at {}",
213                        interface_name,
214                        rust_path.display()
215                    );
216
217                    // Read the Rust file content
218                    let rust_content = fs::read_to_string(&rust_path).unwrap();
219
220                    // Assert that each name has a corresponding doc alias
221                    for name in &names_to_check {
222                        let alias_str = format!("#[doc(alias = \"{}\")]", name);
223                        assert!(
224                            rust_content.contains(&alias_str),
225                            "Missing doc alias '{}' for interface '{}' in file {}",
226                            alias_str,
227                            interface_name,
228                            rust_path.display()
229                        );
230                    }
231                }
232            }
233        }
234    }
235
236    #[cfg(feature = "backend")]
237    #[test]
238    fn all_interfaces_have_backend_implementations() {
239        let rust_file_mappings: HashMap<&str, &str> = HashMap::from([(
240            "org.freedesktop.impl.portal.ScreenCast",
241            "src/backend/screencast.rs",
242        )]);
243
244        const IGNORED_BACKEND_PORTALS: &[&str; 7] = &[
245            "Clipboard",
246            "DynamicLauncher",
247            "GlobalShortcuts",
248            "Inhibit",
249            "InputCapture",
250            "Notification",
251            "RemoteDesktop",
252        ];
253
254        let config = TestConfig {
255            interfaces_dir: PathBuf::from("./../interfaces/backend"),
256            rust_src_prefix: "src/backend/",
257            rust_file_mappings,
258            ignored_interfaces: IGNORED_BACKEND_PORTALS,
259            interface_prefix: "org.freedesktop.impl.portal.",
260        };
261
262        check_doc_aliases_for_config(config);
263    }
264
265    #[test]
266    fn all_interfaces_have_implementations() {
267        let rust_file_mappings: HashMap<&str, &str> = HashMap::from([
268            (
269                "org.freedesktop.portal.ScreenCast",
270                "src/desktop/screencast.rs",
271            ),
272            ("org.freedesktop.portal.OpenURI", "src/desktop/open_uri.rs"),
273            (
274                "org.freedesktop.portal.FileTransfer",
275                "src/documents/file_transfer.rs",
276            ),
277            ("org.freedesktop.portal.Documents", "src/documents/mod.rs"),
278            ("org.freedesktop.portal.Flatpak", "src/flatpak/mod.rs"),
279            (
280                "org.freedesktop.portal.Flatpak.UpdateMonitor",
281                "src/flatpak/update_monitor.rs",
282            ),
283        ]);
284
285        const NO_IGNORED_INTERFACES: &[&str; 0] = &[];
286
287        let config = TestConfig {
288            interfaces_dir: PathBuf::from("./../interfaces"),
289            rust_src_prefix: "src/desktop/",
290            rust_file_mappings,
291            ignored_interfaces: NO_IGNORED_INTERFACES,
292            interface_prefix: "org.freedesktop.portal.",
293        };
294
295        check_doc_aliases_for_config(config);
296    }
297}