Merge branch 'master' into ironrdp-web-add-clippy-index-slicing-lint

This commit is contained in:
Alex Yusiuk 2025-10-08 12:52:05 +03:00 committed by GitHub
commit c0730dc2b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
136 changed files with 1233 additions and 762 deletions

527
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -94,6 +94,7 @@ ptr_cast_constness = "warn"
# == Correctness == #
#indexing_slicing = "warn" TODO: enable this lint project wide.
#as_conversions = "warn"
cast_lossless = "warn"
cast_possible_truncation = "warn"
cast_possible_wrap = "warn"
@ -116,6 +117,7 @@ same_name_method = "warn"
string_slice = "warn"
suspicious_xor_used_as_pow = "warn"
unused_result_ok = "warn"
missing_panics_doc = "warn"
# == Style, readability == #
semicolon_outside_block = "warn" # With semicolon-outside-block-ignore-multiline = true
@ -140,8 +142,12 @@ unused_self = "warn"
useless_let_if_seq = "warn"
string_add = "warn"
range_plus_one = "warn"
# TODO: self_named_module_files = "warn"
self_named_module_files = "warn"
# TODO: partial_pub_fields = "warn" (should we enable only in pdu crates?)
redundant_type_annotations = "warn"
unnecessary_self_imports = "warn"
try_err = "warn"
rest_pat_in_fully_bound_structs = "warn"
# == Compile-time / optimization == #
doc_include_without_cfg = "warn"

View file

@ -17,7 +17,7 @@ qoiz = ["ironrdp/qoiz"]
[dependencies]
anyhow = "1.0.99"
async-trait = "0.1.89"
bytesize = "2.0.1"
bytesize = "2.1.0"
ironrdp = { path = "../crates/ironrdp", features = [
"server",
"pdu",

View file

@ -203,7 +203,7 @@ impl core::str::FromStr for OptCodec {
"qoi" => Ok(Self::Qoi),
#[cfg(feature = "qoiz")]
"qoiz" => Ok(Self::QoiZ),
_ => Err(anyhow::anyhow!("unknown codec: {}", s)),
_ => anyhow::bail!("unknown codec: {s}"),
}
}
}

View file

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.6.0...iron-remote-desktop-v0.7.0)] - 2025-09-29
### <!-- 4 -->Bug Fixes
- [**breaking**] Changed onClipboardChanged to not consume the input (#992) ([6127e13c83](https://github.com/Devolutions/IronRDP/commit/6127e13c836d06764d483b6b55188fd23a4314a2))
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/iron-remote-desktop-v0.5.0...iron-remote-desktop-v0.6.0)] - 2025-08-29
### <!-- 1 -->Features

View file

@ -1,6 +1,6 @@
[package]
name = "iron-remote-desktop"
version = "0.6.0"
version = "0.7.0"
readme = "README.md"
description = "Helper crate for building WASM modules compatible with iron-remote-desktop WebComponent"
edition.workspace = true

View file

@ -159,8 +159,8 @@ macro_rules! make_bridge {
}
#[wasm_bindgen(js_name = onClipboardPaste)]
pub async fn on_clipboard_paste(&self, content: ClipboardData) -> Result<(), IronError> {
$crate::Session::on_clipboard_paste(&self.0, content.0)
pub async fn on_clipboard_paste(&self, content: &ClipboardData) -> Result<(), IronError> {
$crate::Session::on_clipboard_paste(&self.0, &content.0)
.await
.map_err(IronError)
}

View file

@ -84,7 +84,7 @@ pub trait Session {
fn on_clipboard_paste(
&self,
content: Self::ClipboardData,
content: &Self::ClipboardData,
) -> impl core::future::Future<Output = Result<(), Self::Error>>;
fn resize(

View file

@ -4,9 +4,8 @@ use ironrdp_connector::{
reason_err, ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written,
};
use ironrdp_core::WriteBuf;
use ironrdp_pdu::mcs;
use ironrdp_pdu::x224::X224;
use ironrdp_pdu::{self as pdu};
use pdu::mcs;
use tracing::debug;
#[derive(Debug)]
@ -57,13 +56,13 @@ impl State for ChannelConnectionState {
}
impl Sequence for ChannelConnectionSequence {
fn next_pdu_hint(&self) -> Option<&dyn pdu::PduHint> {
fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> {
match &self.state {
ChannelConnectionState::Consumed => None,
ChannelConnectionState::WaitErectDomainRequest => Some(&pdu::X224_HINT),
ChannelConnectionState::WaitAttachUserRequest => Some(&pdu::X224_HINT),
ChannelConnectionState::WaitErectDomainRequest => Some(&ironrdp_pdu::X224_HINT),
ChannelConnectionState::WaitAttachUserRequest => Some(&ironrdp_pdu::X224_HINT),
ChannelConnectionState::SendAttachUserConfirm => None,
ChannelConnectionState::WaitChannelJoinRequest { .. } => Some(&pdu::X224_HINT),
ChannelConnectionState::WaitChannelJoinRequest { .. } => Some(&ironrdp_pdu::X224_HINT),
ChannelConnectionState::SendChannelJoinConfirm { .. } => None,
ChannelConnectionState::AllJoined => None,
}

View file

@ -123,6 +123,9 @@ impl Acceptor {
}
}
/// # Panics
///
/// Panics if state is not [AcceptorState::SecurityUpgrade].
pub fn mark_security_upgrade_as_done(&mut self) {
assert!(self.reached_security_upgrade().is_some());
self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
@ -133,6 +136,9 @@ impl Acceptor {
matches!(self.state, AcceptorState::Credssp { .. })
}
/// # Panics
///
/// Panics if state is not [AcceptorState::Credssp].
pub fn mark_credssp_as_done(&mut self) {
assert!(self.should_perform_credssp());
let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state");

View file

@ -1,8 +1,7 @@
use ironrdp_connector::{ConnectorError, ConnectorErrorExt as _, ConnectorResult, Sequence, State, Written};
use ironrdp_core::WriteBuf;
use ironrdp_pdu::rdp;
use ironrdp_pdu::x224::X224;
use ironrdp_pdu::{self as pdu};
use pdu::rdp;
use tracing::debug;
use crate::util::{self, wrap_share_data};
@ -60,13 +59,13 @@ impl State for FinalizationState {
}
impl Sequence for FinalizationSequence {
fn next_pdu_hint(&self) -> Option<&dyn pdu::PduHint> {
fn next_pdu_hint(&self) -> Option<&dyn ironrdp_pdu::PduHint> {
match &self.state {
FinalizationState::Consumed => None,
FinalizationState::WaitSynchronize => Some(&pdu::X224Hint),
FinalizationState::WaitControlCooperate => Some(&pdu::X224Hint),
FinalizationState::WaitRequestControl => Some(&pdu::X224Hint),
FinalizationState::WaitFontList => Some(&pdu::RdpHint),
FinalizationState::WaitSynchronize => Some(&ironrdp_pdu::X224Hint),
FinalizationState::WaitControlCooperate => Some(&ironrdp_pdu::X224Hint),
FinalizationState::WaitRequestControl => Some(&ironrdp_pdu::X224Hint),
FinalizationState::WaitFontList => Some(&ironrdp_pdu::RdpHint),
FinalizationState::SendSynchronizeConfirm => None,
FinalizationState::SendControlCooperateConfirm => None,
FinalizationState::SendGrantedControlConfirm => None,
@ -221,7 +220,7 @@ fn create_font_map() -> rdp::headers::ShareDataPdu {
}
fn decode_share_control(input: &[u8]) -> ConnectorResult<rdp::headers::ShareControlHeader> {
let data_request = ironrdp_core::decode::<X224<pdu::mcs::SendDataRequest<'_>>>(input)
let data_request = ironrdp_core::decode::<X224<ironrdp_pdu::mcs::SendDataRequest<'_>>>(input)
.map_err(ConnectorError::decode)
.map(|p| p.0)?;
let share_control = ironrdp_core::decode::<rdp::headers::ShareControlHeader>(data_request.user_data.as_ref())
@ -230,7 +229,7 @@ fn decode_share_control(input: &[u8]) -> ConnectorResult<rdp::headers::ShareCont
}
fn decode_font_list(input: &[u8]) -> Result<rdp::finalization_messages::FontPdu, ()> {
use pdu::rdp::headers::{ShareControlPdu, ShareDataPdu};
use ironrdp_pdu::rdp::headers::{ShareControlPdu, ShareDataPdu};
let share_control = decode_share_control(input).map_err(|_| ())?;

View file

@ -30,6 +30,9 @@ where
Ok(ShouldUpgrade)
}
/// # Panics
///
/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade].
pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade {
assert!(connector.should_perform_security_upgrade());
ShouldUpgrade

View file

@ -115,6 +115,7 @@ where
if self.buf.len() >= length {
return Ok(self.buf.split_to(length));
} else {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")]
self.buf
.reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()"));
}

View file

@ -1,3 +1,5 @@
#![expect(clippy::missing_panics_doc, reason = "panics in benches are allowed")]
use core::num::{NonZeroU16, NonZeroUsize};
use criterion::{criterion_group, criterion_main, Criterion};

View file

@ -32,6 +32,9 @@ where
Ok(ShouldUpgrade)
}
/// # Panics
///
/// Panics if connector state is not [ClientConnectorState::EnhancedSecurityUpgrade].
pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade {
assert!(connector.should_perform_security_upgrade());
ShouldUpgrade

View file

@ -51,6 +51,7 @@ where
if self.buf.len() >= length {
return Ok(self.buf.split_to(length));
} else {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked underflow)")]
self.buf
.reserve(length.checked_sub(self.buf.len()).expect("length > self.buf.len()"));
}

View file

@ -32,7 +32,7 @@ qoiz = ["ironrdp/qoiz"]
[dependencies]
# Protocols
ironrdp = { path = "../ironrdp", version = "0.12", features = [
ironrdp = { path = "../ironrdp", version = "0.13", features = [
"session",
"input",
"graphics",
@ -72,7 +72,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Async, futures
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7" }
tokio-tungstenite = "0.27"
tokio-tungstenite = "0.28"
transport = { git = "https://github.com/Devolutions/devolutions-gateway", rev = "06e91dfe82751a6502eaf74b6a99663f06f0236d" }
futures-util = { version = "0.3", features = ["sink"] }

View file

@ -162,7 +162,7 @@ impl WinClipboard {
// SAFETY: low-level WinAPI call
let atom = unsafe { RegisterClassA(&wc) };
if atom == 0 {
return Err(Error::from_win32())?;
return Err(WinCliprdrError::from(Error::from_win32()));
}
// SAFETY: low-level WinAPI call
@ -184,7 +184,7 @@ impl WinClipboard {
};
if window.is_invalid() {
return Err(Error::from_win32())?;
return Err(WinCliprdrError::from(Error::from_win32()));
}
// Init clipboard processing for WinAPI event loop
//

View file

@ -94,19 +94,13 @@ impl<'a> FileContentsResponse<'a> {
/// Read data as u64 size value
pub fn data_as_size(&self) -> DecodeResult<u64> {
if self.data.len() != 8 {
return Err(invalid_field_err!(
"requestedFileContentsData",
"Invalid data size for u64 size"
));
}
let chunk = self
.data
.as_ref()
.try_into()
.map_err(|_| invalid_field_err!("requestedFileContentsData", "not enough bytes for u64 size"))?;
Ok(u64::from_le_bytes(
self.data
.as_ref()
.try_into()
.expect("data contains exactly eight u8 elements"),
))
Ok(u64::from_le_bytes(chunk))
}
}

View file

@ -176,6 +176,9 @@ impl ClientConnector {
matches!(self.state, ClientConnectorState::EnhancedSecurityUpgrade { .. })
}
/// # Panics
///
/// Panics if state is not [ClientConnectorState::EnhancedSecurityUpgrade].
pub fn mark_security_upgrade_as_done(&mut self) {
assert!(self.should_perform_security_upgrade());
self.step(&[], &mut WriteBuf::new()).expect("transition to next state");
@ -186,6 +189,9 @@ impl ClientConnector {
matches!(self.state, ClientConnectorState::Credssp { .. })
}
/// # Panics
///
/// Panics if state is not [ClientConnectorState::Credssp].
pub fn mark_credssp_as_done(&mut self) {
assert!(self.should_perform_credssp());
let res = self.step(&[], &mut WriteBuf::new()).expect("transition to next state");

View file

@ -1,7 +1,7 @@
use core::mem;
use ironrdp_pdu::rdp;
use ironrdp_pdu::rdp::capability_sets::CapabilitySet;
use ironrdp_pdu::rdp::{self};
use tracing::{debug, warn};
use crate::{
@ -155,7 +155,7 @@ impl Sequence for ConnectionActivationSequence {
});
let client_confirm_active = rdp::headers::ShareControlPdu::ClientConfirmActive(
create_client_confirm_active(&self.config, capability_sets, desktop_size)?,
create_client_confirm_active(&self.config, capability_sets, desktop_size),
);
debug!(message = ?client_confirm_active, "Send");
@ -263,7 +263,7 @@ fn create_client_confirm_active(
config: &Config,
mut server_capability_sets: Vec<CapabilitySet>,
desktop_size: DesktopSize,
) -> ConnectorResult<rdp::capability_sets::ClientConfirmActive> {
) -> rdp::capability_sets::ClientConfirmActive {
use ironrdp_pdu::rdp::capability_sets::{
client_codecs_capabilities, Bitmap, BitmapCache, BitmapDrawingFlags, Brush, CacheDefinition, CacheEntry,
ClientConfirmActive, CmdFlags, DemandActive, FrameAcknowledge, General, GeneralExtraFlags, GlyphCache,
@ -386,11 +386,11 @@ fn create_client_confirm_active(
}));
}
Ok(ClientConfirmActive {
ClientConfirmActive {
originator_id: SERVER_CHANNEL_ID,
pdu: DemandActive {
source_descriptor: "IRONRDP".to_owned(),
capability_sets: server_capability_sets,
},
})
}
}

View file

@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.2.0...ironrdp-dvc-pipe-proxy-v0.2.1)] - 2025-09-24
### <!-- 4 -->Bug Fixes
- Change dvc proxy pipe mode from Message to Byte on Windows (#986) ([5f52a44b84](https://github.com/Devolutions/IronRDP/commit/5f52a44b840dd71eae6a355be00f1c4c671b3b58))
- Add blocking logic for sending dvc pipe messages ([3182a018e2](https://github.com/Devolutions/IronRDP/commit/3182a018e2972eb77c52ea248387c96a9eb6a6a6))
## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.1.0...ironrdp-dvc-pipe-proxy-v0.2.0)] - 2025-08-29
### <!-- 1 -->Features

View file

@ -1,6 +1,6 @@
[package]
name = "ironrdp-dvc-pipe-proxy"
version = "0.2.0"
version = "0.2.1"
readme = "README.md"
description = "DVC named pipe proxy for IronRDP"
edition.workspace = true

View file

@ -73,6 +73,8 @@ pub fn encode_dvc_messages(
while off < total_length {
let first = off == 0;
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked underflow)")]
let remaining_length = total_length.checked_sub(off).expect("never overflow");
let size = core::cmp::min(remaining_length, DrdynvcDataPdu::MAX_DATA_SIZE);
let end = off

View file

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.5.0...ironrdp-graphics-v0.6.0)] - 2025-06-27
### <!-- 4 -->Bug Fixes
- `to_64x64_ycbcr_tile` now returns a `Result`
## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.4.0...ironrdp-graphics-v0.4.1)] - 2025-06-27
### <!-- 7 -->Build
@ -20,15 +26,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Add some helper to find "damaged" regions, as 64x64 tiles.
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.2.0...ironrdp-graphics-v0.3.0)] - 2025-03-12
### <!-- 7 -->Build
- Bump ironrdp-pdu
## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.1.2...ironrdp-graphics-v0.2.0)] - 2025-03-07
### Performance
@ -46,8 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.1.0...ironrdp-graphics-v0.1.1)] - 2024-12-14
### Other

View file

@ -1,6 +1,6 @@
[package]
name = "ironrdp-graphics"
version = "0.5.0"
version = "0.6.0"
readme = "README.md"
description = "RDP image processing primitives"
edition.workspace = true
@ -29,7 +29,7 @@ yuv = { version = "0.8", features = ["rdp"] }
[dev-dependencies]
bmp = "0.5"
bytemuck = "1.23"
bytemuck = "1.24"
expect-test.workspace = true
[lints]

View file

@ -40,6 +40,10 @@ pub fn ycbcr_to_rgba(input: YCbCrBuffer<'_>, output: &mut [u8]) -> io::Result<()
rdp_yuv444_to_rgba(&planar, output, len).map_err(io::Error::other)
}
/// # Panics
///
/// - Panics if `width` > 64.
/// - Panics if `height` > 64.
#[expect(clippy::too_many_arguments)]
pub fn to_64x64_ycbcr_tile(
input: &[u8],

View file

@ -21,6 +21,9 @@ pub mod zgfx;
mod utils;
/// # Panics
///
/// Panics if `input.len()` is not 4096 (64 * 46).
pub fn rfx_encode_component(
input: &mut [i16],
output: &mut [u8],

View file

@ -106,9 +106,11 @@ pub fn encode(mode: EntropyAlgorithm, input: &[i16], tile: &mut [u8]) -> Result<
k = kp >> LS_GR;
}
CompressionMode::GolombRice => {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior check)")]
let input_first = *input
.next()
.expect("value is guaranteed to be `Some` due to the prior check");
match mode {
EntropyAlgorithm::Rlgr1 => {
let two_ms = get_2magsign(input_first);
@ -360,17 +362,17 @@ impl From<u32> for CompressionMode {
#[derive(Debug)]
pub enum RlgrError {
IoError(io::Error),
YuvError(YuvError),
Io(io::Error),
Yuv(YuvError),
EmptyTile,
}
impl core::fmt::Display for RlgrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::IoError(_error) => write!(f, "IO error"),
Self::Io(_) => write!(f, "IO error"),
Self::Yuv(_) => write!(f, "YUV error"),
Self::EmptyTile => write!(f, "the input tile is empty"),
Self::YuvError(error) => write!(f, "YUV error: {error}"),
}
}
}
@ -378,8 +380,8 @@ impl core::fmt::Display for RlgrError {
impl core::error::Error for RlgrError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::IoError(error) => Some(error),
Self::YuvError(error) => Some(error),
Self::Io(error) => Some(error),
Self::Yuv(error) => Some(error),
Self::EmptyTile => None,
}
}
@ -387,6 +389,6 @@ impl core::error::Error for RlgrError {
impl From<io::Error> for RlgrError {
fn from(err: io::Error) -> Self {
Self::IoError(err)
Self::Io(err)
}
}

View file

@ -86,6 +86,7 @@ impl Scancode {
pub fn as_idx(self) -> usize {
if self.extended {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (integer upcast)")]
usize::from(self.code).checked_add(256).expect("never overflow")
} else {
usize::from(self.code)
@ -343,6 +344,7 @@ impl Database {
let mut events = SmallVec::new();
for idx in self.mouse_buttons.iter_ones() {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
let button = MouseButton::from_idx(idx).expect("in-range index");
let event = match MouseButtonFlags::from(button) {
@ -365,9 +367,12 @@ impl Database {
// The keyboard bit array size is 512.
for idx in self.keyboard.iter_ones() {
let (scancode, extended) = if idx >= 256 {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")]
let extended_code = idx.checked_sub(256).expect("never underflow");
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
(u8::try_from(extended_code).expect("always in the range"), true)
} else {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
(u8::try_from(idx).expect("always in the range"), false)
};

View file

@ -32,7 +32,7 @@ ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["std"] }
ironrdp-error = { path = "../ironrdp-error", version = "0.1" }
ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" }
log = "0.4"
tokio-tungstenite = { version = "0.27" }
tokio-tungstenite = { version = "0.28" }
tokio-util = { version = "0.7" }
tokio = { version = "1.43", features = ["macros", "rt"] }
uuid = { version = "1.16", features = ["v4"] }

View file

@ -144,7 +144,7 @@ impl GwClient {
.header(hyper::header::SEC_WEBSOCKET_VERSION, "13")
.header(hyper::header::SEC_WEBSOCKET_KEY, generate_key())
.body(http_body_util::Empty::<Bytes>::new())
.expect("Failed to build request");
.map_err(|e| custom_err!("failed to build request", e))?;
let stream = hyper_util::rt::tokio::TokioIo::new(stream);
let (mut sender, mut conn) = hyper::client::conn::http1::handshake(stream)
@ -200,8 +200,7 @@ impl GwClient {
let work = tokio::spawn(async move {
let iv = Duration::from_secs(15 * 60);
let mut keepalive_interval: tokio::time::Interval =
tokio::time::interval_at(tokio::time::Instant::now() + iv, iv);
let mut keepalive_interval = tokio::time::interval_at(tokio::time::Instant::now() + iv, iv);
loop {
let mut wsbuf = [0u8; 8192];

View file

@ -7,8 +7,8 @@ use core::fmt::{self, Debug};
use bitflags::bitflags;
use ironrdp_core::{
ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
WriteCursor,
cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult,
ReadCursor, WriteCursor,
};
use crate::geometry::InclusiveRectangle;
@ -41,11 +41,9 @@ impl Encode for BitmapUpdateData<'_> {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
if self.rectangles.len() > u16::MAX as usize {
return Err(invalid_field_err!("numberRectangles", "rectangle count is too big"));
}
let rectangle_count = cast_length!("number of rectangles", self.rectangles.len())?;
Self::encode_header(self.rectangles.len() as u16, dst)?;
Self::encode_header(rectangle_count, dst)?;
for bitmap_data in self.rectangles.iter() {
bitmap_data.encode(dst)?;
@ -74,10 +72,10 @@ impl<'de> Decode<'de> for BitmapUpdateData<'de> {
return Err(invalid_field_err!("updateType", "invalid update type"));
}
let rectangles_number = src.read_u16() as usize;
let mut rectangles = Vec::with_capacity(rectangles_number);
let rectangle_count = usize::from(src.read_u16());
let mut rectangles = Vec::with_capacity(rectangle_count);
for _ in 0..rectangles_number {
for _ in 0..rectangle_count {
rectangles.push(BitmapData::decode(src)?);
}
@ -111,16 +109,14 @@ impl Encode for BitmapData<'_> {
ensure_size!(in: dst, size: self.size());
let encoded_bitmap_data_length = self.encoded_bitmap_data_length();
if encoded_bitmap_data_length > u16::MAX as usize {
return Err(invalid_field_err!("bitmapLength", "bitmap data length is too big"));
}
let encoded_bitmap_data_length = cast_length!("bitmap data length", encoded_bitmap_data_length)?;
self.rectangle.encode(dst)?;
dst.write_u16(self.width);
dst.write_u16(self.height);
dst.write_u16(self.bits_per_pixel);
dst.write_u16(self.compression_flags.bits());
dst.write_u16(encoded_bitmap_data_length as u16);
dst.write_u16(encoded_bitmap_data_length);
if let Some(compressed_data_header) = &self.compressed_data_header {
compressed_data_header.encode(dst)?;
};
@ -150,25 +146,25 @@ impl<'de> Decode<'de> for BitmapData<'de> {
// A 16-bit, unsigned integer. The size in bytes of the data in the bitmapComprHdr
// and bitmapDataStream fields.
let encoded_bitmap_data_length = src.read_u16();
let encoded_bitmap_data_length = usize::from(src.read_u16());
ensure_size!(in: src, size: encoded_bitmap_data_length as usize);
ensure_size!(in: src, size: encoded_bitmap_data_length);
let (compressed_data_header, buffer_length) = if compression_flags.contains(Compression::BITMAP_COMPRESSION)
&& !compression_flags.contains(Compression::NO_BITMAP_COMPRESSION_HDR)
{
// Check if encoded_bitmap_data_length is at least CompressedDataHeader::ENCODED_SIZE
if encoded_bitmap_data_length < CompressedDataHeader::ENCODED_SIZE as u16 {
if encoded_bitmap_data_length < CompressedDataHeader::ENCODED_SIZE {
return Err(invalid_field_err!(
"cbCompEncodedBitmapDataLength",
"length is less than CompressedDataHeader::ENCODED_SIZE"
));
}
let buffer_length = encoded_bitmap_data_length as usize - CompressedDataHeader::ENCODED_SIZE;
let buffer_length = encoded_bitmap_data_length - CompressedDataHeader::ENCODED_SIZE;
(Some(CompressedDataHeader::decode(src)?), buffer_length)
} else {
(None, encoded_bitmap_data_length as usize)
(None, encoded_bitmap_data_length)
};
let bitmap_data = src.read_slice(buffer_length);

View file

@ -56,7 +56,7 @@ impl Encode for BitmapStreamHeader {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
let mut header = ((self.enable_rle_compression as u8) << 4) | ((!self.use_alpha as u8) << 5);
let mut header = (u8::from(self.enable_rle_compression) << 4) | (u8::from(!self.use_alpha) << 5);
match self.color_plane_definition {
ColorPlaneDefinition::Argb => {
@ -68,7 +68,7 @@ impl Encode for BitmapStreamHeader {
..
} => {
// Add cll and cs flags to header
header |= (color_loss_level & 0x07) | ((use_chroma_subsampling as u8) << 3);
header |= (color_loss_level & 0x07) | (u8::from(use_chroma_subsampling) << 3);
}
}

View file

@ -4,8 +4,8 @@ mod tests;
use bit_field::BitField as _;
use bitflags::bitflags;
use ironrdp_core::{
decode_cursor, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeError, DecodeResult, Encode,
EncodeResult, InvalidFieldErr as _, ReadCursor, WriteCursor,
cast_length, decode_cursor, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeError,
DecodeResult, Encode, EncodeResult, InvalidFieldErr as _, ReadCursor, WriteCursor,
};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive as _;
@ -42,7 +42,7 @@ impl FastPathHeader {
// it may then be +2 if > 0x7f
let len = self.data_length + Self::FIXED_PART_SIZE + 1;
Self::FIXED_PART_SIZE + per::sizeof_length(len as u16)
Self::FIXED_PART_SIZE + per::sizeof_length(len)
}
}
@ -56,15 +56,13 @@ impl Encode for FastPathHeader {
dst.write_u8(header);
let length = self.data_length + self.size();
if length > u16::MAX as usize {
return Err(invalid_field_err!("length", "fastpath PDU length is too big"));
}
let length = cast_length!("length", length)?;
if self.forced_long_length {
// Preserve same layout for header as received
per::write_long_length(dst, length as u16);
per::write_long_length(dst, length);
} else {
per::write_length(dst, length as u16);
per::write_length(dst, length);
}
Ok(())
@ -93,14 +91,15 @@ impl<'de> Decode<'de> for FastPathHeader {
let (length, sizeof_length) = per::read_length(src).map_err(|e| {
DecodeError::invalid_field("", "length", "Invalid encoded fast path PDU length").with_source(e)
})?;
if (length as usize) < sizeof_length + Self::FIXED_PART_SIZE {
let length = usize::from(length);
if length < sizeof_length + Self::FIXED_PART_SIZE {
return Err(invalid_field_err!(
"length",
"received fastpath PDU length is smaller than header size"
));
}
let data_length = length as usize - sizeof_length - Self::FIXED_PART_SIZE;
// Detect case, when received packet has non-optimal packet length packing
let data_length = length - sizeof_length - Self::FIXED_PART_SIZE;
// Detect case, when received packet has non-optimal packet length packing.
let forced_long_length = per::sizeof_length(length) != sizeof_length;
Ok(FastPathHeader {
@ -131,9 +130,7 @@ impl Encode for FastPathUpdatePdu<'_> {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
if self.data.len() > u16::MAX as usize {
return Err(invalid_field_err!("data", "fastpath PDU data is too big"));
}
let data_len = cast_length!("data length", self.data.len())?;
let mut header = 0u8;
header.set_bits(0..4, self.update_code.as_u8());
@ -148,7 +145,7 @@ impl Encode for FastPathUpdatePdu<'_> {
dst.write_u8(compression_flags_with_type);
}
dst.write_u16(self.data.len() as u16);
dst.write_u16(data_len);
dst.write_slice(self.data);
Ok(())
@ -200,7 +197,7 @@ impl<'de> Decode<'de> for FastPathUpdatePdu<'de> {
(None, None)
};
let data_length = src.read_u16() as usize;
let data_length = usize::from(src.read_u16());
ensure_size!(in: src, size: data_length);
let data = src.read_slice(data_length);

View file

@ -1,6 +1,6 @@
use ironrdp_core::{
ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
WriteCursor,
cast_int, cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode,
EncodeResult, ReadCursor, WriteCursor,
};
// Represents `TS_POINT16` described in [MS-RDPBCGR] 2.2.9.1.1.4.1
@ -69,21 +69,24 @@ macro_rules! check_masks_alignment {
($and_mask:expr, $xor_mask:expr, $pointer_height:expr, $large_ptr:expr) => {{
const AND_MASK_SIZE_FIELD: &str = "lengthAndMask";
const XOR_MASK_SIZE_FIELD: &str = "lengthXorMask";
const U32_MAX: usize = 0xFFFFFFFF;
let pointer_height: usize = cast_int!("pointer height", $pointer_height)?;
let check_mask = |mask: &[u8], field: &'static str| {
if $pointer_height == 0 {
return Err(invalid_field_err!(field, "pointer height cannot be zero"));
}
if $large_ptr && (mask.len() > u32::MAX as usize) {
if $large_ptr && (mask.len() > U32_MAX) {
return Err(invalid_field_err!(field, "pointer mask is too big for u32 size"));
}
if !$large_ptr && (mask.len() > u16::MAX as usize) {
if !$large_ptr && (mask.len() > usize::from(u16::MAX)) {
return Err(invalid_field_err!(field, "pointer mask is too big for u16 size"));
}
if (mask.len() % $pointer_height as usize) != 0 {
if (mask.len() % pointer_height) != 0 {
return Err(invalid_field_err!(field, "pointer mask have incomplete scanlines"));
}
if (mask.len() / $pointer_height as usize) % 2 != 0 {
if (mask.len() / pointer_height) % 2 != 0 {
return Err(invalid_field_err!(
field,
"pointer mask scanlines should be aligned to 16 bits"
@ -108,8 +111,8 @@ impl Encode for ColorPointerAttribute<'_> {
dst.write_u16(self.width);
dst.write_u16(self.height);
dst.write_u16(self.and_mask.len() as u16);
dst.write_u16(self.xor_mask.len() as u16);
dst.write_u16(cast_length!("and mask length", self.and_mask.len())?);
dst.write_u16(cast_length!("xor mask length", self.xor_mask.len())?);
// Note that masks are written in reverse order. It is not a mistake, that is how the
// message is defined in [MS-RDPBCGR]
dst.write_slice(self.xor_mask);
@ -135,15 +138,15 @@ impl<'a> Decode<'a> for ColorPointerAttribute<'a> {
let hot_spot = Point16::decode(src)?;
let width = src.read_u16();
let height = src.read_u16();
let length_and_mask = src.read_u16();
let length_xor_mask = src.read_u16();
// Convert to usize during the addition to prevent overflow and match expected type
let expected_masks_size = (length_and_mask as usize) + (length_xor_mask as usize);
let length_and_mask = usize::from(src.read_u16());
let length_xor_mask = usize::from(src.read_u16());
let expected_masks_size = length_and_mask + length_xor_mask;
ensure_size!(in: src, size: expected_masks_size);
let xor_mask = src.read_slice(length_xor_mask as usize);
let and_mask = src.read_slice(length_and_mask as usize);
let xor_mask = src.read_slice(length_xor_mask);
let and_mask = src.read_slice(length_and_mask);
check_masks_alignment!(and_mask, xor_mask, height, false)?;
@ -270,8 +273,8 @@ impl Encode for LargePointerAttribute<'_> {
dst.write_u16(self.width);
dst.write_u16(self.height);
dst.write_u32(self.and_mask.len() as u32);
dst.write_u32(self.xor_mask.len() as u32);
dst.write_u32(cast_length!("and mask length", self.and_mask.len())?);
dst.write_u32(cast_length!("xor mask length", self.xor_mask.len())?);
// See comment in `ColorPointerAttribute::encode` about encoding order
dst.write_slice(self.xor_mask);
dst.write_slice(self.and_mask);
@ -298,8 +301,8 @@ impl<'a> Decode<'a> for LargePointerAttribute<'a> {
let width = src.read_u16();
let height = src.read_u16();
// Convert to usize to prevent overflow during addition
let length_and_mask = src.read_u32() as usize;
let length_xor_mask = src.read_u32() as usize;
let length_and_mask = cast_length!("and mask length", src.read_u32())?;
let length_xor_mask = cast_length!("xor mask length", src.read_u32())?;
let expected_masks_size = length_and_mask + length_xor_mask;
ensure_size!(in: src, size: expected_masks_size);

View file

@ -3,10 +3,10 @@ mod tests;
use bitflags::bitflags;
use ironrdp_core::{
ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
WriteCursor,
cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult,
ReadCursor, WriteCursor,
};
use num_derive::{FromPrimitive, ToPrimitive};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive as _;
use crate::geometry::ExclusiveRectangle;
@ -126,7 +126,7 @@ impl Encode for FrameMarkerPdu {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_fixed_part_size!(in: dst);
dst.write_u16(self.frame_action as u16);
dst.write_u16(self.frame_action.as_u16());
dst.write_u32(self.frame_id.unwrap_or(0));
Ok(())
@ -197,9 +197,7 @@ impl Encode for ExtendedBitmapDataPdu<'_> {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
if self.data.len() > u32::MAX as usize {
return Err(invalid_field_err!("bitmapDataLength", "bitmap data is too big"));
}
let data_len = cast_length!("bitmap data length", self.data.len())?;
dst.write_u8(self.bpp);
let flags = if self.header.is_some() {
@ -212,7 +210,7 @@ impl Encode for ExtendedBitmapDataPdu<'_> {
dst.write_u8(self.codec_id);
dst.write_u16(self.width);
dst.write_u16(self.height);
dst.write_u32(self.data.len() as u32);
dst.write_u32(data_len);
if let Some(header) = &self.header {
header.encode(dst)?;
}
@ -240,7 +238,7 @@ impl<'de> Decode<'de> for ExtendedBitmapDataPdu<'de> {
let codec_id = src.read_u8();
let width = src.read_u16();
let height = src.read_u16();
let data_length = src.read_u32() as usize;
let data_length = cast_length!("bitmap data length", src.read_u32())?;
let expected_remaining_size = if flags.contains(BitmapDataFlags::COMPRESSED_BITMAP_HEADER_PRESENT) {
data_length + BitmapDataHeader::ENCODED_SIZE
@ -352,13 +350,23 @@ impl From<&SurfaceCommand<'_>> for SurfaceCommandType {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)]
#[repr(u16)]
pub enum FrameAction {
Begin = 0x00,
End = 0x01,
}
impl FrameAction {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
pub fn as_u16(self) -> u16 {
self as u16
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct BitmapDataFlags: u8 {

View file

@ -3,13 +3,25 @@ use ironrdp_core::{cast_length, ensure_size, invalid_field_err, ReadCursor, Writ
use crate::{DecodeResult, EncodeResult};
#[repr(u8)]
#[derive(Copy, Clone)]
pub(crate) enum Pc {
Primitive = 0x00,
Construct = 0x20,
}
impl Pc {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
}
#[repr(u8)]
#[expect(unused)]
#[derive(Copy, Clone)]
enum Class {
Universal = 0x00,
Application = 0x40,
@ -17,8 +29,19 @@ enum Class {
Private = 0xC0,
}
impl Class {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
}
#[repr(u8)]
#[expect(unused)]
#[derive(Copy, Clone)]
enum Tag {
Mask = 0x1F,
Boolean = 0x01,
@ -30,6 +53,16 @@ enum Tag {
Sequence = 0x10,
}
impl Tag {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
}
pub(crate) const SIZEOF_ENUMERATED: usize = 3;
pub(crate) const SIZEOF_BOOL: usize = 3;
@ -46,7 +79,7 @@ pub(crate) fn sizeof_sequence_tag(length: u16) -> usize {
}
pub(crate) fn sizeof_octet_string(length: u16) -> usize {
1 + sizeof_length(length) + length as usize
1 + sizeof_length(length) + usize::from(length)
}
pub(crate) fn sizeof_integer(value: u32) -> usize {
@ -71,7 +104,7 @@ pub(crate) fn read_sequence_tag(stream: &mut ReadCursor<'_>) -> DecodeResult<u16
ensure_size!(in: stream, size: 1);
let identifier = stream.read_u8();
if identifier != Class::Universal as u8 | Pc::Construct as u8 | (TAG_MASK & Tag::Sequence as u8) {
if identifier != Class::Universal.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & Tag::Sequence.as_u8()) {
Err(invalid_field_err!("identifier", "invalid sequence tag identifier"))
} else {
read_length(stream)
@ -82,11 +115,11 @@ pub(crate) fn write_application_tag(stream: &mut WriteCursor<'_>, tagnum: u8, le
ensure_size!(in: stream, size: sizeof_application_tag(tagnum, length));
let taglen = if tagnum > 0x1E {
stream.write_u8(Class::Application as u8 | Pc::Construct as u8 | TAG_MASK);
stream.write_u8(Class::Application.as_u8() | Pc::Construct.as_u8() | TAG_MASK);
stream.write_u8(tagnum);
2
} else {
stream.write_u8(Class::Application as u8 | Pc::Construct as u8 | (TAG_MASK & tagnum));
stream.write_u8(Class::Application.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & tagnum));
1
};
@ -98,14 +131,14 @@ pub(crate) fn read_application_tag(stream: &mut ReadCursor<'_>, tagnum: u8) -> D
let identifier = stream.read_u8();
if tagnum > 0x1E {
if identifier != Class::Application as u8 | Pc::Construct as u8 | TAG_MASK {
if identifier != Class::Application.as_u8() | Pc::Construct.as_u8() | TAG_MASK {
return Err(invalid_field_err!("identifier", "invalid application tag identifier"));
}
ensure_size!(in: stream, size: 1);
if stream.read_u8() != tagnum {
return Err(invalid_field_err!("tagnum", "invalid application tag identifier"));
}
} else if identifier != Class::Application as u8 | Pc::Construct as u8 | (TAG_MASK & tagnum) {
} else if identifier != Class::Application.as_u8() | Pc::Construct.as_u8() | (TAG_MASK & tagnum) {
return Err(invalid_field_err!("identifier", "invalid application tag identifier"));
}
@ -146,20 +179,22 @@ pub(crate) fn write_integer(stream: &mut WriteCursor<'_>, value: u32) -> EncodeR
if value < 0x0000_0080 {
write_length(stream, 1)?;
ensure_size!(in: stream, size: 1);
stream.write_u8(value as u8);
stream.write_u8(u8::try_from(value).expect("value is guaranteed to fit into u8 due to the prior check"));
Ok(3)
} else if value < 0x0000_8000 {
write_length(stream, 2)?;
ensure_size!(in: stream, size: 2);
stream.write_u16_be(value as u16);
stream.write_u16_be(u16::try_from(value).expect("value is guaranteed to fit into u16 due to the prior check"));
Ok(4)
} else if value < 0x0080_0000 {
write_length(stream, 3)?;
ensure_size!(in: stream, size: 3);
stream.write_u8((value >> 16) as u8);
stream.write_u16_be((value & 0xFFFF) as u16);
stream.write_u8(u8::try_from(value >> 16).expect("value is guaranteed to fit into u8 due to the prior check"));
stream.write_u16_be(
u16::try_from(value & 0xFFFF).expect("masking with 0xFFFF ensures that the value fits into u16"),
);
Ok(5)
} else {
@ -251,7 +286,7 @@ pub(crate) fn read_octet_string_tag(stream: &mut ReadCursor<'_>) -> DecodeResult
fn write_universal_tag(stream: &mut WriteCursor<'_>, tag: Tag, pc: Pc) -> EncodeResult<usize> {
ensure_size!(in: stream, size: 1);
let identifier = Class::Universal as u8 | pc as u8 | (TAG_MASK & tag as u8);
let identifier = Class::Universal.as_u8() | pc.as_u8() | (TAG_MASK & tag.as_u8());
stream.write_u8(identifier);
Ok(1)
@ -262,7 +297,7 @@ fn read_universal_tag(stream: &mut ReadCursor<'_>, tag: Tag, pc: Pc) -> DecodeRe
let identifier = stream.read_u8();
if identifier != Class::Universal as u8 | pc as u8 | (TAG_MASK & tag as u8) {
if identifier != Class::Universal.as_u8() | pc.as_u8() | (TAG_MASK & tag.as_u8()) {
Err(invalid_field_err!("identifier", "invalid universal tag identifier"))
} else {
Ok(())
@ -279,11 +314,11 @@ fn write_length(stream: &mut WriteCursor<'_>, length: u16) -> EncodeResult<usize
Ok(3)
} else if length > 0x7F {
stream.write_u8(0x80 ^ 0x1);
stream.write_u8(length as u8);
stream.write_u8(u8::try_from(length).expect("length is guaranteed to fit into u8 due to the prior check"));
Ok(2)
} else {
stream.write_u8(length as u8);
stream.write_u8(u8::try_from(length).expect("length is guaranteed to fit into u8 due to the prior check"));
Ok(1)
}

View file

@ -304,7 +304,7 @@ impl Encode for TileSetPdu<'_> {
dst.write_u16(properties);
dst.write_u8(cast_length!("numQuant", self.quants.len())?);
dst.write_u8(TILE_SIZE as u8);
dst.write_u8(u8::try_from(TILE_SIZE).expect("TILE_SIZE value fits into u8"));
dst.write_u16(cast_length!("numTiles", self.tiles.len())?);
let tiles_data_size = self.tiles.iter().map(|t| Block::Tile(t.clone()).size()).sum::<usize>();
@ -384,7 +384,7 @@ impl<'de> Decode<'de> for TileSetPdu<'de> {
}
let number_of_tiles = usize::from(src.read_u16());
let _tiles_data_size = src.read_u32() as usize;
let _tiles_data_size = src.read_u32();
let quants = iter::repeat_with(|| Quant::decode(src))
.take(number_of_quants)
@ -544,24 +544,32 @@ impl Encode for Quant {
impl<'de> Decode<'de> for Quant {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
#![allow(clippy::similar_names)] // Its hard to do better than ll3, lh3, etc without going overly verbose.
#![allow(
clippy::similar_names,
reason = "its hard to do better than ll3, lh3, etc without going overly verbose"
)]
ensure_fixed_part_size!(in: src);
let level3 = src.read_u16();
let ll3 = level3.get_bits(0..4) as u8;
let lh3 = level3.get_bits(4..8) as u8;
let hl3 = level3.get_bits(8..12) as u8;
let hh3 = level3.get_bits(12..16) as u8;
let ll3lh3 = src.read_u8();
let ll3 = ll3lh3.get_bits(0..4);
let lh3 = ll3lh3.get_bits(4..8);
let level2_with_lh1 = src.read_u16();
let lh2 = level2_with_lh1.get_bits(0..4) as u8;
let hl2 = level2_with_lh1.get_bits(4..8) as u8;
let hh2 = level2_with_lh1.get_bits(8..12) as u8;
let lh1 = level2_with_lh1.get_bits(12..16) as u8;
let hl3hh3 = src.read_u8();
let hl3 = hl3hh3.get_bits(0..4);
let hh3 = hl3hh3.get_bits(4..8);
let level1 = src.read_u8();
let hl1 = level1.get_bits(0..4);
let hh1 = level1.get_bits(4..8);
let lh2hl2 = src.read_u8();
let lh2 = lh2hl2.get_bits(0..4);
let hl2 = lh2hl2.get_bits(4..8);
let hh2lh1 = src.read_u8();
let hh2 = hh2lh1.get_bits(0..4);
let lh1 = hh2lh1.get_bits(4..8);
let hl1hh1 = src.read_u8();
let hl1 = hl1hh1.get_bits(0..4);
let hh1 = hl1hh1.get_bits(4..8);
Ok(Self {
ll3,

View file

@ -208,7 +208,7 @@ impl<'de> Decode<'de> for BlockHeader {
let ty = src.read_u16();
let ty = BlockType::from_u16(ty).ok_or_else(|| invalid_field_err!("blockType", "Invalid block type"))?;
let data_length = src.read_u32() as usize;
let data_length: usize = cast_length!("block length", src.read_u32())?;
data_length
.checked_sub(Self::FIXED_PART_SIZE)
.ok_or_else(|| invalid_field_err!("blockLen", "Invalid block length"))?;

View file

@ -11,12 +11,12 @@ impl Rc4 {
pub(crate) fn new(key: &[u8]) -> Self {
// key scheduling
let mut state = State::default();
for (i, item) in state.iter_mut().enumerate().take(256) {
*item = i as u8;
for (i, item) in (0..=255).zip(state.iter_mut()) {
*item = i;
}
let mut j = 0usize;
for i in 0..256 {
j = (j + state[i] as usize + key[i % key.len()] as usize) % 256;
j = (j + usize::from(state[i]) + usize::from(key[i % key.len()])) % 256;
state.swap(i, j);
}
@ -28,9 +28,9 @@ impl Rc4 {
let mut output = Vec::with_capacity(message.len());
while output.capacity() > output.len() {
self.i = (self.i + 1) % 256;
self.j = (self.j + self.state[self.i] as usize) % 256;
self.j = (self.j + usize::from(self.state[self.i])) % 256;
self.state.swap(self.i, self.j);
let idx_k = (self.state[self.i] as usize + self.state[self.j] as usize) % 256;
let idx_k = (usize::from(self.state[self.i]) + usize::from(self.state[self.j])) % 256;
let k = self.state[idx_k];
let idx_msg = output.len();
output.push(k ^ message[idx_msg]);

View file

@ -56,9 +56,8 @@ impl<'de> Decode<'de> for ClientClusterData {
let flags = RedirectionFlags::from_bits(flags_with_version & !REDIRECTION_VERSION_MASK)
.ok_or_else(|| invalid_field_err!("flags", "invalid redirection flags"))?;
let redirection_version =
RedirectionVersion::from_u8(((flags_with_version & REDIRECTION_VERSION_MASK) >> 2) as u8)
.ok_or_else(|| invalid_field_err!("redirVersion", "invalid redirection version"))?;
let redirection_version = RedirectionVersion::from_u32((flags_with_version & REDIRECTION_VERSION_MASK) >> 2)
.ok_or_else(|| invalid_field_err!("redirVersion", "invalid redirection version"))?;
Ok(Self {
flags,

View file

@ -114,9 +114,9 @@ impl Encode for ConferenceCreateRequest {
per::CHOICE_SIZE
+ CONFERENCE_REQUEST_OBJECT_ID.len()
+ per::sizeof_length(req_length)
+ per::sizeof_length(usize::from(req_length))
+ CONFERENCE_REQUEST_CONNECT_PDU_SIZE
+ per::sizeof_length(length)
+ per::sizeof_length(usize::from(length))
+ gcc_blocks_buffer_length
}
}
@ -286,9 +286,9 @@ impl Encode for ConferenceCreateResponse {
per::CHOICE_SIZE
+ CONFERENCE_REQUEST_OBJECT_ID.len()
+ per::sizeof_length(req_length)
+ per::sizeof_length(usize::from(req_length))
+ CONFERENCE_RESPONSE_CONNECT_PDU_SIZE
+ per::sizeof_length(length)
+ per::sizeof_length(usize::from(length))
+ gcc_blocks_buffer_length
}
}

View file

@ -49,13 +49,13 @@ impl<'de> Decode<'de> for ClientMonitorData {
ensure_fixed_part_size!(in: src);
let _flags = src.read_u32(); // is unused
let monitor_count = src.read_u32();
let monitor_count = cast_length!("number of monitors", src.read_u32())?;
if monitor_count > MONITOR_COUNT_MAX as u32 {
if monitor_count > MONITOR_COUNT_MAX {
return Err(invalid_field_err!("nMonitors", "too many monitors"));
}
let mut monitors = Vec::with_capacity(monitor_count as usize);
let mut monitors = Vec::with_capacity(monitor_count);
for _ in 0..monitor_count {
monitors.push(Monitor::decode(src)?);
}

View file

@ -25,6 +25,8 @@ const SERVER_CHANNEL_SIZE: usize = 2;
/// is using all the code values from 0 to 255, as such any u8 value is a valid ANSI character.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChannelName {
/// INVARIANT: A null-terminated 8-byte array.
/// INVARIANT: Contains at most seven ANSI characters.
inner: Cow<'static, [u8; Self::SIZE]>,
}
@ -79,14 +81,16 @@ impl ChannelName {
self.inner.as_ref()
}
/// Get a &str if this channel name is a valid ASCII string.
pub fn as_str(&self) -> Option<&str> {
if self.inner.iter().all(u8::is_ascii) {
#[expect(clippy::missing_panics_doc, reason = "never panics per invariant on self.inner")]
let terminator_idx = self
.inner
.iter()
.position(|c| *c == 0)
.expect("null-terminated ASCII string");
#[expect(clippy::missing_panics_doc, reason = "never panics per invariant on self.inner")]
Some(str::from_utf8(&self.inner[..terminator_idx]).expect("ASCII characters"))
} else {
None

View file

@ -51,9 +51,7 @@ impl Encode for FastPathInputHeader {
fn size(&self) -> usize {
let num_events_length = if self.num_events < 16 { 0 } else { 1 };
Self::FIXED_PART_SIZE
+ per::sizeof_length(self.data_length as u16 + num_events_length as u16 + 1)
+ num_events_length
Self::FIXED_PART_SIZE + per::sizeof_length(self.data_length + num_events_length + 1) + num_events_length
}
}
@ -78,7 +76,7 @@ impl<'de> Decode<'de> for FastPathInputHeader {
0
};
let data_length = length as usize - sizeof_length - 1 - num_events_length;
let data_length = usize::from(length) - sizeof_length - 1 - num_events_length;
Ok(FastPathInputHeader {
flags,
@ -110,7 +108,7 @@ impl FastpathInputEventType {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FastPathInputEvent {
KeyboardEvent(KeyboardFlags, u8),
UnicodeKeyboardEvent(KeyboardFlags, u16),
@ -255,10 +253,31 @@ bitflags! {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FastPathInput(pub Vec<FastPathInputEvent>);
pub struct FastPathInput(
/// INVARIANT: (1..=255).contains(len()) = at least one, and at most 255 elements.
Vec<FastPathInputEvent>,
);
impl FastPathInput {
const NAME: &'static str = "FastPathInput";
pub fn new(input_events: Vec<FastPathInputEvent>) -> DecodeResult<Self> {
// Ensure the invariant on `input_events.len()` is respected.
if !(1..=255usize).contains(&input_events.len()) {
return Err(invalid_field_err!("nEvents", "invalid number of input events"));
}
Ok(Self(input_events))
}
pub fn single(input_event: FastPathInputEvent) -> Self {
// A single element upholds the invariant.
Self(vec![input_event])
}
pub fn input_events(&self) -> &[FastPathInputEvent] {
&self.0
}
}
impl Encode for FastPathInput {
@ -271,7 +290,7 @@ impl Encode for FastPathInput {
let data_length = self.0.iter().map(Encode::size).sum::<usize>();
let header = FastPathInputHeader {
num_events: self.0.len() as u8,
num_events: u8::try_from(self.0.len()).expect("per invariant (1..=255).contains(num_events.len())"),
flags: EncryptionFlags::empty(),
data_length,
};
@ -291,7 +310,8 @@ impl Encode for FastPathInput {
fn size(&self) -> usize {
let data_length = self.0.iter().map(Encode::size).sum::<usize>();
let header = FastPathInputHeader {
num_events: self.0.len() as u8,
num_events: u8::try_from(self.0.len())
.expect("INVARIANT: num_events is within the range of 1 to 255, inclusive"),
flags: EncryptionFlags::empty(),
data_length,
};
@ -306,6 +326,6 @@ impl<'de> Decode<'de> for FastPathInput {
.take(usize::from(header.num_events))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(events))
Self::new(events)
}
}

View file

@ -1,8 +1,8 @@
use std::io;
use ironrdp_core::{
ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode, DecodeResult, Encode,
EncodeResult, ReadCursor, WriteCursor,
cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, read_padding, write_padding, Decode,
DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor,
};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive as _;
@ -38,7 +38,7 @@ impl Encode for InputEventPdu {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
dst.write_u16(self.0.len() as u16);
dst.write_u16(cast_length!("input events count", self.0.len())?);
write_padding!(dst, 2);
for event in self.0.iter() {

View file

@ -1,7 +1,7 @@
use bitflags::bitflags;
use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MousePdu {
pub flags: PointerFlags,
pub number_of_wheel_rotation_units: i16,
@ -25,7 +25,14 @@ impl Encode for MousePdu {
PointerFlags::empty().bits()
};
let wheel_rotations_bits = u16::from(self.number_of_wheel_rotation_units as u8); // truncate
#[expect(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
reason = "truncation intended"
)]
let truncated_wheel_rotation_units = self.number_of_wheel_rotation_units as u8;
let wheel_rotations_bits = u16::from(truncated_wheel_rotation_units);
let flags = self.flags.bits() | wheel_negative_bit | wheel_rotations_bits;
@ -53,7 +60,12 @@ impl<'de> Decode<'de> for MousePdu {
let flags = PointerFlags::from_bits_truncate(flags_raw);
let wheel_rotations_bits = flags_raw as u8; // truncate
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "truncation intended"
)]
let wheel_rotations_bits = flags_raw as u8;
let number_of_wheel_rotation_units = if flags.contains(PointerFlags::WHEEL_NEGATIVE) {
-i16::from(wheel_rotations_bits)

View file

@ -1,7 +1,7 @@
use bitflags::bitflags;
use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseRelPdu {
pub flags: PointerRelFlags,
pub x_delta: i16,

View file

@ -1,7 +1,7 @@
use bitflags::bitflags;
use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor};
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseXPdu {
pub flags: PointerXFlags,
pub x_position: u16,

View file

@ -1,10 +1,6 @@
#![cfg_attr(doc, doc = include_str!("../README.md"))]
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
#![allow(clippy::cast_lossless)] // FIXME: remove
#![allow(clippy::cast_possible_truncation)] // FIXME: remove
#![allow(clippy::cast_possible_wrap)] // FIXME: remove
#![allow(clippy::cast_sign_loss)] // FIXME: remove
use core::fmt;
@ -104,6 +100,10 @@ impl Action {
}
}
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
pub fn as_u8(self) -> u8 {
self as u8
}

View file

@ -232,6 +232,10 @@ impl DomainMcsPdu {
}
}
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
@ -622,11 +626,7 @@ impl<'de> McsPdu<'de> for SendDataRequest<'de> {
}
fn mcs_size(&self) -> usize {
per::CHOICE_SIZE
+ per::U16_SIZE * 2
+ 1
+ per::sizeof_length(u16::try_from(self.user_data.len()).unwrap_or(u16::MAX))
+ self.user_data.len()
per::CHOICE_SIZE + per::U16_SIZE * 2 + 1 + per::sizeof_length(self.user_data.len()) + self.user_data.len()
}
}
@ -703,11 +703,7 @@ impl<'de> McsPdu<'de> for SendDataIndication<'de> {
}
fn mcs_size(&self) -> usize {
per::CHOICE_SIZE
+ per::U16_SIZE * 2
+ 1
+ per::sizeof_length(u16::try_from(self.user_data.len()).unwrap_or(u16::MAX))
+ self.user_data.len()
per::CHOICE_SIZE + per::U16_SIZE * 2 + 1 + per::sizeof_length(self.user_data.len()) + self.user_data.len()
}
}
@ -723,7 +719,11 @@ pub enum DisconnectReason {
}
impl DisconnectReason {
pub fn as_u8(self) -> u8 {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
@ -944,7 +944,7 @@ mod legacy {
use std::io;
use ironrdp_core::{Decode, DecodeResult, Encode};
use ironrdp_core::{cast_int, Decode, DecodeResult, Encode};
use thiserror::Error;
use super::{
@ -977,11 +977,15 @@ mod legacy {
const NAME: &'static str = "ConnectInitial";
fn fields_buffer_ber_length(&self) -> usize {
ber::sizeof_octet_string(self.calling_domain_selector.len() as u16)
+ ber::sizeof_octet_string(self.called_domain_selector.len() as u16)
+ ber::SIZEOF_BOOL
+ (self.target_parameters.size() + self.min_parameters.size() + self.max_parameters.size())
+ ber::sizeof_octet_string(self.conference_create_request.size() as u16)
// Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error.
#[expect(clippy::cast_possible_truncation, clippy::as_conversions)]
{
ber::sizeof_octet_string(self.calling_domain_selector.len() as u16)
+ ber::sizeof_octet_string(self.called_domain_selector.len() as u16)
+ ber::SIZEOF_BOOL
+ (self.target_parameters.size() + self.min_parameters.size() + self.max_parameters.size())
+ ber::sizeof_octet_string(self.conference_create_request.size() as u16)
}
}
}
@ -989,7 +993,8 @@ mod legacy {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
ber::write_application_tag(dst, MCS_TYPE_CONNECT_INITIAL, self.fields_buffer_ber_length() as u16)?;
let field_buffer_ber_length = cast_length!("field_buffer_ber_length", self.fields_buffer_ber_length())?;
ber::write_application_tag(dst, MCS_TYPE_CONNECT_INITIAL, field_buffer_ber_length)?;
ber::write_octet_string(dst, self.calling_domain_selector.as_ref())?;
ber::write_octet_string(dst, self.called_domain_selector.as_ref())?;
ber::write_bool(dst, self.upward_flag)?;
@ -1008,9 +1013,12 @@ mod legacy {
fn size(&self) -> usize {
let fields_buffer_ber_length = self.fields_buffer_ber_length();
// Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error.
#[expect(clippy::cast_possible_truncation, clippy::as_conversions)]
let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16;
fields_buffer_ber_length
+ ber::sizeof_application_tag(MCS_TYPE_CONNECT_INITIAL, fields_buffer_ber_length as u16)
+ ber::sizeof_application_tag(MCS_TYPE_CONNECT_INITIAL, fields_buffer_ber_length_u16)
}
}
@ -1042,10 +1050,14 @@ mod legacy {
const NAME: &'static str = "ConnectResponse";
fn fields_buffer_ber_length(&self) -> usize {
ber::SIZEOF_ENUMERATED
+ ber::sizeof_integer(self.called_connect_id)
+ self.domain_parameters.size()
+ ber::sizeof_octet_string(self.conference_create_response.size() as u16)
// Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error.
#[expect(clippy::cast_possible_truncation, clippy::as_conversions)]
{
ber::SIZEOF_ENUMERATED
+ ber::sizeof_integer(self.called_connect_id)
+ self.domain_parameters.size()
+ ber::sizeof_octet_string(self.conference_create_response.size() as u16)
}
}
}
@ -1053,7 +1065,8 @@ mod legacy {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_size!(in: dst, size: self.size());
ber::write_application_tag(dst, MCS_TYPE_CONNECT_RESPONSE, self.fields_buffer_ber_length() as u16)?;
let field_buffer_ber_length = cast_length!("field_buffer_ber_length", self.fields_buffer_ber_length())?;
ber::write_application_tag(dst, MCS_TYPE_CONNECT_RESPONSE, field_buffer_ber_length)?;
ber::write_enumerated(dst, 0)?;
ber::write_integer(dst, self.called_connect_id)?;
self.domain_parameters.encode(dst)?;
@ -1069,9 +1082,11 @@ mod legacy {
fn size(&self) -> usize {
let fields_buffer_ber_length = self.fields_buffer_ber_length();
// Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error.
#[expect(clippy::cast_possible_truncation, clippy::as_conversions)]
let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16;
fields_buffer_ber_length
+ ber::sizeof_application_tag(MCS_TYPE_CONNECT_RESPONSE, fields_buffer_ber_length as u16)
+ ber::sizeof_application_tag(MCS_TYPE_CONNECT_RESPONSE, fields_buffer_ber_length_u16)
}
}
@ -1079,7 +1094,7 @@ mod legacy {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ber::read_application_tag(src, MCS_TYPE_CONNECT_RESPONSE)?;
ber::read_enumerated(src, RESULT_ENUM_LENGTH)?;
let called_connect_id = ber::read_integer(src)? as u32;
let called_connect_id = cast_int!("called_connect_id", ber::read_integer(src)?)?;
let domain_parameters = DomainParameters::decode(src)?;
let _user_data_buffer_length = ber::read_octet_string_tag(src)?;
let conference_create_response = ConferenceCreateResponse::decode(src)?;
@ -1131,22 +1146,25 @@ mod legacy {
fn size(&self) -> usize {
let fields_buffer_ber_length = self.fields_buffer_ber_length();
// Can't rewrite in `as`-less way, because it's used in `Encode::size` which doesn't return an error.
#[expect(clippy::cast_possible_truncation, clippy::as_conversions)]
let fields_buffer_ber_length_u16 = fields_buffer_ber_length as u16;
// FIXME: maybe size should return PduResult...
fields_buffer_ber_length + ber::sizeof_sequence_tag(fields_buffer_ber_length as u16)
fields_buffer_ber_length + ber::sizeof_sequence_tag(fields_buffer_ber_length_u16)
}
}
impl<'de> Decode<'de> for DomainParameters {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ber::read_sequence_tag(src)?;
let max_channel_ids = ber::read_integer(src)? as u32;
let max_user_ids = ber::read_integer(src)? as u32;
let max_token_ids = ber::read_integer(src)? as u32;
let num_priorities = ber::read_integer(src)? as u32;
let min_throughput = ber::read_integer(src)? as u32;
let max_height = ber::read_integer(src)? as u32;
let max_mcs_pdu_size = ber::read_integer(src)? as u32;
let protocol_version = ber::read_integer(src)? as u32;
let max_channel_ids = cast_int!("max_channel_ids", ber::read_integer(src)?)?;
let max_user_ids = cast_int!("max_user_ids", ber::read_integer(src)?)?;
let max_token_ids = cast_int!("max_token_ids", ber::read_integer(src)?)?;
let num_priorities = cast_int!("num_priorities", ber::read_integer(src)?)?;
let min_throughput = cast_int!("min_throughput", ber::read_integer(src)?)?;
let max_height = cast_int!("max_height", ber::read_integer(src)?)?;
let max_mcs_pdu_size = cast_int!("max_mcs_pdu_size", ber::read_integer(src)?)?;
let protocol_version = cast_int!("protocol_version", ber::read_integer(src)?)?;
Ok(Self {
max_channel_ids,

View file

@ -114,7 +114,7 @@ pub(crate) fn write_long_length(dst: &mut WriteCursor<'_>, length: u16) {
dst.write_u16_be(length | 0x8000)
}
pub(crate) fn sizeof_length(length: u16) -> usize {
pub(crate) fn sizeof_length(length: usize) -> usize {
if length > 0x7f {
2
} else {
@ -243,7 +243,10 @@ pub(crate) fn read_object_id(src: &mut ReadCursor<'_>) -> Result<[u8; OBJECT_ID_
}
pub(crate) fn write_object_id(dst: &mut WriteCursor<'_>, object_ids: [u8; OBJECT_ID_SIZE]) {
write_length(dst, OBJECT_ID_SIZE as u16 - 1);
write_length(
dst,
u16::try_from(OBJECT_ID_SIZE).expect("OBJECT_ID_SIZE fits into u16") - 1,
);
let first_two_tuples = object_ids[0] * 40 + object_ids[1];
dst.write_u8(first_two_tuples);
@ -338,8 +341,8 @@ pub(crate) mod legacy {
Ok(2)
}
pub(crate) fn write_short_length(mut stream: impl io::Write, length: u16) -> io::Result<usize> {
stream.write_u8(length as u8)?;
pub(crate) fn write_short_length(mut stream: impl io::Write, length: u8) -> io::Result<usize> {
stream.write_u8(length)?;
Ok(1)
}
@ -347,6 +350,12 @@ pub(crate) mod legacy {
if length > 0x7f {
write_long_length(stream, length)
} else {
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "cast is valid due to prior check"
)]
let length = length as u8;
write_short_length(stream, length)
}
}
@ -413,12 +422,25 @@ pub(crate) mod legacy {
pub(crate) fn write_u32(mut stream: impl io::Write, value: u32) -> io::Result<usize> {
if value <= 0xff {
let size = write_length(&mut stream, 1)?;
stream.write_u8(value as u8)?;
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "cast is valid due to prior check"
)]
let value = value as u8;
stream.write_u8(value)?;
Ok(size + 1)
} else if value <= 0xffff {
let size = write_length(&mut stream, 2)?;
stream.write_u16::<BigEndian>(value as u16)?;
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "cast is valid due to prior check"
)]
let value = value as u16;
stream.write_u16::<BigEndian>(value)?;
Ok(size + 2)
} else {
@ -488,7 +510,10 @@ pub(crate) mod legacy {
}
pub(crate) fn write_object_id(mut stream: impl io::Write, object_ids: [u8; OBJECT_ID_SIZE]) -> io::Result<usize> {
let size = write_length(&mut stream, OBJECT_ID_SIZE as u16 - 1)?;
let object_oid_size: u16 = OBJECT_ID_SIZE
.try_into()
.expect("OBJECT_ID_SIZE is known to fit into u16");
let size = write_length(&mut stream, object_oid_size - 1)?;
let first_two_tuples = object_ids[0] * 40 + object_ids[1];
stream.write_u8(first_two_tuples)?;
@ -503,7 +528,7 @@ pub(crate) mod legacy {
pub(crate) fn read_octet_string(mut stream: impl io::Read, min: usize) -> io::Result<Vec<u8>> {
let (read_length, _) = read_length(&mut stream)?;
let mut read_octet_string = vec![0; min + read_length as usize];
let mut read_octet_string = vec![0; min + usize::from(read_length)];
stream.read_exact(read_octet_string.as_mut())?;
Ok(read_octet_string)
@ -516,7 +541,9 @@ pub(crate) mod legacy {
min
};
let size = write_length(&mut stream, length as u16)?;
let length = u16::try_from(length)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid octet string length"))?;
let size = write_length(&mut stream, length)?;
stream.write_all(octet_string)?;
Ok(size + octet_string.len())
@ -527,7 +554,7 @@ pub(crate) mod legacy {
let length = (read_length + min).div_ceil(2);
let mut read_numeric_string = vec![0; length as usize];
let mut read_numeric_string = vec![0; usize::from(length)];
stream.read_exact(read_numeric_string.as_mut())?;
Ok(())
@ -536,7 +563,9 @@ pub(crate) mod legacy {
pub(crate) fn write_numeric_string(mut stream: impl io::Write, num_str: &[u8], min: usize) -> io::Result<usize> {
let length = if num_str.len() >= min { num_str.len() - min } else { min };
let mut size = write_length(&mut stream, length as u16)?;
let length = u16::try_from(length)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid numeric string length"))?;
let mut size = write_length(&mut stream, length)?;
let magic_transform = |elem| (elem - 0x30) % 10;

View file

@ -141,10 +141,10 @@ impl<'de> Decode<'de> for BitmapCodecs {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ensure_fixed_part_size!(in: src);
let codecs_count = src.read_u8();
let codec_count = src.read_u8();
let mut codecs = Vec::with_capacity(codecs_count as usize);
for _ in 0..codecs_count {
let mut codecs = Vec::with_capacity(usize::from(codec_count));
for _ in 0..codec_count {
codecs.push(Codec::decode(src)?);
}
@ -552,7 +552,7 @@ impl<'de> Decode<'de> for RfxCapset {
let num_icaps = src.read_u16();
let _icaps_len = src.read_u16();
let mut icaps_data = Vec::with_capacity(num_icaps as usize);
let mut icaps_data = Vec::with_capacity(usize::from(num_icaps));
for _ in 0..num_icaps {
icaps_data.push(RfxICap::decode(src)?);
}

View file

@ -215,9 +215,9 @@ impl<'de> Decode<'de> for DemandActive {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ensure_fixed_part_size!(in: src);
let source_descriptor_length = src.read_u16() as usize;
let source_descriptor_length = usize::from(src.read_u16());
// The combined size in bytes of the numberCapabilities, pad2Octets, and capabilitySets fields.
let _combined_capabilities_length = src.read_u16() as usize;
let _combined_capabilities_length = usize::from(src.read_u16());
ensure_size!(in: src, size: source_descriptor_length);
let source_descriptor = utils::decode_string(
@ -227,7 +227,7 @@ impl<'de> Decode<'de> for DemandActive {
)?;
ensure_size!(in: src, size: 2 + 2);
let capability_sets_count = src.read_u16() as usize;
let capability_sets_count = usize::from(src.read_u16());
let _padding = src.read_u16();
let mut capability_sets = Vec::with_capacity(capability_sets_count);
@ -509,7 +509,7 @@ impl<'de> Decode<'de> for CapabilitySet {
)
})?;
let length = src.read_u16() as usize;
let length = usize::from(src.read_u16());
if length < CAPABILITY_SET_TYPE_FIELD_SIZE + CAPABILITY_SET_LENGTH_FIELD_SIZE {
return Err(invalid_field_err!("len", "invalid capability set length"));

View file

@ -9,6 +9,7 @@ const ORD_LEVEL_1_ORDERS: u16 = 1;
const SUPPORT_ARRAY_LEN: usize = 32;
const DESKTOP_SAVE_Y_GRAN_VAL: u16 = 20;
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum OrderSupportIndex {
DstBlt = 0x00,
@ -34,6 +35,16 @@ pub enum OrderSupportIndex {
Index = 0x1B,
}
impl OrderSupportIndex {
#[expect(
clippy::as_conversions,
reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive"
)]
fn as_u8(self) -> u8 {
self as u8
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OrderFlags: u16 {
@ -83,11 +94,11 @@ impl Order {
}
pub fn set_support_flag(&mut self, flag: OrderSupportIndex, value: bool) {
self.order_support[flag as usize] = u8::from(value)
self.order_support[usize::from(flag.as_u8())] = u8::from(value)
}
pub fn get_support_flag(&mut self, flag: OrderSupportIndex) -> bool {
self.order_support[flag as usize] == 1
self.order_support[usize::from(flag.as_u8())] == 1
}
}

View file

@ -30,27 +30,27 @@ lazy_static! {
order_support: {
let mut array = [0u8; 32];
array[OrderSupportIndex::DstBlt as usize] = 1;
array[OrderSupportIndex::PatBlt as usize] = 1;
array[OrderSupportIndex::ScrBlt as usize] = 1;
array[OrderSupportIndex::MemBlt as usize] = 1;
array[OrderSupportIndex::Mem3Blt as usize] = 1;
array[OrderSupportIndex::DrawnInEGrid as usize] = 1;
array[OrderSupportIndex::LineTo as usize] = 1;
array[OrderSupportIndex::MultiDrawnInEGrid as usize] = 1;
array[OrderSupportIndex::SaveBitmap as usize] = 1;
array[OrderSupportIndex::MultiDstBlt as usize] = 1;
array[OrderSupportIndex::MultiPatBlt as usize] = 1;
array[OrderSupportIndex::MultiScrBlt as usize] = 1;
array[OrderSupportIndex::MultiOpaqueRect as usize] = 1;
array[OrderSupportIndex::Fast as usize] = 1;
array[OrderSupportIndex::PolygonSC as usize] = 1;
array[OrderSupportIndex::PolygonCB as usize] = 1;
array[OrderSupportIndex::Polyline as usize] = 1;
array[OrderSupportIndex::FastGlyph as usize] = 1;
array[OrderSupportIndex::EllipseSC as usize] = 1;
array[OrderSupportIndex::EllipseCB as usize] = 1;
array[OrderSupportIndex::Index as usize] = 1;
array[usize::from(OrderSupportIndex::DstBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::PatBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::ScrBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MemBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::Mem3Blt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::DrawnInEGrid.as_u8())] = 1;
array[usize::from(OrderSupportIndex::LineTo.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MultiDrawnInEGrid.as_u8())] = 1;
array[usize::from(OrderSupportIndex::SaveBitmap.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MultiDstBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MultiPatBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MultiScrBlt.as_u8())] = 1;
array[usize::from(OrderSupportIndex::MultiOpaqueRect.as_u8())] = 1;
array[usize::from(OrderSupportIndex::Fast.as_u8())] = 1;
array[usize::from(OrderSupportIndex::PolygonSC.as_u8())] = 1;
array[usize::from(OrderSupportIndex::PolygonCB.as_u8())] = 1;
array[usize::from(OrderSupportIndex::Polyline.as_u8())] = 1;
array[usize::from(OrderSupportIndex::FastGlyph.as_u8())] = 1;
array[usize::from(OrderSupportIndex::EllipseSC.as_u8())] = 1;
array[usize::from(OrderSupportIndex::EllipseCB.as_u8())] = 1;
array[usize::from(OrderSupportIndex::Index.as_u8())] = 1;
array
},

View file

@ -145,9 +145,8 @@ impl<'de> Decode<'de> for ClientInfo {
let flags = ClientInfoFlags::from_bits(flags_with_compression_type & !COMPRESSION_TYPE_MASK)
.ok_or_else(|| invalid_field_err!("flags", "invalid ClientInfoFlags"))?;
let compression_type =
CompressionType::from_u8(((flags_with_compression_type & COMPRESSION_TYPE_MASK) >> 9) as u8)
.ok_or_else(|| invalid_field_err!("flags", "invalid CompressionType"))?;
let compression_type = CompressionType::from_u32((flags_with_compression_type & COMPRESSION_TYPE_MASK) >> 9)
.ok_or_else(|| invalid_field_err!("flags", "invalid CompressionType"))?;
let character_set = if flags.contains(ClientInfoFlags::UNICODE) {
CharacterSet::Unicode
@ -157,11 +156,11 @@ impl<'de> Decode<'de> for ClientInfo {
// Sizes exclude the length of the mandatory null terminator
let nt = usize::from(character_set.as_u16());
let domain_size = src.read_u16() as usize + nt;
let user_name_size = src.read_u16() as usize + nt;
let password_size = src.read_u16() as usize + nt;
let alternate_shell_size = src.read_u16() as usize + nt;
let work_dir_size = src.read_u16() as usize + nt;
let domain_size = usize::from(src.read_u16()) + nt;
let user_name_size = usize::from(src.read_u16()) + nt;
let password_size = usize::from(src.read_u16()) + nt;
let alternate_shell_size = usize::from(src.read_u16()) + nt;
let work_dir_size = usize::from(src.read_u16()) + nt;
ensure_size!(in: src, size: domain_size + user_name_size + password_size + alternate_shell_size + work_dir_size);
let domain = utils::decode_string(src.read_slice(domain_size), character_set, true)?;
@ -226,12 +225,12 @@ impl ExtendedClientInfo {
let address_family = AddressFamily::from_u16(src.read_u16());
// This size includes the length of the mandatory null terminator.
let address_size = src.read_u16() as usize;
let address_size = usize::from(src.read_u16());
ensure_size!(in: src, size: address_size + CLIENT_DIR_LENGTH_SIZE);
let address = utils::decode_string(src.read_slice(address_size), character_set, false)?;
// This size includes the length of the mandatory null terminator.
let dir_size = src.read_u16() as usize;
let dir_size = usize::from(src.read_u16());
ensure_size!(in: src, size: dir_size);
let dir = utils::decode_string(src.read_slice(dir_size), character_set, false)?;
@ -324,7 +323,7 @@ impl Encode for ExtendedClientOptionalInfo {
dst.write_u32(performance_flags.bits());
}
if let Some(reconnect_cookie) = self.reconnect_cookie {
dst.write_u16(RECONNECT_COOKIE_LEN as u16);
dst.write_u16(u16::try_from(RECONNECT_COOKIE_LEN).expect("RECONNECT_COOKIE_LEN fit into u16"));
dst.write_array(reconnect_cookie);
}
@ -381,7 +380,9 @@ impl<'de> Decode<'de> for ExtendedClientOptionalInfo {
return Ok(optional_data);
}
let reconnect_cookie_size = src.read_u16();
if reconnect_cookie_size != RECONNECT_COOKIE_LEN as u16 && reconnect_cookie_size != 0 {
if reconnect_cookie_size != u16::try_from(RECONNECT_COOKIE_LEN).expect("RECONNECT_COOKIE_LEN fit into u16")
&& reconnect_cookie_size != 0
{
return Err(invalid_field_err!("cbAutoReconnectCookie", "invalid cookie size"));
}
if reconnect_cookie_size != 0 {

View file

@ -220,7 +220,10 @@ impl<'de> Decode<'de> for MonitorLayoutPdu {
return Err(invalid_field_err!("nMonitors", "invalid monitor count"));
}
let mut monitors = Vec::with_capacity(monitor_count as usize);
let mut monitors = Vec::with_capacity(
usize::try_from(monitor_count)
.expect("monitor_count is guaranteed to fit into usize due to the prior check"),
);
for _ in 0..monitor_count {
monitors.push(gcc::Monitor::decode(src)?);
}

View file

@ -115,7 +115,7 @@ impl<'de> Decode<'de> for ShareControlHeader {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ensure_fixed_part_size!(in: src);
let total_length = src.read_u16() as usize;
let total_length = usize::from(src.read_u16());
let pdu_type_with_version = src.read_u16();
let pdu_source = src.read_u16();
let share_id = src.read_u32();

View file

@ -80,7 +80,7 @@ impl ClientLicenseInfo {
let mut rc4 = Rc4::new(&license_key);
let encrypted_hwid = rc4.process(&hardware_id);
let mac_data = compute_mac_data(mac_salt_key, &hardware_id);
let mac_data = compute_mac_data(mac_salt_key, &hardware_id)?;
let size = RANDOM_NUMBER_SIZE
+ PREAMBLE_SIZE
@ -97,7 +97,8 @@ impl ClientLicenseInfo {
preamble_message_type: PreambleType::LicenseInfo,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (size) as u16,
preamble_message_size: u16::try_from(size)
.map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?,
};
Ok((

View file

@ -98,14 +98,17 @@ impl ClientNewLicenseRequest {
preamble_message_type: PreambleType::NewLicenseRequest,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (RANDOM_NUMBER_SIZE
+ PREAMBLE_SIZE
+ LICENSE_REQUEST_STATIC_FIELDS_SIZE
+ encrypted_premaster_secret.len()
+ client_machine_name.len()
+ UTF8_NULL_TERMINATOR_SIZE
+ client_username.len()
+ UTF8_NULL_TERMINATOR_SIZE) as u16,
preamble_message_size: u16::try_from(
RANDOM_NUMBER_SIZE
+ PREAMBLE_SIZE
+ LICENSE_REQUEST_STATIC_FIELDS_SIZE
+ encrypted_premaster_secret.len()
+ client_machine_name.len()
+ UTF8_NULL_TERMINATOR_SIZE
+ client_username.len()
+ UTF8_NULL_TERMINATOR_SIZE,
)
.map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?,
};
Ok((

View file

@ -68,14 +68,14 @@ lazy_static! {
preamble_message_type: PreambleType::NewLicenseRequest,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (PREAMBLE_SIZE
preamble_message_size: u16::try_from(PREAMBLE_SIZE
+ RANDOM_NUMBER_SIZE
+ LICENSE_REQUEST_STATIC_FIELDS_SIZE
+ ENCRYPTED_PREMASTER_SECRET.len()
+ CLIENT_MACHINE_NAME.len()
+ UTF8_NULL_TERMINATOR_SIZE
+ CLIENT_USERNAME.len()
+ UTF8_NULL_TERMINATOR_SIZE) as u16,
+ UTF8_NULL_TERMINATOR_SIZE).expect("can't panic"),
},
client_random: Vec::from(CLIENT_RANDOM_BUFFER.as_ref()),
encrypted_premaster_secret: Vec::from(ENCRYPTED_PREMASTER_SECRET.as_ref()),
@ -86,11 +86,11 @@ lazy_static! {
pub static ref REQUEST_BUFFER: Vec<u8> = {
let username_len = CLIENT_USERNAME.len() + UTF8_NULL_TERMINATOR_SIZE;
let mut username_len_buf = Vec::new();
username_len_buf.write_u16::<LittleEndian>(username_len as u16).unwrap();
username_len_buf.write_u16::<LittleEndian>(u16::try_from(username_len).expect("can't panic")).unwrap();
let machine_name_len = CLIENT_MACHINE_NAME.len() + UTF8_NULL_TERMINATOR_SIZE;
let mut machine_name_len_buf = Vec::new();
machine_name_len_buf.write_u16::<LittleEndian>(machine_name_len as u16).unwrap();
machine_name_len_buf.write_u16::<LittleEndian>(u16::try_from(machine_name_len).unwrap()).unwrap();
let buf = [
&[0x01u8, 0x00, 0x00, 0x00, // preferred_key_exchange_algorithm
@ -109,7 +109,7 @@ lazy_static! {
&[0x00]] // null
.concat();
let preamble_size_field = (buf.len() + PREAMBLE_SIZE) as u16;
let preamble_size_field = u16::try_from(buf.len() + PREAMBLE_SIZE).expect("can't panic");
[
LICENSE_HEADER_BUFFER_NO_SIZE.as_ref(),
@ -270,7 +270,7 @@ lazy_static! {
}),
scope_list: vec![Scope(String::from("microsoft.com"))],
};
req.license_header.preamble_message_size = req.size() as u16;
req.license_header.preamble_message_size = u16::try_from(req.size()).expect("can't panic");
req.into()
};
}

View file

@ -46,7 +46,7 @@ impl ClientPlatformChallengeResponse {
let decrypted_challenge = rc4.process(platform_challenge.encrypted_platform_challenge.as_slice());
let decrypted_challenge_mac =
super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_challenge.as_slice());
super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_challenge.as_slice())?;
if decrypted_challenge_mac != platform_challenge.mac_data {
return Err(ServerLicenseError::InvalidMacData);
@ -56,7 +56,10 @@ impl ClientPlatformChallengeResponse {
challenge_response_data.write_u16::<LittleEndian>(RESPONSE_DATA_VERSION)?;
challenge_response_data.write_u16::<LittleEndian>(ClientType::Other.as_u16())?;
challenge_response_data.write_u16::<LittleEndian>(LicenseDetailLevel::Detail.as_u16())?;
challenge_response_data.write_u16::<LittleEndian>(decrypted_challenge.len() as u16)?;
challenge_response_data.write_u16::<LittleEndian>(
u16::try_from(decrypted_challenge.len())
.map_err(|_| ServerLicenseError::InvalidField("decrypted challenge len"))?,
)?;
challenge_response_data.write_all(&decrypted_challenge)?;
let mut hardware_id = Vec::with_capacity(CLIENT_HARDWARE_IDENTIFICATION_SIZE);
@ -75,7 +78,7 @@ impl ClientPlatformChallengeResponse {
let mac_data = super::compute_mac_data(
encryption_data.mac_salt_key.as_slice(),
challenge_response_data.as_slice(),
);
)?;
let license_header = LicenseHeader {
security_header: BasicSecurityHeader {
@ -84,10 +87,12 @@ impl ClientPlatformChallengeResponse {
preamble_message_type: PreambleType::PlatformChallengeResponse,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (PREAMBLE_SIZE
preamble_message_size: u16::try_from(
PREAMBLE_SIZE
+ (BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE) * 2 // 2 blobs in this structure
+ MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len())
as u16,
+ MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len(),
)
.map_err(|_| ServerLicenseError::InvalidField("preamble message size"))?,
};
Ok(Self {

View file

@ -63,8 +63,10 @@ lazy_static! {
preamble_message_type: PreambleType::PlatformChallengeResponse,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE)
as u16,
preamble_message_size: u16::try_from(
CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE
)
.expect("can't panic"),
},
encrypted_challenge_response_data: Vec::from(&CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[12..30]),
encrypted_hwid: Vec::from(&CLIENT_PLATFORM_CHALLENGE_RESPONSE_BUFFER[34..54]),
@ -165,7 +167,8 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl
preamble_message_type: PreambleType::PlatformChallenge,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (encrypted_platform_challenge.len() + mac_data.len() + PREAMBLE_SIZE) as u16,
preamble_message_size: u16::try_from(encrypted_platform_challenge.len() + mac_data.len() + PREAMBLE_SIZE)
.expect("can't panic"),
},
encrypted_platform_challenge,
mac_data,
@ -200,7 +203,8 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl
let mac_data = crate::rdp::server_license::compute_mac_data(
encryption_data.mac_salt_key.as_slice(),
[response_data.as_ref(), hardware_id.as_slice()].concat().as_slice(),
);
)
.expect("can't panic");
let correct_challenge_response = ClientPlatformChallengeResponse {
license_header: LicenseHeader {
@ -210,10 +214,12 @@ fn challenge_response_creates_from_server_challenge_and_encryption_data_correctl
preamble_message_type: PreambleType::PlatformChallengeResponse,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (PREAMBLE_SIZE
preamble_message_size: u16::try_from(
PREAMBLE_SIZE
+ (BLOB_TYPE_SIZE + BLOB_LENGTH_SIZE) * 2 // 2 blobs in this structure
+ MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len())
as u16,
+ MAC_SIZE + encrypted_challenge_response_data.len() + encrypted_hwid.len(),
)
.expect("can't panic"),
},
encrypted_challenge_response_data,
encrypted_hwid,

View file

@ -26,7 +26,7 @@ lazy_static! {
state_transition: LicensingStateTransition::NoTransition,
error_info: Vec::new(),
};
pdu.license_header.preamble_message_size = pdu.size() as u16;
pdu.license_header.preamble_message_size = u16::try_from(pdu.size()).expect("can't panic");
pdu.into()
};
}

View file

@ -207,6 +207,8 @@ pub enum ServerLicenseError {
Utf8Error(#[from] std::string::FromUtf8Error),
#[error("DER error: {0}")]
DerError(#[from] pkcs1::der::Error),
#[error("invalid `{0}`: out of range integral type conversion")]
InvalidField(&'static str),
#[error("invalid preamble field: {0}")]
InvalidPreamble(String),
#[error("invalid preamble message type field")]
@ -338,8 +340,10 @@ impl<'de> Decode<'de> for BlobHeader {
}
}
fn compute_mac_data(mac_salt_key: &[u8], data: &[u8]) -> Vec<u8> {
let data_len_buffer = (data.len() as u32).to_le_bytes();
fn compute_mac_data(mac_salt_key: &[u8], data: &[u8]) -> Result<Vec<u8>, ServerLicenseError> {
let data_len_buffer = u32::try_from(data.len())
.map_err(|_| ServerLicenseError::InvalidField("MAC data length"))?
.to_le_bytes();
let pad_one: [u8; 40] = [0x36; 40];
@ -360,7 +364,7 @@ fn compute_mac_data(mac_salt_key: &[u8], data: &[u8]) -> Vec<u8> {
.as_slice(),
);
md5.finalize().to_vec()
Ok(md5.finalize().to_vec())
}
#[derive(Debug, PartialEq)]

View file

@ -108,7 +108,10 @@ impl ServerLicenseRequest {
return Err(invalid_field_err!("scopeCount", "invalid scope count"));
}
let mut scope_list = Vec::with_capacity(scope_count as usize);
let mut scope_list = Vec::with_capacity(
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer underflow)")]
usize::try_from(scope_count).expect("scope_count is guaranteed to fit into usize due to the prior check"),
);
for _ in 0..scope_count {
scope_list.push(Scope::decode(src)?);

View file

@ -252,7 +252,7 @@ lazy_static! {
}),
scope_list: vec![Scope(String::from("microsoft.com"))],
};
req.license_header.preamble_message_size = req.size() as u16;
req.license_header.preamble_message_size = u16::try_from(req.size()).expect("can't panic");
req.into()
};
pub static ref X509_CERTIFICATE: ServerCertificate = ServerCertificate {
@ -323,7 +323,7 @@ fn from_buffer_correctly_parses_server_license_request_no_certificate() {
server_certificate: None,
scope_list: vec![Scope(String::from("microsoft.com"))],
};
request.license_header.preamble_message_size = request.size() as u16;
request.license_header.preamble_message_size = u16::try_from(request.size()).expect("can't panic");
let request: LicensePdu = request.into();
assert_eq!(request, decode(&request_buffer).unwrap());
@ -370,7 +370,7 @@ fn to_buffer_correctly_serializes_server_license_request() {
}),
scope_list: vec![Scope(String::from("microsoft.com"))],
};
request.license_header.preamble_message_size = request.size() as u16;
request.license_header.preamble_message_size = u16::try_from(request.size()).unwrap();
let request: LicensePdu = request.into();
let serialized_request = encode_vec(&request).unwrap();

View file

@ -35,7 +35,8 @@ lazy_static! {
preamble_message_type: PreambleType::PlatformChallenge,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (PLATFORM_CHALLENGE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE) as u16,
preamble_message_size: u16::try_from(PLATFORM_CHALLENGE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE)
.expect("can't panic"),
},
encrypted_platform_challenge: Vec::from(CHALLENGE_BUFFER.as_ref()),
mac_data: Vec::from(MAC_DATA_BUFFER.as_ref()),

View file

@ -69,7 +69,7 @@ impl ServerUpgradeLicense {
pub fn verify_server_license(&self, encryption_data: &LicenseEncryptionData) -> Result<(), ServerLicenseError> {
let decrypted_license_info = self.decrypted_license_info(encryption_data);
let mac_data =
super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_license_info.as_ref());
super::compute_mac_data(encryption_data.mac_salt_key.as_slice(), decrypted_license_info.as_ref())?;
if mac_data != self.mac_data {
return Err(ServerLicenseError::InvalidMacData);

View file

@ -260,7 +260,8 @@ lazy_static! {
preamble_message_type: PreambleType::NewLicense,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (SERVER_UPGRADE_LICENSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE) as u16,
preamble_message_size: u16::try_from(SERVER_UPGRADE_LICENSE_BUFFER.len() - BASIC_SECURITY_HEADER_SIZE)
.expect("buffer size is too large"),
},
encrypted_license_info: Vec::from(
&SERVER_UPGRADE_LICENSE_BUFFER[12..SERVER_UPGRADE_LICENSE_BUFFER.len() - MAC_SIZE]
@ -618,11 +619,10 @@ fn upgrade_license_verifies_correctly() {
preamble_message_type: PreambleType::NewLicense,
preamble_flags: PreambleFlags::empty(),
preamble_version: PreambleVersion::V3,
preamble_message_size: (PREAMBLE_SIZE
+ BLOB_LENGTH_SIZE
+ BLOB_TYPE_SIZE
+ encrypted_license_info.len()
+ MAC_SIZE) as u16,
preamble_message_size: u16::try_from(
PREAMBLE_SIZE + BLOB_LENGTH_SIZE + BLOB_TYPE_SIZE + encrypted_license_info.len() + MAC_SIZE,
)
.expect("can't panic"),
},
encrypted_license_info,
mac_data,

View file

@ -106,7 +106,7 @@ fn mac_data_computes_correctly() {
let decrypted_server_challenge: [u8; 10] = [0x54, 0x0, 0x45, 0x0, 0x53, 0x0, 0x54, 0x0, 0x0, 0x0];
assert_eq!(
compute_mac_data(mac_salt_key.as_ref(), decrypted_server_challenge.as_ref()),
compute_mac_data(mac_salt_key.as_ref(), decrypted_server_challenge.as_ref()).unwrap(),
server_mac_data.as_ref()
);
}

View file

@ -112,8 +112,8 @@ impl Encode for ServerAutoReconnect {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_fixed_part_size!(in: dst);
dst.write_u32(AUTO_RECONNECT_PACKET_SIZE as u32);
dst.write_u32(AUTO_RECONNECT_PACKET_SIZE as u32);
dst.write_u32(u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32"));
dst.write_u32(u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32"));
dst.write_u32(AUTO_RECONNECT_VERSION_1);
dst.write_u32(self.logon_id);
dst.write_slice(self.random_bits.as_ref());
@ -136,7 +136,8 @@ impl<'de> Decode<'de> for ServerAutoReconnect {
let _data_length = src.read_u32();
let packet_length = src.read_u32();
if packet_length != AUTO_RECONNECT_PACKET_SIZE as u32 {
if packet_length != u32::try_from(AUTO_RECONNECT_PACKET_SIZE).expect("AUTO_RECONNECT_PACKET_SIZE fits into u32")
{
return Err(invalid_field_err!("packetLen", "invalid auto-reconnect packet size"));
}
@ -171,7 +172,7 @@ impl Encode for LogonErrorsInfo {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
ensure_fixed_part_size!(in: dst);
dst.write_u32(LOGON_ERRORS_INFO_SIZE as u32);
dst.write_u32(u32::try_from(LOGON_ERRORS_INFO_SIZE).expect("LOGON_ERRORS_INFO_SIZE fits into u32"));
dst.write_u32(self.error_type.as_u32());
dst.write_u32(self.error_data.to_u32());

View file

@ -111,7 +111,7 @@ impl Encode for LogonInfoVersion2 {
ensure_size!(in: dst, size: self.size());
dst.write_u16(SAVE_SESSION_PDU_VERSION_ONE);
dst.write_u32(LOGON_INFO_V2_SIZE as u32);
dst.write_u32(u32::try_from(LOGON_INFO_V2_SIZE).expect("LOGON_INFO_V2_SIZE fits into u32"));
dst.write_u32(self.logon_info.session_id);
dst.write_u32(cast_length!(
"domainNameSize",

View file

@ -112,9 +112,9 @@ impl<'de> Decode<'de> for Avc420BitmapStream<'de> {
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
ensure_fixed_part_size!(in: src);
let num_regions = src.read_u32();
let mut rectangles = Vec::with_capacity(num_regions as usize);
let mut quant_qual_vals = Vec::with_capacity(num_regions as usize);
let num_regions = cast_length!("number of regions", src.read_u32())?;
let mut rectangles = Vec::with_capacity(num_regions);
let mut quant_qual_vals = Vec::with_capacity(num_regions);
for _ in 0..num_regions {
rectangles.push(InclusiveRectangle::decode(src)?);
}
@ -158,7 +158,7 @@ impl Encode for Avc444BitmapStream<'_> {
let mut stream_info = 0u32;
stream_info.set_bits(0..30, cast_length!("stream1size", self.stream1.size())?);
stream_info.set_bits(30..32, self.encoding.bits() as u32);
stream_info.set_bits(30..32, u32::from(self.encoding.bits()));
dst.write_u32(stream_info);
self.stream1.encode(dst)?;
if let Some(stream) = self.stream2.as_ref() {
@ -188,7 +188,8 @@ impl<'de> Decode<'de> for Avc444BitmapStream<'de> {
let stream_info = src.read_u32();
let stream_len = stream_info.get_bits(0..30);
let encoding = Encoding::from_bits_truncate(stream_info.get_bits(30..32) as u8);
let encoding =
Encoding::from_bits_truncate(u8::try_from(stream_info.get_bits(30..32)).expect("value fits into u8"));
if stream_len == 0 {
if encoding == Encoding::LUMA_AND_CHROMA {
@ -202,7 +203,7 @@ impl<'de> Decode<'de> for Avc444BitmapStream<'de> {
stream2: None,
})
} else {
let (mut stream1, mut stream2) = src.split_at(stream_len as usize);
let (mut stream1, mut stream2) = src.split_at(cast_length!("first stream length", stream_len)?);
let stream1 = Avc420BitmapStream::decode(&mut stream1)?;
let stream2 = if encoding == Encoding::LUMA_AND_CHROMA {
Some(Avc420BitmapStream::decode(&mut stream2)?)

View file

@ -231,7 +231,7 @@ impl Encode for SolidFillPdu {
dst.write_u16(self.surface_id);
self.fill_pixel.encode(dst)?;
dst.write_u16(self.rectangles.len() as u16);
dst.write_u16(cast_length!("number of rectangles", self.rectangles.len())?);
for rectangle in self.rectangles.iter() {
rectangle.encode(dst)?;
@ -1026,10 +1026,10 @@ impl<'a> Decode<'a> for Timestamp {
let timestamp = src.read_u32();
let milliseconds = timestamp.get_bits(..10) as u16;
let seconds = timestamp.get_bits(10..16) as u8;
let minutes = timestamp.get_bits(16..22) as u8;
let hours = timestamp.get_bits(22..) as u16;
let milliseconds = u16::try_from(timestamp.get_bits(..10)).expect("value fits into u16");
let seconds = u8::try_from(timestamp.get_bits(10..16)).expect("value fits into u8");
let minutes = u8::try_from(timestamp.get_bits(16..22)).expect("value fits into u8");
let hours = u16::try_from(timestamp.get_bits(22..)).expect("value fits into u16");
Ok(Self {
milliseconds,

View file

@ -8,8 +8,11 @@ use num_derive::FromPrimitive;
use crate::{DecodeResult, EncodeResult};
pub fn split_u64(value: u64) -> (u32, u32) {
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
let low =
u32::try_from(value & 0xFFFF_FFFF).expect("masking with 0xFFFF_FFFF ensures that the value fits into u32");
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (checked integer downcast)")]
let high = u32::try_from(value >> 32).expect("(u64 >> 32) fits into u32");
(low, high)
@ -31,6 +34,8 @@ pub fn to_utf16_bytes(value: &str) -> Vec<u8> {
pub fn from_utf16_bytes(mut value: &[u8]) -> String {
let mut value_u16 = vec![0x00; value.len() / 2];
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior constrain)")]
value
.read_u16_into::<LittleEndian>(value_u16.as_mut())
.expect("read_u16_into cannot fail at this point");
@ -106,6 +111,7 @@ pub fn read_string_from_cursor(
let str_buffer = &mut slice;
let mut u16_buffer = vec![0u16; str_buffer.len() / 2];
#[expect(clippy::missing_panics_doc, reason = "unreachable panic (prior constrain)")]
str_buffer
.read_u16_into::<LittleEndian>(u16_buffer.as_mut())
.expect("BUG: str_buffer is always even for UTF16");
@ -295,7 +301,12 @@ where
)
}
// Utility function that panics on overflow
/// Utility function that panics on overflow
///
/// # Panics
///
/// Panics if sum of values overflows.
// FIXME: Is it really something we want to expose from ironrdp-pdu?
pub fn strict_sum<T>(values: &[T]) -> T
where
T: CheckedAdd + Copy + Debug,

View file

@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.2.0...ironrdp-rdcleanpath-v0.2.1)] - 2025-10-02
### <!-- 1 -->Features
- Human-readable descriptions for RDCleanPath errors (#999) ([18c81ed5d8](https://github.com/Devolutions/IronRDP/commit/18c81ed5d8d3bf13b3d10fe15209233c0c10bb62))
More munging to give human-readable webclient-side errors for
RDCleanPath general/negotiation errors, including strings for WSA and
TLS and HTTP error conditions.
## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.1.3...ironrdp-rdcleanpath-v0.2.0)] - 2025-08-29
### <!-- 1 -->Features

View file

@ -1,6 +1,6 @@
[package]
name = "ironrdp-rdcleanpath"
version = "0.2.0"
version = "0.2.1"
readme = "README.md"
description = "RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway"
edition.workspace = true

Some files were not shown because too many files have changed in this diff Show more