ironrdp_pdu/
lib.rs

1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
4
5use core::fmt;
6
7// TODO(#583): uncomment once re-exports are removed.
8// use ironrdp_core::{unexpected_message_type_err, DecodeResult, EncodeResult, ReadCursor};
9use ironrdp_error::Source;
10
11mod macros;
12
13pub mod codecs;
14pub mod gcc;
15pub mod geometry;
16pub mod input;
17pub mod mcs;
18pub mod nego;
19pub mod pcb;
20pub mod rdp;
21pub mod tpdu;
22pub mod tpkt;
23pub mod utf16;
24pub mod utils;
25pub mod x224;
26
27pub(crate) mod basic_output;
28pub(crate) mod ber;
29pub(crate) mod crypto;
30pub(crate) mod per;
31
32pub use crate::basic_output::{bitmap, fast_path, pointer, surface_commands};
33pub use crate::rdp::vc::dvc;
34
35pub type PduResult<T> = Result<T, PduError>;
36
37pub type PduError = ironrdp_error::Error<PduErrorKind>;
38
39#[non_exhaustive]
40#[derive(Clone, Debug)]
41pub enum PduErrorKind {
42    Encode,
43    Decode,
44    Other { description: &'static str },
45}
46
47pub trait PduErrorExt {
48    fn decode<E: Source>(context: &'static str, source: E) -> Self;
49
50    fn encode<E: Source>(context: &'static str, source: E) -> Self;
51}
52
53impl PduErrorExt for PduError {
54    fn decode<E: Source>(context: &'static str, source: E) -> Self {
55        Self::new(context, PduErrorKind::Decode).with_source(source)
56    }
57
58    fn encode<E: Source>(context: &'static str, source: E) -> Self {
59        Self::new(context, PduErrorKind::Encode).with_source(source)
60    }
61}
62
63impl core::error::Error for PduErrorKind {}
64
65impl fmt::Display for PduErrorKind {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::Encode => {
69                write!(f, "encode error")
70            }
71            Self::Decode => {
72                write!(f, "decode error")
73            }
74            Self::Other { description } => {
75                write!(f, "other ({description})")
76            }
77        }
78    }
79}
80
81/// An RDP PDU.
82pub trait Pdu {
83    /// Name associated to this PDU.
84    const NAME: &'static str;
85}
86
87#[derive(Debug, Copy, Clone, PartialEq, Eq)]
88#[repr(u8)]
89pub enum Action {
90    FastPath = 0x00,
91    X224 = 0x03,
92}
93
94impl Action {
95    pub fn from_fp_output_header(fp_output_header: u8) -> Result<Self, u8> {
96        match fp_output_header & 0b11 {
97            0x00 => Ok(Self::FastPath),
98            0x03 => Ok(Self::X224),
99            unknown_action_bits => Err(unknown_action_bits),
100        }
101    }
102
103    #[expect(
104        clippy::as_conversions,
105        reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
106    )]
107    pub fn as_u8(self) -> u8 {
108        self as u8
109    }
110}
111
112#[derive(Debug, Copy, Clone, PartialEq, Eq)]
113pub struct PduInfo {
114    pub action: Action,
115    pub length: usize,
116}
117
118/// Finds next RDP PDU size by reading the next few bytes.
119pub fn find_size(bytes: &[u8]) -> DecodeResult<Option<PduInfo>> {
120    macro_rules! ensure_enough {
121        ($bytes:expr, $len:expr) => {
122            if $bytes.len() < $len {
123                return Ok(None);
124            }
125        };
126    }
127
128    ensure_enough!(bytes, 1);
129    let fp_output_header = bytes[0];
130
131    let action = Action::from_fp_output_header(fp_output_header)
132        .map_err(|unknown_action| unexpected_message_type_err("fpOutputHeader", unknown_action))?;
133
134    match action {
135        Action::X224 => {
136            ensure_enough!(bytes, tpkt::TpktHeader::SIZE);
137            let tpkt = tpkt::TpktHeader::read(&mut ReadCursor::new(bytes))?;
138
139            Ok(Some(PduInfo {
140                action,
141                length: tpkt.packet_length(),
142            }))
143        }
144        Action::FastPath => {
145            ensure_enough!(bytes, 2);
146            let a = bytes[1];
147
148            let fast_path_length = if a & 0x80 != 0 {
149                ensure_enough!(bytes, 3);
150                let b = bytes[2];
151
152                ((u16::from(a) & !0x80) << 8) + u16::from(b)
153            } else {
154                u16::from(a)
155            };
156
157            Ok(Some(PduInfo {
158                action,
159                length: usize::from(fast_path_length),
160            }))
161        }
162    }
163}
164
165pub trait PduHint: Send + Sync + fmt::Debug + 'static {
166    /// Finds next PDU size by reading the next few bytes.
167    ///
168    /// Returns `Some((hint_matching, size))` if the size is known.
169    /// Returns `None` if the size cannot be determined yet.
170    fn find_size(&self, bytes: &[u8]) -> DecodeResult<Option<(bool, usize)>>;
171}
172
173// Matches both X224 and FastPath pdus
174#[derive(Clone, Copy, Debug)]
175pub struct RdpHint;
176
177pub const RDP_HINT: RdpHint = RdpHint;
178
179impl PduHint for RdpHint {
180    fn find_size(&self, bytes: &[u8]) -> DecodeResult<Option<(bool, usize)>> {
181        find_size(bytes).map(|opt| opt.map(|info| (true, info.length)))
182    }
183}
184
185#[derive(Clone, Copy, Debug)]
186pub struct X224Hint;
187
188pub const X224_HINT: X224Hint = X224Hint;
189
190impl PduHint for X224Hint {
191    fn find_size(&self, bytes: &[u8]) -> DecodeResult<Option<(bool, usize)>> {
192        match find_size(bytes)? {
193            Some(pdu_info) => {
194                let res = (pdu_info.action == Action::X224, pdu_info.length);
195                Ok(Some(res))
196            }
197            None => Ok(None),
198        }
199    }
200}
201
202#[derive(Clone, Copy, Debug)]
203pub struct FastPathHint;
204
205pub const FAST_PATH_HINT: FastPathHint = FastPathHint;
206
207impl PduHint for FastPathHint {
208    fn find_size(&self, bytes: &[u8]) -> DecodeResult<Option<(bool, usize)>> {
209        match find_size(bytes)? {
210            Some(pdu_info) => {
211                let res = (pdu_info.action == Action::FastPath, pdu_info.length);
212                Ok(Some(res))
213            }
214            None => Ok(None),
215        }
216    }
217}
218
219// Private! Used by the macros.
220#[doc(hidden)]
221pub use ironrdp_core;
222
223// -- Temporary re-exports to ease teleport’s migration to the newer versions -- //
224// TODO(#583): remove once Teleport migrated to the newer item paths.
225// NOTE: #[deprecated] has no effect on re-exports, so this is mostly for documenting the code at this point.
226#[doc(hidden)]
227#[deprecated(since = "0.1.0", note = "use ironrdp_core::{ReadCursor, WriteCursor}")]
228pub mod cursor {
229    pub use ironrdp_core::{ReadCursor, WriteCursor};
230}
231
232#[doc(hidden)]
233#[deprecated(since = "0.1.0", note = "use ironrdp_core::WriteBuf")]
234pub mod write_buf {
235    pub use ironrdp_core::WriteBuf;
236}
237
238#[doc(hidden)]
239#[deprecated(since = "0.1.0", note = "use ironrdp_core")]
240pub use ironrdp_core::*;
241
242#[doc(hidden)]
243#[deprecated(since = "0.1.0")]
244#[macro_export]
245macro_rules! custom_err {
246    ( $description:expr, $source:expr $(,)? ) => {{
247        $crate::PduError::new(
248            $description,
249            $crate::PduErrorKind::Other {
250                description: $description,
251            },
252        )
253        .with_source($source)
254    }};
255    ( $source:expr $(,)? ) => {{
256        $crate::custom_err!($crate::function!(), $source)
257    }};
258}
259
260#[doc(hidden)]
261#[deprecated(since = "0.1.0", note = "use ironrdp_core::other_err")]
262pub use crate::pdu_other_err as other_err;