1mod connect_instruction;
4pub use connect_instruction::ConnectAddress;
5
6use crate::errors::DisplayParsingError;
7use alloc::string::{String, ToString};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ParsedDisplay {
12 pub host: String,
17 pub protocol: Option<String>,
22 pub display: u16,
24 pub screen: u16,
27}
28
29impl ParsedDisplay {
30 pub fn connect_instruction(&self) -> impl Iterator<Item = ConnectAddress<'_>> {
33 connect_instruction::connect_addresses(self)
34 }
35}
36
37#[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 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
59pub 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 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 let pos = remaining.rfind(':').ok_or_else(malformed)?;
88 let (host, remaining) = (&remaining[..pos], &remaining[pos + 1..]);
89
90 let (display, screen) = match remaining.find('.') {
92 Some(pos) => (&remaining[..pos], &remaining[pos + 1..]),
93 None => (remaining, "0"),
94 };
95
96 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
112fn 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 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 #[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 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 fn xcb_good_cases() {
232 let existing_file = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
234
235 for (input, output) in &[
236 (
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 (
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 (
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 (
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 (
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 (
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 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}