Skip to main content

ironrdp_pdu/codecs/clearcodec/
mod.rs

1//! ClearCodec bitmap compression codec (MS-RDPEGFX 2.2.4.1).
2//!
3//! ClearCodec is a mandatory lossless codec for all EGFX versions (V8-V10.7).
4//! It uses a three-layer composite architecture: residual (BGR RLE), bands
5//! (V-bar cached columns), and subcodec (raw / NSCodec / RLEX).
6//!
7//! The codec is transported inside `WireToSurface1Pdu` with `codecId = 0x0008`.
8
9mod bands;
10mod residual;
11mod rlex;
12mod subcodec;
13
14use ironrdp_core::{DecodeResult, ReadCursor, cast_length, ensure_size, invalid_field_err};
15
16pub use self::bands::{
17    Band, MAX_BAND_HEIGHT, SHORT_VBAR_CACHE_SIZE, ShortVBarCacheMiss, VBAR_CACHE_SIZE, VBar, decode_bands_layer,
18};
19pub use self::residual::{RgbRunSegment, decode_residual_layer, encode_residual_layer};
20pub use self::rlex::{MAX_PALETTE_COUNT, RlexData, RlexSegment, decode_rlex};
21pub use self::subcodec::{Subcodec, SubcodecId, decode_subcodec_layer};
22
23// --- Flag constants ---
24
25/// `glyphIndex` field is present (bitmap area <= 1024 pixels).
26pub const FLAG_GLYPH_INDEX: u8 = 0x01;
27/// Use cached glyph at `glyphIndex`; no composite payload follows.
28pub const FLAG_GLYPH_HIT: u8 = 0x02;
29/// Reset V-Bar and Short V-Bar storage cursors to 0.
30pub const FLAG_CACHE_RESET: u8 = 0x04;
31
32// --- Top-level bitmap stream ---
33
34/// Decoded ClearCodec bitmap stream ([MS-RDPEGFX] 2.2.4.1).
35#[derive(Debug, Clone)]
36pub struct ClearCodecBitmapStream<'a> {
37    /// Combination of `FLAG_GLYPH_INDEX`, `FLAG_GLYPH_HIT`, `FLAG_CACHE_RESET`.
38    pub flags: u8,
39    /// Sequence number (wraps 0xFF -> 0x00).
40    pub seq_number: u8,
41    /// Glyph cache index, present when `FLAG_GLYPH_INDEX` is set.
42    pub glyph_index: Option<u16>,
43    /// Composite payload (three layers), absent when `FLAG_GLYPH_HIT` is set.
44    pub composite: Option<CompositePayload<'a>>,
45}
46
47impl<'a> ClearCodecBitmapStream<'a> {
48    const NAME: &'static str = "ClearCodecBitmapStream";
49
50    /// Decode the complete bitmap stream from raw bytes.
51    pub fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
52        ensure_size!(ctx: Self::NAME, in: src, size: 2);
53        let flags = src.read_u8();
54        let seq_number = src.read_u8();
55
56        let glyph_index = if flags & FLAG_GLYPH_INDEX != 0 {
57            ensure_size!(ctx: Self::NAME, in: src, size: 2);
58            Some(src.read_u16())
59        } else {
60            None
61        };
62
63        // GLYPH_HIT means use cached glyph; no payload follows.
64        let composite = if flags & FLAG_GLYPH_HIT != 0 {
65            None
66        } else if src.is_empty() {
67            // No composite payload (valid for cache reset only messages)
68            None
69        } else {
70            Some(CompositePayload::decode(src)?)
71        };
72
73        Ok(Self {
74            flags,
75            seq_number,
76            glyph_index,
77            composite,
78        })
79    }
80
81    pub fn has_glyph_index(&self) -> bool {
82        self.flags & FLAG_GLYPH_INDEX != 0
83    }
84
85    pub fn is_glyph_hit(&self) -> bool {
86        self.flags & FLAG_GLYPH_HIT != 0
87    }
88
89    pub fn is_cache_reset(&self) -> bool {
90        self.flags & FLAG_CACHE_RESET != 0
91    }
92}
93
94// --- Composite payload (3 layers) ---
95
96/// The three-layer composite payload ([MS-RDPEGFX] 2.2.4.1.1).
97///
98/// Layers are applied in order: residual -> bands -> subcodec.
99/// Each layer composites on top of the previous result.
100#[derive(Debug, Clone)]
101pub struct CompositePayload<'a> {
102    /// Raw bytes for the residual (BGR RLE) layer.
103    pub residual_data: &'a [u8],
104    /// Raw bytes for the bands (V-bar cached columns) layer.
105    pub bands_data: &'a [u8],
106    /// Raw bytes for the subcodec layer.
107    pub subcodec_data: &'a [u8],
108}
109
110impl<'a> CompositePayload<'a> {
111    const NAME: &'static str = "CompositePayload";
112
113    /// Header: 3 x u32 byte counts.
114    const HEADER_SIZE: usize = 12;
115
116    pub fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
117        ensure_size!(ctx: Self::NAME, in: src, size: Self::HEADER_SIZE);
118
119        let residual_byte_count: usize = cast_length!("residualByteCount", src.read_u32())?;
120        let bands_byte_count: usize = cast_length!("bandsByteCount", src.read_u32())?;
121        let subcodec_byte_count: usize = cast_length!("subcodecByteCount", src.read_u32())?;
122
123        let total = residual_byte_count
124            .checked_add(bands_byte_count)
125            .and_then(|s| s.checked_add(subcodec_byte_count))
126            .ok_or_else(|| invalid_field_err!("byteCount", "layer byte counts overflow"))?;
127
128        ensure_size!(ctx: Self::NAME, in: src, size: total);
129
130        let residual_data = src.read_slice(residual_byte_count);
131        let bands_data = src.read_slice(bands_byte_count);
132        let subcodec_data = src.read_slice(subcodec_byte_count);
133
134        Ok(Self {
135            residual_data,
136            bands_data,
137            subcodec_data,
138        })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn decode_glyph_hit() {
148        // flags=0x03 (GLYPH_INDEX | GLYPH_HIT), seq=0x05, glyphIndex=0x0042
149        let data = [0x03, 0x05, 0x42, 0x00];
150        let mut cursor = ReadCursor::new(&data);
151        let stream = ClearCodecBitmapStream::decode(&mut cursor).unwrap();
152        assert!(stream.has_glyph_index());
153        assert!(stream.is_glyph_hit());
154        assert!(!stream.is_cache_reset());
155        assert_eq!(stream.seq_number, 5);
156        assert_eq!(stream.glyph_index, Some(0x0042));
157        assert!(stream.composite.is_none());
158    }
159
160    #[test]
161    fn decode_cache_reset_only() {
162        // flags=0x04 (CACHE_RESET), seq=0x00, no glyph, no composite
163        let data = [0x04, 0x00];
164        let mut cursor = ReadCursor::new(&data);
165        let stream = ClearCodecBitmapStream::decode(&mut cursor).unwrap();
166        assert!(stream.is_cache_reset());
167        assert!(!stream.has_glyph_index());
168        assert!(stream.composite.is_none());
169    }
170
171    #[test]
172    fn decode_composite_payload_empty_layers() {
173        // flags=0x00, seq=0x01, composite with all-zero byte counts
174        let data = [
175            0x00, 0x01, // flags, seq
176            0x00, 0x00, 0x00, 0x00, // residualByteCount = 0
177            0x00, 0x00, 0x00, 0x00, // bandsByteCount = 0
178            0x00, 0x00, 0x00, 0x00, // subcodecByteCount = 0
179        ];
180        let mut cursor = ReadCursor::new(&data);
181        let stream = ClearCodecBitmapStream::decode(&mut cursor).unwrap();
182        let composite = stream.composite.unwrap();
183        assert!(composite.residual_data.is_empty());
184        assert!(composite.bands_data.is_empty());
185        assert!(composite.subcodec_data.is_empty());
186    }
187
188    #[test]
189    fn decode_composite_with_residual_data() {
190        // flags=0x00, seq=0x02, residual=4 bytes, bands=0, subcodec=0
191        let data = [
192            0x00, 0x02, // flags, seq
193            0x04, 0x00, 0x00, 0x00, // residualByteCount = 4
194            0x00, 0x00, 0x00, 0x00, // bandsByteCount = 0
195            0x00, 0x00, 0x00, 0x00, // subcodecByteCount = 0
196            0xFF, 0x00, 0x00, 0x01, // 4 bytes of residual data
197        ];
198        let mut cursor = ReadCursor::new(&data);
199        let stream = ClearCodecBitmapStream::decode(&mut cursor).unwrap();
200        let composite = stream.composite.unwrap();
201        assert_eq!(composite.residual_data, &[0xFF, 0x00, 0x00, 0x01]);
202    }
203}