use std::io::{Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use rlldpctl::{Client, Error};
const PTR_SIZE: usize = std::mem::size_of::<usize>();
const GET_INTERFACES: i32 = 3;
fn temp_socket_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("rlldpctl-test-{name}-{}.sock", std::process::id()))
}
fn pad(buf: &mut Vec<u8>) {
let p = (PTR_SIZE - buf.len() % PTR_SIZE) % PTR_SIZE;
buf.extend(std::iter::repeat_n(0u8, p));
}
fn push_chunk(buf: &mut Vec<u8>, orig: usize, body: &[u8]) {
pad(buf);
buf.extend_from_slice(&orig.to_ne_bytes());
buf.extend_from_slice(&body.len().to_ne_bytes());
buf.extend_from_slice(body);
}
fn push_usize(buf: &mut Vec<u8>, v: usize) {
buf.extend_from_slice(&v.to_ne_bytes());
}
fn frame(ty: i32, payload: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; 4];
out[0..4].copy_from_slice(&ty.to_ne_bytes());
pad(&mut out);
push_usize(&mut out, payload.len());
out.extend_from_slice(payload);
out
}
fn read_frame(stream: &mut UnixStream) -> (i32, Vec<u8>) {
let header_len = 4 + (PTR_SIZE - 4 % PTR_SIZE) % PTR_SIZE + PTR_SIZE;
let mut header = vec![0u8; header_len];
stream.read_exact(&mut header).unwrap();
let ty = i32::from_ne_bytes(header[0..4].try_into().unwrap());
let len_off = header_len - PTR_SIZE;
let mut len_bytes = [0u8; PTR_SIZE];
len_bytes.copy_from_slice(&header[len_off..]);
let len = usize::from_ne_bytes(len_bytes);
let mut payload = vec![0u8; len];
stream.read_exact(&mut payload).unwrap();
(ty, payload)
}
#[test]
fn connect_to_missing_socket_is_an_io_error() {
let err = Client::connect_to("/nonexistent/rlldpctl-test.sock").unwrap_err();
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn interfaces_round_trips_over_a_real_socket() {
let path = temp_socket_path("interfaces");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let server = std::thread::spawn({
let path = path.clone();
move || {
let (mut stream, _) = listener.accept().unwrap();
let (ty, req_payload) = read_frame(&mut stream);
assert_eq!(ty, GET_INTERFACES);
assert!(req_payload.is_empty());
let mut list_body = Vec::new();
push_usize(&mut list_body, 2); push_usize(&mut list_body, 0);
let mut if_body = Vec::new();
push_usize(&mut if_body, 0);
push_usize(&mut if_body, 0);
push_usize(&mut if_body, 3); push_usize(&mut if_body, 0);
let mut payload = Vec::new();
push_chunk(&mut payload, 1, &list_body);
push_chunk(&mut payload, 2, &if_body);
push_chunk(&mut payload, 3, b"eth0\0");
stream.write_all(&frame(GET_INTERFACES, &payload)).unwrap();
let _ = std::fs::remove_file(&path);
}
});
let mut client = Client::connect_to(&path).unwrap();
let interfaces = client.interfaces().unwrap();
assert_eq!(interfaces.len(), 1);
assert_eq!(interfaces[0].name, "eth0");
assert_eq!(interfaces[0].alias, None);
server.join().unwrap();
}
#[test]
fn interface_not_found_surfaces_as_request_rejected() {
let path = temp_socket_path("rejected");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let server = std::thread::spawn({
let path = path.clone();
move || {
let (mut stream, _) = listener.accept().unwrap();
let (_ty, _payload) = read_frame(&mut stream);
stream.write_all(&frame(0, &[])).unwrap();
let _ = std::fs::remove_file(&path);
}
});
let mut client = Client::connect_to(&path).unwrap();
let err = client.interface("does-not-exist").unwrap_err();
assert!(matches!(err, Error::RequestRejected));
server.join().unwrap();
}