gardesk/garwarp / 7b4b1ca

Browse files

add portal request handle model

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
7b4b1ca742d86fbafbbe57afa61452ff18ae6e04
Parents
a8c0e5f
Tree
e692a15

2 changed files

StatusFile+-
M garwarp/src/main.rs 1 0
A garwarp/src/portal.rs 205 0
garwarp/src/main.rsmodified
@@ -4,6 +4,7 @@ mod dbus;
44
 mod error;
55
 mod lock;
66
 mod logging;
7
+mod portal;
78
 mod request;
89
 mod request_store;
910
 mod runtime;
garwarp/src/portal.rsadded
@@ -0,0 +1,205 @@
1
+#![allow(dead_code)]
2
+
3
+use crate::error::PortalError;
4
+use crate::validate::validate_request_id;
5
+
6
+pub const REQUEST_HANDLE_PREFIX: &str = "/org/freedesktop/portal/desktop/request/";
7
+const REQUEST_ID_PREFIX: &str = "req:";
8
+
9
+#[derive(Debug, Clone, PartialEq, Eq)]
10
+pub struct RequestHandle {
11
+    pub sender_segment: String,
12
+    pub token: String,
13
+}
14
+
15
+pub fn parse_request_handle(handle: &str) -> Result<RequestHandle, PortalError> {
16
+    if handle.trim() != handle {
17
+        return Err(PortalError::InvalidRequestPayload);
18
+    }
19
+
20
+    let suffix = handle
21
+        .strip_prefix(REQUEST_HANDLE_PREFIX)
22
+        .ok_or(PortalError::InvalidRequestPayload)?;
23
+
24
+    let mut parts = suffix.split('/');
25
+    let sender_segment = parts.next().ok_or(PortalError::InvalidRequestPayload)?;
26
+    let token = parts.next().ok_or(PortalError::InvalidRequestPayload)?;
27
+
28
+    if sender_segment.is_empty() || token.is_empty() || parts.next().is_some() {
29
+        return Err(PortalError::InvalidRequestPayload);
30
+    }
31
+    if !is_valid_path_segment(sender_segment) || !is_valid_path_segment(token) {
32
+        return Err(PortalError::InvalidRequestPayload);
33
+    }
34
+
35
+    Ok(RequestHandle {
36
+        sender_segment: sender_segment.to_string(),
37
+        token: token.to_string(),
38
+    })
39
+}
40
+
41
+pub fn sender_to_handle_segment(sender: &str) -> Result<String, PortalError> {
42
+    if sender.trim() != sender {
43
+        return Err(PortalError::InvalidRequestPayload);
44
+    }
45
+
46
+    let unique_body = sender
47
+        .strip_prefix(':')
48
+        .ok_or(PortalError::InvalidRequestPayload)?;
49
+
50
+    if unique_body.is_empty() {
51
+        return Err(PortalError::InvalidRequestPayload);
52
+    }
53
+
54
+    let mut segment = String::with_capacity(unique_body.len());
55
+    for ch in unique_body.chars() {
56
+        if ch.is_ascii_alphanumeric() {
57
+            segment.push(ch);
58
+            continue;
59
+        }
60
+        if matches!(ch, '.' | '_' | '-') {
61
+            segment.push('_');
62
+            continue;
63
+        }
64
+        return Err(PortalError::InvalidRequestPayload);
65
+    }
66
+
67
+    if segment.is_empty() || !is_valid_path_segment(&segment) {
68
+        return Err(PortalError::InvalidRequestPayload);
69
+    }
70
+
71
+    Ok(segment)
72
+}
73
+
74
+pub fn derive_request_id(
75
+    caller_sender: &str,
76
+    handle: &RequestHandle,
77
+) -> Result<String, PortalError> {
78
+    let expected_sender_segment = sender_to_handle_segment(caller_sender)?;
79
+    if expected_sender_segment != handle.sender_segment {
80
+        return Err(PortalError::OwnershipMismatch);
81
+    }
82
+
83
+    let request_id = format!(
84
+        "{REQUEST_ID_PREFIX}{expected_sender_segment}:{}",
85
+        handle.token
86
+    );
87
+    validate_request_id(&request_id)?;
88
+    Ok(request_id)
89
+}
90
+
91
+pub fn derive_request_id_from_handle(
92
+    caller_sender: &str,
93
+    request_handle_path: &str,
94
+) -> Result<String, PortalError> {
95
+    let request_handle = parse_request_handle(request_handle_path)?;
96
+    derive_request_id(caller_sender, &request_handle)
97
+}
98
+
99
+fn is_valid_path_segment(value: &str) -> bool {
100
+    value
101
+        .chars()
102
+        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
103
+}
104
+
105
+#[cfg(test)]
106
+mod tests {
107
+    use super::{
108
+        REQUEST_HANDLE_PREFIX, RequestHandle, derive_request_id, derive_request_id_from_handle,
109
+        parse_request_handle, sender_to_handle_segment,
110
+    };
111
+    use crate::error::PortalError;
112
+
113
+    #[test]
114
+    fn parse_valid_request_handle() {
115
+        let handle = format!("{REQUEST_HANDLE_PREFIX}1_42/token_1");
116
+        let parsed = parse_request_handle(&handle).expect("request handle should parse");
117
+        assert_eq!(
118
+            parsed,
119
+            RequestHandle {
120
+                sender_segment: "1_42".to_string(),
121
+                token: "token_1".to_string(),
122
+            }
123
+        );
124
+    }
125
+
126
+    #[test]
127
+    fn parse_rejects_invalid_prefix() {
128
+        let parsed = parse_request_handle("/org/example/request/1_2/token");
129
+        assert!(parsed.is_err());
130
+    }
131
+
132
+    #[test]
133
+    fn parse_rejects_whitespace_wrapped_input() {
134
+        let handle = format!("  {REQUEST_HANDLE_PREFIX}1_42/token_1");
135
+        let parsed = parse_request_handle(&handle);
136
+        assert_eq!(parsed, Err(PortalError::InvalidRequestPayload));
137
+    }
138
+
139
+    #[test]
140
+    fn parse_rejects_missing_segments() {
141
+        let parsed = parse_request_handle(&format!("{REQUEST_HANDLE_PREFIX}only_sender"));
142
+        assert!(parsed.is_err());
143
+    }
144
+
145
+    #[test]
146
+    fn parse_rejects_extra_path_segments() {
147
+        let parsed = parse_request_handle(&format!("{REQUEST_HANDLE_PREFIX}1_42/token_1/extra"));
148
+        assert_eq!(parsed, Err(PortalError::InvalidRequestPayload));
149
+    }
150
+
151
+    #[test]
152
+    fn parse_rejects_invalid_segment_characters() {
153
+        let parsed = parse_request_handle(&format!("{REQUEST_HANDLE_PREFIX}1.42/token_1"));
154
+        assert_eq!(parsed, Err(PortalError::InvalidRequestPayload));
155
+    }
156
+
157
+    #[test]
158
+    fn sender_to_handle_segment_normalizes_unique_name() {
159
+        let segment = sender_to_handle_segment(":1.420-a").expect("sender segment");
160
+        assert_eq!(segment, "1_420_a");
161
+    }
162
+
163
+    #[test]
164
+    fn sender_to_handle_segment_rejects_invalid_sender() {
165
+        let segment = sender_to_handle_segment("org.example.App");
166
+        assert_eq!(segment, Err(PortalError::InvalidRequestPayload));
167
+    }
168
+
169
+    #[test]
170
+    fn derive_request_id_is_deterministic_and_valid() {
171
+        let handle = RequestHandle {
172
+            sender_segment: "1_42".to_string(),
173
+            token: "token_1".to_string(),
174
+        };
175
+        let id = derive_request_id(":1.42", &handle).expect("request id");
176
+        assert_eq!(id, "req:1_42:token_1");
177
+    }
178
+
179
+    #[test]
180
+    fn derive_request_id_rejects_sender_mismatch() {
181
+        let handle = RequestHandle {
182
+            sender_segment: "1_42".to_string(),
183
+            token: "token_1".to_string(),
184
+        };
185
+        let id = derive_request_id(":1.43", &handle);
186
+        assert_eq!(id, Err(PortalError::OwnershipMismatch));
187
+    }
188
+
189
+    #[test]
190
+    fn derive_request_id_rejects_when_composed_id_is_invalid() {
191
+        let handle = RequestHandle {
192
+            sender_segment: "1_42".to_string(),
193
+            token: "x".repeat(200),
194
+        };
195
+        let id = derive_request_id(":1.42", &handle);
196
+        assert_eq!(id, Err(PortalError::InvalidRequestPayload));
197
+    }
198
+
199
+    #[test]
200
+    fn derive_request_id_from_handle_parses_and_derives() {
201
+        let handle = format!("{REQUEST_HANDLE_PREFIX}1_42/token_1");
202
+        let id = derive_request_id_from_handle(":1.42", &handle).expect("derived request id");
203
+        assert_eq!(id, "req:1_42:token_1");
204
+    }
205
+}