gardesk/garwarp / 1fd4b17

Browse files

add portal request dispatcher

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
1fd4b176164fa70604ca00a916122a281279a2f7
Parents
cbfcc1e
Tree
f1485f5

3 changed files

StatusFile+-
M garwarp/src/dbus.rs 140 32
M garwarp/src/main.rs 1 0
A garwarp/src/portal_dispatch.rs 191 0
garwarp/src/dbus.rsmodified
@@ -1,4 +1,7 @@
1
-use std::collections::HashMap;
1
+use std::{
2
+    collections::HashMap,
3
+    sync::{Arc, Mutex},
4
+};
25
 
36
 use zbus::{
47
     blocking::Connection,
@@ -7,8 +10,10 @@ use zbus::{
710
     zvariant::{OwnedObjectPath, OwnedValue},
811
 };
912
 
13
+use crate::config::Config;
1014
 use crate::error::{PortalError, map_portal_error};
1115
 use crate::portal::derive_request_id_from_handle;
16
+use crate::portal_dispatch::PortalDispatch;
1217
 use crate::portal_options::{
1318
     parse_app_chooser_options, parse_file_chooser_options, parse_screenshot_options,
1419
 };
@@ -27,16 +32,24 @@ fn failure_response(error: &PortalError) -> PortalMethodReply {
2732
     (mapping.code as u32, HashMap::new())
2833
 }
2934
 
30
-fn request_id_for_call(
35
+fn request_identity_for_call(
3136
     request_handle: &OwnedObjectPath,
32
-    header: Header<'_>,
33
-) -> Result<String, PortalError> {
37
+    header: &Header<'_>,
38
+) -> Result<(String, String), PortalError> {
3439
     let sender = header
3540
         .sender()
36
-        .map(|sender| sender.as_str())
41
+        .map(|sender| sender.as_str().to_string())
3742
         .ok_or(PortalError::UnauthorizedClient)?;
43
+    let request_id = derive_request_id_from_handle(&sender, request_handle.as_str())?;
44
+    Ok((sender, request_id))
45
+}
3846
 
39
-    derive_request_id_from_handle(sender, request_handle.as_str())
47
+fn with_dispatch<T>(
48
+    dispatch: &Arc<Mutex<PortalDispatch>>,
49
+    callback: impl FnOnce(&mut PortalDispatch) -> Result<T, PortalError>,
50
+) -> Result<T, PortalError> {
51
+    let mut dispatch = dispatch.lock().map_err(|_| PortalError::InternalFailure)?;
52
+    callback(&mut dispatch)
4053
 }
4154
 
4255
 pub struct SessionNameGuard {
@@ -45,13 +58,22 @@ pub struct SessionNameGuard {
4558
 
4659
 impl SessionNameGuard {
4760
     pub fn acquire() -> zbus::Result<Self> {
61
+        let dispatch = Arc::new(Mutex::new(PortalDispatch::new(
62
+            Config::from_env().request_timeout,
63
+        )));
4864
         let connection = Connection::session()?;
4965
         connection.request_name(BACKEND_DBUS_NAME)?;
5066
         {
5167
             let object_server = connection.object_server();
52
-            object_server.at(BACKEND_OBJECT_PATH, ScreenshotPortal)?;
53
-            object_server.at(BACKEND_OBJECT_PATH, FileChooserPortal)?;
54
-            object_server.at(BACKEND_OBJECT_PATH, AppChooserPortal)?;
68
+            object_server.at(
69
+                BACKEND_OBJECT_PATH,
70
+                ScreenshotPortal::new(Arc::clone(&dispatch)),
71
+            )?;
72
+            object_server.at(
73
+                BACKEND_OBJECT_PATH,
74
+                FileChooserPortal::new(Arc::clone(&dispatch)),
75
+            )?;
76
+            object_server.at(BACKEND_OBJECT_PATH, AppChooserPortal::new(dispatch))?;
5577
         }
5678
         Ok(Self {
5779
             _connection: connection,
@@ -60,7 +82,15 @@ impl SessionNameGuard {
6082
 }
6183
 
6284
 #[derive(Debug)]
63
-struct ScreenshotPortal;
85
+struct ScreenshotPortal {
86
+    dispatch: Arc<Mutex<PortalDispatch>>,
87
+}
88
+
89
+impl ScreenshotPortal {
90
+    fn new(dispatch: Arc<Mutex<PortalDispatch>>) -> Self {
91
+        Self { dispatch }
92
+    }
93
+}
6494
 
6595
 #[interface(name = "org.freedesktop.impl.portal.Screenshot")]
6696
 impl ScreenshotPortal {
@@ -75,7 +105,13 @@ impl ScreenshotPortal {
75105
         if let Err(error) = parse_screenshot_options(&_options) {
76106
             return failure_response(&error);
77107
         }
78
-        if let Err(error) = request_id_for_call(&handle, header) {
108
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
109
+            Ok(identity) => identity,
110
+            Err(error) => return failure_response(&error),
111
+        };
112
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
113
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
114
+        }) {
79115
             return failure_response(&error);
80116
         }
81117
         failed_response()
@@ -92,7 +128,13 @@ impl ScreenshotPortal {
92128
         if let Err(error) = parse_screenshot_options(&_options) {
93129
             return failure_response(&error);
94130
         }
95
-        if let Err(error) = request_id_for_call(&handle, header) {
131
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
132
+            Ok(identity) => identity,
133
+            Err(error) => return failure_response(&error),
134
+        };
135
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
136
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
137
+        }) {
96138
             return failure_response(&error);
97139
         }
98140
         failed_response()
@@ -105,7 +147,15 @@ impl ScreenshotPortal {
105147
 }
106148
 
107149
 #[derive(Debug)]
108
-struct FileChooserPortal;
150
+struct FileChooserPortal {
151
+    dispatch: Arc<Mutex<PortalDispatch>>,
152
+}
153
+
154
+impl FileChooserPortal {
155
+    fn new(dispatch: Arc<Mutex<PortalDispatch>>) -> Self {
156
+        Self { dispatch }
157
+    }
158
+}
109159
 
110160
 #[interface(name = "org.freedesktop.impl.portal.FileChooser")]
111161
 impl FileChooserPortal {
@@ -121,7 +171,13 @@ impl FileChooserPortal {
121171
         if let Err(error) = parse_file_chooser_options(&_options) {
122172
             return failure_response(&error);
123173
         }
124
-        if let Err(error) = request_id_for_call(&handle, header) {
174
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
175
+            Ok(identity) => identity,
176
+            Err(error) => return failure_response(&error),
177
+        };
178
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
179
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
180
+        }) {
125181
             return failure_response(&error);
126182
         }
127183
         failed_response()
@@ -139,7 +195,13 @@ impl FileChooserPortal {
139195
         if let Err(error) = parse_file_chooser_options(&_options) {
140196
             return failure_response(&error);
141197
         }
142
-        if let Err(error) = request_id_for_call(&handle, header) {
198
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
199
+            Ok(identity) => identity,
200
+            Err(error) => return failure_response(&error),
201
+        };
202
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
203
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
204
+        }) {
143205
             return failure_response(&error);
144206
         }
145207
         failed_response()
@@ -157,7 +219,13 @@ impl FileChooserPortal {
157219
         if let Err(error) = parse_file_chooser_options(&_options) {
158220
             return failure_response(&error);
159221
         }
160
-        if let Err(error) = request_id_for_call(&handle, header) {
222
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
223
+            Ok(identity) => identity,
224
+            Err(error) => return failure_response(&error),
225
+        };
226
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
227
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
228
+        }) {
161229
             return failure_response(&error);
162230
         }
163231
         failed_response()
@@ -170,7 +238,15 @@ impl FileChooserPortal {
170238
 }
171239
 
172240
 #[derive(Debug)]
173
-struct AppChooserPortal;
241
+struct AppChooserPortal {
242
+    dispatch: Arc<Mutex<PortalDispatch>>,
243
+}
244
+
245
+impl AppChooserPortal {
246
+    fn new(dispatch: Arc<Mutex<PortalDispatch>>) -> Self {
247
+        Self { dispatch }
248
+    }
249
+}
174250
 
175251
 #[interface(name = "org.freedesktop.impl.portal.AppChooser")]
176252
 impl AppChooserPortal {
@@ -186,7 +262,13 @@ impl AppChooserPortal {
186262
         if let Err(error) = parse_app_chooser_options(&_options) {
187263
             return failure_response(&error);
188264
         }
189
-        if let Err(error) = request_id_for_call(&handle, header) {
265
+        let (sender, request_id) = match request_identity_for_call(&handle, &header) {
266
+            Ok(identity) => identity,
267
+            Err(error) => return failure_response(&error),
268
+        };
269
+        if let Err(error) = with_dispatch(&self.dispatch, |dispatch| {
270
+            dispatch.register_unimplemented_call(&request_id, &sender, _app_id, _parent_window)
271
+        }) {
190272
             return failure_response(&error);
191273
         }
192274
         failed_response()
@@ -198,9 +280,14 @@ impl AppChooserPortal {
198280
         _choices: Vec<String>,
199281
         #[zbus(header)] header: Header<'_>,
200282
     ) -> zbus::fdo::Result<()> {
201
-        request_id_for_call(&handle, header)
202
-            .map(|_| ())
203
-            .map_err(|error| zbus::fdo::Error::Failed(map_portal_error(&error).reason.to_string()))
283
+        let (sender, request_id) =
284
+            request_identity_for_call(&handle, &header).map_err(|error| {
285
+                zbus::fdo::Error::Failed(map_portal_error(&error).reason.to_string())
286
+            })?;
287
+        with_dispatch(&self.dispatch, |dispatch| {
288
+            dispatch.validate_update_choices(&request_id, &sender)
289
+        })
290
+        .map_err(|error| zbus::fdo::Error::Failed(map_portal_error(&error).reason.to_string()))
204291
     }
205292
 
206293
     #[zbus(property)]
@@ -211,13 +298,18 @@ impl AppChooserPortal {
211298
 
212299
 #[cfg(test)]
213300
 mod tests {
214
-    use std::collections::HashMap;
301
+    use std::{
302
+        collections::HashMap,
303
+        sync::{Arc, Mutex},
304
+        time::Duration,
305
+    };
215306
 
216307
     use super::{
217308
         AppChooserPortal, BACKEND_OBJECT_PATH, FileChooserPortal, INTERFACE_VERSION,
218309
         ScreenshotPortal,
219310
     };
220311
     use crate::error::PortalResponseCode;
312
+    use crate::portal_dispatch::PortalDispatch;
221313
     use zbus::{
222314
         message::{Header, Message},
223315
         zvariant::{OwnedObjectPath, OwnedValue},
@@ -230,15 +322,25 @@ mod tests {
230322
 
231323
     #[test]
232324
     fn portal_interfaces_report_expected_version() {
233
-        assert_eq!(ScreenshotPortal.version(), INTERFACE_VERSION);
234
-        assert_eq!(FileChooserPortal.version(), INTERFACE_VERSION);
235
-        assert_eq!(AppChooserPortal.version(), INTERFACE_VERSION);
325
+        assert_eq!(
326
+            ScreenshotPortal::new(test_dispatch()).version(),
327
+            INTERFACE_VERSION
328
+        );
329
+        assert_eq!(
330
+            FileChooserPortal::new(test_dispatch()).version(),
331
+            INTERFACE_VERSION
332
+        );
333
+        assert_eq!(
334
+            AppChooserPortal::new(test_dispatch()).version(),
335
+            INTERFACE_VERSION
336
+        );
236337
     }
237338
 
238339
     #[test]
239340
     fn screenshot_and_pick_color_return_failed_placeholder() {
341
+        let portal = ScreenshotPortal::new(test_dispatch());
240342
         let options = HashMap::<String, OwnedValue>::new();
241
-        let (response, results) = ScreenshotPortal.screenshot(
343
+        let (response, results) = portal.screenshot(
242344
             request_handle_path(),
243345
             "org.test.App",
244346
             "x11:0x2a",
@@ -248,7 +350,7 @@ mod tests {
248350
         assert_eq!(response, PortalResponseCode::Failed as u32);
249351
         assert!(results.is_empty());
250352
 
251
-        let (response, results) = ScreenshotPortal.pick_color(
353
+        let (response, results) = portal.pick_color(
252354
             request_handle_path(),
253355
             "org.test.App",
254356
             "",
@@ -261,8 +363,9 @@ mod tests {
261363
 
262364
     #[test]
263365
     fn file_chooser_methods_return_failed_placeholder() {
366
+        let portal = FileChooserPortal::new(test_dispatch());
264367
         let options = HashMap::<String, OwnedValue>::new();
265
-        let (response, results) = FileChooserPortal.open_file(
368
+        let (response, results) = portal.open_file(
266369
             request_handle_path(),
267370
             "org.test.App",
268371
             "",
@@ -273,7 +376,7 @@ mod tests {
273376
         assert_eq!(response, PortalResponseCode::Failed as u32);
274377
         assert!(results.is_empty());
275378
 
276
-        let (response, results) = FileChooserPortal.save_file(
379
+        let (response, results) = portal.save_file(
277380
             request_handle_path(),
278381
             "org.test.App",
279382
             "",
@@ -284,7 +387,7 @@ mod tests {
284387
         assert_eq!(response, PortalResponseCode::Failed as u32);
285388
         assert!(results.is_empty());
286389
 
287
-        let (response, results) = FileChooserPortal.save_files(
390
+        let (response, results) = portal.save_files(
288391
             request_handle_path(),
289392
             "org.test.App",
290393
             "",
@@ -298,9 +401,10 @@ mod tests {
298401
 
299402
     #[test]
300403
     fn app_chooser_methods_have_stable_placeholders() {
404
+        let portal = AppChooserPortal::new(test_dispatch());
301405
         let options = HashMap::<String, OwnedValue>::new();
302406
         let choices = vec!["org.test.Viewer".to_string()];
303
-        let (response, results) = AppChooserPortal.choose_application(
407
+        let (response, results) = portal.choose_application(
304408
             request_handle_path(),
305409
             "org.test.App",
306410
             "",
@@ -311,7 +415,7 @@ mod tests {
311415
         assert_eq!(response, PortalResponseCode::Failed as u32);
312416
         assert!(results.is_empty());
313417
 
314
-        let error = AppChooserPortal
418
+        let error = portal
315419
             .update_choices(request_handle_path(), choices, test_call_header())
316420
             .expect_err("update choices should reject missing sender metadata");
317421
         match error {
@@ -334,4 +438,8 @@ mod tests {
334438
             .expect("test message");
335439
         Box::leak(Box::new(call)).header()
336440
     }
441
+
442
+    fn test_dispatch() -> Arc<Mutex<PortalDispatch>> {
443
+        Arc::new(Mutex::new(PortalDispatch::new(Duration::from_secs(30))))
444
+    }
337445
 }
garwarp/src/main.rsmodified
@@ -5,6 +5,7 @@ mod error;
55
 mod lock;
66
 mod logging;
77
 mod portal;
8
+mod portal_dispatch;
89
 mod portal_options;
910
 mod request;
1011
 mod request_store;
garwarp/src/portal_dispatch.rsadded
@@ -0,0 +1,191 @@
1
+#![allow(dead_code)]
2
+
3
+use std::time::Duration;
4
+
5
+use crate::error::PortalError;
6
+use crate::request::{RequestError, RequestOwner, RequestRecord, RequestRegistry, RequestState};
7
+use crate::validate::validate_request_identity;
8
+use crate::window::parse_optional_parent_window;
9
+
10
+#[derive(Debug)]
11
+pub struct PortalDispatch {
12
+    requests: RequestRegistry,
13
+}
14
+
15
+impl PortalDispatch {
16
+    #[must_use]
17
+    pub fn new(request_timeout: Duration) -> Self {
18
+        Self {
19
+            requests: RequestRegistry::new(request_timeout),
20
+        }
21
+    }
22
+
23
+    pub fn register_unimplemented_call(
24
+        &mut self,
25
+        request_id: &str,
26
+        sender: &str,
27
+        app_id: &str,
28
+        parent_window: &str,
29
+    ) -> Result<(), PortalError> {
30
+        let app_id = normalize_optional(app_id);
31
+        validate_request_identity(request_id, sender, app_id)?;
32
+
33
+        let parent_window = parse_optional_parent_window(normalize_optional(parent_window))
34
+            .map_err(|_| PortalError::InvalidParentWindow)?;
35
+        let owner = RequestOwner::new(sender.to_string(), app_id.map(str::to_string));
36
+        let state = self.begin_request(request_id, owner.clone(), parent_window)?;
37
+
38
+        if !state.is_terminal() {
39
+            self.requests
40
+                .transition(request_id, &owner, RequestState::Failed)
41
+                .map_err(|error| map_request_error_to_portal(&error))?;
42
+        }
43
+
44
+        Ok(())
45
+    }
46
+
47
+    pub fn validate_update_choices(
48
+        &self,
49
+        request_id: &str,
50
+        sender: &str,
51
+    ) -> Result<(), PortalError> {
52
+        validate_request_identity(request_id, sender, None)?;
53
+
54
+        let record = self
55
+            .requests
56
+            .record(request_id)
57
+            .ok_or(PortalError::RequestNotFound)?;
58
+        if record.owner.sender != sender {
59
+            return Err(PortalError::OwnershipMismatch);
60
+        }
61
+        if record.state.is_terminal() {
62
+            return Err(PortalError::InvalidTransition);
63
+        }
64
+        Ok(())
65
+    }
66
+
67
+    #[must_use]
68
+    pub fn record(&self, request_id: &str) -> Option<RequestRecord> {
69
+        self.requests.record(request_id)
70
+    }
71
+
72
+    fn begin_request(
73
+        &mut self,
74
+        request_id: &str,
75
+        owner: RequestOwner,
76
+        parent_window: Option<crate::window::ParentWindowContext>,
77
+    ) -> Result<RequestState, PortalError> {
78
+        match self
79
+            .requests
80
+            .begin(request_id.to_string(), owner.clone(), parent_window)
81
+        {
82
+            Ok(()) => Ok(RequestState::Pending),
83
+            Err(RequestError::AlreadyExists(_)) => match self.requests.record(request_id) {
84
+                Some(record) if record.owner == owner && record.parent_window == parent_window => {
85
+                    Ok(record.state)
86
+                }
87
+                Some(record) if record.owner != owner => Err(PortalError::OwnershipMismatch),
88
+                _ => Err(PortalError::RequestAlreadyExists),
89
+            },
90
+            Err(error) => Err(map_request_error_to_portal(&error)),
91
+        }
92
+    }
93
+}
94
+
95
+fn normalize_optional(input: &str) -> Option<&str> {
96
+    let trimmed = input.trim();
97
+    (!trimmed.is_empty()).then_some(trimmed)
98
+}
99
+
100
+fn map_request_error_to_portal(error: &RequestError) -> PortalError {
101
+    match error {
102
+        RequestError::AlreadyExists(_) => PortalError::RequestAlreadyExists,
103
+        RequestError::NotFound(_) => PortalError::RequestNotFound,
104
+        RequestError::OwnerMismatch(_) => PortalError::OwnershipMismatch,
105
+        RequestError::InvalidTransition { .. } => PortalError::InvalidTransition,
106
+    }
107
+}
108
+
109
+#[cfg(test)]
110
+mod tests {
111
+    use std::time::Duration;
112
+
113
+    use super::PortalDispatch;
114
+    use crate::error::PortalError;
115
+    use crate::request::RequestState;
116
+    use crate::window::ParentWindowContext;
117
+
118
+    #[test]
119
+    fn register_unimplemented_call_transitions_request_to_failed() {
120
+        let mut dispatch = new_dispatch();
121
+        dispatch
122
+            .register_unimplemented_call("req:1_42:token_1", ":1.42", "org.test.App", "x11:0x2a")
123
+            .expect("register request");
124
+
125
+        let record = dispatch
126
+            .record("req:1_42:token_1")
127
+            .expect("record should exist");
128
+        assert_eq!(record.state, RequestState::Failed);
129
+        assert_eq!(record.owner.sender, ":1.42");
130
+        assert_eq!(record.owner.app_id.as_deref(), Some("org.test.App"));
131
+        assert_eq!(
132
+            record.parent_window,
133
+            Some(ParentWindowContext::X11 { window_id: 42 })
134
+        );
135
+    }
136
+
137
+    #[test]
138
+    fn register_unimplemented_call_is_idempotent_for_same_identity() {
139
+        let mut dispatch = new_dispatch();
140
+        dispatch
141
+            .register_unimplemented_call("req:1_42:token_1", ":1.42", "org.test.App", "")
142
+            .expect("first register");
143
+        dispatch
144
+            .register_unimplemented_call("req:1_42:token_1", ":1.42", "org.test.App", "")
145
+            .expect("second register");
146
+
147
+        let record = dispatch
148
+            .record("req:1_42:token_1")
149
+            .expect("record should exist");
150
+        assert_eq!(record.state, RequestState::Failed);
151
+    }
152
+
153
+    #[test]
154
+    fn register_unimplemented_call_rejects_owner_mismatch() {
155
+        let mut dispatch = new_dispatch();
156
+        dispatch
157
+            .register_unimplemented_call("req:1_42:token_1", ":1.42", "org.test.App", "")
158
+            .expect("first register");
159
+
160
+        let result =
161
+            dispatch.register_unimplemented_call("req:1_42:token_1", ":1.43", "org.test.App", "");
162
+        assert_eq!(result, Err(PortalError::OwnershipMismatch));
163
+    }
164
+
165
+    #[test]
166
+    fn register_unimplemented_call_rejects_invalid_parent_window() {
167
+        let mut dispatch = new_dispatch();
168
+        let result = dispatch.register_unimplemented_call(
169
+            "req:1_42:token_1",
170
+            ":1.42",
171
+            "org.test.App",
172
+            "wayland:surface",
173
+        );
174
+        assert_eq!(result, Err(PortalError::InvalidParentWindow));
175
+    }
176
+
177
+    #[test]
178
+    fn update_choices_rejects_terminal_request() {
179
+        let mut dispatch = new_dispatch();
180
+        dispatch
181
+            .register_unimplemented_call("req:1_42:token_1", ":1.42", "org.test.App", "")
182
+            .expect("register request");
183
+
184
+        let result = dispatch.validate_update_choices("req:1_42:token_1", ":1.42");
185
+        assert_eq!(result, Err(PortalError::InvalidTransition));
186
+    }
187
+
188
+    fn new_dispatch() -> PortalDispatch {
189
+        PortalDispatch::new(Duration::from_secs(30))
190
+    }
191
+}