Skip to content

refactor(pdu): hand-roll Display/Error impls and remove thiserror dependency#1264

Merged
Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
lamco-admin:feat/ironrdp-pdu-drop-thiserror
May 13, 2026
Merged

refactor(pdu): hand-roll Display/Error impls and remove thiserror dependency#1264
Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
lamco-admin:feat/ironrdp-pdu-drop-thiserror

Conversation

@glamberson
Copy link
Copy Markdown
Contributor

Closes the proc-macro architectural-invariant violation that issue #1257 surfaced and that I committed to addressing in #1257 (comment).

What changes

ironrdp-pdu is Core Tier per ARCHITECTURE.md. Core Tier carries the architectural invariant that no proc-macro dependency (syn) is allowed, so compilation stays fast for downstream consumers that build only the protocol-decode tree (the fuzzing infrastructure, the web client, no_std users).

thiserror = "2.0" was carried in the legacy block of ironrdp-pdu/Cargo.toml under the existing # TODO: get rid of these dependencies comment. This PR delivers on that TODO. Every #[derive(thiserror::Error)] in the crate is replaced with hand-written impl Display, impl core::error::Error, and impl From<X> blocks that preserve the exact Display output byte-for-byte and the exact Error::source() semantics that the derives generated.

Enums converted (13 total across 13 files)

GCC family:

  • CoreDataError, GccError, NetworkDataError, ClusterDataError, SecurityDataError

RDP family:

  • RdpError, CapabilitySetsError, ChannelError, SessionError, ServerLicenseError, ClientInfoError

Other:

  • InputEventError
  • McsError (legacy submodule in mcs.rs)

Each enum keeps #[derive(Debug)]. The hand-written Display preserves every #[error("...")] format string verbatim, including {0} and {0:?} interpolations and the named-field interpolation in ChannelError::InvalidDvcTotalMessageSize. The Error::source() impl preserves the #[from]-driven source chain plus the named source: field on ServerLicenseError::InvalidX509Certificate. The existing manual From<PduError>, From<RdpError> for io::Error, From<McsError> for io::Error, From<ChannelError> for io::Error, and From<LicensingErrorMessage> for ServerLicenseError impls are preserved unchanged.

Verification

  • cargo expand against the post-change tree shows Display and source impls that are functionally equivalent to the thiserror-generated versions for every variant.
  • cargo check --workspace clean.
  • cargo test --package ironrdp-pdu 850 tests pass, 0 failures.
  • cargo clippy --package ironrdp-pdu --all-targets -- -D warnings clean.
  • cargo xtask check fmt clean.
  • cargo xtask check typos clean.

Downstream impact

External consumers of these types in the workspace (ironrdp-connector for ServerLicenseError, ffi and ironrdp-session for RdpError and SessionError) treat them as opaque error types. A workspace-wide grep finds no variant-level pattern matching on these enums outside ironrdp-pdu itself, so the preserved From conversions and Display output fully cover downstream expectations.

Diff shape

15 files changed, +748 / -216 (net +532). Most of the added lines are the explicit Display match arms and the per-variant From and source() arms that the proc-macro previously generated at compile time.

Related: #1257.

…endency

The `ironrdp-pdu` crate is Core Tier per ARCHITECTURE.md, and Core Tier
crates carry an architectural invariant forbidding proc-macro dependencies
(`syn`) so compilation stays parallel and fast for downstream consumers
that build only the protocol-decode tree (the fuzzing infrastructure, web
client, no_std users).

`thiserror` was carried in the legacy block of `ironrdp-pdu/Cargo.toml`
under the existing `# TODO: get rid of these dependencies` comment. This
PR delivers on that TODO by replacing every `#[derive(thiserror::Error)]`
in the crate with hand-written `impl Display`, `impl core::error::Error`,
and `impl From<X>` blocks that preserve the exact `Display` output
byte-for-byte and the exact `Error::source()` semantics that the derives
generated.

Changes per error enum (13 enums across 13 files):

- `CoreDataError`, `GccError`, `NetworkDataError`, `ClusterDataError`,
  `SecurityDataError` (gcc family)
- `RdpError`, `CapabilitySetsError`, `ChannelError`, `SessionError`,
  `ServerLicenseError`, `ClientInfoError` (rdp family)
- `InputEventError`
- `McsError` (legacy submodule)

Each enum keeps its `#[derive(Debug)]`. The hand-written `Display`
preserves every `#[error("...")]` format string verbatim, including
`{0}`/`{0:?}` interpolation and the named-field interpolation in
`ChannelError::InvalidDvcTotalMessageSize`. `core::error::Error::source`
preserves the `#[from]`-driven source chain plus the named `source:`
field on `ServerLicenseError::InvalidX509Certificate`. The existing
manual `From<PduError>`, `From<RdpError> for io::Error`,
`From<McsError> for io::Error`, `From<ChannelError> for io::Error`, and
`From<LicensingErrorMessage> for ServerLicenseError` impls are preserved.

Verification:
- `cargo expand` against the post-change tree shows `Display` and `source`
  impls that are functionally equivalent to the thiserror-generated ones
  for every variant.
- `cargo check --workspace` clean.
- `cargo test --package ironrdp-pdu` 850 tests pass, 0 failures.
- `cargo clippy --package ironrdp-pdu --all-targets -- -D warnings` clean.
- `cargo xtask check fmt` clean.
- `cargo xtask check typos` clean.
- Only external consumers of these types in the workspace
  (`ironrdp-connector` for `ServerLicenseError`, `ffi` and `ironrdp-session`
  for `RdpError` / `SessionError`) treat them as opaque error types; no
  variant-level pattern matching exists outside the crate, so the
  preserved `From` and `Display` semantics fully cover downstream
  expectations.

Diff: 15 files changed, +748 / -216.

Related: Devolutions#1257.
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, LGTM! Running Copilot just to double check

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the thiserror proc-macro dependency from the Core Tier ironrdp-pdu crate by replacing all #[derive(thiserror::Error)] usages with hand-written Display, core::error::Error, and From implementations, keeping error formatting/source chains consistent with prior behavior.

Changes:

  • Removed thiserror = "2.0" from crates/ironrdp-pdu/Cargo.toml and updated lockfiles accordingly.
  • Replaced thiserror derives across the ironrdp-pdu error enums with explicit fmt::Display, core::error::Error::source, and From impls.
  • Preserved existing From<…> for io::Error conversions while keeping error messages/source semantics stable.

Reviewed changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
fuzz/Cargo.lock Removes thiserror from the fuzz lockfile dependency graph.
Cargo.lock Removes thiserror from the workspace lockfile dependency graph.
crates/ironrdp-pdu/Cargo.toml Drops the direct thiserror dependency from ironrdp-pdu.
crates/ironrdp-pdu/src/rdp/vc/mod.rs Hand-rolls ChannelError Display/Error/From impls.
crates/ironrdp-pdu/src/rdp/session_info/mod.rs Hand-rolls SessionError Display/Error/From impls.
crates/ironrdp-pdu/src/rdp/server_license/mod.rs Hand-rolls ServerLicenseError Display/Error/From impls (incl. source: field handling).
crates/ironrdp-pdu/src/rdp/mod.rs Hand-rolls RdpError Display/Error/From impls and preserves conversion behavior.
crates/ironrdp-pdu/src/rdp/client_info.rs Hand-rolls ClientInfoError Display/Error/From impls.
crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs Hand-rolls CapabilitySetsError Display/Error/From impls.
crates/ironrdp-pdu/src/mcs.rs Hand-rolls legacy McsError Display/Error/From impls.
crates/ironrdp-pdu/src/input/mod.rs Hand-rolls InputEventError Display/Error/From impls.
crates/ironrdp-pdu/src/gcc/security_data.rs Hand-rolls SecurityDataError Display/Error/From impls.
crates/ironrdp-pdu/src/gcc/network_data.rs Hand-rolls NetworkDataError Display/Error/From impls.
crates/ironrdp-pdu/src/gcc/mod.rs Hand-rolls GccError Display/Error/From impls (and nested error sources).
crates/ironrdp-pdu/src/gcc/core_data/mod.rs Hand-rolls CoreDataError Display/Error/From impls.
crates/ironrdp-pdu/src/gcc/cluster_data.rs Hand-rolls ClusterDataError Display/Error/From impls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ironrdp-pdu/src/mcs.rs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@CBenoit Benoît Cortier (CBenoit) enabled auto-merge (squash) May 13, 2026 06:35
@CBenoit Benoît Cortier (CBenoit) merged commit 14c5502 into Devolutions:master May 13, 2026
10 checks passed
@glamberson Greg Lamberson (glamberson) deleted the feat/ironrdp-pdu-drop-thiserror branch May 15, 2026 18:00
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request May 18, 2026
Brings ironrdp-rdpsnd-native into line with the workspace convention
from ARCHITECTURE.md that libraries provide concrete error types
rather than anyhow::Result.

Defines RdpsndNativeErrorKind with four variants covering the failure
modes in DecodeStream::new (UnsupportedFormat, OpusInit, AudioDevice,
StreamBuild). Display and core::error::Error impls are hand-rolled per
the post-Devolutions#1264 pattern in ironrdp-pdu.

DecodeStream::new now returns RdpsndNativeResult<Self> instead of
anyhow::Result<Self>. Foreign error sources (opus2::Error,
cpal::DefaultStreamConfigError, cpal::BuildStreamError) are attached
via Error::with_source so the cause chain remains reachable through
core::error::Error::source().

anyhow moves from [dependencies] to [dev-dependencies] for the cpal
example binary. The internal log of construction failures switches
from "{e:#}" to e.report() to preserve cause-chain output now that
anyhow's alternate-format chain printing is no longer involved.

Signed-off-by: Greg Lamberson <greg@lamco.io>
Assisted-by: Claude Code (Anthropic, Opus)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants