diff --git a/Cargo.lock b/Cargo.lock index cf60c7a1..bfd78466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "bmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69985ff4f58085ac696454692d0b646a66ad1f9cc9be294c91dc51bb5df511ae" +dependencies = [ + "byteorder", +] + [[package]] name = "bufstream" version = "0.1.4" @@ -912,6 +921,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dissimilar" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e" + [[package]] name = "dlib" version = "0.5.0" @@ -969,6 +984,16 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "expect-test" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -1543,7 +1568,9 @@ dependencies = [ "bit_field", "bitflags 2.0.2", "bitvec", + "bmp", "byteorder", + "expect-test", "hex-literal", "ironrdp-pdu", "lazy_static", @@ -1575,6 +1602,7 @@ dependencies = [ "bitflags 2.0.2", "byteorder", "der-parser 8.2.0", + "expect-test", "ironrdp-pdu-samples", "lazy_static", "md-5 0.10.5", diff --git a/Cargo.toml b/Cargo.toml index 0e06baf6..451fa139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,22 +59,23 @@ keywords = ["rdp", "remote-desktop", "network", "client", "protocol"] categories = ["network-programming"] [workspace.dependencies] +expect-test = "1" ironrdp = { version = "0.5", path = "." } -ironrdp-pdu = { version = "0.1", path = "crates/pdu" } +ironrdp-async = { version = "0.1", path = "crates/async" } ironrdp-connector = { version = "0.1", path = "crates/connector" } -ironrdp-session = { version = "0.1", path = "crates/session" } ironrdp-graphics = { version = "0.1", path = "crates/graphics" } ironrdp-input = { version = "0.1", path = "crates/input" } -ironrdp-async = { version = "0.1", path = "crates/async" } -ironrdp-tls = { version = "0.1", path = "crates/tls" } -ironrdp-rdcleanpath = { version = "0.1", path = "crates/rdcleanpath" } -ironrdp-pdu-samples = { path = "crates/pdu-samples" } +ironrdp-pdu = { version = "0.1", path = "crates/pdu" } ironrdp-pdu-generators = { path = "crates/pdu-generators" } +ironrdp-pdu-samples = { path = "crates/pdu-samples" } +ironrdp-rdcleanpath = { version = "0.1", path = "crates/rdcleanpath" } +ironrdp-session = { version = "0.1", path = "crates/session" } ironrdp-session-generators = { path = "crates/session-generators" } -sspi = "0.8.1" -tracing = "0.1.37" +ironrdp-tls = { version = "0.1", path = "crates/tls" } proptest = "1.1.0" rstest = "0.17.0" +sspi = "0.8.1" +tracing = "0.1.37" [profile.dev] opt-level = 1 diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 991d45ac..dae02f82 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -131,6 +131,15 @@ struct Args { #[clap(long)] small_cache: bool, + /// Enable RDP6 lossy bitmap compression algorithm. Please note that lossy compression + /// only works with 32 bit color depth + #[clap(long)] + lossy_bitmap_compression: bool, + + /// Set required color depth. Currently only 32 and 16 bit color depths are supported + #[clap(long)] + color_depth: Option, + /// Enabled capability versions. Each bit represents enabling a capability version /// starting from V8 to V10_7 #[clap(long, value_parser = parse_hex, default_value_t = 0)] @@ -168,6 +177,23 @@ impl Config { .context("Password prompt")? }; + let bitmap = if let Some(color_depth) = args.color_depth { + if color_depth != 16 && color_depth != 32 { + anyhow::bail!("Invalid color depth. Only 16 and 32 bit color depths are supported."); + } + + if color_depth != 32 && args.lossy_bitmap_compression { + anyhow::bail!("Lossy bitmap compression only works with 32 bit color depth."); + } + + Some(connector::BitmapConfig { + color_depth, + lossy_compression: args.lossy_bitmap_compression, + }) + } else { + None + }; + let graphics = if args.avc444 || args.h264 { Some(connector::GraphicsConfig { avc444: args.avc444, @@ -195,6 +221,7 @@ impl Config { height: DEFAULT_HEIGHT, }, graphics, + bitmap, client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) .map(|version| version.major * 100 + version.minor * 10 + version.patch) .unwrap_or(0) diff --git a/crates/connector/src/connection.rs b/crates/connector/src/connection.rs index 2e3f50cf..5b0dc74c 100644 --- a/crates/connector/src/connection.rs +++ b/crates/connector/src/connection.rs @@ -781,6 +781,18 @@ impl Sequence for ClientConnector { fn create_gcc_blocks(config: &Config, selected_protocol: nego::SecurityProtocol) -> gcc::ClientGccBlocks { use ironrdp_pdu::gcc::*; + let color_depth = config + .bitmap + .as_ref() + .map(|bitmap| match bitmap.color_depth { + 15 => SupportedColorDepths::BPP15, + 16 => SupportedColorDepths::BPP16, + 24 => SupportedColorDepths::BPP24, + 32 => SupportedColorDepths::BPP32, + _ => panic!("Unsupported color depth: {}", bitmap.color_depth), + }) + .unwrap_or(SupportedColorDepths::BPP16); + ClientGccBlocks { core: ClientCoreData { version: RdpVersion::V5_PLUS, @@ -800,7 +812,7 @@ fn create_gcc_blocks(config: &Config, selected_protocol: nego::SecurityProtocol) client_product_id: Some(1), serial_number: Some(0), high_color_depth: Some(HighColorDepth::Bpp24), - supported_color_depths: Some(SupportedColorDepths::BPP16), + supported_color_depths: Some(color_depth), early_capability_flags: { let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE | ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU; @@ -893,6 +905,20 @@ fn create_client_confirm_active( server_capability_sets.retain(|capability_set| matches!(capability_set, CapabilitySet::MultiFragmentUpdate(_))); + let lossy_bitmap_compression = config + .bitmap + .as_ref() + .map(|bitmap| bitmap.lossy_compression) + .unwrap_or(false); + + let drawing_flags = if lossy_bitmap_compression { + BitmapDrawingFlags::ALLOW_SKIP_ALPHA + | BitmapDrawingFlags::ALLOW_DYNAMIC_COLOR_FIDELITY + | BitmapDrawingFlags::ALLOW_COLOR_SUBSAMPLING + } else { + BitmapDrawingFlags::ALLOW_SKIP_ALPHA + }; + server_capability_sets.extend_from_slice(&[ CapabilitySet::General(General { major_platform_type: config.platform, @@ -906,7 +932,7 @@ fn create_client_confirm_active( desktop_width: config.desktop_size.width, desktop_height: config.desktop_size.height, desktop_resize_flag: false, - drawing_flags: BitmapDrawingFlags::empty(), + drawing_flags, }), CapabilitySet::Order(Order::new( OrderFlags::NEGOTIATE_ORDER_SUPPORT | OrderFlags::ZERO_BOUNDS_DELTAS_SUPPORT, diff --git a/crates/connector/src/lib.rs b/crates/connector/src/lib.rs index bd944625..b3fb9369 100644 --- a/crates/connector/src/lib.rs +++ b/crates/connector/src/lib.rs @@ -39,6 +39,13 @@ pub struct GraphicsConfig { pub capabilities: u32, } +#[derive(Debug, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct BitmapConfig { + pub lossy_compression: bool, + pub color_depth: u32, +} + #[derive(Debug, Clone)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Config { @@ -56,6 +63,7 @@ pub struct Config { pub keyboard_functional_keys_count: u32, pub ime_file_name: String, pub graphics: Option, + pub bitmap: Option, pub dig_product_id: String, pub client_dir: String, pub platform: capability_sets::MajorPlatformType, diff --git a/crates/graphics/Cargo.toml b/crates/graphics/Cargo.toml index 19746cc5..7f74c3d8 100644 --- a/crates/graphics/Cargo.toml +++ b/crates/graphics/Cargo.toml @@ -23,6 +23,8 @@ bitflags = "2" lazy_static = "1.4.0" [dev-dependencies] +bmp = "0.5" +expect-test.workspace = true hex-literal = "0.3.4" proptest = "1.1.0" rdp-rs = { git = "https://github.com/citronneur/rdp-rs", rev = "7ac880d7efb7f05efef3c84476f7c24f4053e0ea" } diff --git a/crates/graphics/src/color_conversion.rs b/crates/graphics/src/color_conversion.rs index d05187c0..0d1e47d4 100644 --- a/crates/graphics/src/color_conversion.rs +++ b/crates/graphics/src/color_conversion.rs @@ -18,6 +18,10 @@ fn clip(v: i32) -> u8 { min(max(v, 0), 255) as u8 } +fn clip_i16(v: i16) -> u8 { + min(max(v, 0), 255) as u8 +} + #[derive(Debug)] pub struct YCbCrBuffer<'a> { pub y: &'a [i16], @@ -52,6 +56,13 @@ pub struct YCbCr { pub cr: i16, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct YCoCg { + pub y: u8, + pub co: i8, + pub cg: i8, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Rgb { pub r: u8, @@ -79,3 +90,19 @@ impl From for Rgb { Self { r, g, b } } } + +impl From for Rgb { + fn from(YCoCg { y, co, cg }: YCoCg) -> Self { + let y = i16::from(y); + let co = i16::from(co); + let cg = i16::from(cg); + + let t = y - cg / 2; + + let r = clip_i16(t + co / 2); + let g = clip_i16(y + cg / 2); + let b = clip_i16(t - co / 2); + + Self { r, g, b } + } +} diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs index 69d161a8..2d776758 100644 --- a/crates/graphics/src/lib.rs +++ b/crates/graphics/src/lib.rs @@ -2,6 +2,7 @@ pub mod color_conversion; pub mod dwt; pub mod image_processing; pub mod quantization; +pub mod rdp6; pub mod rectangle_processing; pub mod rle; pub mod rlgr; diff --git a/crates/graphics/src/rdp6/bitmap_stream.rs b/crates/graphics/src/rdp6/bitmap_stream.rs new file mode 100644 index 00000000..ce9da29a --- /dev/null +++ b/crates/graphics/src/rdp6/bitmap_stream.rs @@ -0,0 +1,334 @@ +use crate::{ + color_conversion::{Rgb, YCoCg}, + rdp6::rle::{decompress_8bpp_plane, RleError}, +}; +use ironrdp_pdu::{ + bitmap::rdp6::{BitmapStream as BitmapStreamPdu, ColorPlanes}, + decode, Error as PduError, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BitmapDecodeError { + #[error("Failed to decode RDP6 bitmap stream PDU: {0}")] + Pdu(#[from] PduError), + #[error("Failed to perform RLE decompression of RDP6 bitmap stream: {0}")] + Rle(#[from] RleError), + #[error("Color plane data size provided in PDU is not sufficient to reconstruct the bitmap")] + InvalidUncompressedDataSize, +} + +/// Implements decoding of RDP6 bitmap stream PDU (see [`BitmapStreamPdu`]) +#[derive(Debug, Default)] +pub struct BitmapStreamDecoder { + /// Optimization to avoid reallocations, re-use this buffer for all bitmaps in the session + planes_buffer: Vec, +} + +/// Internal implementation of RDP6 bitmap stream PDU decoder for specific image size and format +struct BitmapStreamDecoderImpl<'a> { + bitmap: BitmapStreamPdu<'a>, + image_width: usize, + image_height: usize, + chroma_width: usize, + chroma_height: usize, + full_plane_size: usize, + chroma_plane_size: usize, + uncompressed_planes_size: usize, + color_plane_offsets: [usize; 3], +} + +struct AYCoCgParams { + color_loss_level: u8, + chroma_subsampling: bool, + alpha: bool, +} + +impl<'a> BitmapStreamDecoderImpl<'a> { + pub fn init(bitmap: BitmapStreamPdu<'a>, image_width: usize, image_height: usize) -> Self { + let (chroma_width, chroma_height) = if bitmap.has_subsampled_chroma() { + // When image is subsampled, chroma plane has half the size of the luma plane, however + // its size is rounded up to the nearest greater integer, to take into account odd image + // size (e.g. if width is 3, then chroma plane width is 2, not 1, to take into account + // the odd column which expands to 1 pixel instead of 2 during supersampling) + ((image_width + 1) / 2, (image_height + 1) / 2) + } else { + (image_width, image_height) + }; + + let full_plane_size = image_width * image_height; + let chroma_plane_size = chroma_width * chroma_height; + + let uncompressed_planes_size = if bitmap.has_subsampled_chroma() { + full_plane_size + chroma_plane_size * 2 + } else { + full_plane_size * 3 + }; + + let color_plane_offsets = [0, full_plane_size, full_plane_size + chroma_plane_size]; + + Self { + bitmap, + image_width, + image_height, + chroma_width, + chroma_height, + full_plane_size, + chroma_plane_size, + uncompressed_planes_size, + color_plane_offsets, + } + } + + fn decompress_planes(&'a self, aux_buffer: &'a mut Vec) -> Result<&'a [u8], BitmapDecodeError> { + let planes = if self.bitmap.enable_rle_compression { + // We don't care for the previous content, just resize it to fit the data + aux_buffer.resize(self.uncompressed_planes_size, 0); + let uncompressed_planes_buffer = &mut aux_buffer[..self.uncompressed_planes_size]; + + let compressed = self.bitmap.color_panes_data(); + let mut src_offset = 0; + + // Decompress Alpha plane + if self.bitmap.use_alpha { + // Decompress alpha alpha, but discard it (always 0xFF) + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + uncompressed_planes_buffer, + self.image_width, + self.image_height, + )?; + } + + // Decompress R/Y plane + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[0]..], + self.image_width, + self.image_height, + )?; + + // Decompress G/Co plane + src_offset += decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[1]..], + self.chroma_width, + self.chroma_height, + )?; + + // Decompress B/Cg plane + decompress_8bpp_plane( + &compressed[src_offset..], + &mut uncompressed_planes_buffer[self.color_plane_offsets[2]..], + self.chroma_width, + self.chroma_height, + )?; + + &uncompressed_planes_buffer[..self.uncompressed_planes_size] + } else { + // Discard alpha plane + let color_planes_offset = if self.bitmap.use_alpha { self.full_plane_size } else { 0 }; + + let expected_data_size = color_planes_offset + self.uncompressed_planes_size; + + if self.bitmap.color_panes_data().len() < expected_data_size { + return Err(BitmapDecodeError::InvalidUncompressedDataSize); + } + + &self.bitmap.color_panes_data()[color_planes_offset..] + }; + + Ok(planes) + } + + fn write_argb_planes_to_rgb24(&self, planes: &[u8], dst: &mut Vec) { + // For ARGB comversion is simple - just copy data in correct order + let (r_offset, g_offset, b_offset) = ( + self.color_plane_offsets[0], + self.color_plane_offsets[1], + self.color_plane_offsets[2], + ); + + let r_plane = &planes[r_offset..r_offset + self.full_plane_size]; + let g_plane = &planes[g_offset..g_offset + self.full_plane_size]; + let b_plane = &planes[b_offset..b_offset + self.full_plane_size]; + + for i in 0..self.full_plane_size { + let (r, g, b) = (r_plane[i], g_plane[i], b_plane[i]); + + dst.extend_from_slice(&[r, g, b]); + } + } + + fn write_aycocg_planes_to_rgb24(&self, params: AYCoCgParams, planes: &[u8], dst: &mut Vec) { + // For AYCoCg we need to take color loss level and subsampling into account + let chroma_shift = (params.color_loss_level - 1) as usize; + let sample_shift = params.chroma_subsampling as usize; + + let (y_offset, co_offset, cg_offset) = ( + self.color_plane_offsets[0], + self.color_plane_offsets[1], + self.color_plane_offsets[2], + ); + + let y_plane = &planes[y_offset..y_offset + self.full_plane_size]; + let co_plane = &planes[co_offset..co_offset + self.chroma_plane_size]; + let cg_plane = &planes[cg_offset..cg_offset + self.chroma_plane_size]; + + for (idx, y) in y_plane.iter().copied().enumerate() { + let chroma_row = (idx / self.image_width) >> sample_shift; + let chroma_col = (idx % self.image_width) >> sample_shift; + let chroma_idx = chroma_row * self.chroma_width + chroma_col; + + let co = (co_plane[chroma_idx] << chroma_shift) as i8; + let cg = (cg_plane[chroma_idx] << chroma_shift) as i8; + + let Rgb { r, g, b } = YCoCg { y, co, cg }.into(); + + // As described in 3.1.9.1.2 [MS-RDPEGDI], R and B channels are swapped for + // AYCoCg when 24-bit image is used (no alpha). We swap them back here + if params.alpha { + dst.extend_from_slice(&[r, g, b]); + } else { + dst.extend_from_slice(&[b, g, r]); + } + } + } + + fn decode(self, dst: &mut Vec, aux_buffer: &'a mut Vec) -> Result<(), BitmapDecodeError> { + // Reserve enough space for decoded RGB channels data + dst.reserve(self.image_height * self.image_width * 3); + + match self.bitmap.color_planes { + ColorPlanes::Argb { .. } => { + let color_planes = self.decompress_planes(aux_buffer)?; + self.write_argb_planes_to_rgb24(color_planes, dst); + } + ColorPlanes::AYCoCg { + color_loss_level, + use_chroma_subsampling, + .. + } => { + let params = AYCoCgParams { + color_loss_level, + chroma_subsampling: use_chroma_subsampling, + alpha: self.bitmap.use_alpha, + }; + let color_planes = self.decompress_planes(aux_buffer)?; + self.write_aycocg_planes_to_rgb24(params, color_planes, dst); + } + } + + Ok(()) + } +} + +impl BitmapStreamDecoder { + /// Performs decoding of bitmap stream PDU from `bitmap_data` and writes decoded rgb24 + /// image to `dst` buffer. + pub fn decode_bitmap_stream_to_rgb24( + &mut self, + bitmap_data: &[u8], + dst: &mut Vec, + image_width: usize, + image_height: usize, + ) -> Result<(), BitmapDecodeError> { + let bitmap = decode::(bitmap_data)?; + + let decoder = BitmapStreamDecoderImpl::init(bitmap, image_width, image_height); + + decoder.decode(dst, &mut self.planes_buffer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_decoded_image(pdu: &[u8], expected_bmp: &[u8], width: usize, height: usize) { + let expected_bmp = bmp::from_reader(&mut std::io::Cursor::new(expected_bmp)).unwrap(); + let mut expected_buffer = vec![0; width * height * 3]; + for (idx, (x, y)) in expected_bmp.coordinates().enumerate() { + let pixel = expected_bmp.get_pixel(x, y); + + let offset = idx * 3; + expected_buffer[offset] = pixel.r; + expected_buffer[offset + 1] = pixel.g; + expected_buffer[offset + 2] = pixel.b; + } + + let mut actual = Vec::new(); + + BitmapStreamDecoder::default() + .decode_bitmap_stream_to_rgb24(pdu, &mut actual, width, height) + .unwrap(); + + assert_eq!(actual.as_slice(), expected_buffer.as_slice()); + } + + #[test] + fn decode_32x64_rgb_raw() { + // RGB (No alpha), no RLE + assert_decoded_image( + include_bytes!("test_assets/32x64_rgb_raw.bin"), + include_bytes!("test_assets/32x64_rgb_raw.bmp"), + 32, + 64, + ); + } + + #[test] + fn decode_64x24_argb_rle() { + // ARGB (With alpha), RLE + assert_decoded_image( + include_bytes!("test_assets/64x24_argb_rle.bin"), + include_bytes!("test_assets/64x24_argb_rle.bmp"), + 64, + 24, + ); + } + + #[test] + fn decode_64x24_aycocg_rle() { + // AYCoCg (With alpha), RLE, no chroma subsampling + assert_decoded_image( + include_bytes!("test_assets/64x24_aycocg_rle.bin"), + include_bytes!("test_assets/64x24_aycocg_rle.bmp"), + 64, + 24, + ); + } + + #[test] + fn decode_64x24_ycocg_rle_ss() { + // AYCoCg (No alpha), RLE, with chroma subsampling + assert_decoded_image( + include_bytes!("test_assets/64x24_ycocg_rle_ss.bin"), + include_bytes!("test_assets/64x24_ycocg_rle_ss.bmp"), + 64, + 24, + ); + } + + #[test] + fn decode_64x57_ycocg_rle_ss() { + // AYCoCg (No alpha), RLE, with chroma subsampling + odd resolution + assert_decoded_image( + include_bytes!("test_assets/64x57_ycocg_rle_ss.bin"), + include_bytes!("test_assets/64x57_ycocg_rle_ss.bmp"), + 64, + 57, + ); + } + + #[test] + fn decode_64x64_ycocg_raw_ss() { + // AYCoCg (No alpha), no RLE, with chroma subsampling + assert_decoded_image( + include_bytes!("test_assets/64x64_ycocg_raw_ss.bin"), + include_bytes!("test_assets/64x64_ycocg_raw_ss.bmp"), + 64, + 64, + ); + } +} diff --git a/crates/graphics/src/rdp6/mod.rs b/crates/graphics/src/rdp6/mod.rs new file mode 100644 index 00000000..7f99d1ba --- /dev/null +++ b/crates/graphics/src/rdp6/mod.rs @@ -0,0 +1,7 @@ +//! This module provides the RDP6 bitmap decoder implementation + +pub(crate) mod bitmap_stream; +pub(crate) mod rle; + +pub use bitmap_stream::{BitmapDecodeError, BitmapStreamDecoder}; +pub use rle::RleError; diff --git a/crates/graphics/src/rdp6/rle.rs b/crates/graphics/src/rdp6/rle.rs new file mode 100644 index 00000000..55fff780 --- /dev/null +++ b/crates/graphics/src/rdp6/rle.rs @@ -0,0 +1,334 @@ +use byteorder::ReadBytesExt; +use std::io::{Read, Write}; +use thiserror::Error; + +/// Maximum possible segment size is 47 (run_length = 2, raw_bytes_count = 15), which is treated as +/// special mode segment, which repeats last decoded byte in scanline 32 + raw_bytes_count times +const MAX_DECODED_SEGMENT_SIZE: usize = 47; + +#[derive(Debug, Error)] +pub enum RleError { + #[error("Failed to read RLE-compressed data: {0}")] + ReadCompressedData(#[source] std::io::Error), + + #[error("Failed to write decompressed data: {0}")] + WriteDecompressedData(#[source] std::io::Error), + + #[error("Invalid RLE segment header")] + InvalidSegmentHeader, + + #[error("Decoded scanline segments length exceeds scanline length")] + SegmentDoNotFitScanline, +} + +/// RLE-encoded color plane decoder implementation for RDP6 bitmap stream +#[derive(Debug)] +struct RlePlaneDecoder { + /// RDP6 performs per-scanline encoding, therefore segment decoder require state reset + /// for when each scanline is started (e.g. resetting last decoded byte value to 0) + last_decoded_byte: u8, + + width: usize, + height: usize, + + decoded_data: [u8; MAX_DECODED_SEGMENT_SIZE], + decoded_data_len: usize, +} + +impl RlePlaneDecoder { + pub fn new(width: usize, height: usize) -> Self { + Self { + last_decoded_byte: 0, + width, + height, + decoded_data: [0; MAX_DECODED_SEGMENT_SIZE], + decoded_data_len: 0, + } + } + + fn decompress_next_segment(&mut self, mut src: &[u8]) -> Result { + let control_byte = src.read_u8().map_err(RleError::ReadCompressedData)?; + + if control_byte == 0 { + return Err(RleError::InvalidSegmentHeader); + } + + let rle_bytes_field = control_byte & 0x0F; + let raw_bytes_field = (control_byte >> 4) & 0x0F; + + let (run_length, raw_bytes_count) = match rle_bytes_field { + 1 => (16 + raw_bytes_field as usize, 0), + 2 => (32 + raw_bytes_field as usize, 0), + rle_control => (rle_control as usize, raw_bytes_field as usize), + }; + + self.decoded_data_len = raw_bytes_count + run_length; + + src.read_exact(&mut self.decoded_data[..raw_bytes_count]) + .map_err(RleError::ReadCompressedData)?; + + if raw_bytes_count > 0 { + // save last decoded byte for the next segments decoding + self.last_decoded_byte = self.decoded_data[raw_bytes_count - 1]; + } + + self.decoded_data[raw_bytes_count..self.decoded_data_len].fill(self.last_decoded_byte); + + Ok(raw_bytes_count + 1) + } + + /// Decodes single RLE-encoded scanline, without performing delta transformation + fn decode_scanline(&mut self, src: &[u8], mut dst: &mut [u8]) -> Result { + let mut decoded_columns = 0; + let mut read_bytes = 0; + + self.last_decoded_byte = 0; + + while decoded_columns < self.width { + read_bytes += self.decompress_next_segment(&src[read_bytes..])?; + + if decoded_columns + self.decoded_data_len > self.width { + return Err(RleError::SegmentDoNotFitScanline); + } + + dst.write_all(&self.decoded_data[..self.decoded_data_len]) + .map_err(RleError::WriteDecompressedData)?; + + decoded_columns += self.decoded_data_len; + } + + Ok(read_bytes) + } + + /// Performs delta transformation as described in 3.1.9.2.3 of [MS-RDPEGDI] + fn resolve_scanline_delta(prev_line: &[u8], current_scanline: &mut [u8]) { + assert!(prev_line.len() == current_scanline.len()); + + current_scanline + .iter_mut() + .zip(prev_line.iter()) + .for_each(|(dst, src)| { + let delta = *dst; + let value_above = *src; + + let transformed_delta = if delta % 2 == 1 { + 255u8.wrapping_sub((delta.wrapping_sub(1)) >> 1) + } else { + delta >> 1 + }; + + *dst = value_above.wrapping_add(transformed_delta); + }); + } + + pub fn decode(mut self, src: &[u8], dst: &mut [u8]) -> Result { + let mut read_bytes = 0; + + read_bytes += self.decode_scanline(src, dst)?; + + let (mut prev_scanline, mut dst) = dst.split_at_mut(self.width); + + for _ in 1..self.height { + let current_scanline = &mut dst[..self.width]; + + read_bytes += self.decode_scanline(&src[read_bytes..], current_scanline)?; + Self::resolve_scanline_delta(prev_scanline, current_scanline); + + (prev_scanline, dst) = dst.split_at_mut(self.width); + } + + Ok(read_bytes) + } +} + +/// Performs decompression of 8bpp color plane into slice. +/// Slice must have enough space for decompressed data. +/// Size of data written to dst buffer is exactly equal to `width * height`. +/// +/// Returns number of bytes consumed from src buffer. +pub fn decompress_8bpp_plane( + src: &[u8], + dst: &mut [u8], + width: impl Into, + height: impl Into, +) -> Result { + let width = width.into(); + let height = height.into(); + + RlePlaneDecoder::new(width, height).decode(src, dst) +} + +#[cfg(test)] +mod tests { + use super::*; + + use expect_test::expect; + + /// Performs decompression of 8bpp color plane into vector. Vector will be resized to fit decompressed data. + pub fn decompress( + src: &[u8], + dst: &mut Vec, + width: impl Into, + height: impl Into, + ) -> Result { + let width = width.into(); + let height = height.into(); + // Ensure dest buffer have enough space for decompressed data + dst.resize(width * height, 0); + + decompress_8bpp_plane(src, dst.as_mut_slice(), width, height) + } + + #[test] + fn long_sequence_decode() { + // Example from 3.1.9.2.2 of [MS-RDPEGDI]. + let src = [0x1F, 0x41, 0xF2, 0x52]; + + let width = 100usize; + let height = 1usize; + + let expected = &[0x41u8; 100]; + + let mut actual = Vec::new(); + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn multiline_decode() { + // Example from 3.1.9.2.3 of [MS-RDPEGDI]. + let src = [ + 0x13, 0xFF, 0x20, 0xFE, 0xFD, 0x60, 0x01, 0x7D, 0xF5, 0xC2, 0x9A, 0x38, 0x60, 0x01, 0x67, 0x8B, 0xA3, 0x78, + 0xAF, + ]; + + let width = 6usize; + let height = 3usize; + + let expected = &[ + 255, 255, 255, 255, 254, 253, 254, 192, 132, 96, 75, 25, 253, 140, 62, 14, 135, 193, + ]; + + let mut actual = Vec::new(); + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn each_scanline_resets_last_decoded_byte() { + let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + + let expected = &[ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 0, 253, 1, + ]; + + decompress(&src, &mut actual, width, height).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn segments_out_of_scanline_produce_error() { + let src = [ + 0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image + 0x04, 0x40, 0x01, 0x02, 0x03, 0x04, + ]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + SegmentDoNotFitScanline, + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + + // Same test, but fail on non-first line + let src = [ + 0x17, 0xFF, 0x18, 0xFF, // Will produce 9 bytes which is out of bounds for 8x2 image + ]; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + SegmentDoNotFitScanline, + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn insufficient_raw_bytes_handled() { + let src = [0x18]; // Actually require 1 more byte + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + ReadCompressedData( + Error { + kind: UnexpectedEof, + message: "failed to fill whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn empty_buffer_handled() { + let src = []; + + let width = 8usize; + let height = 2usize; + + let mut actual = Vec::new(); + expect![[r#" + Err( + ReadCompressedData( + Error { + kind: UnexpectedEof, + message: "failed to fill whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress(&src, &mut actual, width, height)); + } + + #[test] + fn too_small_dest_buffer_handled() { + let src = [0x17, 0xFF, 0x04, 0x40, 0x01, 0x02, 0x03, 0x04]; + + let width = 8usize; + let height = 2usize; + + let mut actual = vec![0u8; 7]; + + expect![[r#" + Err( + WriteDecompressedData( + Error { + kind: WriteZero, + message: "failed to write whole buffer", + }, + ), + ) + "#]] + .assert_debug_eq(&decompress_8bpp_plane(&src, &mut actual, width, height)); + + // Check same failure mode, but on non-first line + } +} diff --git a/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bin b/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bin new file mode 100644 index 00000000..ae0608bc Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp b/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp new file mode 100644 index 00000000..77a5f94d Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/32x64_rgb_raw.bmp differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bin b/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bin new file mode 100644 index 00000000..2762c667 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bmp b/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bmp new file mode 100644 index 00000000..e72fdf65 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_argb_rle.bmp differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bin b/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bin new file mode 100644 index 00000000..b5ead214 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bmp b/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bmp new file mode 100644 index 00000000..112bbb8c Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_aycocg_rle.bmp differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bin b/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bin new file mode 100644 index 00000000..ad9b2891 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bmp b/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bmp new file mode 100644 index 00000000..3f087c7f Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x24_ycocg_rle_ss.bmp differ diff --git a/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bin b/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bin new file mode 100644 index 00000000..1125f1b2 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bmp b/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bmp new file mode 100644 index 00000000..55fbc0e9 Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x57_ycocg_rle_ss.bmp differ diff --git a/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin b/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin new file mode 100644 index 00000000..0e28acad Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bin differ diff --git a/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp b/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp new file mode 100644 index 00000000..4a989c5f Binary files /dev/null and b/crates/graphics/src/rdp6/test_assets/64x64_ycocg_raw_ss.bmp differ diff --git a/crates/graphics/src/rle.rs b/crates/graphics/src/rle.rs index 09fa0aa3..8925850f 100644 --- a/crates/graphics/src/rle.rs +++ b/crates/graphics/src/rle.rs @@ -9,7 +9,6 @@ //! - FreeRDP: //! - [interleaved.c](https://github.com/FreeRDP/FreeRDP/blob/db98f16e5bce003c898e8c85eb7af964f22a16a8/libfreerdp/codec/interleaved.c#L3) //! - [bitmap.c](https://github.com/FreeRDP/FreeRDP/blob/3a8dce07ea0262b240025bd68b63801578ca63f0/libfreerdp/codec/include/bitmap.c) - use core::fmt; use std::ops::BitXor; diff --git a/crates/pdu/Cargo.toml b/crates/pdu/Cargo.toml index 3dd631fe..2793a667 100644 --- a/crates/pdu/Cargo.toml +++ b/crates/pdu/Cargo.toml @@ -33,5 +33,6 @@ sha1 = "0.10.5" x509-cert = { version = "0.2.1", default-features = false, features = ["std"] } [dev-dependencies] +expect-test.workspace = true ironrdp-pdu-samples.workspace = true lazy_static = "1.4.0" diff --git a/crates/pdu/src/basic_output/bitmap.rs b/crates/pdu/src/basic_output/bitmap.rs index dac5fd70..e586222b 100644 --- a/crates/pdu/src/basic_output/bitmap.rs +++ b/crates/pdu/src/basic_output/bitmap.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod tests; +pub mod rdp6; + use std::fmt::{self, Debug}; use std::io::{self, Write}; diff --git a/crates/pdu/src/basic_output/bitmap/rdp6.rs b/crates/pdu/src/basic_output/bitmap/rdp6.rs new file mode 100644 index 00000000..186d8d89 --- /dev/null +++ b/crates/pdu/src/basic_output/bitmap/rdp6.rs @@ -0,0 +1,271 @@ +use crate::{Error as PduError, PduDecode, PduEncode, ReadCursor, Result as PduResult, WriteCursor}; + +const NON_RLE_PADDING_SIZE: usize = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorPlanes<'a> { + Argb { + data: &'a [u8], + }, + AYCoCg { + color_loss_level: u8, + use_chroma_subsampling: bool, + data: &'a [u8], + }, +} + +/// Represents `RDP6_BITMAP_STREAM` structure described in [MS-RDPEGDI] 2.2.2.5.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BitmapStream<'a> { + pub enable_rle_compression: bool, + pub use_alpha: bool, + pub color_planes: ColorPlanes<'a>, +} + +impl<'a> BitmapStream<'a> { + pub const NAME: &'static str = "Rdp6BitmapStream"; + const FIXED_PART_SIZE: usize = 1; + + pub fn color_panes_data(&self) -> &'a [u8] { + match self.color_planes { + ColorPlanes::Argb { data } => data, + ColorPlanes::AYCoCg { data, .. } => data, + } + } + + pub fn has_subsampled_chroma(&self) -> bool { + match self.color_planes { + ColorPlanes::Argb { .. } => false, + ColorPlanes::AYCoCg { + use_chroma_subsampling, .. + } => use_chroma_subsampling, + } + } +} + +impl<'a> PduDecode<'a> for BitmapStream<'a> { + fn decode(src: &mut ReadCursor<'a>) -> PduResult { + ensure_fixed_part_size!(in: src); + let header = src.read_u8(); + + let color_loss_level = header & 0x07; + let use_chroma_subsampling = (header & 0x08) != 0; + let enable_rle_compression = (header & 0x10) != 0; + let use_alpha = (header & 0x20) == 0; + + let color_planes_size = if !enable_rle_compression { + // Cut padding field if RLE flags is set to 0 + if src.is_empty() { + return Err(PduError::Other { + context: Self::NAME, + reason: "Missing padding byte from zero-size Non-RLE bitmap data", + }); + } + src.len() - NON_RLE_PADDING_SIZE + } else { + src.len() + }; + + let color_planes_data = src.peek_slice(color_planes_size); + + let color_planes = match color_loss_level { + 0 => { + // ARGB color planes + ColorPlanes::Argb { + data: color_planes_data, + } + } + color_loss_level => ColorPlanes::AYCoCg { + color_loss_level, + use_chroma_subsampling, + data: color_planes_data, + }, + }; + + Ok(Self { + enable_rle_compression, + use_alpha, + color_planes, + }) + } +} + +impl<'a> PduEncode for BitmapStream<'a> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> { + let mut header = ((self.enable_rle_compression as u8) << 4) | ((!self.use_alpha as u8) << 5); + + match self.color_planes { + ColorPlanes::Argb { .. } => { + // ARGB color planes keep cll and cs flags set to 0 + } + ColorPlanes::AYCoCg { + color_loss_level, + use_chroma_subsampling, + .. + } => { + // Add cll and cs flags to header + header |= (color_loss_level & 0x07) | ((use_chroma_subsampling as u8) << 3); + } + } + + ensure_size!(in: dst, size: self.size()); + + dst.write_u8(header); + dst.write_slice(self.color_panes_data()); + + // Write padding + if !self.enable_rle_compression { + dst.write_u8(0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + if self.enable_rle_compression { + Self::FIXED_PART_SIZE + self.color_panes_data().len() + } else { + Self::FIXED_PART_SIZE + NON_RLE_PADDING_SIZE + self.color_panes_data().len() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{decode, encode_buf, PduEncode}; + use expect_test::{expect, Expect}; + + fn assert_roundtrip(buffer: &[u8], expected: Expect) { + let pdu = decode::(buffer).unwrap(); + expected.assert_debug_eq(&pdu); + assert_eq!(pdu.size(), buffer.len()); + let mut reencoded = vec![]; + encode_buf(&pdu, &mut reencoded).unwrap(); + assert_eq!(reencoded.as_slice(), buffer); + } + + fn assert_parsing_failure(buffer: &[u8], expected: Expect) { + let error = decode::(buffer).err().unwrap(); + expected.assert_debug_eq(&error); + } + + #[test] + fn parsing_valid_data_succeeds() { + // AYCoCg color planes, with RLE + assert_roundtrip( + &[0x3F, 0x01, 0x02, 0x03, 0x04], + expect![[r#" + BitmapStream { + enable_rle_compression: true, + use_alpha: false, + color_planes: AYCoCg { + color_loss_level: 7, + use_chroma_subsampling: true, + data: [ + 1, + 2, + 3, + 4, + ], + }, + } + "#]], + ); + + // RGB color planes, with RLE, with alpha + assert_roundtrip( + &[0x10, 0x01, 0x02, 0x03, 0x04], + expect![[r#" + BitmapStream { + enable_rle_compression: true, + use_alpha: true, + color_planes: Argb { + data: [ + 1, + 2, + 3, + 4, + ], + }, + } + "#]], + ); + + // Without RLE, validate that padding is handled correctly + assert_roundtrip( + &[0x20, 0x01, 0x02, 0x03, 0x00], + expect![[r#" + BitmapStream { + enable_rle_compression: false, + use_alpha: false, + color_planes: Argb { + data: [ + 1, + 2, + 3, + ], + }, + } + "#]], + ); + + // Empty color planes, with RLE + assert_roundtrip( + &[0x10], + expect![[r#" + BitmapStream { + enable_rle_compression: true, + use_alpha: true, + color_planes: Argb { + data: [], + }, + } + "#]], + ); + + // Empty color planes, without RLE + assert_roundtrip( + &[0x00, 0x00], + expect![[r#" + BitmapStream { + enable_rle_compression: false, + use_alpha: true, + color_planes: Argb { + data: [], + }, + } + "#]], + ); + } + + #[test] + pub fn failures_handled_gracefully() { + // Empty buffer + assert_parsing_failure( + &[], + expect![[r#" + NotEnoughBytes { + name: "Rdp6BitmapStream", + received: 0, + expected: 1, + } + "#]], + ); + + // Without RLE, Check that missing padding byte is handled correctly + assert_parsing_failure( + &[0x20], + expect![[r#" + Other { + context: "Rdp6BitmapStream", + reason: "Missing padding byte from zero-size Non-RLE bitmap data", + } + "#]], + ); + } +} diff --git a/crates/pdu/src/basic_output/bitmap/tests.rs b/crates/pdu/src/basic_output/bitmap/tests.rs index 9cf718d8..cd75d021 100644 --- a/crates/pdu/src/basic_output/bitmap/tests.rs +++ b/crates/pdu/src/basic_output/bitmap/tests.rs @@ -64,7 +64,7 @@ fn from_buffer_bitmap_data_parsses_correctly() { } #[test] -fn to_buffer_bitmap_data_serializes_correcly() { +fn to_buffer_bitmap_data_serializes_correctly() { let expected = BITMAP_BUFFER.as_ref(); let mut buffer = vec![0; expected.len()]; BITMAP.to_buffer_consume(&mut buffer.as_mut_slice()).unwrap(); diff --git a/crates/session/src/fast_path.rs b/crates/session/src/fast_path.rs index 2addc307..4ed164c3 100644 --- a/crates/session/src/fast_path.rs +++ b/crates/session/src/fast_path.rs @@ -1,4 +1,4 @@ -use ironrdp_graphics::rle::RlePixelFormat; +use ironrdp_graphics::{rdp6::BitmapStreamDecoder, rle::RlePixelFormat}; use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu; use ironrdp_pdu::fast_path::{ FastPathError, FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation, UpdateCode, @@ -17,6 +17,7 @@ pub struct Processor { complete_data: CompleteData, rfx_handler: rfx::DecodingContext, marker_processor: FrameMarkerProcessor, + bitmap_stream_decoder: BitmapStreamDecoder, } impl Processor { @@ -75,7 +76,20 @@ impl Processor { // Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream // structure ([MS-RDPEGDI] section 2.2.2.5.1). trace!("32 bpp compressed RDP6_BITMAP_STREAM"); - warn!("RDP6_BITMAP_STREAM is not yet supported"); // TODO: RDP6 32bpp + + match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24( + update.bitmap_data, + &mut buf, + update.width as usize, + update.height as usize, + ) { + Ok(()) => { + image.apply_rgb24_bitmap(&buf, &update.rectangle); + } + Err(err) => { + warn!("Invalid RDP6_BITMAP_STREAM: {err}"); + } + } } else { // Compressed bitmaps not in 32 bpp format are compressed using Interleaved // RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section @@ -196,6 +210,7 @@ impl ProcessorBuilder { complete_data: CompleteData::new(), rfx_handler: rfx::DecodingContext::new(), marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id), + bitmap_stream_decoder: BitmapStreamDecoder::default(), } } } diff --git a/crates/session/src/image.rs b/crates/session/src/image.rs index a39b573e..76c7ad80 100644 --- a/crates/session/src/image.rs +++ b/crates/session/src/image.rs @@ -114,4 +114,32 @@ impl DecodedImage { }) }); } + + // FIXME: this assumes PixelFormat::RgbA32 + pub(crate) fn apply_rgb24_bitmap(&mut self, rgb24: &[u8], update_rectangle: &Rectangle) { + const SRC_COLOR_DEPTH: usize = 3; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = self.width as usize; + let rectangle_width = usize::from(update_rectangle.width()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + + rgb24 + .chunks_exact(rectangle_width * SRC_COLOR_DEPTH) + .rev() + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + // Copy RGB channels as is + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); + // Set alpha channel to opaque(0xFF) + self.data[dst_idx + 3] = 0xFF; + }) + }); + } } diff --git a/crates/web/src/session.rs b/crates/web/src/session.rs index cffc125f..a40a71cb 100644 --- a/crates/web/src/session.rs +++ b/crates/web/src/session.rs @@ -312,6 +312,7 @@ fn build_config(username: String, password: String, domain: Option) -> c height: DEFAULT_HEIGHT, }, graphics: None, + bitmap: None, client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) .map(|version| version.major * 100 + version.minor * 10 + version.patch) .unwrap_or(0) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0b522383..64329b5b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -30,3 +30,9 @@ name = "rle_decompression" path = "fuzz_targets/rle_decompression.rs" test = false doc = false + +[[bin]] +name = "bitmap_stream" +path = "fuzz_targets/bitmap_stream.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/bitmap_stream.rs b/fuzz/fuzz_targets/bitmap_stream.rs new file mode 100644 index 00000000..80e5f758 --- /dev/null +++ b/fuzz/fuzz_targets/bitmap_stream.rs @@ -0,0 +1,22 @@ +#![no_main] + +use ironrdp_graphics::rdp6::BitmapStreamDecoder; +use libfuzzer_sys::fuzz_target; + +#[derive(arbitrary::Arbitrary, Debug)] +struct Input<'a> { + src: &'a [u8], + width: u8, + height: u8, +} + +fuzz_target!(|input: Input<'_>| { + let mut out = Vec::new(); + + let _ = BitmapStreamDecoder::default().decode_bitmap_stream_to_rgb24( + input.src, + &mut out, + input.width as usize, + input.height as usize, + ); +}); diff --git a/fuzz/fuzz_targets/pdu_decoding.rs b/fuzz/fuzz_targets/pdu_decoding.rs index 0500541a..04a3e24c 100644 --- a/fuzz/fuzz_targets/pdu_decoding.rs +++ b/fuzz/fuzz_targets/pdu_decoding.rs @@ -60,4 +60,6 @@ fuzz_target!(|data: &[u8]| { let _ = input::InputEventPdu::from_buffer(data); let _ = input::InputEvent::from_buffer(data); + + let _ = decode::(data); }); diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs index db108001..df5fa2d9 100644 --- a/xtask/src/tasks.rs +++ b/xtask/src/tasks.rs @@ -5,7 +5,7 @@ use crate::section::Section; const CARGO: &str = env!("CARGO"); const WASM_PACKAGES: &[&str] = &["ironrdp-web"]; -const FUZZ_TARGETS: &[&str] = &["pdu_decoding", "rle_decompression"]; +const FUZZ_TARGETS: &[&str] = &["pdu_decoding", "rle_decompression", "bitmap_stream"]; pub fn check_formatting(sh: &Shell) -> anyhow::Result<()> { let _s = Section::new("FORMATTING");