Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: clipboard support for web client #259

Merged
merged 6 commits into from
Nov 8, 2023
Merged

Conversation

pacmancoder
Copy link
Contributor

@pacmancoder pacmancoder commented Nov 2, 2023

This PR adds clipboard support for IronRDP-web

Features:

  • Most complete support could be achieved on chromium-based browsers

    • Formats: text/plain, text/html, text/png
    • Clipboard monitoring is implemented via monitoring loop
  • Firefox has reduced clipboard support due to browser restrictions:

    • Copy: text/plain
    • Paste: text/plain, text/html, text/png
    • Periodic clipboard monitoring is absent, copy/paste actions executed on demand, may introduce input freezes, the there is no known to me way to overcome this (synchronous onPaste event API is used; clipboardWrite action could only be performed as a response to user input)
  • Internally, the following CLIPRDR formats are handled and converted to/from their MIME counterparts:

    • CF_UNICODETEXT <=> text/plain
    • Registered(CF_HTML<HTML Format>), Registered(text/html) <=> text/html
    • CF_DIB, CF_DIBV5, Registered(PNG), Registered(image/png) <=> image/png

Closes #107

Issue: ARC-125

Copy link

github-actions bot commented Nov 2, 2023

Coverage Report 🤖 ⚙️

Past:
Total lines: 25239
Covered lines: 15675 (62.11%)

New:
Total lines: 25828
Covered lines: 16187 (62.67%)

Diff: +0.57%

[this comment will be updated automatically]

@pacmancoder pacmancoder changed the title [DRAFT] Implemented clipboard for IronrRDP-Web Implemented clipboard for IronrRDP-Web Nov 2, 2023
@pacmancoder pacmancoder marked this pull request as ready for review November 2, 2023 18:39
@pacmancoder
Copy link
Contributor Author

FYI: I was clippy lints regarding arithmetic side-effects, I'll fix that, other than that PR is ready for the review

@CBenoit CBenoit self-assigned this Nov 2, 2023
Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

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

Truly excellent work!
I appreciate you created a re-usable crate ironrdp-cliprdr-format 👍
In addition to the comments below, you may want to do a typos pass.

crates/ironrdp-cliprdr-format/src/lib.rs Outdated Show resolved Hide resolved
crates/ironrdp-cliprdr-format/src/bitmap.rs Outdated Show resolved Hide resolved
crates/ironrdp-cliprdr-format/src/bitmap.rs Outdated Show resolved Hide resolved
crates/ironrdp-cliprdr-format/src/bitmap.rs Outdated Show resolved Hide resolved
crates/ironrdp-fuzzing/src/oracles/mod.rs Show resolved Hide resolved
web-client/iron-remote-gui/src/iron-remote-gui.svelte Outdated Show resolved Hide resolved
crates/ironrdp-web/src/clipboard/mod.rs Outdated Show resolved Hide resolved
Comment on lines +181 to +183
check_invariant(self.width.abs() <= u16::MAX.into())
.ok_or_else(|| invalid_message_err!("biWidth", "width is too big"))?;
check_invariant(self.height.abs() <= u16::MAX.into())
.ok_or_else(|| invalid_message_err!("biHeight", "height is too big"))?;
Copy link
Member

@CBenoit CBenoit Nov 6, 2023

Choose a reason for hiding this comment

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

Invariants should be checked when constructing the struct (when decoding or calling new), so that remaining of the code (encoding, casting…) may rely on this and just assume this to be true without checking again

Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

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

In general, INVARIANT: comments should come where the variable is declared, either in the type definition, or above the let for local variables. The rationale is that invariants should be uphold at all times, and it’s useful to keep them in mind when analyzing the flow of the code.

// INVARIANT: end_pos < headers_cursor.len() - 1
let end_pos = /* … */;

Typically, when a local invariant is defined using INVARIANT:, the reader should not have to backtrack in order to verify it, but read forward instead.

Example in rust-analyzer code:
https://github.com/rust-lang/rust-analyzer/blob/c1c9e10f72ffd2e829d20ff1439ff49c2e121731/crates/hir-ty/src/lower.rs#L1118-L1127

When it’s not obvious why some piece of code is indeed ensuring the invariant holds, a comment may be added:
https://github.com/rust-lang/rust-analyzer/blob/c1c9e10f72ffd2e829d20ff1439ff49c2e121731/crates/hir-ty/src/lower.rs#L1143-L1144

And when explaining an assumption for doing something non obvious such as disabling a lint locally, the invariant may be referenced to without prefixing the comment with INVARIANT: (because it is already defined elsewhere)

// Given the invariant on `end_pos`, this code can’t cause integer overflow
#[allow(clippy::arithmetic_side_effects)]
{
    /* … */
}


let components = color_type.samples();
debug_assert!(components <= 4);
// INVARIANT: height * width * components <= usize::MAX
Copy link
Member

@CBenoit CBenoit Nov 6, 2023

Choose a reason for hiding this comment

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

Invariant bounds are better expressed as

Suggested change
// INVARIANT: height * width * components <= usize::MAX
// INVARIANT: height * width * components <= u16::MAX * u16::MAX * 4 < usize::MAX

_ => unreachable!("possible values are restricted by header validation and logic above"),
};

// INVARIANT: width * components <= usize::MAX
Copy link
Member

@CBenoit CBenoit Nov 6, 2023

Choose a reason for hiding this comment

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

Stronger:

Suggested change
// INVARIANT: width * components <= usize::MAX
// INVARIANT: width * components <= u16::MAX * 4 < usize::MAX


// INVARIANT: stride * height_unsigned <= usize::MAX.
//
// This should be always true, because stide can't be greater than `width_unsigned * 4`,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// This should be always true, because stide can't be greater than `width_unsigned * 4`,
// This never overflows, because stride can't be greater than `width_unsigned * 4`,

// INVARIANT: stride * height_unsigned <= usize::MAX.
//
// This should be always true, because stide can't be greater than `width_unsigned * 4`,
// and `width_unsigned * height_unsigned * 4` is guaranteed to be less or equal to `usize::MAX`.
Copy link
Member

Choose a reason for hiding this comment

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

The guarantee is even stronger

Suggested change
// and `width_unsigned * height_unsigned * 4` is guaranteed to be less or equal to `usize::MAX`.
// and `width_unsigned * height_unsigned * 4` is guaranteed to be lesser to `usize::MAX`.

let width_unsigned = u16::try_from(info.width).unwrap();
let height_unsigned = u16::try_from(info.height).unwrap();

// INVARIANT: stride * height_unsigned <= usize::MAX.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// INVARIANT: stride * height_unsigned <= usize::MAX.
// INVARIANT: stride * height_unsigned < usize::MAX.

Comment on lines 604 to 608
check_invariant(u16::try_from(info.width).is_ok()).ok_or_else(|| BitmapError::WidthTooBig)?;
check_invariant(u16::try_from(info.height).is_ok()).ok_or_else(|| BitmapError::HeightTooBig)?;

let width_unsigned = u16::try_from(info.width).unwrap();
let height_unsigned = u16::try_from(info.height).unwrap();
Copy link
Member

@CBenoit CBenoit Nov 6, 2023

Choose a reason for hiding this comment

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

It’s better style to not split the precondition check from the precondition use when possible

Suggested change
check_invariant(u16::try_from(info.width).is_ok()).ok_or_else(|| BitmapError::WidthTooBig)?;
check_invariant(u16::try_from(info.height).is_ok()).ok_or_else(|| BitmapError::HeightTooBig)?;
let width_unsigned = u16::try_from(info.width).unwrap();
let height_unsigned = u16::try_from(info.height).unwrap();
let width_unsigned: u16 = u16::try_from(info.width).map_err(|_| BitmapError::WidthTooBig)?;
let height_unsigned: u16 = u16::try_from(info.height).map_err(|_| BitmapError::HeightTooBig)?;

};

// Row is in RGBA format
// INVARIANT: width_unsigned * 4 <= usize::MAX
Copy link
Member

Choose a reason for hiding this comment

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

Stronger

Suggested change
// INVARIANT: width_unsigned * 4 <= usize::MAX
// INVARIANT: width_unsigned * 4 <= u16::MAX * 4 < usize::MAX

Comment on lines 70 to 72
// INVARIANT: end_pos < headers_cursor.len() - 1
#[allow(clippy::arithmetic_side_effects)]
Copy link
Member

@CBenoit CBenoit Nov 6, 2023

Choose a reason for hiding this comment

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

The INVARIANT: comment should come where end_pos is declared so the reader knows what is being enforced for this value before reading the logic itself.

// INVARIANT: end_pos < headers_cursor.len() - 1
let end_pos = /* … */;

Then, the invariant can be used to explain why it’s okay to do something non obvious such as disabling a lint:

// Given the invariant on `end_pos`, this code can’t cause integer overflow
#[allow(clippy::arithmetic_side_effects)]
{
    /* … */
}

let start_fragment_pos_value = format!("{:0>10}", start_fragment_pos);
let end_fragment_pos_value = format!("{:0>10}", end_fragment_pos);

// INVARIANT: buffer.len() > placeholder_pos + POS_PLACEHOLDER.len()
Copy link
Member

Choose a reason for hiding this comment

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

It’s generally better to use < and <= over > and >=

placeholder_pos + POS_PLACEHOLDER.len() < buffer.len()

Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

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

You’ll have to rebase on master to fix conflicts on iron-remote-gui.
It’s probably not much.

FYI, formatting and lints checks for TypeScript were added recently.
Here are the commands you should run when checking the code:

$ npm run check
$ npm run format
$ npm run lint

@CBenoit CBenoit enabled auto-merge (squash) November 6, 2023 17:39
Copy link
Member

@CBenoit CBenoit left a comment

Choose a reason for hiding this comment

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

You can address the remaining comments and I’ll leave you merge yourself

@CBenoit CBenoit disabled auto-merge November 6, 2023 17:41
@CBenoit CBenoit changed the title Implemented clipboard for IronrRDP-Web feat: clipboard support for web client Nov 6, 2023
@pacmancoder pacmancoder merged commit 2b50149 into master Nov 8, 2023
6 checks passed
@pacmancoder pacmancoder deleted the feat/clipboard-wasm branch November 8, 2023 11:57
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.

Initial Clipboard support (textual data)
2 participants