pub const XDND_VERSION: u32 = 5;
pub const MIN_SUPPORTED_VERSION: u32 = 3;
const STATUS_ACCEPT: u32 = 1;
const STATUS_WANT_POSITION: u32 = 2;
const ENTER_MORE_TYPES: u32 = 1;
pub fn pack_coords(x: i16, y: i16) -> u32 {
((x as u16 as u32) << 16) | (y as u16 as u32)
}
pub fn unpack_coords(packed: u32) -> (i16, i16) {
(
((packed >> 16) & 0xFFFF) as u16 as i16,
(packed & 0xFFFF) as u16 as i16,
)
}
pub fn negotiate_version(advertised: u32) -> Option<u32> {
if advertised < MIN_SUPPORTED_VERSION {
return None;
}
Some(advertised.min(XDND_VERSION))
}
pub fn enter_version(data1: u32) -> u32 {
data1 >> 24
}
pub fn resolve_proxy(window: u32, window_proxy: Option<u32>, proxy_proxy: Option<u32>) -> u32 {
match window_proxy {
Some(proxy) if proxy != 0 && proxy_proxy == Some(proxy) => proxy,
_ => window,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Enter {
pub source: u32,
pub version: u32,
pub types: Vec<u32>,
pub more_types: bool,
}
pub fn decode_enter(data: [u32; 5]) -> Option<Enter> {
let version = negotiate_version(enter_version(data[1]))?;
let types = data[2..5]
.iter()
.copied()
.filter(|&atom| atom != 0)
.collect();
Some(Enter {
source: data[0],
version,
types,
more_types: data[1] & ENTER_MORE_TYPES != 0,
})
}
pub fn encode_enter(source: u32, version: u32, types: &[u32]) -> [u32; 5] {
let mut data = [0u32; 5];
data[0] = source;
data[1] = version << 24;
if types.len() > 3 {
data[1] |= ENTER_MORE_TYPES;
}
for (slot, atom) in data[2..5].iter_mut().zip(types.iter().copied()) {
*slot = atom;
}
data
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Position {
pub source: u32,
pub root_x: i16,
pub root_y: i16,
pub time: u32,
pub action: u32,
}
pub fn decode_position(data: [u32; 5]) -> Position {
let (root_x, root_y) = unpack_coords(data[2]);
Position {
source: data[0],
root_x,
root_y,
time: data[3],
action: data[4],
}
}
pub fn encode_position(source: u32, root_x: i16, root_y: i16, time: u32, action: u32) -> [u32; 5] {
[source, 0, pack_coords(root_x, root_y), time, action]
}
pub fn encode_status(target: u32, accept: bool, action: u32) -> [u32; 5] {
let mut flags = STATUS_WANT_POSITION;
if accept {
flags |= STATUS_ACCEPT;
}
[target, flags, 0, 0, if accept { action } else { 0 }]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Status {
pub target: u32,
pub accepted: bool,
pub action: u32,
}
pub fn decode_status(data: [u32; 5]) -> Status {
Status {
target: data[0],
accepted: data[1] & STATUS_ACCEPT != 0,
action: data[4],
}
}
pub fn encode_leave(source: u32) -> [u32; 5] {
[source, 0, 0, 0, 0]
}
pub fn encode_drop(source: u32, time: u32) -> [u32; 5] {
[source, 0, time, 0, 0]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Drop {
pub source: u32,
pub time: u32,
}
pub fn decode_drop(data: [u32; 5]) -> Drop {
Drop {
source: data[0],
time: data[2],
}
}
pub fn encode_finished(target: u32, version: u32, accepted: bool, action: u32) -> [u32; 5] {
if version < 5 {
return [target, 0, 0, 0, 0];
}
[
target,
u32::from(accepted),
if accepted { action } else { 0 },
0,
0,
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finished {
pub target: u32,
pub accepted: bool,
pub action: u32,
}
pub fn decode_finished(data: [u32; 5], negotiated_version: u32) -> Finished {
if negotiated_version < 5 {
return Finished {
target: data[0],
accepted: true,
action: 0,
};
}
Finished {
target: data[0],
accepted: data[1] & 1 != 0,
action: data[2],
}
}
pub fn choose_type(offered: &[u32], preferred: &[u32]) -> Option<u32> {
preferred
.iter()
.copied()
.find(|candidate| offered.contains(candidate))
}
#[derive(Debug, Default)]
pub struct IncrAssembler {
buffer: Vec<u8>,
complete: bool,
}
impl IncrAssembler {
pub fn new(expected: usize) -> Self {
Self {
buffer: Vec::with_capacity(expected.min(1 << 20)),
complete: false,
}
}
pub fn push(&mut self, chunk: &[u8]) -> bool {
if chunk.is_empty() {
self.complete = true;
} else {
self.buffer.extend_from_slice(chunk);
}
self.complete
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn finish(self) -> Vec<u8> {
self.buffer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coords_round_trip_including_negatives() {
for (x, y) in [
(0, 0),
(1920, 1080),
(-1, -1),
(-1920, 200),
(i16::MIN, i16::MAX),
] {
assert_eq!(unpack_coords(pack_coords(x, y)), (x, y), "({x}, {y})");
}
}
#[test]
fn coords_pack_x_into_the_high_half() {
assert_eq!(pack_coords(0x1234, 0x5678), 0x1234_5678);
}
#[test]
fn negotiation_clamps_to_our_version() {
assert_eq!(negotiate_version(5), Some(5));
assert_eq!(
negotiate_version(9),
Some(5),
"a newer peer must be clamped down"
);
assert_eq!(negotiate_version(3), Some(3));
assert_eq!(negotiate_version(4), Some(4));
}
#[test]
fn negotiation_refuses_prehistoric_versions() {
assert_eq!(negotiate_version(2), None);
assert_eq!(negotiate_version(0), None);
}
#[test]
fn proxy_is_honoured_when_it_points_at_itself() {
assert_eq!(resolve_proxy(0x100, Some(0x200), Some(0x200)), 0x200);
}
#[test]
fn stale_proxy_falls_back_to_the_original_window() {
assert_eq!(resolve_proxy(0x100, Some(0x200), None), 0x100);
assert_eq!(resolve_proxy(0x100, Some(0x200), Some(0x300)), 0x100);
assert_eq!(resolve_proxy(0x100, Some(0), Some(0)), 0x100);
}
#[test]
fn no_proxy_property_means_use_the_window() {
assert_eq!(resolve_proxy(0x100, None, None), 0x100);
}
#[test]
fn enter_round_trips_with_three_types() {
let encoded = encode_enter(0xAB, 5, &[10, 20, 30]);
let decoded = decode_enter(encoded).expect("v5 is supported");
assert_eq!(
decoded,
Enter {
source: 0xAB,
version: 5,
types: vec![10, 20, 30],
more_types: false
}
);
}
#[test]
fn enter_sets_the_more_types_bit_past_three() {
let encoded = encode_enter(1, 5, &[10, 20, 30, 40]);
let decoded = decode_enter(encoded).unwrap();
assert!(decoded.more_types, "a fourth type must flag XdndTypeList");
assert_eq!(decoded.types, vec![10, 20, 30], "only three travel inline");
}
#[test]
fn enter_drops_empty_type_slots() {
let decoded = decode_enter(encode_enter(1, 5, &[10])).unwrap();
assert_eq!(decoded.types, vec![10], "None atoms are not types");
}
#[test]
fn enter_from_an_ancient_source_is_refused() {
let mut data = encode_enter(1, 5, &[10]);
data[1] = (2 << 24) | (data[1] & 0x00FF_FFFF); assert!(decode_enter(data).is_none());
}
#[test]
fn enter_from_a_newer_source_is_clamped() {
let mut data = encode_enter(1, 5, &[10]);
data[1] = (7 << 24) | (data[1] & 0x00FF_FFFF);
assert_eq!(decode_enter(data).unwrap().version, 5);
}
#[test]
fn position_round_trips() {
let decoded = decode_position(encode_position(0xAB, -300, 900, 12345, 77));
assert_eq!(
decoded,
Position {
source: 0xAB,
root_x: -300,
root_y: 900,
time: 12345,
action: 77
}
);
}
#[test]
fn status_always_requests_further_position_messages() {
let data = encode_status(0x10, true, 42);
assert_eq!(data[2], 0, "rectangle origin must be empty");
assert_eq!(data[3], 0, "rectangle extent must be empty");
assert_ne!(data[1] & STATUS_WANT_POSITION, 0);
}
#[test]
fn status_round_trips_accept_and_reject() {
let accepted = decode_status(encode_status(0x10, true, 42));
assert_eq!(
accepted,
Status {
target: 0x10,
accepted: true,
action: 42
}
);
let rejected = decode_status(encode_status(0x10, false, 42));
assert!(!rejected.accepted);
assert_eq!(
rejected.action, 0,
"a rejecting target must advertise no action"
);
}
#[test]
fn drop_round_trips_its_timestamp() {
assert_eq!(
decode_drop(encode_drop(0xAB, 999)),
Drop {
source: 0xAB,
time: 999
}
);
}
#[test]
fn finished_reports_the_action_on_v5() {
let decoded = decode_finished(encode_finished(0x10, 5, true, 42), 5);
assert_eq!(
decoded,
Finished {
target: 0x10,
accepted: true,
action: 42
}
);
}
#[test]
fn finished_omits_v5_fields_for_older_peers() {
let data = encode_finished(0x10, 3, true, 42);
assert_eq!(data, [0x10, 0, 0, 0, 0]);
}
#[test]
fn finished_from_an_older_peer_is_read_as_success() {
let decoded = decode_finished([0x10, 0, 0, 0, 0], 3);
assert!(
decoded.accepted,
"pre-v5 has no bit to clear, so it means success"
);
}
#[test]
fn finished_rejection_round_trips_on_v5() {
let decoded = decode_finished(encode_finished(0x10, 5, false, 42), 5);
assert!(!decoded.accepted);
assert_eq!(decoded.action, 0);
}
#[test]
fn type_selection_follows_our_preference_not_theirs() {
assert_eq!(choose_type(&[30, 20, 10], &[10, 20, 30]), Some(10));
assert_eq!(choose_type(&[30, 20], &[10, 20, 30]), Some(20));
}
#[test]
fn type_selection_returns_none_without_overlap() {
assert_eq!(choose_type(&[99], &[10, 20]), None);
assert_eq!(choose_type(&[], &[10]), None);
}
#[test]
fn incr_assembles_chunks_until_the_empty_terminator() {
let mut incr = IncrAssembler::new(6);
assert!(!incr.push(b"abc"));
assert!(!incr.push(b"def"));
assert!(incr.push(b""), "an empty chunk terminates the transfer");
assert!(incr.is_complete());
assert_eq!(incr.finish(), b"abcdef".to_vec());
}
#[test]
fn incr_handles_an_immediately_empty_payload() {
let mut incr = IncrAssembler::new(0);
assert!(incr.push(b""));
assert_eq!(incr.finish(), Vec::<u8>::new());
}
#[test]
fn incr_ignores_a_wild_size_hint() {
let mut incr = IncrAssembler::new(usize::MAX);
incr.push(b"x");
incr.push(b"");
assert_eq!(incr.finish(), b"x".to_vec());
}
}