ironrdp_pdu/basic_output/bitmap/
rdp6.rs1use 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 }
65 ColorPlaneDefinition::AYCoCg {
66 color_loss_level,
67 use_chroma_subsampling,
68 ..
69 } => {
70 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#[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 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 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 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 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 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 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 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 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 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}