use std::fs::File;
use std::io::{ErrorKind, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use super::ffi;
pub const TIMEOUT: core::time::Duration = core::time::Duration::from_secs(2);
const MAX_BYTES: usize = 64 << 20;
const CHUNK: usize = 4096;
const READ_CHUNK: usize = 8192;
pub fn adopt(fd: RawFd) -> Option<File> {
if fd < 0 {
return None;
}
Some(unsafe { File::from_raw_fd(fd) })
}
pub fn close(fd: RawFd) {
drop(adopt(fd));
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum State {
Pending,
Done,
Failed,
}
#[derive(Debug)]
pub struct Reading {
file: File,
buffer: Vec<u8>,
idle_deadline_nanos: u64,
}
impl Reading {
#[must_use]
pub fn new(file: File, now_nanos: u64) -> Self {
Self {
file,
buffer: Vec::new(),
idle_deadline_nanos: now_nanos.saturating_add(deadline_nanos()),
}
}
#[must_use]
pub fn raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
#[must_use]
pub fn take(self) -> Vec<u8> {
self.buffer
}
pub fn service(&mut self, now_nanos: u64) -> (State, bool) {
let mut moved = false;
let mut chunk = [0u8; READ_CHUNK];
while ffi::poll_ready(self.file.as_raw_fd(), false, 0) {
match self.file.read(&mut chunk) {
Ok(0) => return (State::Done, moved),
Ok(read) => {
if self.buffer.len().saturating_add(read) > MAX_BYTES {
crcbl_core::log::warn!(
"a clipboard transfer exceeded {MAX_BYTES} bytes and was abandoned"
);
return (State::Failed, moved);
}
self.buffer.extend_from_slice(&chunk[..read]);
moved = true;
}
Err(error) if would_retry(&error) => break,
Err(error) => {
crcbl_core::log::debug!("clipboard read failed: {error}");
return (State::Failed, moved);
}
}
}
if moved {
self.idle_deadline_nanos = now_nanos.saturating_add(deadline_nanos());
}
(self.state_at(now_nanos), moved)
}
fn state_at(&self, now_nanos: u64) -> State {
if now_nanos >= self.idle_deadline_nanos {
crcbl_core::log::debug!("a clipboard transfer timed out with no data from the peer");
State::Failed
} else {
State::Pending
}
}
}
#[derive(Debug)]
pub struct Writing {
file: File,
bytes: Vec<u8>,
offset: usize,
idle_deadline_nanos: u64,
}
impl Writing {
#[must_use]
pub fn new(file: File, bytes: Vec<u8>, now_nanos: u64) -> Self {
Self {
file,
bytes,
offset: 0,
idle_deadline_nanos: now_nanos.saturating_add(deadline_nanos()),
}
}
pub fn service(&mut self, now_nanos: u64) -> (State, bool) {
let mut moved = false;
while self.offset < self.bytes.len() {
if !ffi::poll_ready(self.file.as_raw_fd(), true, 0) {
break;
}
let end = self.bytes.len().min(self.offset + CHUNK);
match self.file.write(&self.bytes[self.offset..end]) {
Ok(0) => break,
Ok(written) => {
self.offset += written;
moved = true;
}
Err(error) if would_retry(&error) => break,
Err(error) => {
crcbl_core::log::debug!("clipboard write ended early: {error}");
return (State::Failed, moved);
}
}
}
if self.offset >= self.bytes.len() {
return (State::Done, moved);
}
if moved {
self.idle_deadline_nanos = now_nanos.saturating_add(deadline_nanos());
}
if now_nanos >= self.idle_deadline_nanos {
crcbl_core::log::debug!("a clipboard transfer timed out with the peer not reading");
return (State::Failed, moved);
}
(State::Pending, moved)
}
}
#[derive(Debug)]
pub struct Writes {
pending: Vec<Writing>,
}
impl Writes {
pub const MAX_PENDING_WRITES: usize = 8;
#[must_use]
pub const fn new() -> Self {
Self {
pending: Vec::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.pending.len()
}
pub fn push(&mut self, writing: Writing) {
if self.pending.len() >= Self::MAX_PENDING_WRITES {
crcbl_core::log::warn!(
"a peer has more clipboard reads open than this client will \
hold at once; the newest was answered with nothing"
);
return;
}
self.pending.push(writing);
}
pub fn service(&mut self, now_nanos: u64) -> bool {
let mut moved = false;
let mut index = 0;
while index < self.pending.len() {
let (state, progressed) = self.pending[index].service(now_nanos);
moved |= progressed;
if state == State::Pending {
index += 1;
} else {
self.pending.remove(index);
}
}
moved
}
}
fn deadline_nanos() -> u64 {
u64::try_from(TIMEOUT.as_nanos()).unwrap_or(u64::MAX)
}
fn would_retry(error: &std::io::Error) -> bool {
matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted)
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> u64 {
ffi::monotonic_nanos()
}
#[test]
fn a_read_completes_when_the_peer_closes_its_end() {
let (reader, mut writer) = std::io::pipe().expect("pipe");
let mut reading = Reading::new(File::from(std::os::fd::OwnedFd::from(reader)), now());
writer.write_all(b"hello ").expect("write");
let (state, moved) = reading.service(now());
assert_eq!(state, State::Pending, "the peer has not closed yet");
assert!(moved);
writer.write_all(b"world").expect("write");
assert_eq!(reading.service(now()).0, State::Pending);
drop(writer);
assert_eq!(reading.service(now()).0, State::Done);
assert_eq!(reading.take(), b"hello world");
}
#[test]
fn a_read_from_a_peer_that_never_writes_times_out_instead_of_hanging() {
let (reader, writer) = std::io::pipe().expect("pipe");
let mut reading = Reading::new(File::from(std::os::fd::OwnedFd::from(reader)), now());
let started = std::time::Instant::now();
assert_eq!(reading.service(now()).0, State::Pending);
assert!(
started.elapsed() < core::time::Duration::from_millis(100),
"servicing a silent transfer must not block, took {:?}",
started.elapsed()
);
let (state, moved) = reading.service(now() + 2 * deadline_nanos());
assert_eq!(state, State::Failed);
assert!(!moved);
drop(writer);
}
#[test]
fn a_transfer_that_keeps_moving_is_never_abandoned_for_being_slow() {
let (reader, mut writer) = std::io::pipe().expect("pipe");
let mut reading = Reading::new(File::from(std::os::fd::OwnedFd::from(reader)), now());
let mut clock = now();
for _ in 0..5 {
clock += deadline_nanos() - 1;
writer.write_all(b"chunk").expect("write");
let (state, moved) = reading.service(clock);
assert_eq!(state, State::Pending, "it is still arriving");
assert!(moved);
}
assert_eq!(reading.buffer.len(), 25);
assert_eq!(
reading.service(clock + deadline_nanos()).0,
State::Failed,
"silence still runs out"
);
drop(writer);
}
#[test]
fn a_write_larger_than_the_pipe_buffer_is_finished_across_several_services() {
let payload: Vec<u8> = (0..200_000u32).map(|byte| byte as u8).collect();
let (reader, writer) = std::io::pipe().expect("pipe");
let mut writing = Writing::new(
File::from(std::os::fd::OwnedFd::from(writer)),
payload.clone(),
now(),
);
let mut reading = Reading::new(File::from(std::os::fd::OwnedFd::from(reader)), now());
let started = std::time::Instant::now();
assert_eq!(
writing.service(now()).0,
State::Pending,
"the pipe is full and the payload is not"
);
assert!(
started.elapsed() < core::time::Duration::from_millis(100),
"a full pipe must not block the caller"
);
let mut writing_state = State::Pending;
for _ in 0..1_000 {
if writing_state == State::Pending {
writing_state = writing.service(now()).0;
}
if reading.service(now()).0 == State::Done {
break;
}
}
assert_eq!(writing_state, State::Done);
drop(writing);
assert_eq!(reading.service(now()).0, State::Done);
assert_eq!(reading.take(), payload);
}
#[test]
fn a_write_to_a_reader_that_hung_up_fails_rather_than_raising_sigpipe() {
let (reader, writer) = std::io::pipe().expect("pipe");
drop(reader);
let mut writing = Writing::new(
File::from(std::os::fd::OwnedFd::from(writer)),
vec![7u8; 128],
now(),
);
assert_eq!(writing.service(now()).0, State::Failed);
}
#[test]
fn a_peer_cannot_make_this_client_hold_one_copy_per_request() {
let payload = b"crcbl selection".to_vec();
let mut writes = Writes::new();
let mut readers = Vec::new();
for _ in 0..Writes::MAX_PENDING_WRITES + 4 {
let (reader, writer) = std::io::pipe().expect("pipe");
readers.push(Reading::new(
File::from(std::os::fd::OwnedFd::from(reader)),
now(),
));
writes.push(Writing::new(
File::from(std::os::fd::OwnedFd::from(writer)),
payload.clone(),
now(),
));
}
assert_eq!(
writes.len(),
Writes::MAX_PENDING_WRITES,
"asking more times must not buy more copies"
);
let (state, moved) = readers[Writes::MAX_PENDING_WRITES].service(now());
assert_eq!(state, State::Done, "the refused peer was answered");
assert!(!moved, "with nothing");
assert!(writes.service(now()), "the payloads went into the pipes");
assert!(writes.is_empty(), "and every one of them finished");
let mut first = readers.remove(0);
assert_eq!(first.service(now()).0, State::Done);
assert_eq!(first.take(), payload);
}
#[test]
fn an_invalid_descriptor_is_not_adopted() {
assert!(adopt(-1).is_none());
close(-1);
}
}