Repository: rust-lang/socket2
Pull request #403 (2023-02-27 17:50:40)

Match the standard library's interface for unix sockets on unix.
Following #332, currently unix only. Should this interface also be made available on Windows?

This PR adds to `SockAddr` methods to get the path of the unix socket address on unix.
It also adds the new methods to the tests.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8bb59e..4e64355 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,6 @@
 * Generate documentation for more targets on docs.rs
   (https://github.com/rust-lang/socket2/pull/398).
 
-
 ## Fixed
 
 * Generatation of documentation on docs.rs
@@ -92,6 +91,18 @@
 * Support for AIX
   (https://github.com/rust-lang/socket2/pull/351).
 
+# 0.4.9
+
+* Fixed compilation on Windows
+  (https://github.com/rust-lang/socket2/pull/409).
+
+# 0.4.8 (yanked)
+
+This release was broken for Windows.
+
+* Add `Socket::peek_sender` (backport)
+  (https://github.com/rust-lang/socket2/pull/404).
+
 # 0.4.7
 
 * Fixes compilation on OpenBSD
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a6e4097..97c87ac 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 0c9c2a5..fbedfe9 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::<sockaddr_in>() 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::<sockaddr_in6>() 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 6c4dd36..102676e 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<SockAddr> {
     // SAFETY: a `sockaddr_storage` of all zeros is valid.
@@ -603,9 +611,7 @@ pub(crate) fn unix_sockaddr(path: &Path) -> io::Result<SockAddr> {
             );
         }
 
-        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::<libc::sockaddr_un>() }
+        })
+    }
+
+    /// 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::<OsStr>(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;


Test patch:
diff --git a/tests/socket.rs b/tests/socket.rs
index 35b90a5..33b92f8 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::<libc::sockaddr_un>()
     );
+    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]