Skip to main content

netget/client/x11/
wire.rs

1//! The X11 core protocol as a client speaks it (X Window System Protocol, X11R6): connection
2//! setup, the requests NetGet issues, and the replies, errors and events it reads. NetGet always
3//! asks for little-endian byte order (`l`), so every multi-byte field here is little-endian in
4//! both directions; the server converts.
5use anyhow::{bail, ensure, Context, Result};
6use tokio::io::{AsyncRead, AsyncReadExt};
7
8/// The most a single reply or generic event may carry. NetGet asks for at most
9/// [`MAX_PROPERTY_BYTES`] of a property, and a QueryTree of 250 000 windows still fits; a reply
10/// announcing more is refused before it is read, and the connection ends.
11pub const MAX_REPLY_BYTES: usize = 1 << 20;
12/// The most of a property NetGet reads (GetProperty's long-length) or writes in one request.
13pub const MAX_PROPERTY_BYTES: usize = 64 * 1024;
14
15pub const MIT_MAGIC_COOKIE: &str = "MIT-MAGIC-COOKIE-1";
16
17pub const CREATE_WINDOW: u8 = 1;
18pub const DESTROY_WINDOW: u8 = 4;
19pub const MAP_WINDOW: u8 = 8;
20pub const UNMAP_WINDOW: u8 = 10;
21pub const CONFIGURE_WINDOW: u8 = 12;
22pub const GET_GEOMETRY: u8 = 14;
23pub const QUERY_TREE: u8 = 15;
24pub const INTERN_ATOM: u8 = 16;
25pub const GET_ATOM_NAME: u8 = 17;
26pub const CHANGE_PROPERTY: u8 = 18;
27pub const DELETE_PROPERTY: u8 = 19;
28pub const GET_PROPERTY: u8 = 20;
29pub const LIST_PROPERTIES: u8 = 21;
30pub const GET_INPUT_FOCUS: u8 = 43;
31pub const LIST_EXTENSIONS: u8 = 99;
32pub const BELL: u8 = 104;
33
34/// Event-mask bits a window can be created watching (core protocol, section 8).
35pub const EVENT_MASKS: &[(&str, u32)] = &[
36    ("keyboard", 0x0000_0003),  // KeyPress | KeyRelease
37    ("pointer", 0x0000_004C),   // ButtonPress | ButtonRelease | PointerMotion
38    ("exposure", 0x0000_8000),  // Exposure
39    ("structure", 0x0002_0000), // StructureNotify: Map/Unmap/Configure/Destroy
40    ("focus", 0x0020_0000),     // FocusChange
41    ("property", 0x0040_0000),  // PropertyChange
42];
43
44/// The 68 atoms every server predefines (core protocol, appendix B), by value.
45pub const PREDEFINED_ATOMS: [&str; 68] = [
46    "PRIMARY",
47    "SECONDARY",
48    "ARC",
49    "ATOM",
50    "BITMAP",
51    "CARDINAL",
52    "COLORMAP",
53    "CURSOR",
54    "CUT_BUFFER0",
55    "CUT_BUFFER1",
56    "CUT_BUFFER2",
57    "CUT_BUFFER3",
58    "CUT_BUFFER4",
59    "CUT_BUFFER5",
60    "CUT_BUFFER6",
61    "CUT_BUFFER7",
62    "DRAWABLE",
63    "FONT",
64    "INTEGER",
65    "PIXMAP",
66    "POINT",
67    "RECTANGLE",
68    "RESOURCE_MANAGER",
69    "RGB_COLOR_MAP",
70    "RGB_BEST_MAP",
71    "RGB_BLUE_MAP",
72    "RGB_DEFAULT_MAP",
73    "RGB_GRAY_MAP",
74    "RGB_GREEN_MAP",
75    "RGB_RED_MAP",
76    "STRING",
77    "VISUALID",
78    "WINDOW",
79    "WM_COMMAND",
80    "WM_HINTS",
81    "WM_CLIENT_MACHINE",
82    "WM_ICON_NAME",
83    "WM_ICON_SIZE",
84    "WM_NAME",
85    "WM_NORMAL_HINTS",
86    "WM_SIZE_HINTS",
87    "WM_ZOOM_HINTS",
88    "MIN_SPACE",
89    "NORM_SPACE",
90    "MAX_SPACE",
91    "END_SPACE",
92    "SUPERSCRIPT_X",
93    "SUPERSCRIPT_Y",
94    "SUBSCRIPT_X",
95    "SUBSCRIPT_Y",
96    "UNDERLINE_POSITION",
97    "UNDERLINE_THICKNESS",
98    "STRIKEOUT_ASCENT",
99    "STRIKEOUT_DESCENT",
100    "ITALIC_ANGLE",
101    "X_HEIGHT",
102    "QUAD_WIDTH",
103    "WEIGHT",
104    "POINT_SIZE",
105    "RESOLUTION",
106    "COPYRIGHT",
107    "NOTICE",
108    "FONT_NAME",
109    "FAMILY_NAME",
110    "FULL_NAME",
111    "CAP_HEIGHT",
112    "WM_CLASS",
113    "WM_TRANSIENT_FOR",
114];
115
116pub fn predefined_atom(name: &str) -> Option<u32> {
117    PREDEFINED_ATOMS
118        .iter()
119        .position(|a| *a == name)
120        .map(|i| i as u32 + 1)
121}
122
123pub fn predefined_name(atom: u32) -> Option<&'static str> {
124    PREDEFINED_ATOMS
125        .get((atom as usize).checked_sub(1)?)
126        .copied()
127}
128
129pub fn request_name(major: u8) -> &'static str {
130    match major {
131        CREATE_WINDOW => "CreateWindow",
132        DESTROY_WINDOW => "DestroyWindow",
133        MAP_WINDOW => "MapWindow",
134        UNMAP_WINDOW => "UnmapWindow",
135        CONFIGURE_WINDOW => "ConfigureWindow",
136        GET_GEOMETRY => "GetGeometry",
137        QUERY_TREE => "QueryTree",
138        INTERN_ATOM => "InternAtom",
139        GET_ATOM_NAME => "GetAtomName",
140        CHANGE_PROPERTY => "ChangeProperty",
141        DELETE_PROPERTY => "DeleteProperty",
142        GET_PROPERTY => "GetProperty",
143        LIST_PROPERTIES => "ListProperties",
144        GET_INPUT_FOCUS => "GetInputFocus",
145        LIST_EXTENSIONS => "ListExtensions",
146        BELL => "Bell",
147        _ => "unknown",
148    }
149}
150
151pub fn error_name(code: u8) -> &'static str {
152    match code {
153        1 => "BadRequest",
154        2 => "BadValue",
155        3 => "BadWindow",
156        4 => "BadPixmap",
157        5 => "BadAtom",
158        6 => "BadCursor",
159        7 => "BadFont",
160        8 => "BadMatch",
161        9 => "BadDrawable",
162        10 => "BadAccess",
163        11 => "BadAlloc",
164        12 => "BadColormap",
165        13 => "BadGContext",
166        14 => "BadIDChoice",
167        15 => "BadName",
168        16 => "BadLength",
169        17 => "BadImplementation",
170        _ => "extension error",
171    }
172}
173
174pub fn event_name(code: u8) -> &'static str {
175    match code {
176        2 => "KeyPress",
177        3 => "KeyRelease",
178        4 => "ButtonPress",
179        5 => "ButtonRelease",
180        6 => "MotionNotify",
181        7 => "EnterNotify",
182        8 => "LeaveNotify",
183        9 => "FocusIn",
184        10 => "FocusOut",
185        11 => "KeymapNotify",
186        12 => "Expose",
187        13 => "GraphicsExposure",
188        14 => "NoExposure",
189        15 => "VisibilityNotify",
190        16 => "CreateNotify",
191        17 => "DestroyNotify",
192        18 => "UnmapNotify",
193        19 => "MapNotify",
194        20 => "MapRequest",
195        21 => "ReparentNotify",
196        22 => "ConfigureNotify",
197        23 => "ConfigureRequest",
198        24 => "GravityNotify",
199        25 => "ResizeRequest",
200        26 => "CirculateNotify",
201        27 => "CirculateRequest",
202        28 => "PropertyNotify",
203        29 => "SelectionClear",
204        30 => "SelectionRequest",
205        31 => "SelectionNotify",
206        32 => "ColormapNotify",
207        33 => "ClientMessage",
208        34 => "MappingNotify",
209        35 => "GenericEvent",
210        _ => "extension event",
211    }
212}
213
214pub fn pad(n: usize) -> usize {
215    (4 - n % 4) % 4
216}
217
218pub fn u16_at(b: &[u8], at: usize) -> u16 {
219    u16::from_le_bytes([b[at], b[at + 1]])
220}
221
222pub fn i16_at(b: &[u8], at: usize) -> i16 {
223    u16_at(b, at) as i16
224}
225
226pub fn u32_at(b: &[u8], at: usize) -> u32 {
227    u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
228}
229
230/// The connection setup request, with MIT-MAGIC-COOKIE-1 when a cookie is given.
231pub fn setup_request(cookie: Option<&[u8]>) -> Vec<u8> {
232    let (name, data): (&[u8], &[u8]) = match cookie {
233        Some(c) => (MIT_MAGIC_COOKIE.as_bytes(), c),
234        None => (b"", b""),
235    };
236    let mut out = vec![b'l', 0];
237    out.extend(11u16.to_le_bytes());
238    out.extend(0u16.to_le_bytes());
239    out.extend((name.len() as u16).to_le_bytes());
240    out.extend((data.len() as u16).to_le_bytes());
241    out.extend([0, 0]);
242    out.extend(name);
243    out.extend(vec![0; pad(name.len())]);
244    out.extend(data);
245    out.extend(vec![0; pad(data.len())]);
246    out
247}
248
249#[derive(Debug, Clone)]
250pub struct Screen {
251    pub root: u32,
252    pub white_pixel: u32,
253    pub black_pixel: u32,
254    pub width: u16,
255    pub height: u16,
256    pub width_mm: u16,
257    pub height_mm: u16,
258    pub root_visual: u32,
259    pub root_depth: u8,
260}
261
262#[derive(Debug, Clone)]
263pub struct Setup {
264    pub protocol_major: u16,
265    pub protocol_minor: u16,
266    pub release: u32,
267    pub vendor: String,
268    pub resource_id_base: u32,
269    pub resource_id_mask: u32,
270    /// Bytes, from the setup's maximum-request-length (in four-byte units).
271    pub max_request_bytes: usize,
272    pub screens: Vec<Screen>,
273}
274
275/// Read the server's answer to the setup request: `Ok(setup)`, or `Err` with the server's own
276/// reason when it refused (status 0) or demands more authentication (status 2).
277pub async fn read_setup<R: AsyncRead + Unpin>(r: &mut R) -> Result<Setup> {
278    let mut head = [0u8; 8];
279    r.read_exact(&mut head)
280        .await
281        .context("the X server closed the connection during setup")?;
282    let len = u16_at(&head, 6) as usize * 4;
283    let mut body = vec![0u8; len];
284    r.read_exact(&mut body)
285        .await
286        .context("the X server closed the connection during setup")?;
287    match head[0] {
288        1 => parse_setup(u16_at(&head, 2), u16_at(&head, 4), &body),
289        0 => {
290            let reason = &body[..(head[1] as usize).min(body.len())];
291            bail!(
292                "the X server refused the connection: {}",
293                crate::utils::sanitize::line_field(String::from_utf8_lossy(reason).trim())
294            )
295        }
296        2 => {
297            let reason = String::from_utf8_lossy(&body);
298            bail!(
299                "the X server asks for further authentication: {}",
300                crate::utils::sanitize::line_field(reason.trim_end_matches('\0').trim())
301            )
302        }
303        other => bail!("the X server answered setup with status {other}"),
304    }
305}
306
307fn parse_setup(major: u16, minor: u16, b: &[u8]) -> Result<Setup> {
308    ensure!(
309        b.len() >= 32,
310        "setup reply of {} bytes is too short",
311        b.len()
312    );
313    let vendor_len = u16_at(b, 16) as usize;
314    let screens_n = b[20] as usize;
315    let formats_n = b[21] as usize;
316    let mut at = 32 + vendor_len + pad(vendor_len);
317    ensure!(at <= b.len(), "setup vendor overruns the reply");
318    let vendor =
319        crate::utils::sanitize::line_field(&String::from_utf8_lossy(&b[32..32 + vendor_len]));
320    at += 8 * formats_n;
321    let mut screens = Vec::with_capacity(screens_n);
322    for _ in 0..screens_n {
323        ensure!(at + 40 <= b.len(), "setup screen overruns the reply");
324        let s = &b[at..];
325        screens.push(Screen {
326            root: u32_at(s, 0),
327            white_pixel: u32_at(s, 8),
328            black_pixel: u32_at(s, 12),
329            width: u16_at(s, 20),
330            height: u16_at(s, 22),
331            width_mm: u16_at(s, 24),
332            height_mm: u16_at(s, 26),
333            root_visual: u32_at(s, 32),
334            root_depth: s[38],
335        });
336        let depths = s[39] as usize;
337        at += 40;
338        for _ in 0..depths {
339            ensure!(at + 8 <= b.len(), "setup depth overruns the reply");
340            let visuals = u16_at(b, at + 2) as usize;
341            at += 8 + 24 * visuals;
342        }
343    }
344    ensure!(at <= b.len(), "setup visuals overrun the reply");
345    Ok(Setup {
346        protocol_major: major,
347        protocol_minor: minor,
348        release: u32_at(b, 0),
349        vendor,
350        resource_id_base: u32_at(b, 4),
351        resource_id_mask: u32_at(b, 8),
352        max_request_bytes: u16_at(b, 18) as usize * 4,
353        screens,
354    })
355}
356
357#[derive(Debug, Clone)]
358pub enum Packet {
359    /// A whole reply, header included (`body[0] == 1`).
360    Reply { seq: u16, body: Vec<u8> },
361    Error {
362        seq: u16,
363        code: u8,
364        bad_value: u32,
365        major: u8,
366        minor: u16,
367    },
368    /// An event, the high "sent by SendEvent" bit removed from `code`.
369    Event {
370        code: u8,
371        synthetic: bool,
372        body: Vec<u8>,
373    },
374}
375
376/// Read one reply, error or event. A reply or generic event announcing more than
377/// [`MAX_REPLY_BYTES`] is refused before its body is read.
378pub async fn read_packet<R: AsyncRead + Unpin>(r: &mut R) -> Result<Option<Packet>> {
379    let mut head = [0u8; 32];
380    match r.read_exact(&mut head).await {
381        Ok(_) => {}
382        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
383        Err(e) => return Err(e.into()),
384    }
385    let seq = u16_at(&head, 2);
386    let code = head[0] & 0x7F;
387    let extended = head[0] == 1 || code == 35;
388    let mut body = head.to_vec();
389    if extended {
390        let extra = (u32_at(&head, 4) as usize).saturating_mul(4);
391        ensure!(
392            32usize.saturating_add(extra) <= MAX_REPLY_BYTES,
393            "the X server announced a {}-byte packet; the bound is {MAX_REPLY_BYTES}",
394            32usize.saturating_add(extra)
395        );
396        body.resize(32 + extra, 0);
397        r.read_exact(&mut body[32..]).await?;
398    }
399    Ok(Some(match head[0] {
400        0 => Packet::Error {
401            seq,
402            code: head[1],
403            bad_value: u32_at(&head, 4),
404            minor: u16_at(&head, 8),
405            major: head[10],
406        },
407        1 => Packet::Reply { seq, body },
408        _ => Packet::Event {
409            code,
410            synthetic: head[0] & 0x80 != 0,
411            body,
412        },
413    }))
414}
415
416/// One request: opcode, the header's data byte, and the body, padded and length-prefixed.
417pub fn request(opcode: u8, data: u8, body: &[u8], max_bytes: usize) -> Result<Vec<u8>> {
418    let total = 4 + body.len() + pad(body.len());
419    ensure!(
420        total <= max_bytes && total / 4 <= u16::MAX as usize,
421        "a {total}-byte {} request exceeds the server's {max_bytes}-byte maximum",
422        request_name(opcode)
423    );
424    let mut out = Vec::with_capacity(total);
425    out.push(opcode);
426    out.push(data);
427    out.extend(((total / 4) as u16).to_le_bytes());
428    out.extend(body);
429    out.resize(total, 0);
430    Ok(out)
431}
432
433pub fn u32s(values: &[u32]) -> Vec<u8> {
434    values.iter().flat_map(|v| v.to_le_bytes()).collect()
435}
436
437#[allow(clippy::too_many_arguments)]
438pub fn create_window(
439    wid: u32,
440    parent: u32,
441    x: i16,
442    y: i16,
443    width: u16,
444    height: u16,
445    border: u16,
446    background: u32,
447    event_mask: u32,
448) -> Vec<u8> {
449    let mut b = u32s(&[wid, parent]);
450    b.extend(x.to_le_bytes());
451    b.extend(y.to_le_bytes());
452    b.extend(width.to_le_bytes());
453    b.extend(height.to_le_bytes());
454    b.extend(border.to_le_bytes());
455    b.extend(1u16.to_le_bytes()); // InputOutput
456                                  // visual CopyFromParent; values: background-pixel (0x2), event-mask (0x800).
457    b.extend(u32s(&[0, 0x0000_0802, background, event_mask]));
458    b
459}
460
461pub fn configure_window(wid: u32, values: &[(u16, u32)]) -> Vec<u8> {
462    let mask = values.iter().fold(0u16, |m, (bit, _)| m | bit);
463    let mut b = u32s(&[wid]);
464    b.extend(mask.to_le_bytes());
465    b.extend([0, 0]);
466    let mut sorted = values.to_vec();
467    sorted.sort_by_key(|(bit, _)| *bit);
468    b.extend(u32s(&sorted.iter().map(|(_, v)| *v).collect::<Vec<_>>()));
469    b
470}
471
472pub fn intern_atom(name: &str) -> Vec<u8> {
473    let mut b = (name.len() as u16).to_le_bytes().to_vec();
474    b.extend([0, 0]);
475    b.extend(name.as_bytes());
476    b
477}
478
479pub fn change_property(wid: u32, property: u32, kind: u32, format: u8, data: &[u8]) -> Vec<u8> {
480    let mut b = u32s(&[wid, property, kind]);
481    b.push(format);
482    b.extend([0, 0, 0]);
483    let units = data.len() / (format as usize / 8);
484    b.extend((units as u32).to_le_bytes());
485    b.extend(data);
486    b
487}
488
489pub fn get_property(wid: u32, property: u32) -> Vec<u8> {
490    // AnyPropertyType, from offset 0, at most MAX_PROPERTY_BYTES (in 32-bit units).
491    u32s(&[wid, property, 0, 0, (MAX_PROPERTY_BYTES / 4) as u32])
492}
493
494/// A reply's variable part: what follows the 32-byte header.
495pub fn tail(body: &[u8]) -> &[u8] {
496    &body[32.min(body.len())..]
497}
498
499/// The strings of a ListExtensions reply (count in the data byte, each length-prefixed).
500pub fn strs(count: usize, b: &[u8]) -> Vec<String> {
501    let mut out = Vec::new();
502    let mut at = 0;
503    for _ in 0..count {
504        let Some(&len) = b.get(at) else { break };
505        let Some(s) = b.get(at + 1..at + 1 + len as usize) else {
506            break;
507        };
508        out.push(String::from_utf8_lossy(s).into_owned());
509        at += 1 + len as usize;
510    }
511    out
512}