use std::collections::VecDeque;
use std::ffi::c_int;
use crate::ClipboardError;
use super::wayland_wire::{MessageHeader, parse_message_header};
pub struct WaylandSocket {
fd: c_int,
rx_buf: VecDeque<u8>,
rx_fds: VecDeque<c_int>,
}
impl WaylandSocket {
pub(crate) fn connect() -> Result<Self, ClipboardError> {
let socket_path = wayland_socket_path()?;
connect_to_path(&socket_path)
}
pub(crate) fn send(&self, bytes: &[u8], fds: &[c_int]) -> Result<(), ClipboardError> {
if bytes.is_empty() {
return Ok(());
}
if fds.is_empty() {
send_plain(self.fd, bytes)
} else {
send_with_fds(self.fd, bytes, fds)
}
}
pub(crate) fn recv(&mut self, blocking: bool) -> Result<(), ClipboardError> {
recv_into(self.fd, &mut self.rx_buf, &mut self.rx_fds, blocking)
}
pub(crate) fn next_message(&mut self) -> Option<(MessageHeader, Vec<u8>)> {
let contiguous = self.rx_buf.make_contiguous();
let (hdr, _) = parse_message_header(contiguous)?;
let total = hdr.size as usize;
if total < 8 {
let drop = 8.min(self.rx_buf.len());
self.rx_buf.drain(..drop);
return None;
}
if self.rx_buf.len() < total {
return None;
}
let msg_bytes: Vec<u8> = self.rx_buf.drain(..total).collect();
let args = msg_bytes[8..].to_vec();
Some((hdr, args))
}
pub(crate) fn next_fd(&mut self) -> Option<c_int> {
self.rx_fds.pop_front()
}
pub(crate) fn raw_fd(&self) -> c_int {
self.fd
}
#[cfg(test)]
pub(crate) unsafe fn from_raw_fd(fd: c_int) -> Self {
Self {
fd,
rx_buf: std::collections::VecDeque::new(),
rx_fds: std::collections::VecDeque::new(),
}
}
}
impl Drop for WaylandSocket {
fn drop(&mut self) {
for fd in self.rx_fds.drain(..) {
unsafe { libc::close(fd) };
}
unsafe { libc::close(self.fd) };
}
}
fn wayland_socket_path() -> Result<String, ClipboardError> {
let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_owned());
if display.starts_with('/') {
return Ok(display);
}
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
let uid = unsafe { libc::getuid() };
format!("/run/user/{uid}")
});
Ok(format!("{runtime_dir}/{display}"))
}
fn connect_to_path(path: &str) -> Result<WaylandSocket, ClipboardError> {
if path.len() >= 108 {
return Err(ClipboardError::io_other("Wayland socket path too long"));
}
let fd = unsafe { libc::socket(libc::AF_UNIX, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0) };
if fd < 0 {
return Err(ClipboardError::io(std::io::Error::last_os_error()));
}
let mut addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
addr.sun_family = libc::AF_UNIX as libc::sa_family_t;
let path_bytes = path.as_bytes();
unsafe {
std::ptr::copy_nonoverlapping(
path_bytes.as_ptr() as *const libc::c_char,
addr.sun_path.as_mut_ptr(),
path_bytes.len(),
);
}
let addr_len = (std::mem::offset_of!(libc::sockaddr_un, sun_path) + path_bytes.len() + 1)
as libc::socklen_t;
let rc = unsafe {
libc::connect(
fd,
&addr as *const libc::sockaddr_un as *const libc::sockaddr,
addr_len,
)
};
if rc != 0 {
unsafe { libc::close(fd) };
return Err(ClipboardError::NoDisplay);
}
Ok(WaylandSocket {
fd,
rx_buf: VecDeque::new(),
rx_fds: VecDeque::new(),
})
}
fn send_plain(fd: c_int, bytes: &[u8]) -> Result<(), ClipboardError> {
let mut sent = 0;
while sent < bytes.len() {
let n = unsafe {
libc::send(
fd,
bytes[sent..].as_ptr() as *const libc::c_void,
bytes.len() - sent,
libc::MSG_NOSIGNAL,
)
};
if n < 0 {
return Err(ClipboardError::io(std::io::Error::last_os_error()));
}
sent += n as usize;
}
Ok(())
}
fn send_with_fds(fd: c_int, bytes: &[u8], fds: &[c_int]) -> Result<(), ClipboardError> {
let cmsg_space =
unsafe { libc::CMSG_SPACE(std::mem::size_of_val(fds) as libc::c_uint) } as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let hdr_size = unsafe { libc::CMSG_LEN(0) } as usize;
let fds_bytes = std::mem::size_of_val(fds);
if fds_bytes + hdr_size > cmsg_space {
return Err(ClipboardError::io_other("too many fds for CMSG buffer"));
}
let mut iov = libc::iovec {
iov_base: bytes.as_ptr() as *mut libc::c_void,
iov_len: bytes.len(),
};
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
#[allow(clippy::useless_conversion)]
{
msg.msg_controllen = cmsg_space
.try_into()
.expect("cmsg_space fits in msg_controllen");
}
let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
if cmsg.is_null() {
return Err(ClipboardError::io_other("CMSG_FIRSTHDR returned null"));
}
unsafe {
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of_val(fds) as libc::c_uint) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut c_int;
std::ptr::copy_nonoverlapping(fds.as_ptr(), data_ptr, fds.len());
}
let n = unsafe { libc::sendmsg(fd, &msg, libc::MSG_NOSIGNAL) };
if n < 0 {
return Err(ClipboardError::io(std::io::Error::last_os_error()));
}
let sent = n as usize;
if sent < bytes.len() {
send_plain(fd, &bytes[sent..])?;
}
Ok(())
}
const MAX_FDS_PER_RECV: usize = 8;
const RECV_BUF_SIZE: usize = 4096;
fn recv_into(
fd: c_int,
rx_buf: &mut VecDeque<u8>,
rx_fds: &mut VecDeque<c_int>,
blocking: bool,
) -> Result<(), ClipboardError> {
let mut data_buf = [0u8; RECV_BUF_SIZE];
let cmsg_space = unsafe {
libc::CMSG_SPACE((MAX_FDS_PER_RECV * std::mem::size_of::<c_int>()) as libc::c_uint)
} as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let mut iov = libc::iovec {
iov_base: data_buf.as_mut_ptr() as *mut libc::c_void,
iov_len: data_buf.len(),
};
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
#[allow(clippy::useless_conversion)]
{
msg.msg_controllen = cmsg_space
.try_into()
.expect("cmsg_space fits in msg_controllen");
}
let flags = if blocking { 0 } else { libc::MSG_DONTWAIT };
let n = unsafe { libc::recvmsg(fd, &mut msg, flags) };
if n < 0 {
let err = std::io::Error::last_os_error();
if !blocking
&& (err.raw_os_error() == Some(libc::EAGAIN)
|| err.raw_os_error() == Some(libc::EWOULDBLOCK))
{
return Ok(());
}
return Err(ClipboardError::io(err));
}
if n == 0 {
return Err(ClipboardError::io_other("Wayland socket closed"));
}
rx_buf.extend(&data_buf[..n as usize]);
let control_len = msg.msg_controllen as usize;
let mut cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
while !cmsg.is_null() {
let level = unsafe { (*cmsg).cmsg_level };
let typ = unsafe { (*cmsg).cmsg_type };
if level == libc::SOL_SOCKET && typ == libc::SCM_RIGHTS {
let data = unsafe { libc::CMSG_DATA(cmsg) };
let cmsg_len = unsafe { (*cmsg).cmsg_len } as usize;
let hdr_size = unsafe { libc::CMSG_LEN(0) } as usize;
if cmsg_len < hdr_size {
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
continue;
}
let data_len = cmsg_len - hdr_size;
let n_fds = data_len / std::mem::size_of::<c_int>();
if n_fds == 0 {
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
continue;
}
let data_end = (data as *const c_int).wrapping_add(n_fds);
let buf_end = unsafe { cmsg_buf.as_ptr().add(control_len) } as *const c_int;
if data_end > buf_end {
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
continue;
}
for i in 0..n_fds {
let received_fd = unsafe { *(data as *const c_int).add(i) };
rx_fds.push_back(received_fd);
}
}
cmsg = unsafe { libc::CMSG_NXTHDR(&msg, cmsg) };
}
Ok(())
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[allow(clippy::useless_conversion)]
fn send_raw_fds(sender_fd: c_int, fds: &[c_int]) {
let dummy: [u8; 1] = [0];
let cmsg_space =
unsafe { libc::CMSG_SPACE(std::mem::size_of_val(fds) as libc::c_uint) } as usize;
let mut cmsg_buf = vec![0u8; cmsg_space];
let mut iov = libc::iovec {
iov_base: dummy.as_ptr() as *mut libc::c_void,
iov_len: dummy.len(),
};
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
msg.msg_controllen = cmsg_space.try_into().expect("cmsg_space fits");
let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
assert!(!cmsg.is_null());
unsafe {
(*cmsg).cmsg_level = libc::SOL_SOCKET;
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
(*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of_val(fds) as libc::c_uint) as _;
let data_ptr = libc::CMSG_DATA(cmsg) as *mut c_int;
std::ptr::copy_nonoverlapping(fds.as_ptr(), data_ptr, fds.len());
}
let n = unsafe { libc::sendmsg(sender_fd, &msg, 0) };
assert!(n >= 0, "sendmsg: {}", std::io::Error::last_os_error());
}
#[test]
fn test_send_recv_fds_socketpair() {
let mut fds = [0i32; 2];
let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
assert_eq!(rc, 0, "socketpair: {}", std::io::Error::last_os_error());
let sender_fd = fds[0];
let receiver_fd = fds[1];
let mut receiver = unsafe { WaylandSocket::from_raw_fd(receiver_fd) };
let null0 = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY) };
assert!(
null0 >= 0,
"open /dev/null: {}",
std::io::Error::last_os_error()
);
let null1 = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY) };
assert!(
null1 >= 0,
"open /dev/null: {}",
std::io::Error::last_os_error()
);
let sent_fds: [c_int; 2] = [null0, null1];
send_raw_fds(sender_fd, &sent_fds);
receiver.recv(true).unwrap();
let mut got = Vec::new();
while let Some(fd) = receiver.next_fd() {
got.push(fd);
}
assert_eq!(got.len(), 2);
for fd in &got {
unsafe { libc::close(*fd) };
}
unsafe {
libc::close(sender_fd);
libc::close(null0);
libc::close(null1);
}
}
#[test]
fn test_recv_no_ancillary_data() {
let mut fds = [0i32; 2];
let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
assert_eq!(rc, 0);
let sender = fds[0];
let receiver_fd = fds[1];
let mut receiver = unsafe { WaylandSocket::from_raw_fd(receiver_fd) };
let byte: [u8; 1] = [0x42];
let n = unsafe { libc::send(sender, byte.as_ptr() as *const libc::c_void, 1, 0) };
assert_eq!(n, 1);
receiver.recv(true).unwrap();
assert_eq!(receiver.next_fd(), None);
assert_eq!(receiver.rx_buf.len(), 1);
assert_eq!(receiver.rx_buf[0], 0x42);
unsafe { libc::close(sender) };
}
#[test]
fn test_connect_path_too_long() {
let long_path = "/".repeat(108);
let result = connect_to_path(&long_path);
match result {
Err(ClipboardError::Io(e)) => {
assert!(e.to_string().contains("too long") || e.to_string().contains("108"))
}
_ => panic!(
"expected ClipboardError::Io, got: {:?}",
result.as_ref().err()
),
}
}
#[test]
fn test_next_message_rejects_small_size() {
let mut socket = WaylandSocket {
fd: -1,
rx_buf: VecDeque::new(),
rx_fds: VecDeque::new(),
};
socket.rx_buf.extend(&[0, 0, 0, 0, 0, 0, 4, 0]);
let result = socket.next_message();
assert!(result.is_none());
assert!(socket.rx_buf.is_empty());
socket.fd = -1;
std::mem::forget(socket);
}
}