diff --git a/Cargo.lock b/Cargo.lock index 6c75ccfed..d5466cb3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1598,6 +1598,14 @@ dependencies = [ "winit", ] +[[package]] +name = "ironrdp-cliprdr" +version = "0.1.0" +dependencies = [ + "bitflags 2.0.2", + "ironrdp-pdu", +] + [[package]] name = "ironrdp-connector" version = "0.1.0" @@ -1629,6 +1637,7 @@ name = "ironrdp-fuzzing" version = "0.0.0" dependencies = [ "arbitrary", + "ironrdp-cliprdr", "ironrdp-graphics", "ironrdp-pdu", ] @@ -1729,6 +1738,7 @@ dependencies = [ "array-concat", "expect-test", "hex", + "ironrdp-cliprdr", "ironrdp-connector", "ironrdp-fuzzing", "ironrdp-graphics", diff --git a/Cargo.toml b/Cargo.toml index 1b66e35e3..7591f38df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ categories = ["network-programming"] [workspace.dependencies] expect-test = "1" ironrdp-async = { version = "0.1", path = "crates/ironrdp-async" } +ironrdp-cliprdr = { version = "0.1", path = "crates/ironrdp-cliprdr" } ironrdp-connector = { version = "0.1", path = "crates/ironrdp-connector" } ironrdp-error = { version = "0.1", path = "crates/ironrdp-error" } ironrdp-futures = { version = "0.1", path = "crates/ironrdp-futures" } diff --git a/crates/ironrdp-cliprdr/Cargo.toml b/crates/ironrdp-cliprdr/Cargo.toml new file mode 100644 index 000000000..7af76bc55 --- /dev/null +++ b/crates/ironrdp-cliprdr/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ironrdp-cliprdr" +version = "0.1.0" +description = "RDP PDU encoding and decoding" +readme = "README.md" +edition.workspace = true +license.workspace = true + +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +ironrdp-pdu.workspace = true + +bitflags = "2" \ No newline at end of file diff --git a/crates/ironrdp-cliprdr/README.md b/crates/ironrdp-cliprdr/README.md new file mode 100644 index 000000000..610c2d188 --- /dev/null +++ b/crates/ironrdp-cliprdr/README.md @@ -0,0 +1,6 @@ +# IronRDP clipboard library + +Cliboard static virtual channel(SVC) implementation. +This library includes: + - Cliboard SVC PDUs parsing + - Clipboard SVC processing (TODO) diff --git a/crates/ironrdp-cliprdr/src/lib.rs b/crates/ironrdp-cliprdr/src/lib.rs new file mode 100644 index 000000000..ff054acfc --- /dev/null +++ b/crates/ironrdp-cliprdr/src/lib.rs @@ -0,0 +1,6 @@ +//! Cliboard static virtual channel(SVC) implementation. +//! This library includes: +//! - Cliboard SVC PDUs parsing +//! - Clipboard SVC processing (TODO) + +pub mod pdu; diff --git a/crates/ironrdp-cliprdr/src/pdu/capabilities.rs b/crates/ironrdp-cliprdr/src/pdu/capabilities.rs new file mode 100644 index 000000000..acc501085 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/capabilities.rs @@ -0,0 +1,245 @@ +use crate::pdu::PartialHeader; +use bitflags::bitflags; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::{ + cast_int, cast_length, ensure_fixed_part_size, ensure_size, invalid_message_err, padding, PduDecode, PduEncode, + PduResult, +}; + +/// Represents `CLIPRDR_CAPS` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Capabilities { + pub capabilities: Vec, +} + +impl Capabilities { + const NAME: &str = "CLIPRDR_CAPS"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() * 2; + + fn inner_size(&self) -> usize { + Self::FIXED_PART_SIZE + self.capabilities.iter().map(|c| c.size()).sum::() + } +} + +impl PduEncode for Capabilities { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", self.inner_size())?); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u16(cast_length!(Self::NAME, "cCapabilitiesSets", self.capabilities.len())?); + padding::write(dst, 2); + + for capability in &self.capabilities { + capability.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.inner_size() + PartialHeader::SIZE + } +} + +impl<'de> PduDecode<'de> for Capabilities { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let capabilities_count = src.read_u16(); + padding::read(src, 2); + + let mut capabilities = Vec::with_capacity(usize::from(capabilities_count)); + + for _ in 0..capabilities_count { + let caps = CapabilitySet::decode(src)?; + capabilities.push(caps); + } + + Ok(Self { capabilities }) + } +} + +/// Represents `CLIPRDR_CAPS_SET` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapabilitySet { + General(GeneralCapabilitySet), +} + +impl CapabilitySet { + const NAME: &str = "CLIPRDR_CAPS_SET"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() * 2; + + const CAPSTYPE_GENERAL: u16 = 0x0001; +} + +impl From for CapabilitySet { + fn from(value: GeneralCapabilitySet) -> Self { + Self::General(value) + } +} + +impl PduEncode for CapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let (caps, length) = match self { + Self::General(value) => { + let length = value.size() + Self::FIXED_PART_SIZE; + (value, length) + } + }; + + ensure_size!(in: dst, size: length); + dst.write_u16(Self::CAPSTYPE_GENERAL); + dst.write_u16(cast_int!("lengthCapability", length)?); + caps.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let variable_size = match self { + Self::General(value) => value.size(), + }; + + Self::FIXED_PART_SIZE + variable_size + } +} + +impl<'de> PduDecode<'de> for CapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let caps_type = src.read_u16(); + let _length = src.read_u16(); + + match caps_type { + Self::CAPSTYPE_GENERAL => { + let general = GeneralCapabilitySet::decode(src)?; + Ok(Self::General(general)) + } + _ => Err(invalid_message_err!( + "capabilitySetType", + "invalid clipboard capability set type" + )), + } + } +} + +/// Represents `CLIPRDR_GENERAL_CAPABILITY` without header +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneralCapabilitySet { + pub version: ClipboardProtocolVersion, + pub general_flags: ClipboardGeneralCapabilityFlags, +} + +impl GeneralCapabilitySet { + const NAME: &str = "CLIPRDR_GENERAL_CAPABILITY"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() * 2; +} + +impl PduEncode for GeneralCapabilitySet { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(self.version.into()); + dst.write_u32(self.general_flags.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> PduDecode<'de> for GeneralCapabilitySet { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let version: ClipboardProtocolVersion = src.read_u32().try_into()?; + let general_flags = ClipboardGeneralCapabilityFlags::from_bits_truncate(src.read_u32()); + + Ok(Self { version, general_flags }) + } +} + +/// Specifies the `Remote Desktop Protocol: Clipboard Virtual Channel Extension` version number. +/// This field is for informational purposes and MUST NOT be used to make protocol capability +/// decisions. The actual features supported are specified via [`ClipboardGeneralCapabilityFlags`] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClipboardProtocolVersion { + V1, + V2, +} + +impl ClipboardProtocolVersion { + const VERSION_VALUE_V1: u32 = 0x00000001; + const VERSION_VALUE_V2: u32 = 0x00000002; + + const NAME: &str = "CLIPRDR_CAPS_VERSION"; +} + +impl From for u32 { + fn from(version: ClipboardProtocolVersion) -> Self { + match version { + ClipboardProtocolVersion::V1 => ClipboardProtocolVersion::VERSION_VALUE_V1, + ClipboardProtocolVersion::V2 => ClipboardProtocolVersion::VERSION_VALUE_V2, + } + } +} + +impl TryFrom for ClipboardProtocolVersion { + type Error = ironrdp_pdu::PduError; + + fn try_from(value: u32) -> Result { + match value { + Self::VERSION_VALUE_V1 => Ok(Self::V1), + Self::VERSION_VALUE_V2 => Ok(Self::V2), + _ => Err(invalid_message_err!( + "version", + "Invalid clipboard capabilities version" + )), + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardGeneralCapabilityFlags: u32 { + /// The Long Format Name variant of the Format List PDU is supported + /// for exchanging updated format names. If this flag is not set, the + /// Short Format Name variant MUST be used. If this flag is set by both + /// protocol endpoints, then the Long Format Name variant MUST be + /// used. + const USE_LONG_FORMAT_NAMES = 0x00000002; + /// File copy and paste using stream-based operations are supported + /// using the File Contents Request PDU and File Contents Response + /// PDU. + const STREAM_FILECLIP_ENABLED = 0x00000004; + /// Indicates that any description of files to copy and paste MUST NOT + /// include the source path of the files. + const FILECLIP_NO_FILE_PATHS = 0x00000008; + /// Locking and unlocking of File Stream data on the clipboard is + /// supported using the Lock Clipboard Data PDU and Unlock Clipboard + /// Data PDU. + const CAN_LOCK_CLIPDATA = 0x00000010; + /// Indicates support for transferring files that are larger than + /// 4,294,967,295 bytes in size. If this flag is not set, then only files of + /// size less than or equal to 4,294,967,295 bytes can be exchanged + /// using the File Contents Request PDU and File Contents + /// Response PDU. + const HUGE_FILE_SUPPORT_ENABLED = 0x00000020; + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs b/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs new file mode 100644 index 000000000..f373e0190 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/client_temporary_directory.rs @@ -0,0 +1,79 @@ +use crate::pdu::PartialHeader; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{read_string_from_cursor, write_string_to_cursor, CharacterSet}; +use ironrdp_pdu::{ + cast_int, ensure_fixed_part_size, ensure_size, invalid_message_err, PduDecode, PduEncode, PduResult, +}; +use std::borrow::Cow; + +/// Represents `CLIPRDR_TEMP_DIRECTORY` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientTemporaryDirectory<'a> { + path_buffer: Cow<'a, [u8]>, +} + +impl ClientTemporaryDirectory<'_> { + const PATH_BUFFER_SIZE: usize = 520; + + const NAME: &str = "CLIPRDR_TEMP_DIRECTORY"; + const FIXED_PART_SIZE: usize = Self::PATH_BUFFER_SIZE; + + /// Creates new `ClientTemporaryDirectory` and encodes given path to UTF-16 representation. + pub fn new(path: String) -> PduResult { + let mut buffer = vec![0x00; Self::PATH_BUFFER_SIZE]; + + { + let mut cursor = WriteCursor::new(&mut buffer); + write_string_to_cursor(&mut cursor, &path, CharacterSet::Unicode, true)?; + } + + Ok(Self { + path_buffer: Cow::Owned(buffer), + }) + } + + /// Returns parsed temporary directory path. + pub fn temporary_directory_path(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.path_buffer); + + read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true) + .map_err(|_| invalid_message_err!("wszTempDir", "failed to decode temp dir path")) + } + + fn inner_size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl PduEncode for ClientTemporaryDirectory<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", self.inner_size())?); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + dst.write_slice(&self.path_buffer); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.inner_size() + } +} + +impl<'de> PduDecode<'de> for ClientTemporaryDirectory<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let buffer = src.read_slice(Self::PATH_BUFFER_SIZE); + + Ok(Self { + path_buffer: Cow::Borrowed(buffer), + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/file_contents.rs b/crates/ironrdp-cliprdr/src/pdu/file_contents.rs new file mode 100644 index 000000000..51568afaa --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/file_contents.rs @@ -0,0 +1,230 @@ +use crate::pdu::{ClipboardPduFlags, PartialHeader}; +use bitflags::bitflags; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{combine_u64, split_u64}; +use ironrdp_pdu::{cast_int, ensure_size, invalid_message_err, PduDecode, PduEncode, PduResult}; +use std::borrow::Cow; + +bitflags! { + /// Represents `dwFlags` field of `CLIPRDR_FILECONTENTS_REQUEST` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct FileContentsFlags: u32 { + /// A request for the size of the file identified by the lindex field. The size MUST be + /// returned as a 64-bit, unsigned integer. The cbRequested field MUST be set to + /// 0x00000008 and both the nPositionLow and nPositionHigh fields MUST be + /// set to 0x00000000. + const SIZE = 0x00000001; + /// A request for the data present in the file identified by the lindex field. The data + /// to be retrieved is extracted starting from the offset given by the nPositionLow + /// and nPositionHigh fields. The maximum number of bytes to extract is specified + /// by the cbRequested field. + const DATA = 0x00000002; + } +} + +/// Represents `CLIPRDR_FILECONTENTS_RESPONSE` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileContentsResponse<'a> { + is_error: bool, + stream_id: u32, + data: Cow<'a, [u8]>, +} + +impl<'a> FileContentsResponse<'a> { + const NAME: &str = "CLIPRDR_FILECONTENTS_RESPONSE"; + const FIXED_PART_SIZE: usize = std::mem::size_of::(); + + fn inner_size(&self) -> usize { + Self::FIXED_PART_SIZE + self.data.len() + } + + /// Creates a new `FileContentsResponse` with u64 size value + pub fn new_size_response(stream_id: u32, size: u64) -> Self { + Self { + is_error: false, + stream_id, + data: Cow::Owned(size.to_le_bytes().to_vec()), + } + } + + /// Creates a new `FileContentsResponse` with file contents value + pub fn new_data_response(stream_id: u32, data: impl Into>) -> Self { + Self { + is_error: false, + stream_id, + data: data.into(), + } + } + + /// Creates new `FileContentsResponse` with error + pub fn new_error(stream_id: u32) -> Self { + Self { + is_error: true, + stream_id, + data: Cow::Borrowed(&[]), + } + } + + pub fn stream_id(&self) -> u32 { + self.stream_id + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Read data as u64 size value + pub fn data_as_size(&self) -> PduResult { + if self.data.len() != 8 { + return Err(invalid_message_err!( + "requestedFileContentsData", + "Invalid data size for u64 size" + )); + } + + Ok(u64::from_le_bytes(self.data.as_ref().try_into().unwrap())) + } +} + +impl PduEncode for FileContentsResponse<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let flags = if self.is_error { + ClipboardPduFlags::RESPONSE_FAIL + } else { + ClipboardPduFlags::RESPONSE_OK + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.inner_size())?, flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u32(self.stream_id); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.inner_size() + } +} + +impl<'de> PduDecode<'de> for FileContentsResponse<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let header = PartialHeader::decode(src)?; + + let is_error = header.message_flags.contains(ClipboardPduFlags::RESPONSE_FAIL); + + ensure_size!(in: src, size: header.data_length()); + + if header.data_length() < Self::FIXED_PART_SIZE { + return Err(invalid_message_err!("requestedFileContentsData", "Invalid data size")); + }; + + let data_size = header.data_length() - Self::FIXED_PART_SIZE; + + let stream_id = src.read_u32(); + let data = src.read_slice(data_size); + + Ok(Self { + is_error, + stream_id, + data: Cow::Borrowed(data), + }) + } +} + +/// Represents `CLIPRDR_FILECONTENTS_REQUEST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileContentsRequest { + pub stream_id: u32, + pub index: u32, + pub flags: FileContentsFlags, + pub position: u64, + pub requested_size: u32, + pub data_id: Option, +} + +impl FileContentsRequest { + const NAME: &str = "CLIPRDR_FILECONTENTS_REQUEST"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() * 4 + std::mem::size_of::(); + + fn inner_size(&self) -> usize { + let data_id_size = match self.data_id { + Some(_) => std::mem::size_of::(), + None => 0, + }; + + Self::FIXED_PART_SIZE + data_id_size + } +} + +impl PduEncode for FileContentsRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", self.inner_size())?); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.inner_size()); + + dst.write_u32(self.stream_id); + dst.write_u32(self.index); + dst.write_u32(self.flags.bits()); + + let (position_lo, position_hi) = split_u64(self.position); + dst.write_u32(position_lo); + dst.write_u32(position_hi); + dst.write_u32(self.requested_size); + + if let Some(data_id) = self.data_id { + dst.write_u32(data_id); + }; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.inner_size() + } +} + +impl<'de> PduDecode<'de> for FileContentsRequest { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let header = PartialHeader::decode(src)?; + + let read_data_id = header.data_length() > Self::FIXED_PART_SIZE; + + let mut expected_size = Self::FIXED_PART_SIZE; + if read_data_id { + expected_size += std::mem::size_of::(); + } + + ensure_size!(in: src, size: expected_size); + + let stream_id = src.read_u32(); + let index = src.read_u32(); + let flags = FileContentsFlags::from_bits_truncate(src.read_u32()); + let position_lo = src.read_u32(); + let position_hi = src.read_u32(); + let position = combine_u64(position_lo, position_hi); + let requested_size = src.read_u32(); + let data_id = if read_data_id { Some(src.read_u32()) } else { None }; + + Ok(Self { + stream_id, + index, + flags, + position, + requested_size, + data_id, + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs new file mode 100644 index 000000000..d4ead3945 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs @@ -0,0 +1,200 @@ +use bitflags::bitflags; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{combine_u64, read_string_from_cursor, split_u64, write_string_to_cursor, CharacterSet}; +use ironrdp_pdu::{cast_length, ensure_fixed_part_size, PduDecode, PduEncode, PduResult}; + +bitflags! { + /// Represents `flags` field of `CLIPRDR_FILEDESCRIPTOR` structure. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardFileFlags: u32 { + /// The fileAttributes field contains valid data. + const ATTRIBUTES = 0x00000004; + /// The fileSizeHigh and fileSizeLow fields contain valid data. + const FILE_SIZE = 0x00000040; + /// The lastWriteTime field contains valid data. + const LAST_WRITE_TIME = 0x00000020; + } +} + +bitflags! { + /// Represents `fileAttributes` of `CLIPRDR_FILEDESCRIPTOR` strucutre. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ClipboardFileAttributes: u32 { + /// A file that is read-only. Applications can read the file, but cannot write to + /// it or delete it + const READONLY = 0x00000001; + /// The file or directory is hidden. It is not included in an ordinary directory + /// listing. + const HIDDEN = 0x00000002; + /// A file or directory that the operating system uses a part of, or uses + /// exclusively. + const SYSTEM = 0x00000004; + /// Identifies a directory. + const DIRECTORY = 0x00000010; + /// A file or directory that is an archive file or directory. Applications typically + /// use this attribute to mark files for backup or removal + const ARCHIVE = 0x00000020; + /// A file that does not have other attributes set. This attribute is valid only + /// when used alone. + const NORMAL = 0x00000080; + } +} + +/// Represents `CLIPRDR_FILEDESCRIPTOR` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileDescriptor { + pub attibutes: Option, + pub last_write_time: Option, + pub file_size: Option, + pub name: String, +} + +impl FileDescriptor { + const NAME: &str = "CLIPRDR_FILEDESCRIPTOR"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() // flags + + 32 // reserved + + std::mem::size_of::() // attributes + + 16 // reserved + + std::mem::size_of::() // last write time + + std::mem::size_of::() // size + + 520; // name + + const SIZE: usize = Self::FIXED_PART_SIZE; +} + +impl PduEncode for FileDescriptor { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_fixed_part_size!(in: dst); + + let mut flags = ClipboardFileFlags::empty(); + if self.attibutes.is_some() { + flags |= ClipboardFileFlags::ATTRIBUTES; + } + if self.last_write_time.is_some() { + flags |= ClipboardFileFlags::LAST_WRITE_TIME; + } + if self.file_size.is_some() { + flags |= ClipboardFileFlags::FILE_SIZE; + } + + dst.write_u32(flags.bits()); + dst.write_array([0u8; 32]); + dst.write_u32(self.attibutes.unwrap_or(ClipboardFileAttributes::empty()).bits()); + dst.write_array([0u8; 16]); + dst.write_u64(self.last_write_time.unwrap_or_default()); + + let (size_lo, size_hi) = split_u64(self.file_size.unwrap_or_default()); + dst.write_u32(size_hi); + dst.write_u32(size_lo); + + { + let mut cursor = WriteCursor::new(dst.remaining_mut()); + write_string_to_cursor(&mut cursor, &self.name, CharacterSet::Unicode, true)?; + } + + dst.advance(520); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'de> PduDecode<'de> for FileDescriptor { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let flags = ClipboardFileFlags::from_bits_truncate(src.read_u32()); + src.read_array::<32>(); + let attibutes = if flags.contains(ClipboardFileFlags::ATTRIBUTES) { + Some(ClipboardFileAttributes::from_bits_truncate(src.read_u32())) + } else { + let _ = src.read_u32(); + None + }; + src.read_array::<16>(); + let last_write_time = if flags.contains(ClipboardFileFlags::LAST_WRITE_TIME) { + Some(src.read_u64()) + } else { + let _ = src.read_u64(); + None + }; + let file_size = if flags.contains(ClipboardFileFlags::FILE_SIZE) { + let size_hi = src.read_u32(); + let size_lo = src.read_u32(); + Some(combine_u64(size_lo, size_hi)) + } else { + let _ = src.read_u64(); + None + }; + + let name = { + let mut cursor = ReadCursor::new(src.remaining()); + read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true)? + }; + + src.advance(520); + + Ok(Self { + attibutes, + last_write_time, + file_size, + name, + }) + } +} + +/// Represents `CLIPRDR_FILELIST` +/// +/// NOTE: `PduDecode` implementation will read all remaining data in cursor as file list. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedFileList { + pub files: Vec, +} + +impl PackedFileList { + const NAME: &str = "CLIPRDR_FILELIST"; + const FIXED_PART_SIZE: usize = std::mem::size_of::(); // file count +} + +impl PduEncode for PackedFileList { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u32(cast_length!(Self::NAME, "cItems", self.files.len())?); + + for file in &self.files { + file.encode(dst)?; + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + FileDescriptor::SIZE * self.files.len() + } +} + +impl<'de> PduDecode<'de> for PackedFileList { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + let file_count = cast_length!(Self::NAME, "cItems", src.read_u32())?; + + let mut files = Vec::with_capacity(file_count); + for _ in 0..file_count { + files.push(FileDescriptor::decode(src)?); + } + + Ok(Self { files }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs new file mode 100644 index 000000000..0ba3f0280 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/metafile.rs @@ -0,0 +1,109 @@ +use bitflags::bitflags; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::{ensure_fixed_part_size, ensure_size, PduDecode, PduEncode, PduResult}; +use std::borrow::Cow; + +bitflags! { + /// Represents `mappingMode` fields of `CLIPRDR_MFPICT` strucutre. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct PackedMetafileMappingMode: u32 { + /// Each logical unit is mapped to one device pixel. Positive x is to the right; positive + /// y is down. + const TEXT = 0x00000001; + /// Each logical unit is mapped to 0.1 millimeter. Positive x is to the right; positive + /// y is up. + const LO_METRIC = 0x00000002; + /// Each logical unit is mapped to 0.01 millimeter. Positive x is to the right; positive + /// y is up. + const HI_METRIC = 0x00000003; + /// Each logical unit is mapped to 0.01 inch. Positive x is to the right; positive y is up. + const LO_ENGLISH = 0x00000004; + /// Each logical unit is mapped to 0.001 inch. Positive x is to the right; positive y is up. + const HI_ENGLISH = 0x00000005; + /// Each logical unit is mapped to 1/20 of a printer's point (1/1440 of an inch), also + /// called a twip. Positive x is to the right; positive y is up. + const TWIPS = 0x00000006; + /// Logical units are mapped to arbitrary units with equally scaled axes; one unit along + /// the x-axis is equal to one unit along the y-axis. + const ISOTROPIC = 0x00000007; + /// Logical units are mapped to arbitrary units with arbitrarily scaled axes. + const ANISOTROPIC = 0x00000008; + } +} + +/// Represents `CLIPRDR_MFPICT` +/// +/// NOTE: `PduDecode` implementation will read all remaining data in cursor as metafile contents. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackedMetafile<'a> { + pub mapping_mode: PackedMetafileMappingMode, + pub x_ext: u32, + pub y_ext: u32, + /// The variable sized contents of the metafile as specified in [MS-WMF] section 2 + data: Cow<'a, [u8]>, +} + +impl PackedMetafile<'_> { + const NAME: &str = "CLIPRDR_MFPICT"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() * 3; + + pub fn new( + mapping_mode: PackedMetafileMappingMode, + x_ext: u32, + y_ext: u32, + data: impl Into>, + ) -> Self { + Self { + mapping_mode, + x_ext, + y_ext, + data: data.into(), + } + } + + pub fn data(&self) -> &[u8] { + &self.data + } +} + +impl PduEncode for PackedMetafile<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u32(self.mapping_mode.bits()); + dst.write_u32(self.x_ext); + dst.write_u32(self.y_ext); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + self.data.len() + } +} + +impl<'de> PduDecode<'de> for PackedMetafile<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let mapping_mode = PackedMetafileMappingMode::from_bits_truncate(src.read_u32()); + let x_ext = src.read_u32(); + let y_ext = src.read_u32(); + + let data_len = src.len(); + + let data = src.read_slice(data_len); + + Ok(Self { + mapping_mode, + x_ext, + y_ext, + data: Cow::Borrowed(data), + }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs new file mode 100644 index 000000000..9bfacfa51 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs @@ -0,0 +1,230 @@ +mod file_list; +mod metafile; +mod palette; + +pub use file_list::*; +pub use metafile::*; +pub use palette::*; + +use crate::pdu::{ClipboardPduFlags, PartialHeader}; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{read_string_from_cursor, to_utf16_bytes, CharacterSet}; +use ironrdp_pdu::{cast_int, ensure_fixed_part_size, ensure_size, PduDecode, PduEncode, PduResult}; +use std::borrow::Cow; + +/// Represents `CLIPRDR_FORMAT_DATA_RESPONSE` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatDataResponse<'a> { + is_error: bool, + data: Cow<'a, [u8]>, +} + +impl<'a> FormatDataResponse<'a> { + const NAME: &str = "CLIPRDR_FORMAT_DATA_RESPONSE"; + + /// Creates new format data response from raw data. + pub fn new_data(data: impl Into>) -> Self { + Self { + is_error: false, + data: data.into(), + } + } + + /// Creates new error format data response. + pub fn new_error() -> Self { + Self { + is_error: true, + data: Cow::Borrowed(&[]), + } + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn is_error(&self) -> bool { + self.is_error + } + + /// Creates new format data response from clipboard palette. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`ClipboardPalette`] prior to the call. + pub fn new_palette(palette: &ClipboardPalette) -> PduResult { + let mut data = vec![0u8; palette.size()]; + + let mut cursor = WriteCursor::new(&mut data); + palette.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from packed metafile. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`PackedMetafile`] prior to the call. + pub fn new_metafile(metafile: &PackedMetafile) -> PduResult { + let mut data = vec![0u8; metafile.size()]; + + let mut cursor = WriteCursor::new(&mut data); + metafile.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from packed file list. Please note that this method + /// allocates memory for the data automatically. If you want to avoid this, you can use + /// `new_data` method and encode [`PackedFileList`] prior to the call. + pub fn new_file_list(list: &PackedFileList) -> PduResult { + let mut data = vec![0u8; list.size()]; + + let mut cursor = WriteCursor::new(&mut data); + list.encode(&mut cursor)?; + + Ok(Self { + is_error: false, + data: data.into(), + }) + } + + /// Creates new format data response from string. + pub fn new_unicode_string(value: &str) -> Self { + let mut encoded = to_utf16_bytes(value); + encoded.push(b'\0'); + encoded.push(b'\0'); + + Self { + is_error: false, + data: encoded.into(), + } + } + + /// Creates new format data response from string. + pub fn new_string(value: &str) -> Self { + let mut encoded = value.as_bytes().to_vec(); + encoded.push(b'\0'); + + Self { + is_error: false, + data: encoded.into(), + } + } + + /// Reads inner data as [`ClipboardPalette`] + pub fn to_palette(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.data); + ClipboardPalette::decode(&mut cursor) + } + + /// Reads inner data as [`PackedMetafile`] + pub fn to_metafile(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.data); + PackedMetafile::decode(&mut cursor) + } + + /// Reads inner data as [`PackedFileList`] + pub fn to_file_list(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.data); + PackedFileList::decode(&mut cursor) + } + + /// Reads inner data as string + pub fn to_string(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.data); + read_string_from_cursor(&mut cursor, CharacterSet::Ansi, true) + } + + /// Reads inner data as unicode string + pub fn to_unicode_string(&self) -> PduResult { + let mut cursor = ReadCursor::new(&self.data); + read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true) + } +} + +impl PduEncode for FormatDataResponse<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let flags = if self.is_error { + ClipboardPduFlags::RESPONSE_FAIL + } else { + ClipboardPduFlags::RESPONSE_OK + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.data.len())?, flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.data.len()); + dst.write_slice(&self.data); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.data.len() + } +} + +impl<'de> PduDecode<'de> for FormatDataResponse<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let header = PartialHeader::decode(src)?; + + let is_error = header.message_flags.contains(ClipboardPduFlags::RESPONSE_FAIL); + + ensure_size!(in: src, size: header.data_length()); + let data = src.read_slice(header.data_length()); + + Ok(Self { + is_error, + data: Cow::Borrowed(data), + }) + } +} + +/// Represents `CLIPRDR_FORMAT_DATA_REQUEST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatDataRequest { + pub format_id: u32, +} + +impl FormatDataRequest { + const NAME: &str = "CLIPRDR_FORMAT_DATA_REQUEST"; + const FIXED_PART_SIZE: usize = std::mem::size_of::(); +} + +impl PduEncode for FormatDataRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", Self::FIXED_PART_SIZE)?); + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.format_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + Self::FIXED_PART_SIZE + } +} + +impl<'de> PduDecode<'de> for FormatDataRequest { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let format_id = src.read_u32(); + + Ok(Self { format_id }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs b/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs new file mode 100644 index 000000000..ebe9718ea --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_data/palette.rs @@ -0,0 +1,71 @@ +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::{PduDecode, PduEncode, PduResult}; + +/// Represents `PALETTEENTRY` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PaletteEntry { + pub red: u8, + pub green: u8, + pub blue: u8, + pub extra: u8, +} + +impl PaletteEntry { + const SIZE: usize = std::mem::size_of::() * 4; +} + +/// Represents `CLIPRDR_PALETTE` +/// +/// NOTE: `PduDecode` implementation will read all remaining data in cursor as the palette entries. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardPalette { + pub entries: Vec, +} + +impl ClipboardPalette { + const NAME: &str = "CLIPRDR_PALETTE"; +} + +impl PduEncode for ClipboardPalette { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + for entry in &self.entries { + dst.write_u8(entry.red); + dst.write_u8(entry.green); + dst.write_u8(entry.blue); + dst.write_u8(entry.extra); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + self.entries.len() * PaletteEntry::SIZE + } +} + +impl<'de> PduDecode<'de> for ClipboardPalette { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let entries_count = src.len() / PaletteEntry::SIZE; + + let mut entries = Vec::with_capacity(entries_count); + for _ in 0..entries_count { + let red = src.read_u8(); + let green = src.read_u8(); + let blue = src.read_u8(); + let extra = src.read_u8(); + + entries.push(PaletteEntry { + red, + green, + blue, + extra, + }); + } + + Ok(Self { entries }) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/format_list.rs b/crates/ironrdp-cliprdr/src/pdu/format_list.rs new file mode 100644 index 000000000..8547305b9 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/format_list.rs @@ -0,0 +1,224 @@ +use crate::pdu::{ClipboardPduFlags, PartialHeader}; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::utils::{read_string_from_cursor, to_utf16_bytes, write_string_to_cursor, CharacterSet}; +use ironrdp_pdu::{cast_int, ensure_size, invalid_message_err, PduDecode, PduEncode, PduResult}; +use std::borrow::Cow; + +/// Represents `CLIPRDR_SHORT_FORMAT_NAME` and `CLIPRDR_LONG_FORMAT_NAME` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipboardFormat { + pub id: u32, + pub name: String, +} + +/// Represents `CLIPRDR_FORMAT_LIST` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FormatList<'a> { + use_ascii: bool, + encoded_formats: Cow<'a, [u8]>, +} + +impl FormatList<'_> { + const NAME: &str = "CLIPRDR_FORMAT_LIST"; + + // `CLIPRDR_SHORT_FORMAT_NAME` size + const SHORT_FORMAT_SIZE: usize = std::mem::size_of::() + 32; + + fn new_impl(formats: &[ClipboardFormat], use_long_format: bool, use_ascii: bool) -> PduResult { + let charset = if use_ascii { + CharacterSet::Ansi + } else { + CharacterSet::Unicode + }; + + let mut bytes_written = 0; + + if use_long_format { + // Sane default for formats buffer size to avoid reallocations + const DEFAULT_STRING_BUFFER_SIZE: usize = 1024; + let mut buffer = vec![0u8; DEFAULT_STRING_BUFFER_SIZE]; + + for format in formats { + let encoded_string = match charset { + CharacterSet::Ansi => { + let mut str_buffer = format.name.as_bytes().to_vec(); + str_buffer.push(b'\0'); + str_buffer + } + CharacterSet::Unicode => { + let mut str_buffer = to_utf16_bytes(&format.name); + str_buffer.push(b'\0'); + str_buffer.push(b'\0'); + str_buffer + } + }; + + let required_size = std::mem::size_of::() + encoded_string.len(); + if buffer.len() - bytes_written < required_size { + buffer.resize(bytes_written + required_size, 0); + } + + let mut cursor = WriteCursor::new(&mut buffer[bytes_written..]); + + // Write will never fail, as we pre-allocated space in buffer + cursor.write_u32(format.id); + cursor.write_slice(&encoded_string); + + bytes_written += required_size; + } + + buffer.truncate(bytes_written); + + Ok(Self { + use_ascii, + encoded_formats: Cow::Owned(buffer), + }) + } else { + let mut buffer = vec![0u8; Self::SHORT_FORMAT_SIZE * formats.len()]; + for (idx, format) in formats.iter().enumerate() { + let mut cursor = WriteCursor::new(&mut buffer[idx * Self::SHORT_FORMAT_SIZE..]); + cursor.write_u32(format.id); + write_string_to_cursor(&mut cursor, &format.name, charset, true)?; + } + + Ok(Self { + use_ascii, + encoded_formats: Cow::Owned(buffer), + }) + } + } + + pub fn new_unicode(formats: &[ClipboardFormat], use_long_format: bool) -> PduResult { + Self::new_impl(formats, use_long_format, false) + } + + pub fn new_ascii(formats: &[ClipboardFormat], use_long_format: bool) -> PduResult { + Self::new_impl(formats, use_long_format, true) + } + + pub fn get_formats(&self, use_long_format: bool) -> PduResult> { + let mut src = ReadCursor::new(self.encoded_formats.as_ref()); + let charset = if self.use_ascii { + CharacterSet::Ansi + } else { + CharacterSet::Unicode + }; + + if use_long_format { + // Minimal `CLIPRDR_LONG_FORMAT_NAME` size (id + null-terminated name) + const MINIMAL_FORMAT_SIZE: usize = std::mem::size_of::() + std::mem::size_of::(); + + let mut formats = Vec::with_capacity(16); + + while src.len() >= MINIMAL_FORMAT_SIZE { + let id = src.read_u32(); + let name = read_string_from_cursor(&mut src, charset, true)?; + + formats.push(ClipboardFormat { id, name }); + } + + Ok(formats) + } else { + let items_count = src.len() / Self::SHORT_FORMAT_SIZE; + + let mut formats = Vec::with_capacity(items_count); + + for _ in 0..items_count { + let id = src.read_u32(); + let name_buffer = src.read_slice(32); + + let mut name_cursor: ReadCursor<'_> = ReadCursor::new(name_buffer); + let name = read_string_from_cursor(&mut name_cursor, charset, true)?; + + formats.push(ClipboardFormat { id, name }); + } + + Ok(formats) + } + } +} + +impl<'de> PduDecode<'de> for FormatList<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let header = PartialHeader::decode(src)?; + + let use_ascii = header.message_flags.contains(ClipboardPduFlags::ASCII_NAMES); + ensure_size!(in: src, size: header.data_length()); + + let encoded_formats = src.read_slice(header.data_length()); + + Ok(Self { + use_ascii, + encoded_formats: Cow::Borrowed(encoded_formats), + }) + } +} + +impl PduEncode for FormatList<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header_flags = if self.use_ascii { + ClipboardPduFlags::ASCII_NAMES + } else { + ClipboardPduFlags::empty() + }; + + let header = PartialHeader::new_with_flags(cast_int!("dataLen", self.encoded_formats.len())?, header_flags); + header.encode(dst)?; + + ensure_size!(in: dst, size: self.encoded_formats.len()); + + dst.write_slice(&self.encoded_formats); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + self.encoded_formats.len() + } +} + +/// Represents `FORMAT_LIST_RESPONSE` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatListResponse { + Ok, + Fail, +} + +impl FormatListResponse { + const NAME: &str = "FORMAT_LIST_RESPONSE"; +} + +impl PduEncode for FormatListResponse { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header_flags = match self { + FormatListResponse::Ok => ClipboardPduFlags::RESPONSE_OK, + FormatListResponse::Fail => ClipboardPduFlags::RESPONSE_FAIL, + }; + + let header = PartialHeader::new_with_flags(0, header_flags); + header.encode(dst) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + } +} + +impl<'de> PduDecode<'de> for FormatListResponse { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let header = PartialHeader::decode(src)?; + match header.message_flags { + ClipboardPduFlags::RESPONSE_OK => Ok(FormatListResponse::Ok), + ClipboardPduFlags::RESPONSE_FAIL => Ok(FormatListResponse::Fail), + _ => Err(invalid_message_err!("msgFlags", "Invalid format list message flags")), + } + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/lock.rs b/crates/ironrdp-cliprdr/src/pdu/lock.rs new file mode 100644 index 000000000..38ef2d642 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/lock.rs @@ -0,0 +1,43 @@ +use crate::pdu::PartialHeader; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::{cast_int, ensure_fixed_part_size, PduDecode, PduEncode, PduResult}; + +/// Represents `CLIPRDR_LOCK_CLIPDATA`/`CLIPRDR_UNLOCK_CLIPDATA` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LockDataId(pub u32); + +impl LockDataId { + const NAME: &str = "CLIPRDR_(UN)LOCK_CLIPDATA"; + const FIXED_PART_SIZE: usize = std::mem::size_of::(); +} + +impl PduEncode for LockDataId { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let header = PartialHeader::new(cast_int!("dataLen", Self::FIXED_PART_SIZE)?); + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.0); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + PartialHeader::SIZE + Self::FIXED_PART_SIZE + } +} + +impl<'de> PduDecode<'de> for LockDataId { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + let _header = PartialHeader::decode(src)?; + + ensure_fixed_part_size!(in: src); + let id = src.read_u32(); + + Ok(Self(id)) + } +} diff --git a/crates/ironrdp-cliprdr/src/pdu/mod.rs b/crates/ironrdp-cliprdr/src/pdu/mod.rs new file mode 100644 index 000000000..c161c8a83 --- /dev/null +++ b/crates/ironrdp-cliprdr/src/pdu/mod.rs @@ -0,0 +1,249 @@ +//! This module implements RDP clipboard channel PDUs encode/decode logic as defined in +//! [MS-RDPECLIP]: Remote Desktop Protocol: Clipboard Virtual Channel Extension + +mod capabilities; +mod client_temporary_directory; +mod file_contents; +mod format_data; +mod format_list; +mod lock; + +pub use capabilities::*; +pub use client_temporary_directory::*; +pub use file_contents::*; +pub use format_data::*; +pub use format_list::*; +pub use lock::*; + +use bitflags::bitflags; +use ironrdp_pdu::cursor::{ReadCursor, WriteCursor}; +use ironrdp_pdu::{ensure_fixed_part_size, invalid_message_err, PduDecode, PduEncode, PduResult}; + +const MSG_TYPE_MONITOR_READY: u16 = 0x0001; +const MSG_TYPE_FORMAT_LIST: u16 = 0x0002; +const MSG_TYPE_FORMAT_LIST_RESPONSE: u16 = 0x0003; +const MSG_TYPE_FORMAT_DATA_REQUEST: u16 = 0x0004; +const MSG_TYPE_FORMAT_DATA_RESPONSE: u16 = 0x0005; +const MSG_TYPE_TEMPORARY_DIRECTORY: u16 = 0x0006; +const MSG_TYPE_CAPABILITIES: u16 = 0x0007; +const MSG_TYPE_FILE_CONTENTS_REQUEST: u16 = 0x0008; +const MSG_TYPE_FILE_CONTENTS_RESPONSE: u16 = 0x0009; +const MSG_TYPE_LOCK_CLIPDATA: u16 = 0x000A; +const MSG_TYPE_UNLOCK_CLIPDATA: u16 = 0x000B; + +pub const FORMAT_ID_PALETTE: u32 = 9; +pub const FORMAT_ID_METAFILE: u32 = 3; +pub const FORMAT_NAME_FILE_LIST: &str = "FileGroupDescriptorW"; + +/// Header without message type included +struct PartialHeader { + pub message_flags: ClipboardPduFlags, + pub data_length: u32, +} + +impl PartialHeader { + const NAME: &str = "CLIPRDR_HEADER"; + const FIXED_PART_SIZE: usize = std::mem::size_of::() + std::mem::size_of::(); + const SIZE: usize = Self::FIXED_PART_SIZE; + + pub fn new(inner_data_length: u32) -> Self { + Self::new_with_flags(inner_data_length, ClipboardPduFlags::empty()) + } + + pub fn new_with_flags(data_length: u32, message_flags: ClipboardPduFlags) -> Self { + Self { + message_flags, + data_length, + } + } + + pub fn data_length(&self) -> usize { + usize::try_from(self.data_length).expect("BUG: Upcasting u32 -> usize should be infallible") + } +} + +impl<'de> PduDecode<'de> for PartialHeader { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let message_flags = ClipboardPduFlags::from_bits_truncate(src.read_u16()); + let data_length = src.read_u32(); + + Ok(Self { + message_flags, + data_length, + }) + } +} + +impl PduEncode for PartialHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.message_flags.bits()); + dst.write_u32(self.data_length); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// Clipboard channel message PDU +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClipboardPdu<'a> { + MonitorReady, + FormatList(FormatList<'a>), + FormatListResponse(FormatListResponse), + FormatDataRequest(FormatDataRequest), + FormatDataResponse(FormatDataResponse<'a>), + TemporaryDirectory(ClientTemporaryDirectory<'a>), + Capabilites(Capabilities), + FileContentsRequest(FileContentsRequest), + FileContentsResponse(FileContentsResponse<'a>), + LockData(LockDataId), + UnlockData(LockDataId), +} + +impl ClipboardPdu<'_> { + const NAME: &str = "CliboardPdu"; + const FIXED_PART_SIZE: usize = std::mem::size_of::(); +} + +impl PduEncode for ClipboardPdu<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + ensure_fixed_part_size!(in: dst); + + let write_empty_pdu = |dst: &mut WriteCursor<'_>| { + let header = PartialHeader::new(0); + header.encode(dst) + }; + + match self { + ClipboardPdu::MonitorReady => { + dst.write_u16(MSG_TYPE_MONITOR_READY); + write_empty_pdu(dst) + } + ClipboardPdu::FormatList(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_LIST); + pdu.encode(dst) + } + ClipboardPdu::FormatListResponse(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_LIST_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::FormatDataRequest(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_DATA_REQUEST); + pdu.encode(dst) + } + ClipboardPdu::FormatDataResponse(pdu) => { + dst.write_u16(MSG_TYPE_FORMAT_DATA_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::TemporaryDirectory(pdu) => { + dst.write_u16(MSG_TYPE_TEMPORARY_DIRECTORY); + pdu.encode(dst) + } + ClipboardPdu::Capabilites(pdu) => { + dst.write_u16(MSG_TYPE_CAPABILITIES); + pdu.encode(dst) + } + ClipboardPdu::FileContentsRequest(pdu) => { + dst.write_u16(MSG_TYPE_FILE_CONTENTS_REQUEST); + pdu.encode(dst) + } + ClipboardPdu::FileContentsResponse(pdu) => { + dst.write_u16(MSG_TYPE_FILE_CONTENTS_RESPONSE); + pdu.encode(dst) + } + ClipboardPdu::LockData(pdu) => { + dst.write_u16(MSG_TYPE_LOCK_CLIPDATA); + pdu.encode(dst) + } + ClipboardPdu::UnlockData(pdu) => { + dst.write_u16(MSG_TYPE_UNLOCK_CLIPDATA); + pdu.encode(dst) + } + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + let empty_size = PartialHeader::SIZE; + + let variable_size = match self { + ClipboardPdu::MonitorReady => empty_size, + ClipboardPdu::FormatList(pdu) => pdu.size(), + ClipboardPdu::FormatListResponse(pdu) => pdu.size(), + ClipboardPdu::FormatDataRequest(pdu) => pdu.size(), + ClipboardPdu::FormatDataResponse(pdu) => pdu.size(), + ClipboardPdu::TemporaryDirectory(pdu) => pdu.size(), + ClipboardPdu::Capabilites(pdu) => pdu.size(), + ClipboardPdu::FileContentsRequest(pdu) => pdu.size(), + ClipboardPdu::FileContentsResponse(pdu) => pdu.size(), + ClipboardPdu::LockData(pdu) => pdu.size(), + ClipboardPdu::UnlockData(pdu) => pdu.size(), + }; + + Self::FIXED_PART_SIZE + variable_size + } +} + +impl<'de> PduDecode<'de> for ClipboardPdu<'de> { + fn decode(src: &mut ReadCursor<'de>) -> PduResult { + ensure_fixed_part_size!(in: src); + + let read_empty_pdu = |src: &mut ReadCursor<'de>| { + let _header = PartialHeader::decode(src)?; + Ok(()) + }; + + let pdu = match src.read_u16() { + MSG_TYPE_MONITOR_READY => { + read_empty_pdu(src)?; + ClipboardPdu::MonitorReady + } + MSG_TYPE_FORMAT_LIST => ClipboardPdu::FormatList(FormatList::decode(src)?), + MSG_TYPE_FORMAT_LIST_RESPONSE => ClipboardPdu::FormatListResponse(FormatListResponse::decode(src)?), + MSG_TYPE_FORMAT_DATA_REQUEST => ClipboardPdu::FormatDataRequest(FormatDataRequest::decode(src)?), + MSG_TYPE_FORMAT_DATA_RESPONSE => ClipboardPdu::FormatDataResponse(FormatDataResponse::decode(src)?), + MSG_TYPE_TEMPORARY_DIRECTORY => ClipboardPdu::TemporaryDirectory(ClientTemporaryDirectory::decode(src)?), + MSG_TYPE_CAPABILITIES => ClipboardPdu::Capabilites(Capabilities::decode(src)?), + MSG_TYPE_FILE_CONTENTS_REQUEST => ClipboardPdu::FileContentsRequest(FileContentsRequest::decode(src)?), + MSG_TYPE_FILE_CONTENTS_RESPONSE => ClipboardPdu::FileContentsResponse(FileContentsResponse::decode(src)?), + MSG_TYPE_LOCK_CLIPDATA => ClipboardPdu::LockData(LockDataId::decode(src)?), + MSG_TYPE_UNLOCK_CLIPDATA => ClipboardPdu::UnlockData(LockDataId::decode(src)?), + _ => return Err(invalid_message_err!("msgType", "Unknown clipboard PDU type")), + }; + + Ok(pdu) + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + /// Represents `msgFlags` field of `CLIPRDR_HEADER` structure + pub struct ClipboardPduFlags: u16 { + /// Used by the Format List Response PDU, Format Data Response PDU, and File + /// Contents Response PDU to indicate that the associated request Format List PDU, + /// Format Data Request PDU, and File Contents Request PDU were processed + /// successfully + const RESPONSE_OK = 0x0001; + /// Used by the Format List Response PDU, Format Data Response PDU, and File + /// Contents Response PDU to indicate that the associated Format List PDU, Format + /// Data Request PDU, and File Contents Request PDU were not processed successfull + const RESPONSE_FAIL = 0x0002; + /// Used by the Short Format Name variant of the Format List Response PDU to indicate + /// that the format names are in ASCII 8 + const ASCII_NAMES = 0x0004; + } +} diff --git a/crates/ironrdp-fuzzing/Cargo.toml b/crates/ironrdp-fuzzing/Cargo.toml index 39516d778..6dba167c5 100644 --- a/crates/ironrdp-fuzzing/Cargo.toml +++ b/crates/ironrdp-fuzzing/Cargo.toml @@ -13,3 +13,4 @@ test = false arbitrary = { version = "1", features = ["derive"] } ironrdp-graphics.workspace = true ironrdp-pdu.workspace = true +ironrdp-cliprdr.workspace = true \ No newline at end of file diff --git a/crates/ironrdp-fuzzing/src/oracles/mod.rs b/crates/ironrdp-fuzzing/src/oracles/mod.rs index f01fdb9df..f53ddb3c1 100644 --- a/crates/ironrdp-fuzzing/src/oracles/mod.rs +++ b/crates/ironrdp-fuzzing/src/oracles/mod.rs @@ -73,6 +73,8 @@ pub fn pdu_decode(data: &[u8]) { let _ = input::InputEvent::from_buffer(data); let _ = decode::(data); + + let _ = decode::(data); } pub fn rle_decompress_bitmap(input: BitmapInput) { diff --git a/crates/ironrdp-pdu/src/macros.rs b/crates/ironrdp-pdu/src/macros.rs index 4a6c0dc94..60e09b8a0 100644 --- a/crates/ironrdp-pdu/src/macros.rs +++ b/crates/ironrdp-pdu/src/macros.rs @@ -155,6 +155,23 @@ macro_rules! cast_length { }}; } +#[macro_export] +macro_rules! cast_int { + ($ctx:expr, $field:expr, $len:expr) => {{ + $len.try_into().map_err(|e| { + <$crate::PduError as $crate::PduErrorExt>::invalid_message( + $ctx, + $field, + "out of range integral type conversion", + ) + .with_source(e) + }) + }}; + ($field:expr, $len:expr) => {{ + $crate::cast_int!(Self::NAME, $field, $len) + }}; +} + /// Asserts that the traits support dynamic dispatch. /// /// From diff --git a/crates/ironrdp-pdu/src/utils.rs b/crates/ironrdp-pdu/src/utils.rs index 62b509b06..71ebee878 100644 --- a/crates/ironrdp-pdu/src/utils.rs +++ b/crates/ironrdp-pdu/src/utils.rs @@ -1,17 +1,37 @@ use std::io; +use crate::{ + cursor::{ReadCursor, WriteCursor}, + PduResult, +}; use byteorder::{LittleEndian, ReadBytesExt as _, WriteBytesExt as _}; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::ToPrimitive as _; -pub(crate) fn to_utf16_bytes(value: &str) -> Vec { +pub fn split_u64(value: u64) -> (u32, u32) { + let bytes = value.to_le_bytes(); + let (low, high) = bytes.split_at(std::mem::size_of::()); + ( + u32::from_le_bytes(low.try_into().unwrap()), + u32::from_le_bytes(high.try_into().unwrap()), + ) +} + +pub fn combine_u64(lo: u32, hi: u32) -> u64 { + let mut position_bytes = [0u8; std::mem::size_of::()]; + position_bytes[..std::mem::size_of::()].copy_from_slice(&lo.to_le_bytes()); + position_bytes[std::mem::size_of::()..].copy_from_slice(&hi.to_le_bytes()); + u64::from_le_bytes(position_bytes) +} + +pub fn to_utf16_bytes(value: &str) -> Vec { value .encode_utf16() .flat_map(|i| i.to_le_bytes().to_vec()) .collect::>() } -pub(crate) fn from_utf16_bytes(mut value: &[u8]) -> String { +pub fn from_utf16_bytes(mut value: &[u8]) -> String { let mut value_u16 = vec![0x00; value.len() / 2]; value .read_u16_into::(value_u16.as_mut()) @@ -21,11 +41,106 @@ pub(crate) fn from_utf16_bytes(mut value: &[u8]) -> String { } #[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] -pub(crate) enum CharacterSet { +pub enum CharacterSet { Ansi = 1, Unicode = 2, } +pub fn read_string_from_cursor( + cursor: &mut ReadCursor<'_>, + character_set: CharacterSet, + read_null_terminator: bool, +) -> PduResult { + let size = if character_set == CharacterSet::Unicode { + let code_units = if read_null_terminator { + // Find null or read all if null is not found + cursor + .remaining() + .chunks_exact(2) + .position(|chunk| chunk[0] == 0 && chunk[1] == 0) + .map(|code_units| code_units + 1) // Read null code point + .unwrap_or(cursor.len() / 2) + } else { + // UTF16 uses 2 bytes per code unit, so we need to read an even number of bytes + cursor.len() / 2 + }; + + code_units * 2 + } else if read_null_terminator { + // Find null or read all if null is not found + cursor + .remaining() + .iter() + .position(|&i| i == 0) + .map(|code_units| code_units + 1) // Read null code point + .unwrap_or(cursor.len()) + } else { + // Read all + cursor.len() + }; + + // Empty string, nothing to do + if size == 0 { + return Ok(String::new()); + } + + let result = match character_set { + CharacterSet::Unicode => { + ensure_size!(ctx: "Decode string (UTF-16)", in: cursor, size: size); + let mut slice = cursor.read_slice(size); + + let str_buffer = &mut slice; + let mut u16_buffer = vec![0u16; str_buffer.len() / 2]; + + str_buffer + .read_u16_into::(u16_buffer.as_mut()) + .expect("BUG: str_buffer is always even for UTF16"); + + String::from_utf16(&u16_buffer) + .map_err(|_| invalid_message_err!("UTF16 decode", "buffer", "Failed to decode UTF16 string"))? + } + CharacterSet::Ansi => { + ensure_size!(ctx: "Decode string (UTF-8)", in: cursor, size: size); + let slice = cursor.read_slice(size); + String::from_utf8(slice.to_vec()) + .map_err(|_| invalid_message_err!("UTF8 decode", "buffer", "Failed to decode UTF8 string"))? + } + }; + + Ok(result.trim_end_matches('\0').into()) +} + +pub fn write_string_to_cursor( + cursor: &mut WriteCursor<'_>, + value: &str, + character_set: CharacterSet, + write_null_terminator: bool, +) -> PduResult<()> { + match character_set { + CharacterSet::Unicode => { + let mut buffer = to_utf16_bytes(value); + if write_null_terminator { + buffer.push(0); + buffer.push(0); + } + + ensure_size!(ctx: "Encode sting (UTF-16)", in: cursor, size: buffer.len()); + cursor.write_slice(&buffer); + } + CharacterSet::Ansi => { + let mut buffer = value.as_bytes().to_vec(); + if write_null_terminator { + buffer.push(0); + } + + ensure_size!(ctx: "Encode sting (UTF-8)", in: cursor, size: buffer.len()); + cursor.write_slice(&buffer); + } + } + + Ok(()) +} + pub(crate) fn read_string( mut stream: impl io::Read, size: usize, diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 8f69d9512..77a98e8a9 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -26,6 +26,7 @@ paste = "1" [dev-dependencies] png = "0.17" hex = "0.4.3" +ironrdp-cliprdr.workspace = true ironrdp-connector.workspace = true ironrdp-fuzzing.workspace = true ironrdp-graphics.workspace = true diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu new file mode 100644 index 000000000..d41f1890c Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/client_temp_dir.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu new file mode 100644 index 000000000..603e08643 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/file_list.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu new file mode 100644 index 000000000..6537176bd Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu new file mode 100644 index 000000000..c25366f99 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/format_list_2.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu new file mode 100644 index 000000000..947167ec6 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/metafile.pdu differ diff --git a/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu new file mode 100644 index 000000000..d46cb6442 Binary files /dev/null and b/crates/ironrdp-testsuite-core/test_data/pdu/clipboard/palette.pdu differ diff --git a/crates/ironrdp-testsuite-core/tests/clipboard.rs b/crates/ironrdp-testsuite-core/tests/clipboard.rs new file mode 100644 index 000000000..c55790d16 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/clipboard.rs @@ -0,0 +1,406 @@ +use expect_test::expect; +use ironrdp_cliprdr::pdu::{ + Capabilities, CapabilitySet, ClipboardFormat, ClipboardGeneralCapabilityFlags, ClipboardPdu, + ClipboardProtocolVersion, FileContentsFlags, FileContentsRequest, FileContentsResponse, FormatDataRequest, + FormatDataResponse, FormatList, FormatListResponse, GeneralCapabilitySet, LockDataId, PackedMetafileMappingMode, +}; +use ironrdp_pdu::PduEncode; +use ironrdp_testsuite_core::encode_decode_test; + +// Test blobs from [MS-RDPECLIP] +encode_decode_test! { + capabilities: + ClipboardPdu::Capabilites( + Capabilities { + capabilities: vec![ + CapabilitySet::General( + GeneralCapabilitySet { + version: ClipboardProtocolVersion::V2, + general_flags: ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES + | ClipboardGeneralCapabilityFlags::STREAM_FILECLIP_ENABLED + | ClipboardGeneralCapabilityFlags::FILECLIP_NO_FILE_PATHS, + } + ) + ] + } + ), + [ + 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0c, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + ]; + + monitor_ready: + ClipboardPdu::MonitorReady, + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + format_list_response: + ClipboardPdu::FormatListResponse(FormatListResponse::Ok), + [ + 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + lock: + ClipboardPdu::LockData(LockDataId(8)), + [ + 0x0a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + ]; + + unlock: + ClipboardPdu::UnlockData(LockDataId(8)), + [ + 0x0b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + ]; + + format_data_request: + ClipboardPdu::FormatDataRequest(FormatDataRequest { + format_id: 0x0d, + }), + [ + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, + ]; + + format_data_response: + ClipboardPdu::FormatDataResponse( + FormatDataResponse::new_data(b"h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d\0\0\0".as_slice()), + ), + [ + 0x05, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, + 0x6f, 0x00, 0x20, 0x00, 0x77, 0x00, 0x6f, 0x00, + 0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x00, 0x00, + ]; + + file_contents_request_size: + ClipboardPdu::FileContentsRequest(FileContentsRequest { + stream_id: 2, + index: 1, + flags: FileContentsFlags::SIZE, + position: 0, + requested_size: 8, + data_id: None, + }), + [ + 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + ]; + + file_contents_request_data: + ClipboardPdu::FileContentsRequest(FileContentsRequest { + stream_id: 2, + index: 1, + flags: FileContentsFlags::DATA, + position: 0, + requested_size: 65536, + data_id: None, + }), + [ + 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + ]; + + file_contents_response_size: + ClipboardPdu::FileContentsResponse(FileContentsResponse::new_size_response(2, 44)), + [ + 0x09, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + file_contents_response_data: + ClipboardPdu::FileContentsResponse(FileContentsResponse::new_data_response( + 2, + b"The quick brown fox jumps over the lazy dog.".as_slice() + )), + [ + 0x09, 0x00, 0x01, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x54, 0x68, 0x65, 0x20, + 0x71, 0x75, 0x69, 0x63, 0x6b, 0x20, 0x62, 0x72, + 0x6f, 0x77, 0x6e, 0x20, 0x66, 0x6f, 0x78, 0x20, + 0x6a, 0x75, 0x6d, 0x70, 0x73, 0x20, 0x6f, 0x76, + 0x65, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6c, + 0x61, 0x7a, 0x79, 0x20, 0x64, 0x6f, 0x67, 0x2e, + ]; +} + +#[test] +fn client_temp_dir_encode_decode_ms_1() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/client_temp_dir.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::TemporaryDirectory(client_temp_dir) = &decoded_pdu { + let path = client_temp_dir.temporary_directory_path().unwrap(); + expect![[r#"C:\DOCUME~1\ELTONS~1.NTD\LOCALS~1\Temp\cdepotslhrdp_1\_TSABD.tmp"#]].assert_eq(&path); + } else { + panic!("Expected ClientTemporaryDirectory"); + } + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn format_list_ms_1() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/format_list.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::FormatList(format_list) = &decoded_pdu { + let formats = format_list.get_formats(true).unwrap(); + + expect![[r#" + [ + ClipboardFormat { + id: 49156, + name: "Native", + }, + ClipboardFormat { + id: 3, + name: "", + }, + ClipboardFormat { + id: 8, + name: "", + }, + ClipboardFormat { + id: 17, + name: "", + }, + ] + "#]] + .assert_debug_eq(&formats); + + formats + } else { + panic!("Expected FormatList"); + }; + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn format_list_ms_2() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/format_list_2.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::FormatList(format_list) = &decoded_pdu { + let formats = format_list.get_formats(true).unwrap(); + + expect![[r#" + [ + ClipboardFormat { + id: 49290, + name: "Rich Text Format", + }, + ClipboardFormat { + id: 49477, + name: "Rich Text Format Without Objects", + }, + ClipboardFormat { + id: 49475, + name: "RTF As Text", + }, + ClipboardFormat { + id: 1, + name: "", + }, + ClipboardFormat { + id: 13, + name: "", + }, + ClipboardFormat { + id: 49156, + name: "Native", + }, + ClipboardFormat { + id: 49166, + name: "Object Descriptor", + }, + ClipboardFormat { + id: 3, + name: "", + }, + ClipboardFormat { + id: 16, + name: "", + }, + ClipboardFormat { + id: 7, + name: "", + }, + ] + "#]] + .assert_debug_eq(&formats); + + formats + } else { + panic!("Expected FormatList"); + }; + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} + +fn fake_format_list(use_ascii: bool, use_long_format: bool) -> Box> { + let formats = vec![ + ClipboardFormat { + id: 42, + name: "Hello".to_string(), + }, + ClipboardFormat { + id: 24, + name: "".to_string(), + }, + ClipboardFormat { + id: 11, + name: "World".to_string(), + }, + ]; + + let list = if use_ascii { + FormatList::new_ascii(&formats, use_long_format).unwrap() + } else { + FormatList::new_unicode(&formats, use_long_format).unwrap() + }; + + Box::new(list) +} + +#[test] +fn format_list_all_encodings() { + // ASCII, short format names + fake_format_list(true, false); + // ASCII, long format names + fake_format_list(true, true); + // Unicode, short format names + fake_format_list(false, false); + // Unicode, long format names + fake_format_list(false, true); +} + +#[test] +fn metafile_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/metafile.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let metafile = response.to_metafile().unwrap(); + + assert_eq!(metafile.mapping_mode, PackedMetafileMappingMode::ANISOTROPIC); + assert_eq!(metafile.x_ext, 556); + assert_eq!(metafile.y_ext, 423); + + // Just check some known arbitrary byte in raw metafile data + assert_eq!(metafile.data()[metafile.data().len() - 6], 0x03); + } else { + panic!("Expected FormatDataResponse"); + }; + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn palette_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/palette.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let palette = response.to_palette().unwrap(); + + assert_eq!(palette.entries.len(), 216); + + // Chack known palette color + assert_eq!(palette.entries[53].red, 0xff); + assert_eq!(palette.entries[53].green, 0x66); + assert_eq!(palette.entries[53].blue, 0x33); + assert_eq!(palette.entries[53].extra, 0x00); + } else { + panic!("Expected FormatDataResponse"); + }; + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} + +#[test] +fn file_list_pdu_ms() { + // Test blob from [MS-RDPECLIP] + let input = include_bytes!("../test_data/pdu/clipboard/file_list.pdu"); + + let decoded_pdu: ClipboardPdu = ironrdp_pdu::decode(input).unwrap(); + + if let ClipboardPdu::FormatDataResponse(response) = &decoded_pdu { + let file_list = response.to_file_list().unwrap(); + + expect![[r#" + [ + FileDescriptor { + attibutes: Some( + ClipboardFileAttributes( + ARCHIVE, + ), + ), + last_write_time: Some( + 129010042240261384, + ), + file_size: Some( + 44, + ), + name: "File1.txt", + }, + FileDescriptor { + attibutes: Some( + ClipboardFileAttributes( + ARCHIVE, + ), + ), + last_write_time: Some( + 129010042240261384, + ), + file_size: Some( + 10, + ), + name: "File2.txt", + }, + ] + "#]] + .assert_debug_eq(&file_list.files) + } else { + panic!("Expected FormatDataResponse"); + }; + + let mut encoded = Vec::with_capacity(decoded_pdu.size()); + let _ = ironrdp_pdu::encode_buf(&decoded_pdu, &mut encoded).unwrap(); + + assert_eq!(&encoded, input); +} diff --git a/crates/ironrdp-testsuite-core/tests/main.rs b/crates/ironrdp-testsuite-core/tests/main.rs index 1b7c3b067..c1fae6550 100644 --- a/crates/ironrdp-testsuite-core/tests/main.rs +++ b/crates/ironrdp-testsuite-core/tests/main.rs @@ -9,6 +9,7 @@ //! Cargo will run all tests from a single binary in parallel, but //! binaries themselves are run sequentally. +mod clipboard; mod graphics; mod input; mod pcb;