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: Chars<'_> = value.chars();
503 let mut decoded: Vec = Vec::new();
504
505 while let Some(c: char) = 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}
959