Overview
The ironrdp-core crate provides foundational traits and types used throughout IronRDP. It defines the core encoding/decoding infrastructure, cursor utilities, and error types that form the basis of all PDU operations.
Key Design Principles
no_std compatible: Core functionality works without the standard library
- Object-safe traits: Enables dynamic dispatch for protocol PDUs
- Zero-copy parsing: Uses cursors instead of allocating intermediate buffers
- Type-safe encoding: Size is computed before writing to prevent buffer overruns
Core Traits
Encode
pub trait Encode {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()>;
fn name(&self) -> &'static str;
fn size(&self) -> usize;
}
Trait for types that can be encoded into binary form. All RDP PDUs implement this trait.
Key Methods:
encode() - Writes the PDU to a WriteCursor
name() - Returns a static name for debugging/logging
size() - Computes the exact size in bytes before encoding
Helper Functions:
// Encode to a fixed-size buffer
pub fn encode<T: Encode>(pdu: &T, dst: &mut [u8]) -> EncodeResult<usize>
// Encode using a WriteCursor
pub fn encode_cursor<T: Encode>(pdu: &T, dst: &mut WriteCursor<'_>) -> EncodeResult<()>
// Encode to a growable buffer (requires alloc feature)
pub fn encode_buf<T: Encode>(pdu: &T, buf: &mut WriteBuf) -> EncodeResult<usize>
// Encode to a new Vec (convenience function)
pub fn encode_vec<T: Encode>(pdu: &T) -> EncodeResult<Vec<u8>>
Decode
pub trait Decode<'de>: Sized {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self>;
}
Trait for types that can be decoded from a byte stream. The lifetime parameter 'de allows zero-copy parsing.
Helper Functions:
// Decode from a byte slice
pub fn decode<'de, T: Decode<'de>>(src: &'de [u8]) -> DecodeResult<T>
// Decode using a ReadCursor
pub fn decode_cursor<'de, T: Decode<'de>>(src: &mut ReadCursor<'de>) -> DecodeResult<T>
DecodeOwned
pub trait DecodeOwned: Sized {
fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult<Self>;
}
Similar to Decode but unconditionally returns an owned type (no lifetime parameter). Use when you need to own the decoded data.
Cursor Types
ReadCursor
pub struct ReadCursor<'a> { /* private fields */ }
Zero-copy cursor for reading bytes from a buffer. Tracks position and provides typed read methods.
Common Methods:
new
fn(bytes: &'a [u8]) -> Self
Create a new cursor from a byte slice
read_slice
fn(&mut self, n: usize) -> &'a [u8]
Read a slice of n bytes
Get the remaining unread bytes
Number of bytes remaining
Current position in the buffer
Example:
use ironrdp_core::ReadCursor;
let data = &[0x01, 0x02, 0x03, 0x04];
let mut cursor = ReadCursor::new(data);
let first = cursor.read_u8(); // 0x01
let second = cursor.read_u16(); // 0x0302 (little-endian)
assert_eq!(cursor.len(), 1); // 1 byte remaining
WriteCursor
pub struct WriteCursor<'a> { /* private fields */ }
Cursor for writing bytes to a mutable buffer. Provides type-safe write methods and tracks position.
Common Methods:
new
fn(bytes: &'a mut [u8]) -> Self
Create a new cursor from a mutable byte slice
write_u16
fn(&mut self, value: u16)
Write a little-endian u16
write_u16_be
fn(&mut self, value: u16)
Write a big-endian u16
write_u32
fn(&mut self, value: u32)
Write a little-endian u32
write_slice
fn(&mut self, slice: &[u8])
Write a slice of bytes
Current position (number of bytes written)
Example:
use ironrdp_core::WriteCursor;
let mut buffer = [0u8; 8];
let mut cursor = WriteCursor::new(&mut buffer);
cursor.write_u8(0x01);
cursor.write_u16(0x0302); // little-endian
cursor.write_u32(0x07060504); // little-endian
assert_eq!(cursor.pos(), 7);
WriteBuf
pub struct WriteBuf { /* private fields */ }
Growable write buffer that manages a Vec<u8> internally. Maintains separate “filled” and “unfilled” regions.
Requires the alloc feature. Use WriteBuf when the output size is not known in advance.
Common Methods:
Create a new empty buffer
unfilled_to
fn(&mut self, size: usize) -> &mut [u8]
Get a mutable slice of at least size bytes, growing if necessary
Mark n bytes as filled after writing to unfilled_to()
Get the filled portion of the buffer
Reset the filled region without deallocating
Example:
use ironrdp_core::{WriteBuf, WriteCursor, encode_buf};
let mut buf = WriteBuf::new();
// Write multiple PDUs
for pdu in pdus {
encode_buf(&pdu, &mut buf)?;
}
// Get all encoded data
let data = buf.filled();
// Reuse buffer for next batch
buf.clear();
Error Types
EncodeError & DecodeError
pub type EncodeResult<T> = Result<T, EncodeError>;
pub type DecodeResult<T> = Result<T, DecodeError>;
pub enum EncodeErrorKind {
NotEnoughBytes { received: usize, expected: usize },
InvalidField { field: &'static str, reason: &'static str },
UnexpectedMessageType { got: u8 },
UnsupportedVersion { got: u8 },
UnsupportedValue { name: &'static str, value: String },
Other { description: &'static str },
}
Both encode and decode operations return structured errors with context.
Example Error Handling:
use ironrdp_core::{decode, DecodeError};
match decode::<MyPdu>(&bytes) {
Ok(pdu) => { /* process pdu */ },
Err(e) => {
eprintln!("Failed to decode: {}", e);
// Error includes context and source chain
}
}
Features
Enables standard library support (includes alloc)
Enables heap allocations (WriteBuf, Vec support)
Usage Notes
WriteCursor vs WriteBuf:
- Use
WriteCursor when you know the exact size and want syscall-free, infallible writes
- Use
WriteBuf when size is unknown and you need the buffer to grow automatically
WriteCursor wraps &mut [u8] for stack/static buffers
WriteBuf manages Vec<u8> internally and can resize
Object Safety:
The Encode trait is intentionally object-safe to enable dynamic dispatch:let pdus: Vec<Box<dyn Encode>> = vec![Box::new(pdu1), Box::new(pdu2)];
See Also