use core::time::Duration;
use crcbl_core::EventTime;
use crate::{ClipboardContent, ClipboardRequestId, MimeType, PhysicalPoint, WindowId};
pub const TIMEOUT: Duration = Duration::from_secs(2);
pub const MAX_BYTES: usize = 64 << 20;
pub const MAX_PENDING_WRITES: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReadState {
AwaitingTargets,
AwaitingNotify,
Incremental,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Step {
AcknowledgeChunk,
Done(ClipboardContent),
Convert(u32),
}
#[derive(Clone, Debug, PartialEq)]
pub enum Delivery {
Clipboard {
request: ClipboardRequestId,
mime: MimeType,
},
Drop {
source: u32,
version: u8,
position: PhysicalPoint,
time: EventTime,
},
}
#[derive(Clone, Debug)]
pub struct Read {
pub window: WindowId,
pub selection: u32,
pub property: u32,
pub candidates: Vec<u32>,
pub answered_target: u32,
pub buffer: Vec<u8>,
pub state: ReadState,
pub last_progress_nanos: u64,
pub delivery: Delivery,
}
impl Read {
#[must_use]
pub fn clipboard(
request: ClipboardRequestId,
window: WindowId,
mime: MimeType,
candidates: Vec<u32>,
selection: u32,
property: u32,
now_nanos: u64,
) -> Self {
Self {
window,
selection,
candidates,
property,
answered_target: 0,
buffer: Vec::new(),
state: ReadState::AwaitingTargets,
last_progress_nanos: now_nanos,
delivery: Delivery::Clipboard { request, mime },
}
}
#[must_use]
pub fn for_drop(
window: WindowId,
target: u32,
selection: u32,
property: u32,
delivery: Delivery,
now_nanos: u64,
) -> Self {
Self {
window,
selection,
candidates: vec![target],
property,
answered_target: 0,
buffer: Vec::new(),
state: ReadState::AwaitingNotify,
last_progress_nanos: now_nanos,
delivery,
}
}
pub fn on_targets(&mut self, offered: &[u32], now_nanos: u64) -> Step {
self.last_progress_nanos = now_nanos;
self.state = ReadState::AwaitingNotify;
if offered.is_empty() {
return match self.candidates.first() {
Some(target) => Step::Convert(*target),
None => Step::Done(ClipboardContent::Unavailable),
};
}
match choose_target(&self.candidates, offered) {
Some(target) => Step::Convert(target),
None => Step::Done(ClipboardContent::Empty),
}
}
pub fn on_selection_notify(
&mut self,
property: u32,
target: u32,
is_incr: bool,
value: &[u8],
now_nanos: u64,
) -> Step {
self.last_progress_nanos = now_nanos;
self.answered_target = target;
if property == 0 {
return Step::Done(ClipboardContent::Unavailable);
}
if is_incr {
self.state = ReadState::Incremental;
return Step::AcknowledgeChunk;
}
Step::Done(ClipboardContent::Bytes(value.to_vec()))
}
pub fn on_chunk(&mut self, value: &[u8], now_nanos: u64) -> Step {
self.last_progress_nanos = now_nanos;
if value.is_empty() {
return Step::Done(ClipboardContent::Bytes(core::mem::take(&mut self.buffer)));
}
if self.buffer.len().saturating_add(value.len()) > MAX_BYTES {
crcbl_core::log::warn!(
"an X11 peer sent more than {MAX_BYTES} bytes for {:?}; abandoning",
self.delivery
);
return Step::Done(ClipboardContent::Unavailable);
}
self.buffer.extend_from_slice(value);
Step::AcknowledgeChunk
}
#[must_use]
pub fn is_stalled(&self, now_nanos: u64) -> bool {
let elapsed = now_nanos.saturating_sub(self.last_progress_nanos);
elapsed >= u64::try_from(TIMEOUT.as_nanos()).unwrap_or(u64::MAX)
}
}
#[must_use]
pub fn choose_target(candidates: &[u32], offered: &[u32]) -> Option<u32> {
candidates
.iter()
.copied()
.find(|candidate| *candidate != 0 && offered.contains(candidate))
}
#[must_use]
pub fn max_property_bytes(maximum_request_length: u16) -> usize {
const HEADER_WORDS: usize = 6;
const MARGIN_WORDS: usize = 2;
let words = usize::from(maximum_request_length).saturating_sub(HEADER_WORDS + MARGIN_WORDS);
words.saturating_mul(4).max(4096)
}
#[derive(Clone, Debug)]
pub struct Write {
pub requestor: u32,
pub property: u32,
pub target: u32,
pub bytes: Vec<u8>,
pub offset: usize,
pub chunk: usize,
pub terminated: bool,
pub last_progress_nanos: u64,
}
impl Write {
#[must_use]
pub fn new(
requestor: u32,
property: u32,
target: u32,
bytes: Vec<u8>,
chunk: usize,
now_nanos: u64,
) -> Self {
Self {
requestor,
property,
target,
bytes,
offset: 0,
chunk: chunk.max(1),
terminated: false,
last_progress_nanos: now_nanos,
}
}
pub fn next_chunk(&mut self, now_nanos: u64) -> Option<&[u8]> {
if self.terminated {
return None;
}
self.last_progress_nanos = now_nanos;
if self.offset >= self.bytes.len() {
self.terminated = true;
return Some(&[]);
}
let end = self.bytes.len().min(self.offset + self.chunk);
let start = self.offset;
self.offset = end;
Some(&self.bytes[start..end])
}
#[must_use]
pub fn is_stalled(&self, now_nanos: u64) -> bool {
let elapsed = now_nanos.saturating_sub(self.last_progress_nanos);
elapsed >= u64::try_from(TIMEOUT.as_nanos()).unwrap_or(u64::MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crcbl_core::Pool;
fn window() -> WindowId {
let mut pool: Pool<u8> = Pool::new();
pool.insert(0).cast()
}
fn pending(now: u64) -> Read {
let mut read = Read::clipboard(
ClipboardRequestId(1),
window(),
MimeType::TextUtf8,
vec![11],
9,
12,
now,
);
let _ = read.on_targets(&[11], now);
read
}
#[test]
fn a_drop_read_skips_the_targets_phase() {
let delivery = Delivery::Drop {
source: 42,
version: 5,
position: PhysicalPoint::new(3.0, 4.0),
time: EventTime::ZERO,
};
let read = Read::for_drop(window(), 77, 9, 12, delivery.clone(), 0);
assert_eq!(read.state, ReadState::AwaitingNotify);
assert_eq!(
read.candidates,
vec![77],
"the one type the source published, and no guesses"
);
assert_eq!(read.selection, 9, "XdndSelection, not CLIPBOARD");
assert_eq!(read.delivery, delivery);
let paste = Read::clipboard(
ClipboardRequestId(1),
window(),
MimeType::UriList,
vec![77],
5,
12,
0,
);
assert_eq!(paste.state, ReadState::AwaitingTargets);
assert_ne!(paste.selection, read.selection);
}
#[test]
fn a_refused_conversion_is_unavailable_and_not_an_empty_clipboard() {
let mut read = pending(0);
assert_eq!(
read.on_selection_notify(0, 11, false, &[], 1_000),
Step::Done(ClipboardContent::Unavailable)
);
}
#[test]
fn a_direct_answer_completes_in_one_step_and_zero_bytes_is_a_success() {
let mut read = pending(0);
assert_eq!(
read.on_selection_notify(12, 11, false, b"hello", 1_000),
Step::Done(ClipboardContent::Bytes(b"hello".to_vec()))
);
let mut empty = pending(0);
let step = empty.on_selection_notify(12, 11, false, &[], 1_000);
assert_eq!(step, Step::Done(ClipboardContent::Bytes(Vec::new())));
assert_ne!(step, Step::Done(ClipboardContent::Empty));
}
#[test]
fn an_incr_transfer_acknowledges_every_chunk_and_ends_on_the_empty_one() {
let mut read = pending(0);
assert_eq!(
read.on_selection_notify(12, 11, true, &4_096_u32.to_ne_bytes(), 10),
Step::AcknowledgeChunk,
"the INCR size estimate is ignored; the delete is what starts it"
);
assert_eq!(read.state, ReadState::Incremental);
assert!(read.buffer.is_empty(), "the estimate is not data");
assert_eq!(read.on_chunk(b"one ", 20), Step::AcknowledgeChunk);
assert_eq!(read.on_chunk(b"two ", 30), Step::AcknowledgeChunk);
assert_eq!(read.on_chunk(b"three", 40), Step::AcknowledgeChunk);
assert_eq!(
read.on_chunk(&[], 50),
Step::Done(ClipboardContent::Bytes(b"one two three".to_vec()))
);
assert_eq!(read.last_progress_nanos, 50);
}
#[test]
fn an_incr_transfer_of_nothing_is_a_terminator_and_an_empty_success() {
let mut read = pending(0);
assert_eq!(
read.on_selection_notify(12, 11, true, &[], 10),
Step::AcknowledgeChunk
);
assert_eq!(
read.on_chunk(&[], 20),
Step::Done(ClipboardContent::Bytes(Vec::new()))
);
}
#[test]
fn a_stalled_read_times_out_rather_than_staying_outstanding_forever() {
let read = pending(1_000);
assert!(!read.is_stalled(1_000));
assert!(!read.is_stalled(1_000 + TIMEOUT.as_nanos() as u64 - 1));
assert!(read.is_stalled(1_000 + TIMEOUT.as_nanos() as u64));
let mut slow = pending(0);
let _ = slow.on_selection_notify(12, 11, true, &[], 0);
for step in 1..=10u64 {
let now = step * (TIMEOUT.as_nanos() as u64 - 1);
assert!(!slow.is_stalled(now), "chunk {step}");
assert_eq!(slow.on_chunk(b"x", now), Step::AcknowledgeChunk);
}
}
#[test]
fn a_peer_that_sends_too_much_is_cut_off_rather_than_believed() {
let mut read = pending(0);
let _ = read.on_selection_notify(12, 11, true, &[], 0);
read.buffer = vec![0; MAX_BYTES - 1];
assert_eq!(
read.on_chunk(&[1, 2, 3], 10),
Step::Done(ClipboardContent::Unavailable)
);
}
#[test]
fn the_best_available_spelling_wins_over_the_first_one_offered() {
let utf8 = 101;
let latin1 = 31;
let candidates = [utf8, latin1];
assert_eq!(choose_target(&candidates, &[latin1, utf8]), Some(utf8));
assert_eq!(choose_target(&candidates, &[latin1]), Some(latin1));
assert_eq!(choose_target(&candidates, &[utf8]), Some(utf8));
assert_eq!(choose_target(&candidates, &[999]), None);
assert_eq!(choose_target(&candidates, &[]), None);
assert_eq!(choose_target(&[0], &[0]), None);
}
#[test]
fn a_peer_with_no_targets_support_is_asked_directly_rather_than_given_up_on() {
let mut read = Read::clipboard(
ClipboardRequestId(2),
window(),
MimeType::TextUtf8,
vec![77, 78],
9,
12,
0,
);
assert_eq!(read.state, ReadState::AwaitingTargets);
assert_eq!(read.on_targets(&[], 10), Step::Convert(77));
assert_eq!(read.state, ReadState::AwaitingNotify);
let mut nothing_in_common = Read::clipboard(
ClipboardRequestId(3),
window(),
MimeType::CrcblRon,
vec![55],
9,
12,
0,
);
assert_eq!(
nothing_in_common.on_targets(&[77, 78], 10),
Step::Done(ClipboardContent::Empty),
"the owner has something, just not this"
);
}
#[test]
fn the_property_chunk_size_respects_the_servers_own_limit() {
assert_eq!(
max_property_bytes(65_535),
(65_535 - 8) * 4,
"a big-requests server"
);
assert_eq!(max_property_bytes(4_096), (4_096 - 8) * 4);
assert_eq!(max_property_bytes(0), 4_096);
assert_eq!(max_property_bytes(8), 4_096);
assert!(
max_property_bytes(u16::MAX).is_multiple_of(4),
"word-aligned"
);
}
#[test]
fn a_write_always_ends_with_an_empty_chunk_even_when_it_divides_evenly() {
let mut write = Write::new(1, 2, 3, b"abcdef".to_vec(), 3, 0);
assert_eq!(write.next_chunk(1), Some(&b"abc"[..]));
assert_eq!(write.next_chunk(2), Some(&b"def"[..]));
assert_eq!(write.next_chunk(3), Some(&[][..]), "the terminator");
assert_eq!(write.next_chunk(4), None, "and nothing after it");
assert!(write.terminated);
let mut ragged = Write::new(1, 2, 3, b"abcde".to_vec(), 3, 0);
assert_eq!(ragged.next_chunk(1), Some(&b"abc"[..]));
assert_eq!(ragged.next_chunk(2), Some(&b"de"[..]));
assert_eq!(ragged.next_chunk(3), Some(&[][..]));
assert_eq!(ragged.next_chunk(4), None);
let mut nothing = Write::new(1, 2, 3, Vec::new(), 4096, 0);
assert_eq!(nothing.next_chunk(1), Some(&[][..]));
assert_eq!(nothing.next_chunk(2), None);
}
#[test]
fn a_write_to_a_peer_that_stops_acknowledging_is_abandoned() {
let mut write = Write::new(1, 2, 3, vec![0; 10], 4, 1_000);
assert!(!write.is_stalled(1_000));
let _ = write.next_chunk(2_000);
assert!(!write.is_stalled(2_000));
assert!(write.is_stalled(2_000 + TIMEOUT.as_nanos() as u64));
assert_eq!(Write::new(1, 2, 3, vec![7], 0, 0).chunk, 1);
}
}