Skip to content

Commit 98f8205

Browse files
committed
feat(zgfx): add segment wrapping utilities
Add wrapper.rs providing ZGFX segment framing utilities for both uncompressed and compressed data: - wrap_uncompressed(): Wraps raw data in ZGFX segment format - wrap_compressed(): Wraps ZGFX-compressed data with COMPRESSED flag Supports both single segments (≤65535 bytes) and multipart segments for larger data. Fully compliant with MS-RDPEGFX specification. This enables server implementations to send EGFX PDUs without compression while maintaining protocol compliance. Refs: #1067
1 parent 25f8133 commit 98f8205

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

crates/ironrdp-graphics/src/zgfx/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
33
mod circular_buffer;
44
mod control_messages;
5+
mod wrapper;
6+
7+
pub use wrapper::{wrap_compressed, wrap_uncompressed};
58

69
use std::io::{self, Write as _};
710
use std::sync::LazyLock;
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
//! ZGFX Uncompressed Wrapper
2+
//!
3+
//! Provides utilities to wrap data in ZGFX segment structure without actual compression.
4+
//! This is spec-compliant per MS-RDPEGFX specification and allows clients to process
5+
//! EGFX PDUs that aren't compressed.
6+
//!
7+
//! # Specification
8+
//!
9+
//! According to MS-RDPEGFX section 2.2.1.1, ZGFX segments can be sent uncompressed by
10+
//! setting the compression type to RDP8 (0x04) and NOT setting the COMPRESSED flag (0x02).
11+
//!
12+
//! ## Single Segment Format
13+
//!
14+
//! ```text
15+
//! Descriptor (1 byte): 0xE0 (ZGFX_SEGMENTED_SINGLE)
16+
//! Flags (1 byte): 0x04 (RDP8 type, not compressed)
17+
//! Data: Raw data bytes
18+
//! ```
19+
//!
20+
//! ## Multipart Segment Format (for data > 65535 bytes)
21+
//!
22+
//! ```text
23+
//! Descriptor (1 byte): 0xE1 (ZGFX_SEGMENTED_MULTIPART)
24+
//! SegmentCount (2 bytes LE): Number of segments
25+
//! UncompressedSize (4 bytes LE): Total data size
26+
//! For each segment:
27+
//! Size (4 bytes LE): Segment size including flags byte
28+
//! Flags (1 byte): 0x04 (RDP8 type, not compressed)
29+
//! Data: Segment data bytes
30+
//! ```
31+
32+
use byteorder::{LittleEndian, WriteBytesExt as _};
33+
34+
/// ZGFX descriptor for single segment
35+
const ZGFX_SEGMENTED_SINGLE: u8 = 0xE0;
36+
37+
/// ZGFX descriptor for multipart segments
38+
const ZGFX_SEGMENTED_MULTIPART: u8 = 0xE1;
39+
40+
/// RDP8 compression type (lower 4 bits of flags byte)
41+
const ZGFX_PACKET_COMPR_TYPE_RDP8: u8 = 0x04;
42+
43+
/// COMPRESSED flag (upper 4 bits of flags byte)
44+
const ZGFX_PACKET_COMPRESSED: u8 = 0x02;
45+
46+
/// Maximum size for a single ZGFX segment (65535 bytes)
47+
const ZGFX_SEGMENTED_MAXSIZE: usize = 65535;
48+
49+
/// Wrap data in ZGFX segment structure (uncompressed)
50+
///
51+
/// This creates a spec-compliant ZGFX packet that clients can process,
52+
/// but doesn't actually compress the data. The COMPRESSED flag (0x02)
53+
/// is NOT set, indicating to the client to use the data directly.
54+
///
55+
/// # Arguments
56+
///
57+
/// * `data` - Raw data to wrap (typically EGFX PDU bytes)
58+
///
59+
/// # Returns
60+
///
61+
/// ZGFX-wrapped data ready for transmission over DVC channel
62+
///
63+
/// # Examples
64+
///
65+
/// ```
66+
/// use ironrdp_graphics::zgfx::wrap_uncompressed;
67+
///
68+
/// let egfx_pdu_bytes = vec![0x01, 0x02, 0x03, 0x04];
69+
/// let wrapped = wrap_uncompressed(&egfx_pdu_bytes);
70+
///
71+
/// // Wrapped data has 2-byte overhead for small data
72+
/// assert_eq!(wrapped.len(), egfx_pdu_bytes.len() + 2);
73+
/// assert_eq!(wrapped[0], 0xE0); // Single segment descriptor
74+
/// assert_eq!(wrapped[1], 0x04); // RDP8 type, not compressed
75+
/// ```
76+
pub fn wrap_uncompressed(data: &[u8]) -> Vec<u8> {
77+
if data.len() <= ZGFX_SEGMENTED_MAXSIZE {
78+
wrap_single_segment(data, false)
79+
} else {
80+
wrap_multipart_segments(data, false)
81+
}
82+
}
83+
84+
/// Wrap already-compressed data in ZGFX segment structure
85+
///
86+
/// This creates a ZGFX packet for data that has already been ZGFX-compressed.
87+
/// The COMPRESSED flag (0x02) IS set, indicating to the client to decompress
88+
/// the data using the ZGFX algorithm.
89+
///
90+
/// # Arguments
91+
///
92+
/// * `compressed_data` - ZGFX-compressed data (from Compressor::compress())
93+
///
94+
/// # Returns
95+
///
96+
/// ZGFX segment-wrapped compressed data ready for transmission
97+
pub fn wrap_compressed(compressed_data: &[u8]) -> Vec<u8> {
98+
if compressed_data.len() <= ZGFX_SEGMENTED_MAXSIZE {
99+
wrap_single_segment(compressed_data, true)
100+
} else {
101+
wrap_multipart_segments(compressed_data, true)
102+
}
103+
}
104+
105+
/// Wrap data in a single ZGFX segment
106+
///
107+
/// # Arguments
108+
///
109+
/// * `data` - Data to wrap
110+
/// * `compressed` - Whether the data is already ZGFX-compressed
111+
fn wrap_single_segment(data: &[u8], compressed: bool) -> Vec<u8> {
112+
let mut output = Vec::with_capacity(data.len() + 2);
113+
114+
// Descriptor
115+
output.push(ZGFX_SEGMENTED_SINGLE);
116+
117+
// Flags: RDP8 type + optional COMPRESSED flag
118+
// Lower 4 bits = compression type, upper 4 bits = flags
119+
let flags = if compressed {
120+
ZGFX_PACKET_COMPR_TYPE_RDP8 | (ZGFX_PACKET_COMPRESSED << 4)
121+
} else {
122+
ZGFX_PACKET_COMPR_TYPE_RDP8
123+
};
124+
output.push(flags);
125+
126+
// Data (raw or compressed)
127+
output.extend_from_slice(data);
128+
129+
output
130+
}
131+
132+
/// Wrap data in multiple ZGFX segments
133+
///
134+
/// # Arguments
135+
///
136+
/// * `data` - Data to wrap
137+
/// * `compressed` - Whether the data is already ZGFX-compressed
138+
fn wrap_multipart_segments(data: &[u8], compressed: bool) -> Vec<u8> {
139+
let segments: Vec<&[u8]> = data.chunks(ZGFX_SEGMENTED_MAXSIZE).collect();
140+
let segment_count = segments.len();
141+
142+
// Estimate size: descriptor(1) + count(2) + uncompressed_size(4) +
143+
// segments * (size(4) + flags(1)) + data
144+
let mut output = Vec::with_capacity(data.len() + 7 + segment_count * 5);
145+
146+
// Descriptor
147+
output.push(ZGFX_SEGMENTED_MULTIPART);
148+
149+
// Segment count (LE u16) - bounded by ZGFX_SEGMENTED_MAXSIZE chunking
150+
output
151+
.write_u16::<LittleEndian>(u16::try_from(segment_count).expect("segment count exceeds u16"))
152+
.expect("write to Vec cannot fail");
153+
154+
// Total uncompressed size (LE u32) - protocol limit per MS-RDPEGFX
155+
output
156+
.write_u32::<LittleEndian>(u32::try_from(data.len()).expect("data exceeds u32"))
157+
.expect("write to Vec cannot fail");
158+
159+
// Each segment
160+
for segment in segments {
161+
// Segment size (includes flags byte) - max ZGFX_SEGMENTED_MAXSIZE + 1
162+
output
163+
.write_u32::<LittleEndian>(u32::try_from(segment.len() + 1).expect("segment size exceeds u32"))
164+
.expect("write to Vec cannot fail");
165+
166+
// Flags: RDP8 type + optional COMPRESSED flag
167+
let flags = if compressed {
168+
ZGFX_PACKET_COMPR_TYPE_RDP8 | (ZGFX_PACKET_COMPRESSED << 4)
169+
} else {
170+
ZGFX_PACKET_COMPR_TYPE_RDP8
171+
};
172+
output.push(flags);
173+
174+
// Segment data
175+
output.extend_from_slice(segment);
176+
}
177+
178+
output
179+
}
180+
181+
#[cfg(test)]
182+
#[expect(clippy::as_conversions)]
183+
mod tests {
184+
use super::*;
185+
186+
#[test]
187+
fn test_wrap_small_data() {
188+
let data = b"Hello, ZGFX!";
189+
let wrapped = wrap_uncompressed(data);
190+
191+
// Should be: descriptor(1) + flags(1) + data
192+
assert_eq!(wrapped.len(), data.len() + 2);
193+
assert_eq!(wrapped[0], 0xE0); // Single segment
194+
assert_eq!(wrapped[1], 0x04); // RDP8, not compressed
195+
assert_eq!(&wrapped[2..], data);
196+
}
197+
198+
#[test]
199+
fn test_wrap_empty_data() {
200+
let data = b"";
201+
let wrapped = wrap_uncompressed(data);
202+
203+
assert_eq!(wrapped.len(), 2);
204+
assert_eq!(wrapped[0], 0xE0);
205+
assert_eq!(wrapped[1], 0x04);
206+
}
207+
208+
#[test]
209+
fn test_wrap_max_single_segment() {
210+
let data = vec![0xAB; 65535]; // Exactly at limit
211+
let wrapped = wrap_uncompressed(&data);
212+
213+
assert_eq!(wrapped[0], 0xE0); // Should still be single segment
214+
assert_eq!(wrapped.len(), 65535 + 2);
215+
}
216+
217+
#[test]
218+
fn test_wrap_large_data() {
219+
let data = vec![0xCD; 100000]; // 100KB > 65KB limit
220+
let wrapped = wrap_uncompressed(&data);
221+
222+
assert_eq!(wrapped[0], 0xE1); // Multipart
223+
224+
// Parse header
225+
let segment_count = u16::from_le_bytes([wrapped[1], wrapped[2]]) as usize;
226+
assert_eq!(segment_count, 2); // 100KB / 65KB = 2 segments
227+
228+
let uncompressed_size = u32::from_le_bytes([wrapped[3], wrapped[4], wrapped[5], wrapped[6]]) as usize;
229+
assert_eq!(uncompressed_size, 100000);
230+
231+
// Verify first segment
232+
let seg1_size = u32::from_le_bytes([wrapped[7], wrapped[8], wrapped[9], wrapped[10]]) as usize;
233+
assert_eq!(seg1_size, 65536); // 65535 data + 1 flags
234+
assert_eq!(wrapped[11], 0x04); // Flags
235+
236+
// Verify second segment starts at correct offset
237+
let seg2_offset = 7 + 4 + seg1_size;
238+
let seg2_size = u32::from_le_bytes([
239+
wrapped[seg2_offset],
240+
wrapped[seg2_offset + 1],
241+
wrapped[seg2_offset + 2],
242+
wrapped[seg2_offset + 3],
243+
]) as usize;
244+
assert_eq!(seg2_size, 100000 - 65535 + 1); // Remaining data + 1 flags
245+
assert_eq!(wrapped[seg2_offset + 4], 0x04); // Flags
246+
}
247+
248+
#[test]
249+
fn test_round_trip_with_decompressor() {
250+
use super::super::Decompressor;
251+
252+
let data = b"Test data for ZGFX round-trip verification";
253+
let wrapped = wrap_uncompressed(data);
254+
255+
// Verify decompressor can handle it
256+
let mut decompressor = Decompressor::new();
257+
let mut output = Vec::new();
258+
decompressor.decompress(&wrapped, &mut output).unwrap();
259+
260+
assert_eq!(&output, data);
261+
}
262+
263+
#[test]
264+
fn test_round_trip_large_data() {
265+
use super::super::Decompressor;
266+
267+
// Test with data that requires multiple segments
268+
let data = vec![0x42; 150000];
269+
let wrapped = wrap_uncompressed(&data);
270+
271+
let mut decompressor = Decompressor::new();
272+
let mut output = Vec::new();
273+
decompressor.decompress(&wrapped, &mut output).unwrap();
274+
275+
assert_eq!(output, data);
276+
}
277+
278+
#[test]
279+
fn test_wrap_typical_egfx_pdu() {
280+
// Simulate a typical EGFX CapabilitiesConfirm PDU (44 bytes)
281+
let egfx_caps_confirm = vec![0x13, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00]; // Simplified header
282+
let wrapped = wrap_uncompressed(&egfx_caps_confirm);
283+
284+
assert_eq!(wrapped[0], 0xE0); // Single segment
285+
assert_eq!(wrapped[1], 0x04); // Not compressed
286+
assert_eq!(wrapped.len(), egfx_caps_confirm.len() + 2);
287+
}
288+
289+
#[test]
290+
fn test_wrap_typical_h264_frame() {
291+
// Simulate a typical 85KB H.264 frame
292+
let h264_frame = vec![0x00; 85000];
293+
let wrapped = wrap_uncompressed(&h264_frame);
294+
295+
assert_eq!(wrapped[0], 0xE1); // Multipart (> 65KB)
296+
297+
// Should produce 2 segments
298+
let segment_count = u16::from_le_bytes([wrapped[1], wrapped[2]]);
299+
assert_eq!(segment_count, 2);
300+
}
301+
}

0 commit comments

Comments
 (0)