1 | //! Utilities for parsing X11 display strings. |
2 | |
3 | #![cfg (feature = "std" )] |
4 | |
5 | mod connect_instruction; |
6 | pub use connect_instruction::ConnectAddress; |
7 | |
8 | use alloc::string::{String, ToString}; |
9 | |
10 | /// A parsed X11 display string. |
11 | #[derive (Debug, Clone, PartialEq, Eq)] |
12 | pub struct ParsedDisplay { |
13 | /// The hostname of the computer we nned to connect to. |
14 | /// |
15 | /// This is an empty string if we are connecting to the |
16 | /// local host. |
17 | pub host: String, |
18 | /// The protocol we are communicating over. |
19 | /// |
20 | /// This is `None` if the protocol may be determined |
21 | /// automatically. |
22 | pub protocol: Option<String>, |
23 | /// The index of the display we are connecting to. |
24 | pub display: u16, |
25 | /// The index of the screen that we are using as the |
26 | /// default screen. |
27 | pub screen: u16, |
28 | } |
29 | |
30 | impl ParsedDisplay { |
31 | /// Get an iterator over `ConnectAddress`es from this parsed display for connecting |
32 | /// to the server. |
33 | pub fn connect_instruction(&self) -> impl Iterator<Item = ConnectAddress<'_>> { |
34 | connect_instruction::connect_addresses(self) |
35 | } |
36 | } |
37 | |
38 | /// Parse an X11 display string. |
39 | /// |
40 | /// If `dpy_name` is `None`, the display is parsed from the environment variable `DISPLAY`. |
41 | pub fn parse_display(dpy_name: Option<&str>) -> Option<ParsedDisplay> { |
42 | // If no dpy name was provided, use the env var. If no env var exists, return None. |
43 | match dpy_name { |
44 | Some(dpy_name: &str) => parse_display_impl(dpy_name), |
45 | None => parse_display_impl(&std::env::var(key:"DISPLAY" ).ok()?), |
46 | } |
47 | } |
48 | |
49 | fn parse_display_impl(dpy_name: &str) -> Option<ParsedDisplay> { |
50 | // Everything up to the last '/' is the protocol. This part is optional. |
51 | let (protocol, remaining) = if let Some(pos) = dpy_name.rfind('/' ) { |
52 | (Some(&dpy_name[..pos]), &dpy_name[pos + 1..]) |
53 | } else { |
54 | (None, dpy_name) |
55 | }; |
56 | |
57 | // Everything up to the last ':' is the host. This part is required. |
58 | let pos = remaining.rfind(':' )?; |
59 | let (host, remaining) = (&remaining[..pos], &remaining[pos + 1..]); |
60 | |
61 | // The remaining part is display.screen. The display is required and the screen optional. |
62 | let (display, screen) = match remaining.find('.' ) { |
63 | Some(pos) => (&remaining[..pos], &remaining[pos + 1..]), |
64 | None => (remaining, "0" ), |
65 | }; |
66 | |
67 | // Parse the display and screen number |
68 | let (display, screen) = (display.parse().ok()?, screen.parse().ok()?); |
69 | |
70 | let host = host.to_string(); |
71 | let protocol = protocol.map(|p| p.to_string()); |
72 | Some(ParsedDisplay { |
73 | host, |
74 | protocol, |
75 | display, |
76 | screen, |
77 | }) |
78 | } |
79 | |
80 | #[cfg (test)] |
81 | mod test { |
82 | use super::{parse_display, ParsedDisplay}; |
83 | use alloc::string::ToString; |
84 | |
85 | fn do_parse_display(input: &str) -> Option<ParsedDisplay> { |
86 | std::env::set_var("DISPLAY" , input); |
87 | let result1 = parse_display(None); |
88 | |
89 | std::env::remove_var("DISPLAY" ); |
90 | let result2 = parse_display(Some(input)); |
91 | |
92 | assert_eq!(result1, result2); |
93 | result1 |
94 | } |
95 | |
96 | // The tests modify environment variables. This is process-global. Thus, the tests in this |
97 | // module cannot be run concurrently. We achieve this by having only a single test functions |
98 | // that calls all other functions. |
99 | #[test ] |
100 | fn test_parsing() { |
101 | test_missing_input(); |
102 | xcb_good_cases(); |
103 | xcb_bad_cases(); |
104 | own_good_cases(); |
105 | } |
106 | |
107 | fn test_missing_input() { |
108 | std::env::remove_var("DISPLAY" ); |
109 | assert_eq!(parse_display(None), None); |
110 | } |
111 | |
112 | fn own_good_cases() { |
113 | // The XCB test suite does not test protocol parsing |
114 | for (input, output) in &[ |
115 | ( |
116 | "foo/bar:1" , |
117 | ParsedDisplay { |
118 | host: "bar" .to_string(), |
119 | protocol: Some("foo" .to_string()), |
120 | display: 1, |
121 | screen: 0, |
122 | }, |
123 | ), |
124 | ( |
125 | "foo/bar:1.2" , |
126 | ParsedDisplay { |
127 | host: "bar" .to_string(), |
128 | protocol: Some("foo" .to_string()), |
129 | display: 1, |
130 | screen: 2, |
131 | }, |
132 | ), |
133 | ( |
134 | "a:b/c/foo:bar:1.2" , |
135 | ParsedDisplay { |
136 | host: "foo:bar" .to_string(), |
137 | protocol: Some("a:b/c" .to_string()), |
138 | display: 1, |
139 | screen: 2, |
140 | }, |
141 | ), |
142 | ] { |
143 | assert_eq!( |
144 | do_parse_display(input).as_ref(), |
145 | Some(output), |
146 | "Failed parsing correctly: {}" , |
147 | input |
148 | ); |
149 | } |
150 | } |
151 | |
152 | // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett |
153 | fn xcb_good_cases() { |
154 | for (input, output) in &[ |
155 | // unix |
156 | ( |
157 | ":0" , |
158 | ParsedDisplay { |
159 | host: "" .to_string(), |
160 | protocol: None, |
161 | display: 0, |
162 | screen: 0, |
163 | }, |
164 | ), |
165 | ( |
166 | ":1" , |
167 | ParsedDisplay { |
168 | host: "" .to_string(), |
169 | protocol: None, |
170 | display: 1, |
171 | screen: 0, |
172 | }, |
173 | ), |
174 | ( |
175 | ":0.1" , |
176 | ParsedDisplay { |
177 | host: "" .to_string(), |
178 | protocol: None, |
179 | display: 0, |
180 | screen: 1, |
181 | }, |
182 | ), |
183 | // ip |
184 | ( |
185 | "x.org:0" , |
186 | ParsedDisplay { |
187 | host: "x.org" .to_string(), |
188 | protocol: None, |
189 | display: 0, |
190 | screen: 0, |
191 | }, |
192 | ), |
193 | ( |
194 | "expo:0" , |
195 | ParsedDisplay { |
196 | host: "expo" .to_string(), |
197 | protocol: None, |
198 | display: 0, |
199 | screen: 0, |
200 | }, |
201 | ), |
202 | ( |
203 | "bigmachine:1" , |
204 | ParsedDisplay { |
205 | host: "bigmachine" .to_string(), |
206 | protocol: None, |
207 | display: 1, |
208 | screen: 0, |
209 | }, |
210 | ), |
211 | ( |
212 | "hydra:0.1" , |
213 | ParsedDisplay { |
214 | host: "hydra" .to_string(), |
215 | protocol: None, |
216 | display: 0, |
217 | screen: 1, |
218 | }, |
219 | ), |
220 | // ipv4 |
221 | ( |
222 | "198.112.45.11:0" , |
223 | ParsedDisplay { |
224 | host: "198.112.45.11" .to_string(), |
225 | protocol: None, |
226 | display: 0, |
227 | screen: 0, |
228 | }, |
229 | ), |
230 | ( |
231 | "198.112.45.11:0.1" , |
232 | ParsedDisplay { |
233 | host: "198.112.45.11" .to_string(), |
234 | protocol: None, |
235 | display: 0, |
236 | screen: 1, |
237 | }, |
238 | ), |
239 | // ipv6 |
240 | ( |
241 | ":::0" , |
242 | ParsedDisplay { |
243 | host: "::" .to_string(), |
244 | protocol: None, |
245 | display: 0, |
246 | screen: 0, |
247 | }, |
248 | ), |
249 | ( |
250 | "1:::0" , |
251 | ParsedDisplay { |
252 | host: "1::" .to_string(), |
253 | protocol: None, |
254 | display: 0, |
255 | screen: 0, |
256 | }, |
257 | ), |
258 | ( |
259 | "::1:0" , |
260 | ParsedDisplay { |
261 | host: "::1" .to_string(), |
262 | protocol: None, |
263 | display: 0, |
264 | screen: 0, |
265 | }, |
266 | ), |
267 | ( |
268 | "::1:0.1" , |
269 | ParsedDisplay { |
270 | host: "::1" .to_string(), |
271 | protocol: None, |
272 | display: 0, |
273 | screen: 1, |
274 | }, |
275 | ), |
276 | ( |
277 | "::127.0.0.1:0" , |
278 | ParsedDisplay { |
279 | host: "::127.0.0.1" .to_string(), |
280 | protocol: None, |
281 | display: 0, |
282 | screen: 0, |
283 | }, |
284 | ), |
285 | ( |
286 | "::ffff:127.0.0.1:0" , |
287 | ParsedDisplay { |
288 | host: "::ffff:127.0.0.1" .to_string(), |
289 | protocol: None, |
290 | display: 0, |
291 | screen: 0, |
292 | }, |
293 | ), |
294 | ( |
295 | "2002:83fc:3052::1:0" , |
296 | ParsedDisplay { |
297 | host: "2002:83fc:3052::1" .to_string(), |
298 | protocol: None, |
299 | display: 0, |
300 | screen: 0, |
301 | }, |
302 | ), |
303 | ( |
304 | "2002:83fc:3052::1:0.1" , |
305 | ParsedDisplay { |
306 | host: "2002:83fc:3052::1" .to_string(), |
307 | protocol: None, |
308 | display: 0, |
309 | screen: 1, |
310 | }, |
311 | ), |
312 | ( |
313 | "[::]:0" , |
314 | ParsedDisplay { |
315 | host: "[::]" .to_string(), |
316 | protocol: None, |
317 | display: 0, |
318 | screen: 0, |
319 | }, |
320 | ), |
321 | ( |
322 | "[1::]:0" , |
323 | ParsedDisplay { |
324 | host: "[1::]" .to_string(), |
325 | protocol: None, |
326 | display: 0, |
327 | screen: 0, |
328 | }, |
329 | ), |
330 | ( |
331 | "[::1]:0" , |
332 | ParsedDisplay { |
333 | host: "[::1]" .to_string(), |
334 | protocol: None, |
335 | display: 0, |
336 | screen: 0, |
337 | }, |
338 | ), |
339 | ( |
340 | "[::1]:0.1" , |
341 | ParsedDisplay { |
342 | host: "[::1]" .to_string(), |
343 | protocol: None, |
344 | display: 0, |
345 | screen: 1, |
346 | }, |
347 | ), |
348 | ( |
349 | "[::127.0.0.1]:0" , |
350 | ParsedDisplay { |
351 | host: "[::127.0.0.1]" .to_string(), |
352 | protocol: None, |
353 | display: 0, |
354 | screen: 0, |
355 | }, |
356 | ), |
357 | ( |
358 | "[2002:83fc:d052::1]:0" , |
359 | ParsedDisplay { |
360 | host: "[2002:83fc:d052::1]" .to_string(), |
361 | protocol: None, |
362 | display: 0, |
363 | screen: 0, |
364 | }, |
365 | ), |
366 | ( |
367 | "[2002:83fc:d052::1]:0.1" , |
368 | ParsedDisplay { |
369 | host: "[2002:83fc:d052::1]" .to_string(), |
370 | protocol: None, |
371 | display: 0, |
372 | screen: 1, |
373 | }, |
374 | ), |
375 | // decnet |
376 | ( |
377 | "myws::0" , |
378 | ParsedDisplay { |
379 | host: "myws:" .to_string(), |
380 | protocol: None, |
381 | display: 0, |
382 | screen: 0, |
383 | }, |
384 | ), |
385 | ( |
386 | "big::0" , |
387 | ParsedDisplay { |
388 | host: "big:" .to_string(), |
389 | protocol: None, |
390 | display: 0, |
391 | screen: 0, |
392 | }, |
393 | ), |
394 | ( |
395 | "hydra::0.1" , |
396 | ParsedDisplay { |
397 | host: "hydra:" .to_string(), |
398 | protocol: None, |
399 | display: 0, |
400 | screen: 1, |
401 | }, |
402 | ), |
403 | ] { |
404 | assert_eq!( |
405 | do_parse_display(input).as_ref(), |
406 | Some(output), |
407 | "Failed parsing correctly: {}" , |
408 | input |
409 | ); |
410 | } |
411 | } |
412 | |
413 | // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett |
414 | fn xcb_bad_cases() { |
415 | for input in &[ |
416 | "" , |
417 | ":" , |
418 | "::" , |
419 | ":::" , |
420 | ":." , |
421 | ":a" , |
422 | ":a." , |
423 | ":0." , |
424 | ":.a" , |
425 | ":.0" , |
426 | ":0.a" , |
427 | ":0.0." , |
428 | "127.0.0.1" , |
429 | "127.0.0.1:" , |
430 | "127.0.0.1::" , |
431 | "::127.0.0.1" , |
432 | "::127.0.0.1:" , |
433 | "::127.0.0.1::" , |
434 | "::ffff:127.0.0.1" , |
435 | "::ffff:127.0.0.1:" , |
436 | "::ffff:127.0.0.1::" , |
437 | "localhost" , |
438 | "localhost:" , |
439 | "localhost::" , |
440 | ] { |
441 | assert_eq!( |
442 | do_parse_display(input), |
443 | None, |
444 | "Unexpectedly parsed: {}" , |
445 | input |
446 | ); |
447 | } |
448 | } |
449 | } |
450 | |