diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6e40972..97c87acc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ platforms the `all` feature is used, to indicate to the user that they're using API that might is not available on all platforms. The main `Socket` type is defined in `src/socket.rs` with additional methods -defined on in the the `src/sys/*.rs` files, as per above. The methods on +defined in the `src/sys/*.rs` files, as per above. The methods on `Socket` are split into multiple `impl` blocks. The first `impl` block contains a collection of system calls for creating and using the socket, e.g. `socket(2)`, `bind(2)`, `listen(2)`, etc. The other implementation blocks are @@ -51,7 +51,7 @@ such as `Socket::freebind` which is (at the time of writing) only available on Android, Linux and Fuchsia, which is defined in the `src/sys/*.rs` files. Other types are mostly defined in `src/lib.rs`, except for `SockAddr` and -`SockRef` which have there own file. These types follow the same structure as +`SockRef` which have their own file. These types follow the same structure as `Socket`, where OS specific methods are defined in `src/sys/*.rs`, e.g. `Type::cloexec`. diff --git a/src/sockaddr.rs b/src/sockaddr.rs index 0c9c2a52..fbedfe99 100644 --- a/src/sockaddr.rs +++ b/src/sockaddr.rs @@ -9,7 +9,7 @@ use windows_sys::Win32::Networking::WinSock::SOCKADDR_IN6_0; use crate::sys::{ c_int, sa_family_t, sockaddr, sockaddr_in, sockaddr_in6, sockaddr_storage, socklen_t, AF_INET, - AF_INET6, + AF_INET6, AF_UNIX, }; use crate::Domain; @@ -184,6 +184,12 @@ impl SockAddr { self.storage.ss_family == AF_INET6 as sa_family_t } + /// Returns true if this address is of a unix socket (for local interprocess communication), + /// i.e. it is from the `AF_UNIX` family, false otherwise. + pub fn is_unix(&self) -> bool { + self.storage.ss_family == AF_UNIX as sa_family_t + } + /// Returns a raw pointer to the address storage. #[cfg(all(unix, not(target_os = "redox")))] pub(crate) const fn as_storage_ptr(&self) -> *const sockaddr_storage { @@ -351,6 +357,8 @@ mod tests { let std = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876); let addr = SockAddr::from(std); assert!(addr.is_ipv4()); + assert!(!addr.is_ipv6()); + assert!(!addr.is_unix()); assert_eq!(addr.family(), AF_INET as sa_family_t); assert_eq!(addr.domain(), Domain::IPV4); assert_eq!(addr.len(), size_of::() as socklen_t); @@ -364,6 +372,11 @@ mod tests { assert_eq!(addr.as_socket(), Some(SocketAddr::V4(std))); assert_eq!(addr.as_socket_ipv4(), Some(std)); assert!(addr.as_socket_ipv6().is_none()); + #[cfg(unix)] + { + assert!(addr.as_pathname().is_none()); + assert!(addr.as_abstract_namespace().is_none()); + } } #[test] @@ -372,6 +385,8 @@ mod tests { let std = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12); let addr = SockAddr::from(std); assert!(addr.is_ipv6()); + assert!(!addr.is_ipv4()); + assert!(!addr.is_unix()); assert_eq!(addr.family(), AF_INET6 as sa_family_t); assert_eq!(addr.domain(), Domain::IPV6); assert_eq!(addr.len(), size_of::() as socklen_t); @@ -385,6 +400,11 @@ mod tests { assert_eq!(addr.as_socket(), Some(SocketAddr::V6(std))); assert!(addr.as_socket_ipv4().is_none()); assert_eq!(addr.as_socket_ipv6(), Some(std)); + #[cfg(unix)] + { + assert!(addr.as_pathname().is_none()); + assert!(addr.as_abstract_namespace().is_none()); + } } #[test] diff --git a/src/sys/unix.rs b/src/sys/unix.rs index 6c4dd363..102676ea 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -7,6 +7,7 @@ // except according to those terms. use std::cmp::min; +use std::ffi::OsStr; #[cfg(not(target_os = "redox"))] use std::io::IoSlice; use std::marker::PhantomData; @@ -569,6 +570,13 @@ impl<'a> MaybeUninitSlice<'a> { } } +/// Returns the offset of the `sun_path` member of the passed unix socket address. +pub(crate) fn offset_of_path(storage: &libc::sockaddr_un) -> usize { + let base = storage as *const _ as usize; + let path = ptr::addr_of!(storage.sun_path) as usize; + path - base +} + #[allow(unsafe_op_in_unsafe_fn)] pub(crate) fn unix_sockaddr(path: &Path) -> io::Result { // SAFETY: a `sockaddr_storage` of all zeros is valid. @@ -603,9 +611,7 @@ pub(crate) fn unix_sockaddr(path: &Path) -> io::Result { ); } - let base = storage as *const _ as usize; - let path = ptr::addr_of!(storage.sun_path) as usize; - let sun_path_offset = path - base; + let sun_path_offset = offset_of_path(storage); sun_path_offset + bytes.len() + match bytes.first() { @@ -659,6 +665,91 @@ impl SockAddr { None } } + + /// Returns true if this address is an unnamed address from the `AF_UNIX` family (for local + /// interprocess communication), false otherwise. + pub fn is_unnamed(&self) -> bool { + self.as_sockaddr_un() + .map(|storage| { + self.len() == offset_of_path(storage) as u32 + // On some non-linux platforms a zeroed path is returned for unnamed. + // Abstract addresses only exist on Linux. + // NOTE: although Fuchsia does define `AF_UNIX` it's not actually implemented. + // See https://github.com/rust-lang/socket2/pull/403#discussion_r1123557978 + || (cfg!(not(any(target_os = "linux", target_os = "android"))) + && storage.sun_path[0] == 0) + }) + .unwrap_or_default() + } + + /// Returns the underlying `sockaddr_un` object if this addres is from the `AF_UNIX` family, + /// otherwise returns `None`. + pub(crate) fn as_sockaddr_un(&self) -> Option<&libc::sockaddr_un> { + self.is_unix().then(|| { + // SAFETY: if unix socket, i.e. the `ss_family` field is `AF_UNIX` then storage must be + // a `sockaddr_un`. + unsafe { &*self.as_ptr().cast::() } + }) + } + + /// Get the length of the path bytes of the address, not including the terminating or initial + /// (for abstract names) null byte. + /// + /// Should not be called on unnamed addresses. + fn path_len(&self, storage: &libc::sockaddr_un) -> usize { + debug_assert!(!self.is_unnamed()); + self.len() as usize - offset_of_path(storage) - 1 + } + + /// Get a u8 slice for the bytes of the pathname or abstract name. + /// + /// Should not be called on unnamed addresses. + fn path_bytes(&self, storage: &libc::sockaddr_un, abstract_name: bool) -> &[u8] { + debug_assert!(!self.is_unnamed()); + // SAFETY: the pointed objects of type `i8` have the same memory layout as `u8`. The path is + // the last field in the storage and so its length is equal to + // TOTAL_LENGTH - OFFSET_OF_PATH -1 + // Where the 1 is either a terminating null if we have a pathname address, or the initial + // null byte, if it's an abstract name address. In the latter case, the path bytes start + // after the initial null byte, hence the `offset`. + // There is no safe way to convert a `&[i8]` to `&[u8]` + unsafe { + slice::from_raw_parts( + (storage.sun_path.as_ptr() as *const u8).offset(abstract_name as isize), + self.path_len(storage), + ) + } + } + + /// Returns this address as a `Path` reference if it is an `AF_UNIX` pathname address, otherwise + /// returns `None`. + pub fn as_pathname(&self) -> Option<&Path> { + self.as_sockaddr_un().and_then(|storage| { + (self.len() > offset_of_path(storage) as u32 && storage.sun_path[0] != 0).then(|| { + let path_slice = self.path_bytes(storage, false); + Path::new::(OsStrExt::from_bytes(path_slice)) + }) + }) + } + + /// Returns this address as a slice of bytes representing an abstract address if it is an + /// `AF_UNIX` abstract address, otherwise returns `None`. + /// + /// Abstract addresses are a Linux extension, so this method returns `None` on all non-Linux + /// platforms. + pub fn as_abstract_namespace(&self) -> Option<&[u8]> { + // NOTE: although Fuchsia does define `AF_UNIX` it's not actually implemented. + // See https://github.com/rust-lang/socket2/pull/403#discussion_r1123557978 + #[cfg(any(target_os = "linux", target_os = "android"))] + { + self.as_sockaddr_un().and_then(|storage| { + (self.len() > offset_of_path(storage) as u32 && storage.sun_path[0] == 0) + .then(|| self.path_bytes(storage, true)) + }) + } + #[cfg(not(any(target_os = "linux", target_os = "android")))] + None + } } pub(crate) type Socket = c_int; diff --git a/tests/socket.rs b/tests/socket.rs index 35b90a52..33b92f84 100644 --- a/tests/socket.rs +++ b/tests/socket.rs @@ -38,6 +38,8 @@ use std::num::NonZeroUsize; use std::os::unix::io::AsRawFd; #[cfg(windows)] use std::os::windows::io::AsRawSocket; +#[cfg(unix)] +use std::path::Path; use std::str; use std::thread; use std::time::Duration; @@ -144,17 +146,49 @@ fn socket_address_unix() { let addr = SockAddr::unix(string).unwrap(); assert!(addr.as_socket_ipv4().is_none()); assert!(addr.as_socket_ipv6().is_none()); + assert!(!addr.is_ipv4()); + assert!(!addr.is_ipv6()); + assert!(addr.is_unix()); + assert_eq!(addr.domain(), Domain::UNIX); + #[cfg(unix)] + { + assert!(!addr.is_unnamed()); + assert_eq!(addr.as_pathname(), Some(Path::new(string))); + assert_eq!(addr.as_abstract_namespace(), None); + } +} + +#[test] +fn socket_address_unix_unnamed() { + let addr = SockAddr::unix("").unwrap(); + assert!(addr.as_socket_ipv4().is_none()); + assert!(addr.as_socket_ipv6().is_none()); + assert!(!addr.is_ipv4()); + assert!(!addr.is_ipv6()); + assert!(addr.is_unix()); + assert_eq!(addr.domain(), Domain::UNIX); + #[cfg(unix)] + { + assert!(addr.is_unnamed()); + assert_eq!(addr.as_pathname(), None); + assert_eq!(addr.as_abstract_namespace(), None); + } } #[test] #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "all"))] fn socket_address_unix_abstract_namespace() { let path = "\0h".repeat(108 / 2); - let addr = SockAddr::unix(path).unwrap(); + let addr = SockAddr::unix(&path).unwrap(); assert_eq!( addr.len() as usize, std::mem::size_of::() ); + assert!(!addr.is_unnamed()); + // The first byte is the opening null bytes of an abstract address, should not be included. + assert_eq!(addr.as_abstract_namespace(), Some(&path.as_bytes()[1..])); + assert!(addr.as_pathname().is_none()); + assert!(!addr.is_unnamed()); } #[test]