x11rb_protocol/parse_display/
mod.rs

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) => 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("DISPLAY") {
52            Ok(dpy_name) => 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(all(test, feature = "std"))]
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: {input}"
215            );
216        }
217    }
218
219    fn own_bad_cases() {
220        let non_existing_file = concat!(env!("CARGO_MANIFEST_DIR"), "/this_file_does_not_exist");
221        assert_eq!(
222            do_parse_display(non_existing_file),
223            Err(DisplayParsingError::MalformedValue(
224                non_existing_file.to_string().into()
225            )),
226            "Unexpectedly parsed: {non_existing_file}"
227        );
228    }
229
230    // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett
231    fn xcb_good_cases() {
232        // The libxcb code creates a temporary file. We can just use a known-to-exist file.
233        let existing_file = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
234
235        for (input, output) in &[
236            // unix in "launchd mode"
237            (
238                existing_file,
239                ParsedDisplay {
240                    host: existing_file.to_string(),
241                    protocol: Some("unix".to_string()),
242                    display: 0,
243                    screen: 0,
244                },
245            ),
246            (
247                &alloc::format!("unix:{existing_file}"),
248                ParsedDisplay {
249                    host: existing_file.to_string(),
250                    protocol: Some("unix".to_string()),
251                    display: 0,
252                    screen: 0,
253                },
254            ),
255            (
256                &alloc::format!("unix:{existing_file}.1"),
257                ParsedDisplay {
258                    host: existing_file.to_string(),
259                    protocol: Some("unix".to_string()),
260                    display: 0,
261                    screen: 1,
262                },
263            ),
264            (
265                &alloc::format!("{existing_file}.1"),
266                ParsedDisplay {
267                    host: existing_file.to_string(),
268                    protocol: Some("unix".to_string()),
269                    display: 0,
270                    screen: 1,
271                },
272            ),
273            // unix
274            (
275                ":0",
276                ParsedDisplay {
277                    host: "".to_string(),
278                    protocol: None,
279                    display: 0,
280                    screen: 0,
281                },
282            ),
283            (
284                ":1",
285                ParsedDisplay {
286                    host: "".to_string(),
287                    protocol: None,
288                    display: 1,
289                    screen: 0,
290                },
291            ),
292            (
293                ":0.1",
294                ParsedDisplay {
295                    host: "".to_string(),
296                    protocol: None,
297                    display: 0,
298                    screen: 1,
299                },
300            ),
301            // ip
302            (
303                "x.org:0",
304                ParsedDisplay {
305                    host: "x.org".to_string(),
306                    protocol: None,
307                    display: 0,
308                    screen: 0,
309                },
310            ),
311            (
312                "expo:0",
313                ParsedDisplay {
314                    host: "expo".to_string(),
315                    protocol: None,
316                    display: 0,
317                    screen: 0,
318                },
319            ),
320            (
321                "bigmachine:1",
322                ParsedDisplay {
323                    host: "bigmachine".to_string(),
324                    protocol: None,
325                    display: 1,
326                    screen: 0,
327                },
328            ),
329            (
330                "hydra:0.1",
331                ParsedDisplay {
332                    host: "hydra".to_string(),
333                    protocol: None,
334                    display: 0,
335                    screen: 1,
336                },
337            ),
338            // ipv4
339            (
340                "198.112.45.11:0",
341                ParsedDisplay {
342                    host: "198.112.45.11".to_string(),
343                    protocol: None,
344                    display: 0,
345                    screen: 0,
346                },
347            ),
348            (
349                "198.112.45.11:0.1",
350                ParsedDisplay {
351                    host: "198.112.45.11".to_string(),
352                    protocol: None,
353                    display: 0,
354                    screen: 1,
355                },
356            ),
357            // ipv6
358            (
359                ":::0",
360                ParsedDisplay {
361                    host: "::".to_string(),
362                    protocol: None,
363                    display: 0,
364                    screen: 0,
365                },
366            ),
367            (
368                "1:::0",
369                ParsedDisplay {
370                    host: "1::".to_string(),
371                    protocol: None,
372                    display: 0,
373                    screen: 0,
374                },
375            ),
376            (
377                "::1:0",
378                ParsedDisplay {
379                    host: "::1".to_string(),
380                    protocol: None,
381                    display: 0,
382                    screen: 0,
383                },
384            ),
385            (
386                "::1:0.1",
387                ParsedDisplay {
388                    host: "::1".to_string(),
389                    protocol: None,
390                    display: 0,
391                    screen: 1,
392                },
393            ),
394            (
395                "::127.0.0.1:0",
396                ParsedDisplay {
397                    host: "::127.0.0.1".to_string(),
398                    protocol: None,
399                    display: 0,
400                    screen: 0,
401                },
402            ),
403            (
404                "::ffff:127.0.0.1:0",
405                ParsedDisplay {
406                    host: "::ffff:127.0.0.1".to_string(),
407                    protocol: None,
408                    display: 0,
409                    screen: 0,
410                },
411            ),
412            (
413                "2002:83fc:3052::1:0",
414                ParsedDisplay {
415                    host: "2002:83fc:3052::1".to_string(),
416                    protocol: None,
417                    display: 0,
418                    screen: 0,
419                },
420            ),
421            (
422                "2002:83fc:3052::1:0.1",
423                ParsedDisplay {
424                    host: "2002:83fc:3052::1".to_string(),
425                    protocol: None,
426                    display: 0,
427                    screen: 1,
428                },
429            ),
430            (
431                "[::]:0",
432                ParsedDisplay {
433                    host: "[::]".to_string(),
434                    protocol: None,
435                    display: 0,
436                    screen: 0,
437                },
438            ),
439            (
440                "[1::]:0",
441                ParsedDisplay {
442                    host: "[1::]".to_string(),
443                    protocol: None,
444                    display: 0,
445                    screen: 0,
446                },
447            ),
448            (
449                "[::1]:0",
450                ParsedDisplay {
451                    host: "[::1]".to_string(),
452                    protocol: None,
453                    display: 0,
454                    screen: 0,
455                },
456            ),
457            (
458                "[::1]:0.1",
459                ParsedDisplay {
460                    host: "[::1]".to_string(),
461                    protocol: None,
462                    display: 0,
463                    screen: 1,
464                },
465            ),
466            (
467                "[::127.0.0.1]:0",
468                ParsedDisplay {
469                    host: "[::127.0.0.1]".to_string(),
470                    protocol: None,
471                    display: 0,
472                    screen: 0,
473                },
474            ),
475            (
476                "[2002:83fc:d052::1]:0",
477                ParsedDisplay {
478                    host: "[2002:83fc:d052::1]".to_string(),
479                    protocol: None,
480                    display: 0,
481                    screen: 0,
482                },
483            ),
484            (
485                "[2002:83fc:d052::1]:0.1",
486                ParsedDisplay {
487                    host: "[2002:83fc:d052::1]".to_string(),
488                    protocol: None,
489                    display: 0,
490                    screen: 1,
491                },
492            ),
493            // decnet
494            (
495                "myws::0",
496                ParsedDisplay {
497                    host: "myws:".to_string(),
498                    protocol: None,
499                    display: 0,
500                    screen: 0,
501                },
502            ),
503            (
504                "big::0",
505                ParsedDisplay {
506                    host: "big:".to_string(),
507                    protocol: None,
508                    display: 0,
509                    screen: 0,
510                },
511            ),
512            (
513                "hydra::0.1",
514                ParsedDisplay {
515                    host: "hydra:".to_string(),
516                    protocol: None,
517                    display: 0,
518                    screen: 1,
519                },
520            ),
521        ] {
522            assert_eq!(
523                do_parse_display(input).as_ref(),
524                Ok(output),
525                "Failed parsing correctly: {input}"
526            );
527        }
528    }
529
530    // Based on libxcb's test suite; (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett
531    fn xcb_bad_cases() {
532        for input in &[
533            "",
534            ":",
535            "::",
536            ":::",
537            ":.",
538            ":a",
539            ":a.",
540            ":0.",
541            ":.a",
542            ":.0",
543            ":0.a",
544            ":0.0.",
545            "127.0.0.1",
546            "127.0.0.1:",
547            "127.0.0.1::",
548            "::127.0.0.1",
549            "::127.0.0.1:",
550            "::127.0.0.1::",
551            "::ffff:127.0.0.1",
552            "::ffff:127.0.0.1:",
553            "::ffff:127.0.0.1::",
554            "localhost",
555            "localhost:",
556            "localhost::",
557        ] {
558            assert_eq!(
559                do_parse_display(input),
560                Err(DisplayParsingError::MalformedValue(
561                    input.to_string().into()
562                )),
563                "Unexpectedly parsed: {input}"
564            );
565        }
566    }
567
568    fn make_unix_path(host: &str, screen: u16) -> Result<ParsedDisplay, DisplayParsingError> {
569        Ok(ParsedDisplay {
570            host: host.to_string(),
571            protocol: Some("unix".to_string()),
572            display: 0,
573            screen,
574        })
575    }
576
577    #[test]
578    fn test_file_exists_callback_direct_path() {
579        fn run_test(display: &str, expected_path: &str) {
580            let called = RefCell::new(0);
581            let callback = |path: &_| {
582                assert_eq!(path, expected_path);
583                let mut called = called.borrow_mut();
584                assert_eq!(*called, 0);
585                *called += 1;
586                true
587            };
588            let result = parse_display_with_file_exists_callback(display, callback);
589            assert_eq!(*called.borrow(), 1);
590            assert_eq!(result, make_unix_path(expected_path, 0));
591        }
592
593        run_test("/path/to/file", "/path/to/file");
594        run_test("/path/to/file.123", "/path/to/file.123");
595        run_test("unix:whatever", "whatever");
596        run_test("unix:whatever.123", "whatever.123");
597    }
598
599    #[test]
600    fn test_file_exists_callback_direct_path_with_screen() {
601        fn run_test(display: &str, expected_path: &str) {
602            let called = RefCell::new(0);
603            let callback = |path: &_| {
604                let mut called = called.borrow_mut();
605                *called += 1;
606                match *called {
607                    1 => {
608                        assert_eq!(path, alloc::format!("{expected_path}.42"));
609                        false
610                    }
611                    2 => {
612                        assert_eq!(path, expected_path);
613                        true
614                    }
615                    _ => panic!("Unexpected call count {}", *called),
616                }
617            };
618            let result = parse_display_with_file_exists_callback(display, callback);
619            assert_eq!(*called.borrow(), 2);
620            assert_eq!(result, make_unix_path(expected_path, 42));
621        }
622
623        run_test("/path/to/file.42", "/path/to/file");
624        run_test("unix:whatever.42", "whatever");
625    }
626
627    #[test]
628    fn test_file_exists_callback_not_called_without_path() {
629        let callback = |path: &str| unreachable!("Called with {path}");
630        let result = parse_display_with_file_exists_callback("foo/bar:1.2", callback);
631        assert_eq!(
632            result,
633            Ok(ParsedDisplay {
634                host: "bar".to_string(),
635                protocol: Some("foo".to_string()),
636                display: 1,
637                screen: 2,
638            },)
639        );
640    }
641}