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(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}