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
14pub type Result<T> = std::result::Result<T, Error>;
16
17static IS_SANDBOXED: OnceLock<bool> = OnceLock::new();
18
19mod activation_token;
20pub mod desktop;
23#[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)]
48pub mod backend;
50#[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
64pub 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 pub trait Sealed {}
85}
86
87pub(crate) use sealed::Sealed;
88
89pub 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 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 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 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 assert!(
211 rust_path.exists(),
212 "Corresponding Rust file not found for interface '{}' at {}",
213 interface_name,
214 rust_path.display()
215 );
216
217 let rust_content = fs::read_to_string(&rust_path).unwrap();
219
220 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}