ironrdp_pdu/basic_output/bitmap/
rdp6.rs

1use ironrdp_core::{
2    ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
3    WriteCursor,
4};
5
6const NON_RLE_PADDING_SIZE: usize = 1;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ColorPlaneDefinition {
10    Argb,
11    AYCoCg {
12        color_loss_level: u8,
13        use_chroma_subsampling: bool,
14    },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct BitmapStreamHeader {
19    pub enable_rle_compression: bool,
20    pub use_alpha: bool,
21    pub color_plane_definition: ColorPlaneDefinition,
22}
23
24impl BitmapStreamHeader {
25    pub const NAME: &'static str = "Rdp6BitmapStreamHeader";
26    const FIXED_PART_SIZE: usize = 1;
27}
28
29impl Decode<'_> for BitmapStreamHeader {
30    fn decode(src: &mut ReadCursor<'_>) -> DecodeResult<Self> {
31        ensure_fixed_part_size!(in: src);
32        let header = src.read_u8();
33
34        let color_loss_level = header & 0x07;
35        let use_chroma_subsampling = (header & 0x08) != 0;
36        let enable_rle_compression = (header & 0x10) != 0;
37        let use_alpha = (header & 0x20) == 0;
38
39        let color_plane_definition = match color_loss_level {
40            0 => ColorPlaneDefinition::Argb,
41            color_loss_level => ColorPlaneDefinition::AYCoCg {
42                color_loss_level,
43                use_chroma_subsampling,
44            },
45        };
46
47        Ok(Self {
48            enable_rle_compression,
49            use_alpha,
50            color_plane_definition,
51        })
52    }
53}
54
55impl Encode for BitmapStreamHeader {
56    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
57        ensure_size!(in: dst, size: self.size());
58
59        let mut header = ((self.enable_rle_compression as u8) << 4) | ((!self.use_alpha as u8) << 5);
60
61        match self.color_plane_definition {
62            ColorPlaneDefinition::Argb => {
63                // ARGB color planes keep cll and cs flags set to 0
64            }
65            ColorPlaneDefinition::AYCoCg {
66                color_loss_level,
67                use_chroma_subsampling,
68                ..
69            } => {
70                // Add cll and cs flags to header
71                header |= (color_loss_level & 0x07) | ((use_chroma_subsampling as u8) << 3);
72            }
73        }
74
75        dst.write_u8(header);
76
77        Ok(())
78    }
79
80    fn name(&self) -> &'static str {
81        Self::NAME
82    }
83
84    fn size(&self) -> usize {
85        Self::FIXED_PART_SIZE
86            + if self.enable_rle_compression {
87                0
88            } else {
89                NON_RLE_PADDING_SIZE
90            }
91    }
92}
93
94/// Represents `RDP6_BITMAP_STREAM` structure described in [MS-RDPEGDI] 2.2.2.5.1
95#[derive(Debug, Clone)]
96pub struct BitmapStream<'a> {
97    pub header: BitmapStreamHeader,
98    pub color_planes: &'a [u8],
99}
100
101impl<'a> BitmapStream<'a> {
102    pub const NAME: &'static str = "Rdp6BitmapStream";
103    const FIXED_PART_SIZE: usize = 1;
104
105    pub fn color_panes_data(&self) -> &'a [u8] {
106        self.color_planes
107    }
108
109    pub fn has_subsampled_chroma(&self) -> bool {
110        match self.header.color_plane_definition {
111            ColorPlaneDefinition::Argb => false,
112            ColorPlaneDefinition::AYCoCg {
113                use_chroma_subsampling, ..
114            } => use_chroma_subsampling,
115        }
116    }
117}
118
119impl<'a> Decode<'a> for BitmapStream<'a> {
120    fn decode(src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
121        ensure_fixed_part_size!(in: src);
122        let header = ironrdp_core::decode_cursor::<BitmapStreamHeader>(src)?;
123
124        let color_planes_size = if !header.enable_rle_compression {
125            // Cut padding field if RLE flags is set to 0
126            if src.is_empty() {
127                return Err(invalid_field_err!(
128                    "padding",
129                    "missing padding byte from zero-sized non-RLE bitmap data",
130                ));
131            }
132            src.len() - NON_RLE_PADDING_SIZE
133        } else {
134            src.len()
135        };
136
137        let color_planes = src.read_slice(color_planes_size);
138
139        Ok(Self { header, color_planes })
140    }
141}
142
143impl Encode for BitmapStream<'_> {
144    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
145        ensure_size!(in: dst, size: self.size());
146
147        ironrdp_core::encode_cursor(&self.header, dst)?;
148        dst.write_slice(self.color_panes_data());
149
150        // Write padding
151        if !self.header.enable_rle_compression {
152            dst.write_u8(0);
153        }
154
155        Ok(())
156    }
157
158    fn name(&self) -> &'static str {
159        Self::NAME
160    }
161
162    fn size(&self) -> usize {
163        self.header.size() + self.color_panes_data().len()
164    }
165}
166
167#[cfg(test)]
168#[cfg(feature = "alloc")]
169#[expect(
170    clippy::needless_raw_strings,
171    reason = "the lint is disable to not interfere with expect! macro"
172)]
173mod tests {
174    use expect_test::{expect, Expect};
175
176    use super::*;
177
178    fn assert_roundtrip(buffer: &[u8], expected: Expect) {
179        let pdu = ironrdp_core::decode::<BitmapStream<'_>>(buffer).unwrap();
180        expected.assert_debug_eq(&pdu);
181        assert_eq!(pdu.size(), buffer.len());
182        let reencoded = ironrdp_core::encode_vec(&pdu).unwrap();
183        assert_eq!(reencoded.as_slice(), buffer);
184    }
185
186    fn assert_parsing_failure(buffer: &[u8], expected: Expect) {
187        let error = ironrdp_core::decode::<BitmapStream<'_>>(buffer).err().unwrap();
188        expected.assert_debug_eq(&error);
189    }
190
191    #[test]
192    fn parsing_valid_data_succeeds() {
193        // AYCoCg color planes, with RLE
194        assert_roundtrip(
195            &[0x3F, 0x01, 0x02, 0x03, 0x04],
196            expect![[r#"
197                BitmapStream {
198                    header: BitmapStreamHeader {
199                        enable_rle_compression: true,
200                        use_alpha: false,
201                        color_plane_definition: AYCoCg {
202                            color_loss_level: 7,
203                            use_chroma_subsampling: true,
204                        },
205                    },
206                    color_planes: [
207                        1,
208                        2,
209                        3,
210                        4,
211                    ],
212                }
213            "#]],
214        );
215
216        // RGB color planes, with RLE, with alpha
217        assert_roundtrip(
218            &[0x10, 0x01, 0x02, 0x03, 0x04],
219            expect![[r#"
220                BitmapStream {
221                    header: BitmapStreamHeader {
222                        enable_rle_compression: true,
223                        use_alpha: true,
224                        color_plane_definition: Argb,
225                    },
226                    color_planes: [
227                        1,
228                        2,
229                        3,
230                        4,
231                    ],
232                }
233            "#]],
234        );
235
236        // Without RLE, validate that padding is handled correctly
237        assert_roundtrip(
238            &[0x20, 0x01, 0x02, 0x03, 0x00],
239            expect![[r#"
240                BitmapStream {
241                    header: BitmapStreamHeader {
242                        enable_rle_compression: false,
243                        use_alpha: false,
244                        color_plane_definition: Argb,
245                    },
246                    color_planes: [
247                        1,
248                        2,
249                        3,
250                    ],
251                }
252            "#]],
253        );
254
255        // Empty color planes, with RLE
256        assert_roundtrip(
257            &[0x10],
258            expect![[r#"
259                BitmapStream {
260                    header: BitmapStreamHeader {
261                        enable_rle_compression: true,
262                        use_alpha: true,
263                        color_plane_definition: Argb,
264                    },
265                    color_planes: [],
266                }
267            "#]],
268        );
269
270        // Empty color planes, without RLE
271        assert_roundtrip(
272            &[0x00, 0x00],
273            expect![[r#"
274                BitmapStream {
275                    header: BitmapStreamHeader {
276                        enable_rle_compression: false,
277                        use_alpha: true,
278                        color_plane_definition: Argb,
279                    },
280                    color_planes: [],
281                }
282            "#]],
283        );
284    }
285
286    #[test]
287    fn failures_handled_gracefully() {
288        // Empty buffer
289        assert_parsing_failure(
290            &[],
291            expect![[r#"
292                Error {
293                    context: "<ironrdp_pdu::basic_output::bitmap::rdp6::BitmapStream as ironrdp_core::decode::Decode>::decode",
294                    kind: NotEnoughBytes {
295                        received: 0,
296                        expected: 1,
297                    },
298                    source: None,
299                }
300            "#]],
301        );
302
303        // Without RLE, Check that missing padding byte is handled correctly
304        assert_parsing_failure(
305            &[0x20],
306            expect![[r#"
307                Error {
308                    context: "<ironrdp_pdu::basic_output::bitmap::rdp6::BitmapStream as ironrdp_core::decode::Decode>::decode",
309                    kind: InvalidField {
310                        field: "padding",
311                        reason: "missing padding byte from zero-sized non-RLE bitmap data",
312                    },
313                    source: None,
314                }
315            "#]],
316        );
317    }
318}