zbus/
address.rs

1#[cfg(target_os = "macos")]
2use crate::process::run;
3#[cfg(windows)]
4use crate::win32::windows_autolaunch_bus_address;
5use crate::{Error, Result};
6#[cfg(not(feature = "tokio"))]
7use async_io::Async;
8#[cfg(all(unix, not(target_os = "macos")))]
9use nix::unistd::Uid;
10#[cfg(not(feature = "tokio"))]
11use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
12#[cfg(all(unix, not(feature = "tokio")))]
13use std::os::unix::net::UnixStream;
14use std::{collections::HashMap, convert::TryFrom, env, str::FromStr};
15#[cfg(feature = "tokio")]
16use tokio::net::TcpStream;
17#[cfg(all(unix, feature = "tokio"))]
18use tokio::net::UnixStream;
19#[cfg(feature = "tokio-vsock")]
20use tokio_vsock::VsockStream;
21#[cfg(all(windows, not(feature = "tokio")))]
22use uds_windows::UnixStream;
23#[cfg(all(feature = "vsock", not(feature = "tokio")))]
24use vsock::VsockStream;
25
26use std::{
27    ffi::OsString,
28    fmt::{Display, Formatter},
29    str::from_utf8_unchecked,
30};
31
32/// A `tcp:` address family.
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum TcpAddressFamily {
35    Ipv4,
36    Ipv6,
37}
38
39/// A `tcp:` D-Bus address.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct TcpAddress {
42    pub(crate) host: String,
43    pub(crate) bind: Option<String>,
44    pub(crate) port: u16,
45    pub(crate) family: Option<TcpAddressFamily>,
46}
47
48impl TcpAddress {
49    /// Returns the `tcp:` address `host` value.
50    pub fn host(&self) -> &str {
51        &self.host
52    }
53
54    /// Returns the `tcp:` address `bind` value.
55    pub fn bind(&self) -> Option<&str> {
56        self.bind.as_deref()
57    }
58
59    /// Returns the `tcp:` address `port` value.
60    pub fn port(&self) -> u16 {
61        self.port
62    }
63
64    /// Returns the `tcp:` address `family` value.
65    pub fn family(&self) -> Option<TcpAddressFamily> {
66        self.family
67    }
68
69    // Helper for FromStr
70    fn from_tcp(opts: HashMap<&str, &str>) -> Result<Self> {
71        let bind = None;
72        if opts.contains_key("bind") {
73            return Err(Error::Address("`bind` isn't yet supported".into()));
74        }
75
76        let host = opts
77            .get("host")
78            .ok_or_else(|| Error::Address("tcp address is missing `host`".into()))?
79            .to_string();
80        let port = opts
81            .get("port")
82            .ok_or_else(|| Error::Address("tcp address is missing `port`".into()))?;
83        let port = port
84            .parse::<u16>()
85            .map_err(|_| Error::Address("invalid tcp `port`".into()))?;
86        let family = opts
87            .get("family")
88            .map(|f| TcpAddressFamily::from_str(f))
89            .transpose()?;
90
91        Ok(Self {
92            host,
93            bind,
94            port,
95            family,
96        })
97    }
98
99    fn write_options(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        f.write_str("host=")?;
101
102        encode_percents(f, self.host.as_ref())?;
103
104        write!(f, ",port={}", self.port)?;
105
106        if let Some(bind) = &self.bind {
107            f.write_str(",bind=")?;
108            encode_percents(f, bind.as_ref())?;
109        }
110
111        if let Some(family) = &self.family {
112            write!(f, ",family={family}")?;
113        }
114
115        Ok(())
116    }
117}
118
119#[cfg(any(
120    all(feature = "vsock", not(feature = "tokio")),
121    feature = "tokio-vsock"
122))]
123/// A `tcp:` D-Bus address.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct VsockAddress {
126    pub(crate) cid: u32,
127    pub(crate) port: u32,
128}
129
130#[cfg(any(
131    all(feature = "vsock", not(feature = "tokio")),
132    feature = "tokio-vsock"
133))]
134impl VsockAddress {
135    /// Create a new VSOCK address.
136    pub fn new(cid: u32, port: u32) -> Self {
137        Self { cid, port }
138    }
139}
140
141/// A bus address
142#[derive(Clone, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum Address {
145    /// A path on the filesystem
146    Unix(OsString),
147    /// TCP address details
148    Tcp(TcpAddress),
149    /// TCP address details with nonce file path
150    NonceTcp {
151        addr: TcpAddress,
152        nonce_file: Vec<u8>,
153    },
154    /// Autolaunch address with optional scope
155    Autolaunch(Option<String>),
156    /// Launchd address with a required env key
157    Launchd(String),
158    #[cfg(any(
159        all(feature = "vsock", not(feature = "tokio")),
160        feature = "tokio-vsock"
161    ))]
162    /// VSOCK address
163    ///
164    /// This variant is only available when either `vsock` or `tokio-vsock` feature is enabled. The
165    /// type of `stream` is `vsock::VsockStream` with `vsock` feature and
166    /// `tokio_vsock::VsockStream` with `tokio-vsock` feature.
167    Vsock(VsockAddress),
168}
169
170#[cfg(not(feature = "tokio"))]
171#[derive(Debug)]
172pub(crate) enum Stream {
173    Unix(Async<UnixStream>),
174    Tcp(Async<TcpStream>),
175    #[cfg(feature = "vsock")]
176    Vsock(Async<VsockStream>),
177}
178
179#[cfg(feature = "tokio")]
180#[derive(Debug)]
181pub(crate) enum Stream {
182    #[cfg(unix)]
183    Unix(UnixStream),
184    Tcp(TcpStream),
185    #[cfg(feature = "tokio-vsock")]
186    Vsock(VsockStream),
187}
188
189#[cfg(not(feature = "tokio"))]
190async fn connect_tcp(addr: TcpAddress) -> Result<Async<TcpStream>> {
191    let addrs = crate::Task::spawn_blocking(
192        move || -> Result<Vec<SocketAddr>> {
193            let addrs = (addr.host(), addr.port()).to_socket_addrs()?.filter(|a| {
194                if let Some(family) = addr.family() {
195                    if family == TcpAddressFamily::Ipv4 {
196                        a.is_ipv4()
197                    } else {
198                        a.is_ipv6()
199                    }
200                } else {
201                    true
202                }
203            });
204            Ok(addrs.collect())
205        },
206        "connect tcp",
207    )
208    .await
209    .map_err(|e| Error::Address(format!("Failed to receive TCP addresses: {e}")))?;
210
211    // we could attempt connections in parallel?
212    let mut last_err = Error::Address("Failed to connect".into());
213    for addr in addrs {
214        match Async::<TcpStream>::connect(addr).await {
215            Ok(stream) => return Ok(stream),
216            Err(e) => last_err = e.into(),
217        }
218    }
219
220    Err(last_err)
221}
222
223#[cfg(feature = "tokio")]
224async fn connect_tcp(addr: TcpAddress) -> Result<TcpStream> {
225    TcpStream::connect((addr.host(), addr.port()))
226        .await
227        .map_err(|e| Error::InputOutput(e.into()))
228}
229
230#[cfg(target_os = "macos")]
231pub(crate) async fn macos_launchd_bus_address(env_key: &str) -> Result<Address> {
232    let output = run("launchctl", ["getenv", env_key])
233        .await
234        .expect("failed to wait on launchctl output");
235
236    if !output.status.success() {
237        return Err(crate::Error::Address(format!(
238            "launchctl terminated with code: {}",
239            output.status
240        )));
241    }
242
243    let addr = String::from_utf8(output.stdout).map_err(|e| {
244        crate::Error::Address(format!("Unable to parse launchctl output as UTF-8: {}", e))
245    })?;
246
247    format!("unix:path={}", addr.trim()).parse()
248}
249
250impl Address {
251    #[async_recursion::async_recursion]
252    pub(crate) async fn connect(self) -> Result<Stream> {
253        match self {
254            Address::Unix(p) => {
255                #[cfg(not(feature = "tokio"))]
256                {
257                    #[cfg(windows)]
258                    {
259                        let stream = crate::Task::spawn_blocking(
260                            move || UnixStream::connect(p),
261                            "unix stream connection",
262                        )
263                        .await?;
264                        Async::new(stream)
265                            .map(Stream::Unix)
266                            .map_err(|e| Error::InputOutput(e.into()))
267                    }
268
269                    #[cfg(not(windows))]
270                    {
271                        Async::<UnixStream>::connect(p)
272                            .await
273                            .map(Stream::Unix)
274                            .map_err(|e| Error::InputOutput(e.into()))
275                    }
276                }
277
278                #[cfg(feature = "tokio")]
279                {
280                    #[cfg(unix)]
281                    {
282                        UnixStream::connect(p)
283                            .await
284                            .map(Stream::Unix)
285                            .map_err(|e| Error::InputOutput(e.into()))
286                    }
287
288                    #[cfg(not(unix))]
289                    {
290                        let _ = p;
291                        Err(Error::Unsupported)
292                    }
293                }
294            }
295
296            #[cfg(all(feature = "vsock", not(feature = "tokio")))]
297            Address::Vsock(addr) => {
298                let stream = VsockStream::connect_with_cid_port(addr.cid, addr.port)?;
299                Async::new(stream).map(Stream::Vsock).map_err(Into::into)
300            }
301
302            #[cfg(feature = "tokio-vsock")]
303            Address::Vsock(addr) => VsockStream::connect(addr.cid, addr.port)
304                .await
305                .map(Stream::Vsock)
306                .map_err(Into::into),
307
308            Address::Tcp(addr) => connect_tcp(addr).await.map(Stream::Tcp),
309
310            Address::NonceTcp { addr, nonce_file } => {
311                let mut stream = connect_tcp(addr).await?;
312
313                #[cfg(unix)]
314                let nonce_file = {
315                    use std::os::unix::ffi::OsStrExt;
316                    std::ffi::OsStr::from_bytes(&nonce_file)
317                };
318
319                #[cfg(windows)]
320                let nonce_file = std::str::from_utf8(&nonce_file)
321                    .map_err(|_| Error::Address("nonce file path is invalid UTF-8".to_owned()))?;
322
323                #[cfg(not(feature = "tokio"))]
324                {
325                    let nonce = std::fs::read(nonce_file)?;
326                    let mut nonce = &nonce[..];
327
328                    while !nonce.is_empty() {
329                        let len = stream
330                            .write_with_mut(|s| std::io::Write::write(s, nonce))
331                            .await?;
332                        nonce = &nonce[len..];
333                    }
334                }
335
336                #[cfg(feature = "tokio")]
337                {
338                    let nonce = tokio::fs::read(nonce_file).await?;
339                    tokio::io::AsyncWriteExt::write_all(&mut stream, &nonce).await?;
340                }
341
342                Ok(Stream::Tcp(stream))
343            }
344
345            #[cfg(not(windows))]
346            Address::Autolaunch(_) => Err(Error::Address(
347                "Autolaunch addresses are only supported on Windows".to_owned(),
348            )),
349
350            #[cfg(windows)]
351            Address::Autolaunch(Some(_)) => Err(Error::Address(
352                "Autolaunch scopes are currently unsupported".to_owned(),
353            )),
354
355            #[cfg(windows)]
356            Address::Autolaunch(None) => {
357                let addr = windows_autolaunch_bus_address()?;
358                addr.connect().await
359            }
360
361            #[cfg(not(target_os = "macos"))]
362            Address::Launchd(_) => Err(Error::Address(
363                "Launchd addresses are only supported on macOS".to_owned(),
364            )),
365
366            #[cfg(target_os = "macos")]
367            Address::Launchd(env) => {
368                let addr = macos_launchd_bus_address(&env).await?;
369                addr.connect().await
370            }
371        }
372    }
373
374    /// Get the address for session socket respecting the DBUS_SESSION_BUS_ADDRESS environment
375    /// variable. If we don't recognize the value (or it's not set) we fall back to
376    /// $XDG_RUNTIME_DIR/bus
377    pub fn session() -> Result<Self> {
378        match env::var("DBUS_SESSION_BUS_ADDRESS") {
379            Ok(val) => Self::from_str(&val),
380            _ => {
381                #[cfg(windows)]
382                {
383                    #[cfg(feature = "windows-gdbus")]
384                    return Self::from_str("autolaunch:");
385
386                    #[cfg(not(feature = "windows-gdbus"))]
387                    return Self::from_str("autolaunch:scope=*user");
388                }
389
390                #[cfg(all(unix, not(target_os = "macos")))]
391                {
392                    let runtime_dir = env::var("XDG_RUNTIME_DIR")
393                        .unwrap_or_else(|_| format!("/run/user/{}", Uid::effective()));
394                    let path = format!("unix:path={runtime_dir}/bus");
395
396                    Self::from_str(&path)
397                }
398
399                #[cfg(target_os = "macos")]
400                return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET");
401            }
402        }
403    }
404
405    /// Get the address for system bus respecting the DBUS_SYSTEM_BUS_ADDRESS environment
406    /// variable. If we don't recognize the value (or it's not set) we fall back to
407    /// /var/run/dbus/system_bus_socket
408    pub fn system() -> Result<Self> {
409        match env::var("DBUS_SYSTEM_BUS_ADDRESS") {
410            Ok(val) => Self::from_str(&val),
411            _ => {
412                #[cfg(all(unix, not(target_os = "macos")))]
413                return Self::from_str("unix:path=/var/run/dbus/system_bus_socket");
414
415                #[cfg(windows)]
416                return Self::from_str("autolaunch:");
417
418                #[cfg(target_os = "macos")]
419                return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET");
420            }
421        }
422    }
423
424    // Helper for FromStr
425    #[cfg(any(unix, not(feature = "tokio")))]
426    fn from_unix(opts: HashMap<&str, &str>) -> Result<Self> {
427        let path = if let Some(abs) = opts.get("abstract") {
428            if opts.get("path").is_some() {
429                return Err(Error::Address(
430                    "`path` and `abstract` cannot be specified together".into(),
431                ));
432            }
433            let mut s = OsString::from("\0");
434            s.push(abs);
435            s
436        } else if let Some(path) = opts.get("path") {
437            OsString::from(path)
438        } else {
439            return Err(Error::Address(
440                "unix address is missing path or abstract".to_owned(),
441            ));
442        };
443
444        Ok(Address::Unix(path))
445    }
446
447    #[cfg(all(feature = "vsock", not(feature = "tokio")))]
448    fn from_vsock(opts: HashMap<&str, &str>) -> Result<Self> {
449        let cid = opts
450            .get("cid")
451            .ok_or_else(|| Error::Address("VSOCK address is missing cid=".into()))?;
452        let cid = cid
453            .parse::<u32>()
454            .map_err(|e| Error::Address(format!("Failed to parse VSOCK cid `{}`: {}", cid, e)))?;
455        let port = opts
456            .get("port")
457            .ok_or_else(|| Error::Address("VSOCK address is missing port=".into()))?;
458        let port = port
459            .parse::<u32>()
460            .map_err(|e| Error::Address(format!("Failed to parse VSOCK port `{}`: {}", port, e)))?;
461
462        Ok(Address::Vsock(VsockAddress { cid, port }))
463    }
464}
465
466impl FromStr for TcpAddressFamily {
467    type Err = Error;
468
469    fn from_str(family: &str) -> Result<Self> {
470        match family {
471            "ipv4" => Ok(Self::Ipv4),
472            "ipv6" => Ok(Self::Ipv6),
473            _ => Err(Error::Address(format!(
474                "invalid tcp address `family`: {family}"
475            ))),
476        }
477    }
478}
479
480impl Display for TcpAddressFamily {
481    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
482        match self {
483            Self::Ipv4 => write!(f, "ipv4"),
484            Self::Ipv6 => write!(f, "ipv6"),
485        }
486    }
487}
488
489fn decode_hex(c: char) -> Result<u8> {
490    match c {
491        '0'..='9' => Ok(c as u8 - b'0'),
492        'a'..='f' => Ok(c as u8 - b'a' + 10),
493        'A'..='F' => Ok(c as u8 - b'A' + 10),
494
495        _ => Err(Error::Address(
496            "invalid hexadecimal character in percent-encoded sequence".to_owned(),
497        )),
498    }
499}
500
501fn decode_percents(value: &str) -> Result<Vec<u8>> {
502    let mut iter = value.chars();
503    let mut decoded = Vec::new();
504
505    while let Some(c) = iter.next() {
506        if matches!(c, '-' | '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' | '/' | '.' | '\\' | '*') {
507            decoded.push(c as u8)
508        } else if c == '%' {
509            decoded.push(
510                decode_hex(iter.next().ok_or_else(|| {
511                    Error::Address("incomplete percent-encoded sequence".to_owned())
512                })?)?
513                    << 4
514                    | decode_hex(iter.next().ok_or_else(|| {
515                        Error::Address("incomplete percent-encoded sequence".to_owned())
516                    })?)?,
517            );
518        } else {
519            return Err(Error::Address("Invalid character in address".to_owned()));
520        }
521    }
522
523    Ok(decoded)
524}
525
526fn encode_percents(f: &mut Formatter<'_>, mut value: &[u8]) -> std::fmt::Result {
527    const LOOKUP: &str = "\
528%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f\
529%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f\
530%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f\
531%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f\
532%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f\
533%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f\
534%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f\
535%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f\
536%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f\
537%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f\
538%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af\
539%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf\
540%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf\
541%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df\
542%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef\
543%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff";
544
545    loop {
546        let pos = value.iter().position(
547            |c| !matches!(c, b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'/' | b'.' | b'\\' | b'*'),
548        );
549
550        if let Some(pos) = pos {
551            // SAFETY: The above `position()` call made sure that only ASCII chars are in the string
552            // up to `pos`
553            f.write_str(unsafe { from_utf8_unchecked(&value[..pos]) })?;
554
555            let c = value[pos];
556            value = &value[pos + 1..];
557
558            let pos = c as usize * 3;
559            f.write_str(&LOOKUP[pos..pos + 3])?;
560        } else {
561            // SAFETY: The above `position()` call made sure that only ASCII chars are in the rest
562            // of the string
563            f.write_str(unsafe { from_utf8_unchecked(value) })?;
564            return Ok(());
565        }
566    }
567}
568
569impl Display for Address {
570    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
571        match self {
572            Self::Tcp(addr) => {
573                f.write_str("tcp:")?;
574                addr.write_options(f)?;
575            }
576
577            Self::NonceTcp { addr, nonce_file } => {
578                f.write_str("nonce-tcp:noncefile=")?;
579                encode_percents(f, nonce_file)?;
580                f.write_str(",")?;
581                addr.write_options(f)?;
582            }
583
584            Self::Unix(path) => {
585                #[cfg(unix)]
586                {
587                    use std::os::unix::ffi::OsStrExt;
588                    f.write_str("unix:path=")?;
589                    encode_percents(f, path.as_bytes())?;
590                }
591
592                #[cfg(windows)]
593                write!(f, "unix:path={}", path.to_str().ok_or(std::fmt::Error)?)?;
594            }
595
596            #[cfg(any(
597                all(feature = "vsock", not(feature = "tokio")),
598                feature = "tokio-vsock"
599            ))]
600            Self::Vsock(addr) => {
601                write!(f, "vsock:cid={},port={}", addr.cid, addr.port)?;
602            }
603
604            Self::Autolaunch(scope) => {
605                write!(f, "autolaunch:")?;
606                if let Some(scope) = scope {
607                    write!(f, "scope={scope}")?;
608                }
609            }
610
611            Self::Launchd(env) => {
612                write!(f, "launchd:env={}", env)?;
613            }
614        }
615
616        Ok(())
617    }
618}
619
620impl FromStr for Address {
621    type Err = Error;
622
623    /// Parse a D-BUS address and return its path if we recognize it
624    fn from_str(address: &str) -> Result<Self> {
625        let col = address
626            .find(':')
627            .ok_or_else(|| Error::Address("address has no colon".to_owned()))?;
628        let transport = &address[..col];
629        let mut options = HashMap::new();
630
631        if address.len() > col + 1 {
632            for kv in address[col + 1..].split(',') {
633                let (k, v) = match kv.find('=') {
634                    Some(eq) => (&kv[..eq], &kv[eq + 1..]),
635                    None => {
636                        return Err(Error::Address(
637                            "missing = when parsing key/value".to_owned(),
638                        ))
639                    }
640                };
641                if options.insert(k, v).is_some() {
642                    return Err(Error::Address(format!(
643                        "Key `{k}` specified multiple times"
644                    )));
645                }
646            }
647        }
648
649        match transport {
650            #[cfg(any(unix, not(feature = "tokio")))]
651            "unix" => Self::from_unix(options),
652            "tcp" => TcpAddress::from_tcp(options).map(Self::Tcp),
653
654            "nonce-tcp" => Ok(Self::NonceTcp {
655                nonce_file: decode_percents(
656                    options
657                        .get("noncefile")
658                        .ok_or_else(|| Error::Address("missing nonce file parameter".into()))?,
659                )?,
660                addr: TcpAddress::from_tcp(options)?,
661            }),
662            #[cfg(all(feature = "vsock", not(feature = "tokio")))]
663            "vsock" => Self::from_vsock(options),
664            "autolaunch" => Ok(Self::Autolaunch(
665                options
666                    .get("scope")
667                    .map(|scope| -> Result<_> {
668                        String::from_utf8(decode_percents(scope)?).map_err(|_| {
669                            Error::Address("autolaunch scope is not valid UTF-8".to_owned())
670                        })
671                    })
672                    .transpose()?,
673            )),
674            "launchd" => Ok(Self::Launchd(
675                options
676                    .get("env")
677                    .ok_or_else(|| Error::Address("missing env key".into()))?
678                    .to_string(),
679            )),
680
681            _ => Err(Error::Address(format!(
682                "unsupported transport '{transport}'"
683            ))),
684        }
685    }
686}
687
688impl TryFrom<&str> for Address {
689    type Error = Error;
690
691    fn try_from(value: &str) -> Result<Self> {
692        Self::from_str(value)
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::Address;
699    use crate::{Error, TcpAddress, TcpAddressFamily};
700    use std::str::FromStr;
701    use test_log::test;
702
703    #[test]
704    fn parse_dbus_addresses() {
705        match Address::from_str("").unwrap_err() {
706            Error::Address(e) => assert_eq!(e, "address has no colon"),
707            _ => panic!(),
708        }
709        match Address::from_str("foo").unwrap_err() {
710            Error::Address(e) => assert_eq!(e, "address has no colon"),
711            _ => panic!(),
712        }
713        match Address::from_str("foo:opt").unwrap_err() {
714            Error::Address(e) => assert_eq!(e, "missing = when parsing key/value"),
715            _ => panic!(),
716        }
717        match Address::from_str("foo:opt=1,opt=2").unwrap_err() {
718            Error::Address(e) => assert_eq!(e, "Key `opt` specified multiple times"),
719            _ => panic!(),
720        }
721        match Address::from_str("tcp:host=localhost").unwrap_err() {
722            Error::Address(e) => assert_eq!(e, "tcp address is missing `port`"),
723            _ => panic!(),
724        }
725        match Address::from_str("tcp:host=localhost,port=32f").unwrap_err() {
726            Error::Address(e) => assert_eq!(e, "invalid tcp `port`"),
727            _ => panic!(),
728        }
729        match Address::from_str("tcp:host=localhost,port=123,family=ipv7").unwrap_err() {
730            Error::Address(e) => assert_eq!(e, "invalid tcp address `family`: ipv7"),
731            _ => panic!(),
732        }
733        match Address::from_str("unix:foo=blah").unwrap_err() {
734            Error::Address(e) => assert_eq!(e, "unix address is missing path or abstract"),
735            _ => panic!(),
736        }
737        match Address::from_str("unix:path=/tmp,abstract=foo").unwrap_err() {
738            Error::Address(e) => {
739                assert_eq!(e, "`path` and `abstract` cannot be specified together")
740            }
741            _ => panic!(),
742        }
743        assert_eq!(
744            Address::Unix("/tmp/dbus-foo".into()),
745            Address::from_str("unix:path=/tmp/dbus-foo").unwrap()
746        );
747        assert_eq!(
748            Address::Unix("/tmp/dbus-foo".into()),
749            Address::from_str("unix:path=/tmp/dbus-foo,guid=123").unwrap()
750        );
751        assert_eq!(
752            Address::Tcp(TcpAddress {
753                host: "localhost".into(),
754                port: 4142,
755                bind: None,
756                family: None
757            }),
758            Address::from_str("tcp:host=localhost,port=4142").unwrap()
759        );
760        assert_eq!(
761            Address::Tcp(TcpAddress {
762                host: "localhost".into(),
763                port: 4142,
764                bind: None,
765                family: Some(TcpAddressFamily::Ipv4)
766            }),
767            Address::from_str("tcp:host=localhost,port=4142,family=ipv4").unwrap()
768        );
769        assert_eq!(
770            Address::Tcp(TcpAddress {
771                host: "localhost".into(),
772                port: 4142,
773                bind: None,
774                family: Some(TcpAddressFamily::Ipv6)
775            }),
776            Address::from_str("tcp:host=localhost,port=4142,family=ipv6").unwrap()
777        );
778        assert_eq!(
779            Address::Tcp(TcpAddress {
780                host: "localhost".into(),
781                port: 4142,
782                bind: None,
783                family: Some(TcpAddressFamily::Ipv6)
784            }),
785            Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path")
786                .unwrap()
787        );
788        assert_eq!(
789            Address::NonceTcp {
790                addr: TcpAddress {
791                    host: "localhost".into(),
792                    port: 4142,
793                    bind: None,
794                    family: Some(TcpAddressFamily::Ipv6),
795                },
796                nonce_file: b"/a/file/path to file 1234".to_vec()
797            },
798            Address::from_str(
799                "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234"
800            )
801            .unwrap()
802        );
803        assert_eq!(
804            Address::Autolaunch(None),
805            Address::from_str("autolaunch:").unwrap()
806        );
807        assert_eq!(
808            Address::Autolaunch(Some("*my_cool_scope*".to_owned())),
809            Address::from_str("autolaunch:scope=*my_cool_scope*").unwrap()
810        );
811        assert_eq!(
812            Address::Launchd("my_cool_env_key".to_owned()),
813            Address::from_str("launchd:env=my_cool_env_key").unwrap()
814        );
815
816        #[cfg(all(feature = "vsock", not(feature = "tokio")))]
817        assert_eq!(
818            Address::Vsock(crate::VsockAddress {
819                cid: 98,
820                port: 2934
821            }),
822            Address::from_str("vsock:cid=98,port=2934,guid=123").unwrap()
823        );
824    }
825
826    #[test]
827    fn stringify_dbus_addresses() {
828        assert_eq!(
829            Address::Unix("/tmp/dbus-foo".into()).to_string(),
830            "unix:path=/tmp/dbus-foo"
831        );
832        assert_eq!(
833            Address::Tcp(TcpAddress {
834                host: "localhost".into(),
835                port: 4142,
836                bind: None,
837                family: None
838            })
839            .to_string(),
840            "tcp:host=localhost,port=4142"
841        );
842        assert_eq!(
843            Address::Tcp(TcpAddress {
844                host: "localhost".into(),
845                port: 4142,
846                bind: None,
847                family: Some(TcpAddressFamily::Ipv4)
848            })
849            .to_string(),
850            "tcp:host=localhost,port=4142,family=ipv4"
851        );
852        assert_eq!(
853            Address::Tcp(TcpAddress {
854                host: "localhost".into(),
855                port: 4142,
856                bind: None,
857                family: Some(TcpAddressFamily::Ipv6)
858            })
859            .to_string(),
860            "tcp:host=localhost,port=4142,family=ipv6"
861        );
862        assert_eq!(
863            Address::NonceTcp {
864                addr: TcpAddress {
865                    host: "localhost".into(),
866                    port: 4142,
867                    bind: None,
868                    family: Some(TcpAddressFamily::Ipv6),
869                },
870                nonce_file: b"/a/file/path to file 1234".to_vec()
871            }
872            .to_string(),
873            "nonce-tcp:noncefile=/a/file/path%20to%20file%201234,host=localhost,port=4142,family=ipv6"
874        );
875        assert_eq!(Address::Autolaunch(None).to_string(), "autolaunch:");
876        assert_eq!(
877            Address::Autolaunch(Some("*my_cool_scope*".to_owned())).to_string(),
878            "autolaunch:scope=*my_cool_scope*"
879        );
880        assert_eq!(
881            Address::Launchd("my_cool_key".to_owned()).to_string(),
882            "launchd:env=my_cool_key"
883        );
884
885        #[cfg(all(feature = "vsock", not(feature = "tokio")))]
886        assert_eq!(
887            Address::Vsock(crate::VsockAddress {
888                cid: 98,
889                port: 2934
890            })
891            .to_string(),
892            "vsock:cid=98,port=2934,guid=123",
893        );
894    }
895
896    #[test]
897    fn connect_tcp() {
898        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
899        let port = listener.local_addr().unwrap().port();
900        let addr = Address::from_str(&format!("tcp:host=localhost,port={port}")).unwrap();
901        crate::utils::block_on(async { addr.connect().await }).unwrap();
902    }
903
904    #[test]
905    fn connect_nonce_tcp() {
906        struct PercentEncoded<'a>(&'a [u8]);
907
908        impl std::fmt::Display for PercentEncoded<'_> {
909            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
910                super::encode_percents(f, self.0)
911            }
912        }
913
914        use std::io::Write;
915
916        const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE";
917
918        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
919        let port = listener.local_addr().unwrap().port();
920
921        let mut cookie = tempfile::NamedTempFile::new().unwrap();
922        cookie.as_file_mut().write_all(TEST_COOKIE).unwrap();
923
924        let encoded_path = format!(
925            "{}",
926            PercentEncoded(cookie.path().to_str().unwrap().as_ref())
927        );
928
929        let addr = Address::from_str(&format!(
930            "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}"
931        ))
932        .unwrap();
933
934        let (sender, receiver) = std::sync::mpsc::sync_channel(1);
935
936        std::thread::spawn(move || {
937            use std::io::Read;
938
939            let mut client = listener.incoming().next().unwrap().unwrap();
940
941            let mut buf = [0u8; 16];
942            client.read_exact(&mut buf).unwrap();
943
944            sender.send(buf == TEST_COOKIE).unwrap();
945        });
946
947        crate::utils::block_on(addr.connect()).unwrap();
948
949        let saw_cookie = receiver
950            .recv_timeout(std::time::Duration::from_millis(100))
951            .expect("nonce file content hasn't been received by server thread in time");
952
953        assert!(
954            saw_cookie,
955            "nonce file content has been received, but was invalid"
956        );
957    }
958}