Skip to main content

vnc/
desktop.rs

1//! Negotiated desktop layout and resize results. Geometry is always server-confirmed.
2use crate::{limits::dimensions, Rect, VncError};
3use tokio::io::{AsyncRead, AsyncReadExt};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct ScreenLayout {
7    pub id: u32,
8    pub x: u16,
9    pub y: u16,
10    pub width: u16,
11    pub height: u16,
12    pub flags: u32,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DesktopLayout {
17    pub width: u16,
18    pub height: u16,
19    pub screens: Vec<ScreenLayout>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DesktopReason {
24    Server,
25    ThisClient,
26    OtherClient,
27    Unknown(u16),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DesktopStatus {
32    Success,
33    Prohibited,
34    OutOfResources,
35    InvalidLayout,
36    Forwarded,
37    Unknown(u16),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct DesktopUpdate {
42    pub reason: DesktopReason,
43    pub status: DesktopStatus,
44    /// Absent on failed/forwarded replies, whose geometry is undefined on the wire.
45    pub layout: Option<DesktopLayout>,
46}
47
48// Bound retained layouts, including each screen allocation, while accepting queued
49// single-screen updates across the full u16 framebuffer rectangle count.
50#[derive(Default)]
51pub(crate) struct UpdateBatch {
52    updates: Vec<DesktopUpdate>,
53    retained_bytes: usize,
54}
55
56impl UpdateBatch {
57    const MAX_RETAINED_BYTES: usize = 4 * 1024 * 1024;
58
59    pub(crate) fn push(&mut self, update: DesktopUpdate) -> Result<(), VncError> {
60        let screens = update
61            .layout
62            .as_ref()
63            .map_or(0, |layout| layout.screens.capacity());
64        let bytes = screens
65            .checked_mul(std::mem::size_of::<ScreenLayout>())
66            .and_then(|bytes| bytes.checked_add(std::mem::size_of::<DesktopUpdate>()))
67            .and_then(|bytes| bytes.checked_add(self.retained_bytes))
68            .filter(|bytes| *bytes <= Self::MAX_RETAINED_BYTES)
69            .ok_or(VncError::InvalidImageData)?;
70        self.updates.push(update);
71        self.retained_bytes = bytes;
72        Ok(())
73    }
74
75    pub(crate) fn is_empty(&self) -> bool {
76        self.updates.is_empty()
77    }
78
79    pub(crate) fn into_updates(self) -> Vec<DesktopUpdate> {
80        self.updates
81    }
82}
83
84impl DesktopLayout {
85    pub(crate) fn validate(&self) -> Result<(), VncError> {
86        dimensions(self.width, self.height)?;
87        if self.screens.len() > 255 {
88            return Err(VncError::InvalidImageData);
89        }
90        for (index, s) in self.screens.iter().enumerate() {
91            if s.width == 0
92                || s.height == 0
93                || u32::from(s.x) + u32::from(s.width) > u32::from(self.width)
94                || u32::from(s.y) + u32::from(s.height) > u32::from(self.height)
95            {
96                return Err(VncError::InvalidImageData);
97            }
98            if self.screens[..index].iter().any(|other| other.id == s.id) {
99                return Err(VncError::InvalidImageData);
100            }
101        }
102        Ok(())
103    }
104
105    #[cfg(not(target_arch = "wasm32"))]
106    pub(crate) fn resized(&self, width: u16, height: u16) -> Result<Self, ResizeError> {
107        dimensions(width, height).map_err(|_| ResizeError::InvalidDimensions)?;
108        let [screen] = self.screens.as_slice() else {
109            return Err(ResizeError::UnsupportedLayout);
110        };
111        if screen.x != 0
112            || screen.y != 0
113            || screen.width != self.width
114            || screen.height != self.height
115        {
116            return Err(ResizeError::UnsupportedLayout);
117        }
118        Ok(Self {
119            width,
120            height,
121            screens: vec![ScreenLayout {
122                width,
123                height,
124                ..*screen
125            }],
126        })
127    }
128}
129
130impl DesktopUpdate {
131    pub(crate) async fn read<S: AsyncRead + Unpin>(
132        reader: &mut S,
133        rect: Rect,
134    ) -> Result<Self, VncError> {
135        let count = reader.read_u8().await?;
136        let mut padding = [0; 3];
137        reader.read_exact(&mut padding).await?;
138        let mut screens = Vec::with_capacity(usize::from(count));
139        for _ in 0..count {
140            screens.push(ScreenLayout {
141                id: reader.read_u32().await?,
142                x: reader.read_u16().await?,
143                y: reader.read_u16().await?,
144                width: reader.read_u16().await?,
145                height: reader.read_u16().await?,
146                flags: reader.read_u32().await?,
147            });
148        }
149        let reason = match rect.x {
150            0 => DesktopReason::Server,
151            1 => DesktopReason::ThisClient,
152            2 => DesktopReason::OtherClient,
153            n => DesktopReason::Unknown(n),
154        };
155        let status = if reason != DesktopReason::ThisClient {
156            DesktopStatus::Success
157        } else {
158            match rect.y {
159                0 => DesktopStatus::Success,
160                1 => DesktopStatus::Prohibited,
161                2 => DesktopStatus::OutOfResources,
162                3 => DesktopStatus::InvalidLayout,
163                4 => DesktopStatus::Forwarded,
164                n => DesktopStatus::Unknown(n),
165            }
166        };
167        let layout = if status == DesktopStatus::Success {
168            let layout = DesktopLayout {
169                width: rect.width,
170                height: rect.height,
171                screens,
172            };
173            layout.validate()?;
174            Some(layout)
175        } else {
176            None
177        };
178        Ok(Self {
179            reason,
180            status,
181            layout,
182        })
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
187pub enum ResizeError {
188    #[error("server has not advertised desktop resizing")]
189    Unsupported,
190    #[error("only a single screen covering the desktop can be resized")]
191    UnsupportedLayout,
192    #[error("desktop dimensions exceed decoder limits")]
193    InvalidDimensions,
194    #[error("another resize is pending")]
195    Busy,
196    #[error("resize denied: {0:?}")]
197    Denied(DesktopStatus),
198    #[error("resize could not be dispatched before the deadline")]
199    DispatchTimeout,
200    #[error("resize was not confirmed before the deadline; do not retry blindly")]
201    Timeout,
202    #[error("resize outcome is uncertain; reconnect and observe before retrying")]
203    Uncertain,
204    #[error("connection closed before resize could be dispatched")]
205    Disconnected,
206}