1//! Utilities for parsing X11 display strings.
2
3mod connect_instruction;
4pub use connect_instruction::ConnectAddress;
5
6use crate::errors::DisplayParsingError;
7use alloc::string::{String, ToString};
8
9/// A parsed X11 display string.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ParsedDisplay {
12 /// The hostname of the computer we nned to connect to.
13 ///
14 /// This is an empty string if we are connecting to the
15 /// local host.
16 pub host: String,
17 /// The protocol we are communicating over.
18 ///
19 /// This is `None` if the protocol may be determined
20 /// automatically.
21 pub protocol: Option<String>,
22 /// The index of the display we are connecting to.
23 pub display: u16,
24 /// The index of the screen that we are using as the
25 /// default screen.
26 pub screen: u16,
27}
28
29impl ParsedDisplay {
30 /// Get an iterator over `ConnectAddress`es from this parsed display for connecting
31 /// to the server.
32 pub fn connect_instruction(&self) -> impl Iterator<Item = ConnectAddress<'_>> {
33 connect_instruction::connect_addresses(self)
34 }
35}
36
37/// Parse an X11 display string.
38///
39/// If `dpy_name` is `None`, the display is parsed from the environment variable `DISPLAY`.
40///
41/// This function is only available when the `std` feature is enabled.
42#[cfg(feature = "std")]
43pub fn parse_display(dpy_name: Option<&str>) -> Result<ParsedDisplay, DisplayParsingError> {
44 fn file_exists(path: &str) -> bool {
45 std::path::Path::new(path).exists()
46 }
47
48 match dpy_name {
49 Some(dpy_name: &str) => parse_display_with_file_exists_callback(dpy_name, file_exists),
50 // If no dpy name was provided, use the env var. If no env var exists, return an error.
51 None => match std::env::var(key:"DISPLAY") {
52 Ok(dpy_name: String) => parse_display_with_file_exists_callback(&dpy_name, file_exists),
53 Err(std::env::VarError::NotPresent) => Err(DisplayParsingError::DisplayNotSet),
54 Err(std::env::VarError::NotUnicode(_)) => Err(DisplayParsingError::NotUnicode),
55 },
56 }
57}
58
59/// Parse an X11 display string.
60///
61/// If `dpy_name` is `None`, the display is parsed from the environment variable `DISPLAY`.
62///
63/// The parameter `file_exists` is called to check whether a given string refers to an existing
64/// file. This function does not need to check the file type.
65pub fn parse_display_with_file_exists_callback(
66 dpy_name: &str,
67 file_exists: impl Fn(&str) -> bool,
68) -> Result<ParsedDisplay, DisplayParsingError> {
69 let malformed = || DisplayParsingError::MalformedValue(dpy_name.to_string().into());
70 let map_malformed = |_| malformed();
71
72 if dpy_name.starts_with('/') {
73 return parse_display_direct_path(dpy_name, file_exists);
74 }
75 if let Some(remaining) = dpy_name.strip_prefix("unix:") {
76 return parse_display_direct_path(remaining, file_exists);
77 }
78
79 // Everything up to the last '/' is the protocol. This part is optional.
80 let (protocol, remaining) = if let Some(pos) = dpy_name.rfind('/') {
81 (Some(&dpy_name[..pos]), &dpy_name[pos + 1..])
82 } else {
83 (None, dpy_name)
84 };
85
86 // Everything up to the last ':' is the host. This part is required.
87 let pos = remaining.rfind(':').ok_or_else(malformed)?;
88 let (host, remaining) = (&remaining[..pos], &remaining[pos + 1..]);
89
90 // The remaining part is display.screen. The display is required and the screen optional.
91 let (display, screen) = match remaining.find('.') {
92 Some(pos) => (&remaining[..pos], &remaining[pos + 1..]),
93 None => (remaining, "0"),
94 };
95
96 // Parse the display and screen number
97 let (display, screen) = (
98 display.parse().map_err(map_malformed)?,
99 screen.parse().map_err(map_malformed)?,
100 );
101
102 let host = host.to_string();
103 let protocol = protocol.map(|p| p.to_string());
104 Ok(ParsedDisplay {
105 host,
106 protocol,
107 display,
108 screen,
109 })
110}
111
112// Check for "launchd mode" where we get the full path to a unix socket
113fn parse_display_direct_path(
114 dpy_name: &str,
115 file_exists: impl Fn(&str) -> bool,
116) -> Result<ParsedDisplay, DisplayParsingError> {
117 if file_exists(dpy_name) {
118 return Ok(ParsedDisplay {
119 host: dpy_name.to_string(),
120 protocol: Some("unix".to_string()),
121 display: 0,
122 screen: 0,
123 });
124 }
125
126 // Optionally, a screen number may be appended as ".n".
127 if let Some((path, screen)) = dpy_name.rsplit_once('.') {
128 if file_exists(path) {
129 return Ok(ParsedDisplay {
130 host: path.to_string(),
131 protocol: Some("unix".to_string()),
132 display: 0,
133 screen: screen.parse().map_err(|_| {
134 DisplayParsingError::MalformedValue(dpy_name.to_string().into())
135 })?,
136 });
137 }
138 }
139 Err(DisplayParsingError::MalformedValue(
140 dpy_name.to_string().into(),
141 ))
142}
143
144#[cfg(test)]
145mod test {
146 use super::{
147 parse_display, parse_display_with_file_exists_callback, DisplayParsingError, ParsedDisplay,
148 };
149 use alloc::string::ToString;
150 use core::cell::RefCell;
151
152 fn do_parse_display(input: &str) -> Result<ParsedDisplay, DisplayParsingError> {
153 std::env::set_var("DISPLAY", input);
154 let result1 = parse_display(None);
155
156 std::env::remove_var("DISPLAY");
157 let result2 = parse_display(Some(input));
158
159 assert_eq!(result1, result2);
160 result1
161 }
162
163 // The tests modify environment variables. This is process-global. Thus, the tests in this
164 // module cannot be run concurrently. We achieve this by having only a single test functions
165 // that calls all other functions.
166 #[test]
167 fn test_parsing() {
168 test_missing_input();
169 xcb_good_cases();
170 xcb_bad_cases();
171 own_good_cases();
172 own_bad_cases();
173 }
174
175 fn test_missing_input() {
176 std::env::remove_var("DISPLAY");
177 assert_eq!(parse_display(None), Err(DisplayParsingError::DisplayNotSet));
178 }
179
180 fn own_good_cases() {
181 // The XCB test suite does not test protocol parsing
182 for (input, output) in &[
183 (
184 "foo/bar:1",
185 ParsedDisplay {
186 host: "bar".to_string(),
187 protocol: Some("foo".to_string()),
188 display: 1,
189 screen: 0,
190 },
191 ),
192 (
193 "foo/bar:1.2",
194 ParsedDisplay {
195 host: "bar".to_string(),
196 protocol: Some("foo".to_string()),
197 display: 1,
198 screen: 2,
199 },
200 ),
201 (
202 "a:b/c/foo:bar:1.2",
203 ParsedDisplay {
204 host: "foo:bar".to_string(),
205 protocol: Some("a:b/c".to_string()),
206 display: 1,
207 screen: 2,
208 },
209 ),
210 ] {
211 assert_eq!(
212 do_parse_display(input).as_ref(),
213 Ok(output),
214 "Failed parsing correctly: {}",
215 input
216 );
217 }
218 }
219
220 fn own_bad_cases() {
221 let non_existing_file = concat!(env!("CARGO_MANIFEST_DIR"), "/this_file_does_not_exist");
222 assert_eq!(
223 do_parse_display(non_existing_file),
224 Err(DisplayParsingError::MalformedValue(
225 non_existing_file.to_string().into()
226 )),
227 "Unexpectedly parsed: {}",
228 non_existing_file
229 );
230 }
231
232 // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett
233 fn xcb_good_cases() {
234 // The libxcb code creates a temporary file. We can just use a known-to-exist file.
235 let existing_file = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
236
237 for (input, output) in &[
238 // unix in "launchd mode"
239 (
240 existing_file,
241 ParsedDisplay {
242 host: existing_file.to_string(),
243 protocol: Some("unix".to_string()),
244 display: 0,
245 screen: 0,
246 },
247 ),
248 (
249 &alloc::format!("unix:{existing_file}"),
250 ParsedDisplay {
251 host: existing_file.to_string(),
252 protocol: Some("unix".to_string()),
253 display: 0,
254 screen: 0,
255 },
256 ),
257 (
258 &alloc::format!("unix:{existing_file}.1"),
259 ParsedDisplay {
260 host: existing_file.to_string(),
261 protocol: Some("unix".to_string()),
262 display: 0,
263 screen: 1,
264 },
265 ),
266 (
267 &alloc::format!("{existing_file}.1"),
268 ParsedDisplay {
269 host: existing_file.to_string(),
270 protocol: Some("unix".to_string()),
271 display: 0,
272 screen: 1,
273 },
274 ),
275 // unix
276 (
277 ":0",
278 ParsedDisplay {
279 host: "".to_string(),
280 protocol: None,
281 display: 0,
282 screen: 0,
283 },
284 ),
285 (
286 ":1",
287 ParsedDisplay {
288 host: "".to_string(),
289 protocol: None,
290 display: 1,
291 screen: 0,
292 },
293 ),
294 (
295 ":0.1",
296 ParsedDisplay {
297 host: "".to_string(),
298 protocol: None,
299 display: 0,
300 screen: 1,
301 },
302 ),
303 // ip
304 (
305 "x.org:0",
306 ParsedDisplay {
307 host: "x.org".to_string(),
308 protocol: None,
309 display: 0,
310 screen: 0,
311 },
312 ),
313 (
314 "expo:0",
315 ParsedDisplay {
316 host: "expo".to_string(),
317 protocol: None,
318 display: 0,
319 screen: 0,
320 },
321 ),
322 (
323 "bigmachine:1",
324 ParsedDisplay {
325 host: "bigmachine".to_string(),
326 protocol: None,
327 display: 1,
328 screen: 0,
329 },
330 ),
331 (
332 "hydra:0.1",
333 ParsedDisplay {
334 host: "hydra".to_string(),
335 protocol: None,
336 display: 0,
337 screen: 1,
338 },
339 ),
340 // ipv4
341 (
342 "198.112.45.11:0",
343 ParsedDisplay {
344 host: "198.112.45.11".to_string(),
345 protocol: None,
346 display: 0,
347 screen: 0,
348 },
349 ),
350 (
351 "198.112.45.11:0.1",
352 ParsedDisplay {
353 host: "198.112.45.11".to_string(),
354 protocol: None,
355 display: 0,
356 screen: 1,
357 },
358 ),
359 // ipv6
360 (
361 ":::0",
362 ParsedDisplay {
363 host: "::".to_string(),
364 protocol: None,
365 display: 0,
366 screen: 0,
367 },
368 ),
369 (
370 "1:::0",
371 ParsedDisplay {
372 host: "1::".to_string(),
373 protocol: None,
374 display: 0,
375 screen: 0,
376 },
377 ),
378 (
379 "::1:0",
380 ParsedDisplay {
381 host: "::1".to_string(),
382 protocol: None,
383 display: 0,
384 screen: 0,
385 },
386 ),
387 (
388 "::1:0.1",
389 ParsedDisplay {
390 host: "::1".to_string(),
391 protocol: None,
392 display: 0,
393 screen: 1,
394 },
395 ),
396 (
397 "::127.0.0.1:0",
398 ParsedDisplay {
399 host: "::127.0.0.1".to_string(),
400 protocol: None,
401 display: 0,
402 screen: 0,
403 },
404 ),
405 (
406 "::ffff:127.0.0.1:0",
407 ParsedDisplay {
408 host: "::ffff:127.0.0.1".to_string(),
409 protocol: None,
410 display: 0,
411 screen: 0,
412 },
413 ),
414 (
415 "2002:83fc:3052::1:0",
416 ParsedDisplay {
417 host: "2002:83fc:3052::1".to_string(),
418 protocol: None,
419 display: 0,
420 screen: 0,
421 },
422 ),
423 (
424 "2002:83fc:3052::1:0.1",
425 ParsedDisplay {
426 host: "2002:83fc:3052::1".to_string(),
427 protocol: None,
428 display: 0,
429 screen: 1,
430 },
431 ),
432 (
433 "[::]:0",
434 ParsedDisplay {
435 host: "[::]".to_string(),
436 protocol: None,
437 display: 0,
438 screen: 0,
439 },
440 ),
441 (
442 "[1::]:0",
443 ParsedDisplay {
444 host: "[1::]".to_string(),
445 protocol: None,
446 display: 0,
447 screen: 0,
448 },
449 ),
450 (
451 "[::1]:0",
452 ParsedDisplay {
453 host: "[::1]".to_string(),
454 protocol: None,
455 display: 0,
456 screen: 0,
457 },
458 ),
459 (
460 "[::1]:0.1",
461 ParsedDisplay {
462 host: "[::1]".to_string(),
463 protocol: None,
464 display: 0,
465 screen: 1,
466 },
467 ),
468 (
469 "[::127.0.0.1]:0",
470 ParsedDisplay {
471 host: "[::127.0.0.1]".to_string(),
472 protocol: None,
473 display: 0,
474 screen: 0,
475 },
476 ),
477 (
478 "[2002:83fc:d052::1]:0",
479 ParsedDisplay {
480 host: "[2002:83fc:d052::1]".to_string(),
481 protocol: None,
482 display: 0,
483 screen: 0,
484 },
485 ),
486 (
487 "[2002:83fc:d052::1]:0.1",
488 ParsedDisplay {
489 host: "[2002:83fc:d052::1]".to_string(),
490 protocol: None,
491 display: 0,
492 screen: 1,
493 },
494 ),
495 // decnet
496 (
497 "myws::0",
498 ParsedDisplay {
499 host: "myws:".to_string(),
500 protocol: None,
501 display: 0,
502 screen: 0,
503 },
504 ),
505 (
506 "big::0",
507 ParsedDisplay {
508 host: "big:".to_string(),
509 protocol: None,
510 display: 0,
511 screen: 0,
512 },
513 ),
514 (
515 "hydra::0.1",
516 ParsedDisplay {
517 host: "hydra:".to_string(),
518 protocol: None,
519 display: 0,
520 screen: 1,
521 },
522 ),
523 ] {
524 assert_eq!(
525 do_parse_display(input).as_ref(),
526 Ok(output),
527 "Failed parsing correctly: {}",
528 input
529 );
530 }
531 }
532
533 // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett
534 fn xcb_bad_cases() {
535 for input in &[
536 "",
537 ":",
538 "::",
539 ":::",
540 ":.",
541 ":a",
542 ":a.",
543 ":0.",
544 ":.a",
545 ":.0",
546 ":0.a",
547 ":0.0.",
548 "127.0.0.1",
549 "127.0.0.1:",
550 "127.0.0.1::",
551 "::127.0.0.1",
552 "::127.0.0.1:",
553 "::127.0.0.1::",
554 "::ffff:127.0.0.1",
555 "::ffff:127.0.0.1:",
556 "::ffff:127.0.0.1::",
557 "localhost",
558 "localhost:",
559 "localhost::",
560 ] {
561 assert_eq!(
562 do_parse_display(input),
563 Err(DisplayParsingError::MalformedValue(
564 input.to_string().into()
565 )),
566 "Unexpectedly parsed: {}",
567 input
568 );
569 }
570 }
571
572 fn make_unix_path(host: &str, screen: u16) -> Result<ParsedDisplay, DisplayParsingError> {
573 Ok(ParsedDisplay {
574 host: host.to_string(),
575 protocol: Some("unix".to_string()),
576 display: 0,
577 screen,
578 })
579 }
580
581 #[test]
582 fn test_file_exists_callback_direct_path() {
583 fn run_test(display: &str, expected_path: &str) {
584 let called = RefCell::new(0);
585 let callback = |path: &_| {
586 assert_eq!(path, expected_path);
587 let mut called = called.borrow_mut();
588 assert_eq!(*called, 0);
589 *called += 1;
590 true
591 };
592 let result = parse_display_with_file_exists_callback(display, callback);
593 assert_eq!(*called.borrow(), 1);
594 assert_eq!(result, make_unix_path(expected_path, 0));
595 }
596
597 run_test("/path/to/file", "/path/to/file");
598 run_test("/path/to/file.123", "/path/to/file.123");
599 run_test("unix:whatever", "whatever");
600 run_test("unix:whatever.123", "whatever.123");
601 }
602
603 #[test]
604 fn test_file_exists_callback_direct_path_with_screen() {
605 fn run_test(display: &str, expected_path: &str) {
606 let called = RefCell::new(0);
607 let callback = |path: &_| {
608 let mut called = called.borrow_mut();
609 *called += 1;
610 match *called {
611 1 => {
612 assert_eq!(path, alloc::format!("{expected_path}.42"));
613 false
614 }
615 2 => {
616 assert_eq!(path, expected_path);
617 true
618 }
619 _ => panic!("Unexpected call count {}", *called),
620 }
621 };
622 let result = parse_display_with_file_exists_callback(display, callback);
623 assert_eq!(*called.borrow(), 2);
624 assert_eq!(result, make_unix_path(expected_path, 42));
625 }
626
627 run_test("/path/to/file.42", "/path/to/file");
628 run_test("unix:whatever.42", "whatever");
629 }
630
631 #[test]
632 fn test_file_exists_callback_not_called_without_path() {
633 let callback = |path: &str| unreachable!("Called with {path}");
634 let result = parse_display_with_file_exists_callback("foo/bar:1.2", callback);
635 assert_eq!(
636 result,
637 Ok(ParsedDisplay {
638 host: "bar".to_string(),
639 protocol: Some("foo".to_string()),
640 display: 1,
641 screen: 2,
642 },)
643 );
644 }
645}
646