Skip to content

feat: add ironrdp-str crate with typed wire-aware RDP string primitives#1159

Merged
Benoît Cortier (CBenoit) merged 20 commits intomasterfrom
feat/ironrdp-str
Mar 10, 2026
Merged

feat: add ironrdp-str crate with typed wire-aware RDP string primitives#1159
Benoît Cortier (CBenoit) merged 20 commits intomasterfrom
feat/ironrdp-str

Conversation

@CBenoit
Copy link
Member

@CBenoit Benoît Cortier (CBenoit) commented Mar 10, 2026

Introduces ironrdp-str, a new crate providing lazily-validated string
types covering all RDP UTF-16LE field shapes:

  • FixedString<N>: fixed-size fields (e.g. clientName, fileName),
    zero-padded on encode, trailing-null stripped on decode.
  • PrefixedString<P, N>: length-prefixed fields with configurable
    LengthPrefix (CchU16, CchU32, CbU16) and NullTerminatorPolicy
    (NullCounted, NullUncounted, NoNull) type parameters.
  • UnframedString: externally-lengthed fields whose length comes from a
    sibling field in the containing message.
  • MultiSzString: MULTI_SZ string lists (e.g. HardwareIds in
    MS-RDPEUSB §2.2.4.2), with a u32 cch prefix counting all null
    terminators including the final sentinel.

Key design invariant: wire bytes are stored as Vec<u16> and never
eagerly converted to Rust strings, enabling single-allocation decode and
zero-cost decode→encode passthrough for proxy use cases. Conversion is
deferred to explicit to_native() / to_native_lossy() calls.

Uses bytemuck for zero-copy LE→u16 reinterpretation on little-endian
targets; falls back to per-element byte-swap on big-endian.

Introduces `ironrdp-str`, a new crate providing zero-copy, lazily-validated
string types covering all RDP UTF-16LE field shapes:

- `FixedSizeUnicodeString<N>`: fixed-size fields (e.g. clientName, fileName)
- `UnicodeStringField<P, N>`: length-prefixed fields with configurable cch/cb
  prefix and null-terminator policy (NullCounted, NullUncounted, NoNull)
- `ExternallyLengthedString`: externally-lengthed fields (length from sibling)
- `MultiSzString`: MULTI_SZ string lists (e.g. HardwareIds in MS-RDPEUSB)

Key design invariant: wire bytes are stored as `Vec<u16>` and never eagerly
converted to Rust strings, enabling single-allocation decode and zero-cost
decode-encode passthrough for proxy use cases. Conversion is deferred to
explicit `to_native()` / `to_native_lossy()` calls.

Uses `bytemuck::try_cast_slice` for zero-copy LE→u16 decode on little-endian
targets, with per-element fallback for misaligned buffers and big-endian.
@CBenoit
Copy link
Member Author

Related to #130

@github-actions
Copy link

github-actions bot commented Mar 10, 2026

Coverage Report 🤖 ⚙️

Past:
Total lines: 36581
Covered lines: 23762 (64.96%)

New:
Total lines: 37300
Covered lines: 24193 (64.86%)

Diff: -0.10%

[this comment will be updated automatically]

Copy link

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

Adds a new ironrdp-str crate providing typed, wire-aware UTF-16LE string primitives for multiple RDP field shapes, plus a dedicated testsuite module validating encoding/decoding behavior and edge cases.

Changes:

  • Introduce ironrdp-str crate with fixed-size, length-prefixed, externally-lengthed, and MULTI_SZ string types using lazy native conversion.
  • Add comprehensive tests in ironrdp-testsuite-core covering round-trips, odd-length rejection, null handling, non-BMP, and surrogate behavior.
  • Wire the new crate into the workspace/tests by updating Cargo.toml, test module registration, and Cargo.lock.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
crates/ironrdp-str/Cargo.toml New crate manifest and feature/dependency setup
crates/ironrdp-str/src/lib.rs Public API surface + UTF-16 helper functions and error type
crates/ironrdp-str/src/repr.rs Shared internal dual representation (Wire(Vec<u16>) / Native(String)) and conversion helpers
crates/ironrdp-str/src/fixed.rs Fixed-size UTF-16LE field type implementation
crates/ironrdp-str/src/prefixed.rs Length-prefixed UTF-16LE field type implementation with prefix/policy markers
crates/ironrdp-str/src/unframed.rs Externally-lengthed UTF-16LE field type implementation
crates/ironrdp-str/src/multi_sz.rs MULTI_SZ list type implementation
crates/ironrdp-str/README.md Crate documentation and design invariants
crates/ironrdp-testsuite-core/Cargo.toml Add ironrdp-str dependency to testsuite
crates/ironrdp-testsuite-core/tests/main.rs Register new str_types test module
crates/ironrdp-testsuite-core/tests/str_types/mod.rs New test module root
crates/ironrdp-testsuite-core/tests/str_types/convert.rs Tests for UTF-16 byte/unit conversion helpers
crates/ironrdp-testsuite-core/tests/str_types/fixed.rs Tests for fixed-size string behavior
crates/ironrdp-testsuite-core/tests/str_types/prefixed.rs Tests for prefixed field variants and edge cases
crates/ironrdp-testsuite-core/tests/str_types/unframed.rs Tests for externally-lengthed strings
crates/ironrdp-testsuite-core/tests/str_types/multi_sz.rs Tests for MULTI_SZ encoding/decoding and builders
Cargo.lock Lockfile updates for new crate

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

You can also share your feedback on Copilot code review. Take the survey.

@CBenoit
Copy link
Member Author

Copilot open a new pull request to apply changes based on the comments in this thread. Also fix all the clippy warnings.

Copy link

Copilot AI commented Mar 10, 2026

Benoît Cortier (@CBenoit) I've opened a new pull request, #1160, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 3 commits March 10, 2026 19:01
…ts_flat panic, clippy (#1160)

Addresses all review comments on the `ironrdp-str` crate: arithmetic
overflow on 32-bit targets in decode/encode paths, a panic on empty
input in `from_wire_units_flat`, unnecessary allocation in `TryFrom`
impls, unsafe `as usize` casts, and several clippy lints.

## Correctness fixes

- **`from_wire_units_flat` empty-input panic** — `if let Some(&u) =
last() && u != 0` silently passed for empty `Vec`, then hit `len() - 1`
underflow. Replaced with `if last() != Some(&0)` which rejects empty
input naturally.
- **`checked_mul(2)` in decode** (`prefixed`, `multi_sz`, `unframed`) —
a large wire-supplied length on 32-bit/wasm32 wraps to a small value,
making `ensure_size!` pass and causing under-read + cursor desync. Each
decode path now returns `InvalidField` on overflow.
- **`checked_mul(2)` in encode** (`prefixed`) — byte-count prefix
`counted_cch * 2` can overflow; now returns `InvalidField`.
- **`saturating_mul`/`saturating_add` in `size()`** (`multi_sz`) —
overflow wrapped to a small value would let `ensure_size!` succeed with
an undersized buffer; saturation to `usize::MAX` forces the check to
fail instead.

## Allocation / API fixes

- **`TryFrom` impls use `into_native()`** (`fixed`, `prefixed`,
`unframed`) — `to_native().map(Cow::into_owned)` always allocates;
`into_native()` is zero-cost for the `Native` variant.

```rust
// Before — always allocates even for String-constructed values
fn try_from(s: FixedSizeUnicodeString<N>) -> Result<Self, Self::Error> {
    s.0.to_native().map(Cow::into_owned)
}

// After — zero-cost for native-constructed values
fn try_from(s: FixedSizeUnicodeString<N>) -> Result<Self, Self::Error> {
    s.0.into_native()
}
```

## Clippy

- `as usize` on `read_u32()` → `cast_length!()` (`prefixed`
`CchU32`/`CbU32`, `multi_sz`)
- `std::error::Error` → `core::error::Error` (`lib.rs`, `fixed.rs`)
- `actual <= WCHAR_COUNT - 1` → `actual < WCHAR_COUNT` (`fixed.rs`)
- `#[allow(...)]` → `#[expect(...)]` (`multi_sz.rs`)

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: CBenoit <3809077+CBenoit@users.noreply.github.com>
Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

Choose a reason for hiding this comment

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

Impressive work, looking good! Copilot made some good comments, but other than that, I have nothing to add 🥇

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

@CBenoit
Copy link
Member Author

Benoît Cortier (CBenoit) commented Mar 10, 2026

Vladyslav Nikonov (@vnikonov-devolutions) Thanks! I made a few extra Copilot rounds to be safe! (Big diff + subtle logic)

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

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

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


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

You can also share your feedback on Copilot code review. Take the survey.

Copy link

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

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

@CBenoit Benoît Cortier (CBenoit) enabled auto-merge (squash) March 10, 2026 15:11
@CBenoit Benoît Cortier (CBenoit) merged commit a67b1aa into master Mar 10, 2026
10 checks passed
@CBenoit Benoît Cortier (CBenoit) deleted the feat/ironrdp-str branch March 10, 2026 15:27
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