use std::fmt;
use std::io::{ErrorKind, Read, Write};
use super::{RpcError, RpcResult};
mod mem;
#[cfg(feature = "rpc-tcp-debug")]
mod tcp_debug;
#[cfg(feature = "rpc-tls")]
mod tls;
pub(crate) mod unix;
#[cfg(all(feature = "rpc-vsock", any(target_os = "linux", target_os = "android")))]
mod vsock;
pub use mem::MemTransport;
#[cfg(feature = "rpc-tcp-debug")]
pub use tcp_debug::{insecure_warning_emitted, TcpDebugTransport};
#[cfg(feature = "rpc-tls")]
pub use tls::{TlsStream, TlsTransport};
pub use unix::UnixTransport;
#[cfg(all(feature = "rpc-vsock", any(target_os = "linux", target_os = "android")))]
pub use vsock::VsockTransport;
pub const MAX_FRAME_LEN: usize = 64 * 1024 * 1024;
pub trait RpcTransport: Send + Sync {
fn send_frame(&self, buf: &[u8]) -> RpcResult<()>;
fn recv_frame(&self) -> RpcResult<Vec<u8>>;
fn peer_identity(&self) -> PeerIdentity;
fn describe(&self) -> &str;
fn set_read_timeout(&self, _timeout: Option<std::time::Duration>) -> RpcResult<()> {
Ok(())
}
fn set_write_timeout(&self, _timeout: Option<std::time::Duration>) -> RpcResult<()> {
Ok(())
}
fn set_liveness(&self, _timeout: Option<std::time::Duration>) -> RpcResult<()> {
Ok(())
}
fn peer_closed(&self) -> Option<bool> {
None
}
fn shutdown_handle(&self) -> Option<Box<dyn FnOnce() + Send>> {
None
}
fn shutdown(&self) -> RpcResult<()>;
fn supports_fd_passing(&self) -> bool {
false
}
fn send_frame_with_fds(
&self,
buf: &[u8],
fds: &[std::os::fd::BorrowedFd<'_>],
) -> RpcResult<()> {
if fds.is_empty() {
self.send_frame(buf)
} else {
debug_assert!(!self.supports_fd_passing());
Err(RpcError::Protocol(
"this transport cannot pass file descriptors (UDS only)",
))
}
}
fn recv_frame_with_fds(&self) -> RpcResult<(Vec<u8>, Vec<std::os::fd::OwnedFd>)> {
Ok((self.recv_frame()?, Vec::new()))
}
fn send_raw(&self, _buf: &[u8]) -> RpcResult<()> {
Err(RpcError::Protocol("this transport has no raw byte access"))
}
fn recv_raw(&self, _buf: &mut [u8]) -> RpcResult<usize> {
Err(RpcError::Protocol("this transport has no raw byte access"))
}
fn send_raw_with_fds(&self, buf: &[u8], fds: &[std::os::fd::BorrowedFd<'_>]) -> RpcResult<()> {
if fds.is_empty() {
self.send_raw(buf)
} else {
debug_assert!(!self.supports_fd_passing());
Err(RpcError::Protocol(
"this transport cannot pass file descriptors (UDS only)",
))
}
}
fn recv_raw_with_fds(&self, buf: &mut [u8]) -> RpcResult<(usize, Vec<std::os::fd::OwnedFd>)> {
Ok((self.recv_raw(buf)?, Vec::new()))
}
fn send_raw_draining(
&self,
buf: &[u8],
fds: &[std::os::fd::BorrowedFd<'_>],
drain: &mut dyn FnMut() -> RpcResult<()>,
) -> RpcResult<()> {
let _ = drain;
self.send_raw_with_fds(buf, fds)
}
}
pub(crate) fn read_side_failure(e: RpcError) -> RpcError {
match e {
RpcError::Protocol(_) | RpcError::FrameTooLarge { .. } => {
RpcError::Io(std::io::Error::other(e.to_string()))
}
e => e,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PeerIdentity {
Local {
uid: u32,
pid: i32,
},
Vsock {
cid: u32,
},
Certificate(CertId),
Anonymous,
}
#[derive(Clone, PartialEq, Eq)]
pub struct CertId {
subject: String,
fingerprint: [u8; 32],
}
impl CertId {
pub fn new(subject: impl Into<String>, fingerprint: [u8; 32]) -> Self {
CertId {
subject: subject.into(),
fingerprint,
}
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn fingerprint(&self) -> &[u8; 32] {
&self.fingerprint
}
pub fn fingerprint_hex(&self) -> String {
self.fingerprint
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
}
impl fmt::Debug for CertId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CertId")
.field("subject", &self.subject)
.field("fingerprint", &self.fingerprint_hex())
.finish()
}
}
impl PeerIdentity {
pub fn is_local(&self) -> bool {
matches!(self, PeerIdentity::Local { .. })
}
pub fn uid(&self) -> Option<u32> {
match self {
PeerIdentity::Local { uid, .. } => Some(*uid),
_ => None,
}
}
pub fn pid(&self) -> Option<i32> {
match self {
PeerIdentity::Local { pid, .. } if *pid >= 0 => Some(*pid),
_ => None,
}
}
}
impl fmt::Display for PeerIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PeerIdentity::Local { uid, pid } => write!(f, "local(uid={uid}, pid={pid})"),
PeerIdentity::Vsock { cid } => {
write!(f, "vsock(cid={cid}; routing only, NOT an ACL basis)")
}
PeerIdentity::Certificate(c) => {
write!(
f,
"cert(subject={:?}, sha256={})",
c.subject(),
c.fingerprint_hex()
)
}
PeerIdentity::Anonymous => {
write!(
f,
"anonymous(NO peer identity — access control NOT possible)"
)
}
}
}
}
#[cfg(any(
test,
feature = "rpc-tls",
all(feature = "rpc-vsock", any(target_os = "linux", target_os = "android"))
))]
pub(crate) fn write_frame<W: Write>(w: &mut W, buf: &[u8]) -> RpcResult<()> {
if buf.len() > MAX_FRAME_LEN {
return Err(RpcError::FrameTooLarge {
declared: buf.len(),
max: MAX_FRAME_LEN,
});
}
let mut framed = Vec::with_capacity(4 + buf.len());
framed.extend_from_slice(&(buf.len() as u32).to_le_bytes());
framed.extend_from_slice(buf);
w.write_all(&framed)?;
w.flush()?;
Ok(())
}
pub(crate) fn write_all_reporting<W: Write>(w: &mut W, buf: &[u8]) -> RpcResult<()> {
let mut sent = 0;
while sent < buf.len() {
match w.write(&buf[sent..]) {
Ok(0) => return Err(std::io::Error::from(ErrorKind::WriteZero).into()),
Ok(n) => sent += n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) if sent == 0 && is_timeout(&e) => return Err(RpcError::Timeout),
Err(e) => return Err(RpcError::from(e)),
}
}
Ok(())
}
#[cfg(feature = "rpc-tcp-debug")]
pub(crate) fn write_raw<W: Write>(w: &mut W, buf: &[u8]) -> RpcResult<()> {
write_all_reporting(w, buf)?;
w.flush()?;
Ok(())
}
pub(crate) fn read_raw<R: Read>(r: &mut R, buf: &mut [u8]) -> RpcResult<usize> {
loop {
return match r.read(buf) {
Ok(n) => Ok(n),
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) if is_timeout(&e) => Err(RpcError::Timeout),
Err(e) => Err(RpcError::from(e)),
};
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn queued_bytes(sock: std::os::fd::BorrowedFd<'_>) -> rustix::io::Result<u32> {
use rustix::ioctl::{ioctl, Getter, Opcode};
let n = unsafe {
ioctl(
sock,
Getter::<{ libc::TIOCOUTQ as Opcode }, libc::c_int>::new(),
)
}?;
u32::try_from(n).map_err(|_| rustix::io::Errno::INVAL)
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn queued_bytes(_sock: std::os::fd::BorrowedFd<'_>) -> rustix::io::Result<u32> {
Err(rustix::io::Errno::NOSYS)
}
#[derive(Debug, Default)]
pub(crate) struct OutQueue {
refused: std::sync::atomic::AtomicBool,
}
impl OutQueue {
pub(crate) fn bytes(&self, sock: std::os::fd::BorrowedFd<'_>) -> Option<u32> {
use std::sync::atomic::Ordering::Relaxed;
if self.refused.load(Relaxed) {
return None;
}
match queued_bytes(sock) {
Ok(n) => Some(n),
Err(e) => {
let first = !self.refused.swap(true, Relaxed);
if first && cfg!(any(target_os = "linux", target_os = "android")) {
log::debug!(
"rsbinder RPC: SIOCOUTQ refused ({e}); this socket's deadlines count \
only the bytes a send accepts"
);
}
None
}
}
}
}
pub(crate) fn socket_shutdown_handle(
sock: std::os::fd::BorrowedFd<'_>,
) -> Option<Box<dyn FnOnce() + Send>> {
let dup = match sock.try_clone_to_owned() {
Ok(dup) => dup,
Err(e) => {
log::warn!(
"rsbinder RPC: cannot duplicate a socket for its deadline ({e}); this phase is \
bounded per wait only"
);
return None;
}
};
Some(Box::new(move || {
let _ = rustix::net::shutdown(&dup, rustix::net::Shutdown::Both);
}))
}
pub(crate) fn look_every(d: std::time::Duration) -> std::time::Duration {
const MIN: std::time::Duration = std::time::Duration::from_millis(10);
const MAX: std::time::Duration = std::time::Duration::from_millis(250);
(d / 8).clamp(MIN, MAX)
}
#[derive(Debug, Default)]
pub(crate) struct ReadDeadline {
limit_ns: std::sync::atomic::AtomicU64,
}
impl ReadDeadline {
pub(crate) fn note(&self, timeout: Option<std::time::Duration>) {
let ns = timeout.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
self.limit_ns
.store(ns, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn read<T>(
&self,
sock: Option<std::os::fd::BorrowedFd<'_>>,
queue: &OutQueue,
read: impl FnOnce() -> RpcResult<T>,
) -> RpcResult<T> {
use rustix::event::{poll, PollFd, PollFlags, Timespec};
let ns = self.limit_ns.load(std::sync::atomic::Ordering::Relaxed);
let (Some(sock), true) = (sock, ns != 0) else {
return read();
};
let Some(mut queued) = queue.bytes(sock).filter(|q| *q > 0) else {
return read();
};
let limit = std::time::Duration::from_nanos(ns);
let mut since = std::time::Instant::now();
loop {
let left = match limit.checked_sub(since.elapsed()) {
Some(left) if !left.is_zero() => left,
_ => return Err(RpcError::Timeout),
};
let pause = if queued > 0 {
left.min(look_every(limit))
} else {
left
};
let ts = Timespec {
tv_sec: pause.as_secs().try_into().unwrap_or(i64::MAX),
tv_nsec: pause.subsec_nanos() as _,
};
let mut fds = [PollFd::from_borrowed_fd(sock, PollFlags::IN)];
match poll(&mut fds, Some(&ts)) {
Ok(n) if n > 0 => return read(),
Ok(_) => {
if queued > 0 {
let now = queue.bytes(sock).unwrap_or(queued);
if now < queued {
since = std::time::Instant::now();
}
queued = now;
}
}
Err(rustix::io::Errno::INTR) => {}
Err(e) => return Err(std::io::Error::from(e).into()),
}
}
}
}
fn read_header<R: Read>(r: &mut R, buf: &mut [u8]) -> RpcResult<()> {
let mut filled = 0;
while filled < buf.len() {
match r.read(&mut buf[filled..]) {
Ok(0) => {
return Err(if filled == 0 {
RpcError::EndOfStream
} else {
RpcError::Truncated
});
}
Ok(n) => filled += n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) if is_timeout(&e) => {
return Err(if filled == 0 {
RpcError::Timeout
} else {
RpcError::DeadlineMidFrame
});
}
Err(e) if e.kind() == ErrorKind::UnexpectedEof && filled > 0 => {
return Err(RpcError::Truncated);
}
Err(e) => {
return Err(match RpcError::from(e) {
RpcError::EndOfStream if filled > 0 => RpcError::Truncated,
other => other,
})
}
}
}
Ok(())
}
pub(crate) fn is_timeout(e: &std::io::Error) -> bool {
e.kind() == ErrorKind::WouldBlock
}
#[cfg(any(feature = "rpc-tcp-debug", feature = "rpc-tls"))]
const KEEPALIVE_PROBES: u32 = 3;
#[cfg(any(feature = "rpc-tcp-debug", feature = "rpc-tls"))]
const MAX_KEEPALIVE_SECS: u64 = 32767;
#[derive(Clone, Copy)]
pub(crate) enum SocketKind {
UnixDomain,
#[cfg_attr(
not(any(feature = "rpc-tcp-debug", feature = "rpc-tls", feature = "rpc-vsock")),
allow(
dead_code,
reason = "only the TCP, TLS and vsock transports construct it"
)
)]
TcpOrVsock,
}
pub(crate) fn socket_peer_closed(
fd: std::os::fd::BorrowedFd<'_>,
kind: SocketKind,
) -> Option<bool> {
use rustix::event::{poll, PollFd, PollFlags, Timespec};
#[cfg(any(target_os = "linux", target_os = "android"))]
let (ask, closed) = {
let _ = kind;
(
PollFlags::RDHUP,
PollFlags::RDHUP | PollFlags::HUP | PollFlags::ERR,
)
};
#[cfg(not(any(target_os = "linux", target_os = "android")))]
let (ask, closed) = match kind {
SocketKind::UnixDomain => (PollFlags::HUP, PollFlags::HUP | PollFlags::ERR),
#[cfg(target_vendor = "apple")]
SocketKind::TcpOrVsock => (PollFlags::HUP, PollFlags::HUP | PollFlags::ERR),
#[cfg(not(target_vendor = "apple"))]
SocketKind::TcpOrVsock => return None,
};
let mut fds = [PollFd::from_borrowed_fd(fd, ask)];
let now = Timespec {
tv_sec: 0,
tv_nsec: 0,
};
match poll(&mut fds, Some(&now)) {
Ok(_) => Some(fds[0].revents().intersects(closed)),
Err(_) => None,
}
}
#[cfg(any(feature = "rpc-tcp-debug", feature = "rpc-tls"))]
fn keepalive_intervals(d: std::time::Duration) -> (std::time::Duration, std::time::Duration) {
let secs = |n: u64| std::time::Duration::from_secs(n.clamp(1, MAX_KEEPALIVE_SECS));
(secs(d.as_secs() / 2), secs(d.as_secs() / 6))
}
#[cfg(any(feature = "rpc-tcp-debug", feature = "rpc-tls"))]
pub(crate) fn tcp_liveness(
fd: std::os::fd::BorrowedFd<'_>,
timeout: Option<std::time::Duration>,
) -> std::io::Result<()> {
use rustix::net::sockopt;
let mut first_err: Option<std::io::Error> = None;
let mut apply = |r: rustix::io::Result<()>| {
if let Err(e) = r {
first_err.get_or_insert(e.into());
}
};
apply(sockopt::set_socket_keepalive(fd, true));
#[cfg(any(target_os = "linux", target_os = "android"))]
apply(sockopt::set_tcp_user_timeout(
fd,
timeout.map_or(0, |d| {
u32::try_from(d.as_millis())
.unwrap_or(u32::MAX)
.clamp(1, i32::MAX as u32)
}),
));
if let Some(d) = timeout {
let (idle, interval) = keepalive_intervals(d);
apply(sockopt::set_tcp_keepidle(fd, idle));
apply(sockopt::set_tcp_keepintvl(fd, interval));
apply(sockopt::set_tcp_keepcnt(fd, KEEPALIVE_PROBES));
}
first_err.map_or(Ok(()), Err)
}
pub(crate) fn absorb_already_shut(r: std::io::Result<()>) -> RpcResult<()> {
match r {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::NotConnected => Ok(()),
Err(e) => Err(e.into()),
}
}
fn read_body<R: Read>(r: &mut R, buf: &mut [u8]) -> RpcResult<()> {
let mut filled = 0;
while filled < buf.len() {
match r.read(&mut buf[filled..]) {
Ok(0) => return Err(RpcError::Truncated),
Ok(n) => filled += n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) if is_timeout(&e) => return Err(RpcError::DeadlineMidFrame),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Err(RpcError::Truncated),
Err(e) => {
return Err(match RpcError::from(e) {
RpcError::EndOfStream => RpcError::Truncated,
other => other,
})
}
}
}
Ok(())
}
pub(crate) fn read_frame<R: Read>(r: &mut R) -> RpcResult<Vec<u8>> {
let mut len_buf = [0u8; 4];
read_header(r, &mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
if len > MAX_FRAME_LEN {
return Err(RpcError::FrameTooLarge {
declared: len,
max: MAX_FRAME_LEN,
});
}
let mut body = vec![0u8; len];
read_body(r, &mut body)?;
Ok(body)
}
#[cfg(any(test, feature = "fuzzing"))]
#[doc(hidden)]
pub fn __fuzz_decode_frame(input: &[u8]) -> RpcResult<Vec<u8>> {
read_frame(&mut std::io::Cursor::new(input))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_roundtrip_over_cursor() {
for size in [0usize, 1, 4, 64, 4096, 1 << 20] {
let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let mut buf = Vec::new();
write_frame(&mut buf, &payload).expect("write");
let got = read_frame(&mut std::io::Cursor::new(&buf)).expect("read");
assert_eq!(got, payload, "roundtrip mismatch at size {size}");
}
}
#[test]
fn two_frames_back_to_back_preserve_order() {
let mut buf = Vec::new();
write_frame(&mut buf, b"first").unwrap();
write_frame(&mut buf, b"second").unwrap();
let mut cur = std::io::Cursor::new(&buf);
assert_eq!(read_frame(&mut cur).unwrap(), b"first");
assert_eq!(read_frame(&mut cur).unwrap(), b"second");
}
#[test]
fn hostile_frame_headers_are_rejected_safely() {
let huge = u32::MAX.to_le_bytes();
assert!(matches!(
__fuzz_decode_frame(&huge),
Err(RpcError::FrameTooLarge { .. })
));
let over = ((MAX_FRAME_LEN + 1) as u32).to_le_bytes();
assert!(matches!(
__fuzz_decode_frame(&over),
Err(RpcError::FrameTooLarge { .. })
));
assert!(matches!(
__fuzz_decode_frame(&[]),
Err(RpcError::EndOfStream)
));
assert!(matches!(
__fuzz_decode_frame(&[1, 0]),
Err(RpcError::Truncated)
));
let mut framed = 8u32.to_le_bytes().to_vec();
framed.extend_from_slice(&[1, 2, 3]);
assert!(matches!(
__fuzz_decode_frame(&framed),
Err(RpcError::Truncated)
));
let zeros = vec![0u8; 4 * 1000];
let mut cur = std::io::Cursor::new(&zeros[..]);
for _ in 0..1000 {
assert_eq!(read_frame(&mut cur).unwrap(), Vec::<u8>::new());
}
}
#[test]
fn write_frame_rejects_oversize_payload() {
struct Trap;
impl Write for Trap {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
panic!("oversize payload must be rejected before any write");
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let big = vec![0u8; MAX_FRAME_LEN + 1];
assert!(matches!(
write_frame(&mut Trap, &big),
Err(RpcError::FrameTooLarge { .. })
));
}
#[test]
fn a_send_deadline_is_a_timeout_only_before_the_first_byte() {
struct Stall(usize);
impl Write for Stall {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.0 == 0 {
return Err(std::io::Error::from(ErrorKind::WouldBlock));
}
let n = self.0.min(buf.len());
self.0 -= n;
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
assert!(matches!(
write_all_reporting(&mut Stall(0), b"frame"),
Err(RpcError::Timeout)
));
assert!(
matches!(
write_all_reporting(&mut Stall(2), b"frame"),
Err(RpcError::Io(ref e)) if e.kind() == ErrorKind::WouldBlock
),
"a deadline past the first byte left a partial frame on the wire"
);
assert!(write_all_reporting(&mut Stall(5), b"frame").is_ok());
}
#[cfg(any(feature = "rpc-tcp-debug", feature = "rpc-tls"))]
#[test]
fn tcp_liveness_sizes_keepalive_to_the_timeout() {
use rustix::net::sockopt;
use std::os::fd::AsFd;
use std::time::Duration;
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let stream = std::net::TcpStream::connect(listener.local_addr().unwrap()).expect("connect");
let fd = stream.as_fd();
#[cfg(any(target_os = "linux", target_os = "android"))]
let user_timeout = || sockopt::tcp_user_timeout(fd).expect("TCP_USER_TIMEOUT");
tcp_liveness(fd, None).expect("no timeout");
assert!(
sockopt::socket_keepalive(fd).unwrap(),
"keepalive is on with no timeout"
);
#[cfg(any(target_os = "linux", target_os = "android"))]
assert_eq!(user_timeout(), 0, "the kernel default");
tcp_liveness(fd, Some(Duration::from_secs(12))).expect("12 s");
assert_eq!(sockopt::tcp_keepidle(fd).unwrap(), Duration::from_secs(6));
assert_eq!(sockopt::tcp_keepintvl(fd).unwrap(), Duration::from_secs(2));
assert_eq!(sockopt::tcp_keepcnt(fd).unwrap(), KEEPALIVE_PROBES);
#[cfg(any(target_os = "linux", target_os = "android"))]
assert_eq!(user_timeout(), 12_000);
tcp_liveness(fd, Some(Duration::from_millis(1500))).expect("1.5 s");
assert_eq!(sockopt::tcp_keepidle(fd).unwrap(), Duration::from_secs(1));
assert_eq!(sockopt::tcp_keepintvl(fd).unwrap(), Duration::from_secs(1));
#[cfg(any(target_os = "linux", target_os = "android"))]
assert_eq!(user_timeout(), 1_500);
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let month = Duration::from_secs(30 * 24 * 3600);
tcp_liveness(fd, Some(month)).expect("30 days");
let cap = Duration::from_secs(MAX_KEEPALIVE_SECS);
assert_eq!(sockopt::tcp_keepidle(fd).unwrap(), cap);
assert_eq!(sockopt::tcp_keepintvl(fd).unwrap(), cap);
let (max, got) = (i32::MAX as u32, user_timeout());
assert!((max..=max + 10).contains(&got), "{got}");
}
tcp_liveness(fd, None).expect("back to none");
assert!(sockopt::socket_keepalive(fd).unwrap());
#[cfg(any(target_os = "linux", target_os = "android"))]
assert_eq!(
user_timeout(),
0,
"None turns TCP_USER_TIMEOUT back to the default"
);
}
#[test]
fn the_kernels_etimedout_is_a_lost_connection_not_a_deadline() {
struct Fails(ErrorKind);
impl Read for Fails {
fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
Err(self.0.into())
}
}
impl Write for Fails {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(self.0.into())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let lost =
|r: RpcResult<_>| matches!(r, Err(RpcError::Io(e)) if e.kind() == ErrorKind::TimedOut);
let mut dead = Fails(ErrorKind::TimedOut);
assert!(
lost(write_all_reporting(&mut dead, b"frame")),
"nothing went out, yet the connection is gone"
);
assert!(lost(read_frame(&mut dead).map(drop)));
assert!(matches!(
read_frame(&mut Fails(ErrorKind::WouldBlock)),
Err(RpcError::Timeout)
));
}
#[test]
fn peer_identity_display_and_accessors() {
let local = PeerIdentity::Local { uid: 1000, pid: 42 };
assert!(local.is_local());
assert_eq!(local.uid(), Some(1000));
assert_eq!(local.pid(), Some(42));
assert_eq!(format!("{local}"), "local(uid=1000, pid=42)");
let no_pid = PeerIdentity::Local { uid: 0, pid: -1 };
assert_eq!(no_pid.pid(), None, "-1 pid is reported as unavailable");
let anon = PeerIdentity::Anonymous;
assert!(!anon.is_local());
assert_eq!(anon.uid(), None);
assert!(
format!("{anon}").contains("NO peer identity"),
"Anonymous Display must make the missing-identity state loud"
);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[test]
fn a_socket_that_refuses_siocoutq_is_not_asked_again() {
use std::io::Write;
use std::os::fd::AsFd;
let (mut ours, _theirs) = std::os::unix::net::UnixStream::pair().expect("socketpair");
ours.write_all(b"unread").expect("write");
let answers = OutQueue::default().bytes(ours.as_fd());
assert!(
answers.is_some_and(|n| n > 0),
"SIOCOUTQ on a socket: {answers:?}"
);
let refuses = std::fs::File::open("/dev/null").expect("/dev/null");
let queue = OutQueue::default();
assert_eq!(queue.bytes(refuses.as_fd()), None);
assert_eq!(queue.bytes(ours.as_fd()), None, "a refusal was asked again");
}
}