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/master/ashpd-demo/data/icons/com.belmoussaoui.ashpd.demo.svg",
6 html_favicon_url = "https://raw.githubusercontent.com/bilelmoussaoui/ashpd/master/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-std"), not(doc)))]
10compile_error!("You can't enable both async-std & tokio features at once");
11#[cfg(all(not(feature = "tokio"), not(feature = "async-std"), not(doc)))]
12compile_error!("Either the `async-std` 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;
23pub mod documents;
25mod error;
26mod window_identifier;
27
28pub use self::{activation_token::ActivationToken, window_identifier::WindowIdentifier};
29mod app_id;
30mod registry;
31pub use self::{app_id::AppID, registry::register_host_app};
32mod file_path;
33pub use self::file_path::FilePath;
34
35mod proxy;
36
37#[cfg(feature = "backend")]
38#[cfg_attr(docsrs, doc(cfg(feature = "backend")))]
39pub use self::window_identifier::WindowIdentifierType;
40#[cfg(feature = "backend")]
41#[cfg_attr(docsrs, doc(cfg(feature = "backend")))]
42#[allow(missing_docs)]
43pub mod backend;
45pub mod flatpak;
48mod helpers;
49use std::sync::OnceLock;
50
51#[cfg(feature = "backend")]
52#[cfg_attr(docsrs, doc(cfg(feature = "backend")))]
53pub use async_trait;
54pub use enumflags2;
55pub use url;
56pub use zbus::{self, zvariant};
57
58pub async fn is_sandboxed() -> bool {
65 if let Some(cached_value) = IS_SANDBOXED.get() {
66 return *cached_value;
67 }
68 let new_value = crate::helpers::is_flatpak().await
69 || crate::helpers::is_snap().await
70 || std::env::var("GTK_USE_PORTAL")
71 .map(|v| v == "1")
72 .unwrap_or(false);
73
74 *IS_SANDBOXED.get_or_init(|| new_value)
75}
76
77pub use self::error::{Error, PortalError};
78
79mod sealed {
80 pub trait Sealed {}
83}
84
85pub(crate) use sealed::Sealed;
86
87pub type Pid = u32;
91
92#[cfg(test)]
93mod tests {
94 use std::{collections::HashMap, fs, path::PathBuf};
95
96 use quick_xml::{events::Event, Reader};
97
98 fn pascal_to_snake_case(s: &str) -> String {
100 let mut result = String::new();
101 for (i, c) in s.chars().enumerate() {
102 if c.is_ascii_uppercase() {
103 if i > 0 {
104 result.push('_');
105 }
106 result.push(c.to_ascii_lowercase());
107 } else {
108 result.push(c);
109 }
110 }
111 result
112 }
113
114 fn extract_names_from_xml(xml_content: &str) -> HashMap<String, Vec<String>> {
115 let mut interfaces = HashMap::new();
116 let mut reader = Reader::from_str(xml_content);
117 reader.config_mut().trim_text(true);
118 let mut buf = Vec::new();
119 let mut current_interface_name = String::new();
120 let mut current_names = Vec::new();
121
122 loop {
123 match reader.read_event_into(&mut buf) {
124 Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
125 Ok(Event::Eof) => break,
126 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
127 if let Some(Ok(attr)) = e
129 .attributes()
130 .find(|a| a.as_ref().map_or(false, |a| a.key.as_ref() == b"name"))
131 {
132 if let Ok(value) = attr.decode_and_unescape_value(reader.decoder()) {
133 match e.name().as_ref() {
134 b"interface" => {
135 current_interface_name = value.to_string();
136 current_names.clear();
137 }
138 b"method" | b"property" | b"signal" => {
139 if value != "version" {
140 current_names.push(value.to_string());
141 }
142 }
143 _ => (),
144 }
145 }
146 }
147 }
148 Ok(Event::End(e)) => {
149 if e.name().as_ref() == b"interface" {
150 interfaces.insert(current_interface_name.clone(), current_names.clone());
151 }
152 }
153 _ => (),
154 }
155 buf.clear();
156 }
157 interfaces
158 }
159
160 struct TestConfig {
161 interfaces_dir: PathBuf,
162 rust_src_prefix: &'static str,
163 rust_file_mappings: HashMap<&'static str, &'static str>,
164 ignored_interfaces: &'static [&'static str],
165 interface_prefix: &'static str,
166 }
167
168 fn check_doc_aliases_for_config(config: TestConfig) {
169 assert!(
170 config.interfaces_dir.exists(),
171 "Interfaces directory not found at {}",
172 config.interfaces_dir.display()
173 );
174
175 let entries =
176 fs::read_dir(&config.interfaces_dir).expect("Failed to read interfaces directory");
177
178 for entry in entries.filter_map(|e| e.ok()) {
179 let path = entry.path();
180 if path.is_file() && path.extension().map_or(false, |ext| ext == "xml") {
181 println!("Checking XML file: {}", path.display());
182
183 let xml_content = fs::read_to_string(&path).unwrap();
184 let interfaces = extract_names_from_xml(&xml_content);
185
186 for (interface_name, names_to_check) in interfaces {
187 let interface_name_suffix = interface_name
189 .strip_prefix(config.interface_prefix)
190 .expect("Interface name does not have the expected prefix.");
191
192 if config.ignored_interfaces.contains(&interface_name_suffix) {
193 continue;
194 }
195
196 let rust_path = if let Some(mapped_path) =
197 config.rust_file_mappings.get(interface_name.as_str())
198 {
199 PathBuf::from(mapped_path)
200 } else {
201 let rust_file_name_snake = pascal_to_snake_case(interface_name_suffix);
202 PathBuf::from(format!(
203 "{}{}.rs",
204 config.rust_src_prefix, rust_file_name_snake
205 ))
206 };
207
208 assert!(
210 rust_path.exists(),
211 "Corresponding Rust file not found for interface '{}' at {}",
212 interface_name,
213 rust_path.display()
214 );
215
216 let rust_content = fs::read_to_string(&rust_path).unwrap();
218
219 for name in &names_to_check {
221 let alias_str = format!("#[doc(alias = \"{}\")]", name);
222 assert!(
223 rust_content.contains(&alias_str),
224 "Missing doc alias '{}' for interface '{}' in file {}",
225 alias_str,
226 interface_name,
227 rust_path.display()
228 );
229 }
230 }
231 }
232 }
233 }
234
235 #[cfg(feature = "backend")]
236 #[test]
237 fn all_interfaces_have_backend_implementations() {
238 let rust_file_mappings: HashMap<&str, &str> = HashMap::from([(
239 "org.freedesktop.impl.portal.ScreenCast",
240 "src/backend/screencast.rs",
241 )]);
242
243 const IGNORED_BACKEND_PORTALS: &[&str; 7] = &[
244 "Clipboard",
245 "DynamicLauncher",
246 "GlobalShortcuts",
247 "Inhibit",
248 "InputCapture",
249 "Notification",
250 "RemoteDesktop",
251 ];
252
253 let config = TestConfig {
254 interfaces_dir: PathBuf::from("./interfaces/backend"),
255 rust_src_prefix: "src/backend/",
256 rust_file_mappings,
257 ignored_interfaces: IGNORED_BACKEND_PORTALS,
258 interface_prefix: "org.freedesktop.impl.portal.",
259 };
260
261 check_doc_aliases_for_config(config);
262 }
263
264 #[test]
265 fn all_interfaces_have_implementations() {
266 let rust_file_mappings: HashMap<&str, &str> = HashMap::from([
267 (
268 "org.freedesktop.portal.ScreenCast",
269 "src/desktop/screencast.rs",
270 ),
271 ("org.freedesktop.portal.OpenURI", "src/desktop/open_uri.rs"),
272 (
273 "org.freedesktop.portal.FileTransfer",
274 "src/documents/file_transfer.rs",
275 ),
276 ("org.freedesktop.portal.Documents", "src/documents/mod.rs"),
277 ("org.freedesktop.portal.Flatpak", "src/flatpak/mod.rs"),
278 (
279 "org.freedesktop.portal.Flatpak.UpdateMonitor",
280 "src/flatpak/update_monitor.rs",
281 ),
282 ]);
283
284 const NO_IGNORED_INTERFACES: &[&str; 0] = &[];
285
286 let config = TestConfig {
287 interfaces_dir: PathBuf::from("./interfaces"),
288 rust_src_prefix: "src/desktop/",
289 rust_file_mappings,
290 ignored_interfaces: NO_IGNORED_INTERFACES,
291 interface_prefix: "org.freedesktop.portal.",
292 };
293
294 check_doc_aliases_for_config(config);
295 }
296}