1//! Utilities for parsing X11 display strings.
2
3#![cfg(feature = "std")]
4
5mod connect_instruction;
6pub use connect_instruction::ConnectAddress;
7
8use alloc::string::{String, ToString};
9
10/// A parsed X11 display string.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub 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
30impl 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`.
41pub 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
49fn 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)]
81mod 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