refactor(ironrdp-web): implement JsValue conversion from ClipboardTransaction and ClipboardContent

This commit is contained in:
Alex Yusiuk 2025-04-18 14:07:35 +00:00
parent ec1832bba0
commit e7c4bcd416
8 changed files with 76 additions and 42 deletions

View file

@ -1,13 +1,15 @@
use crate::IronError;
use wasm_bindgen::JsValue;
use web_sys::js_sys;
pub trait ClipboardTransaction {
type ClipboardContent: ClipboardContent;
type Error: IronError;
fn init() -> Self;
fn add_content(&mut self, content: Self::ClipboardContent);
fn is_empty(&self) -> bool;
fn contents(&self) -> js_sys::Array;
fn contents(&self) -> Result<js_sys::Array, Self::Error>;
}
pub trait ClipboardContent {

View file

@ -5,7 +5,7 @@ pub trait IronError {
fn kind(&self) -> IronErrorKind;
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
#[wasm_bindgen]
pub enum IronErrorKind {
/// Catch-all error kind

View file

@ -331,8 +331,8 @@ macro_rules! export {
self.0.is_empty()
}
pub fn content(&self) -> js_sys::Array {
iron_remote_desktop::ClipboardTransaction::contents(&self.0)
pub fn content(&self) -> Result<js_sys::Array, IronError> {
iron_remote_desktop::ClipboardTransaction::contents(&self.0).map_err(IronError)
}
}

View file

@ -13,8 +13,7 @@
mod transaction;
use std::collections::HashMap;
use anyhow::anyhow;
use futures_channel::mpsc;
use iron_remote_desktop::{ClipboardContent as _, ClipboardTransaction as _};
use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend};
@ -25,13 +24,14 @@ use ironrdp::cliprdr::pdu::{
use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5};
use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html};
use ironrdp_core::{impl_as_any, IntoOwned};
use std::collections::HashMap;
use transaction::ClipboardContentValue;
use wasm_bindgen::prelude::*;
use crate::session::RdpInputEvent;
#[rustfmt::skip]
pub(crate) use transaction::{RdpClipboardTransaction, RdpClipboardContent};
pub(crate) use transaction::{ClipboardTransaction, ClipboardContent};
const MIME_TEXT: &str = "text/plain";
const MIME_HTML: &str = "text/html";
@ -104,7 +104,7 @@ impl WasmClipboardMessageProxy {
/// Messages sent by the JS code or CLIPRDR to the backend implementation.
#[derive(Debug)]
pub(crate) enum WasmClipboardBackendMessage {
LocalClipboardChanged(RdpClipboardTransaction),
LocalClipboardChanged(ClipboardTransaction),
RemoteDataRequest(ClipboardFormatId),
RemoteClipboardChanged(Vec<ClipboardFormat>),
@ -117,8 +117,8 @@ pub(crate) enum WasmClipboardBackendMessage {
/// Clipboard backend implementation for web. This object should be created once per session and
/// kept alive until session is terminated.
pub(crate) struct WasmClipboard {
local_clipboard: Option<RdpClipboardTransaction>,
remote_clipboard: RdpClipboardTransaction,
local_clipboard: Option<ClipboardTransaction>,
remote_clipboard: ClipboardTransaction,
remote_mapping: HashMap<ClipboardFormatId, String>,
remote_formats_to_read: Vec<ClipboardFormatId>,
@ -138,7 +138,7 @@ impl WasmClipboard {
pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self {
Self {
local_clipboard: None,
remote_clipboard: RdpClipboardTransaction::init(),
remote_clipboard: ClipboardTransaction::init(),
proxy: message_proxy,
js_callbacks,
@ -156,7 +156,7 @@ impl WasmClipboard {
fn handle_local_clipboard_changed(
&mut self,
transaction: RdpClipboardTransaction,
transaction: ClipboardTransaction,
) -> anyhow::Result<Vec<ClipboardFormat>> {
let mut formats = Vec::new();
transaction.contents().iter().for_each(|content| {
@ -372,21 +372,21 @@ impl WasmClipboard {
let content = match pending_format {
ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_TEXT, &text)),
Ok(text) => Some(ClipboardContent::new_text(MIME_TEXT, &text)),
Err(err) => {
error!("CF_UNICODETEXT decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) {
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)),
Err(err) => {
warn!("DIB decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) {
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)),
Err(err) => {
warn!("DIBv5 decode error: {}", err);
None
@ -396,21 +396,21 @@ impl WasmClipboard {
let format_name = self.remote_mapping.get(&registered).map(|s| s.as_str());
match format_name {
Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, text)),
Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, text)),
Err(err) => {
warn!("CF_HTML decode error: {}", err);
None
}
},
Some(FORMAT_MIME_HTML_NAME) => match response.to_string() {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, &text)),
Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, &text)),
Err(err) => {
warn!("text/html decode error: {}", err);
None
}
},
Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => {
Some(RdpClipboardContent::new_binary(MIME_PNG, response.data()))
Some(ClipboardContent::new_binary(MIME_PNG, response.data()))
}
_ => {
// Not supported format
@ -438,7 +438,10 @@ impl WasmClipboard {
// Set clipboard when all formats were read
self.js_callbacks
.on_remote_clipboard_changed
.call1(&JsValue::NULL, &JsValue::from(transaction))
.call1(
&JsValue::NULL,
&transaction.to_js_value().map_err(|e| anyhow!("{:?}", e))?,
)
.expect("failed to call JS callback");
}
@ -507,7 +510,7 @@ impl WasmClipboard {
} else {
// If no initial clipboard callback was set, send empty format list instead
return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(
RdpClipboardTransaction::init(),
ClipboardTransaction::init(),
));
}
}

View file

@ -1,25 +1,42 @@
use wasm_bindgen::prelude::wasm_bindgen;
use crate::error::IronError;
use anyhow::anyhow;
use js_sys::{Object, Reflect};
use wasm_bindgen::JsValue;
/// Object which represents complete clipboard transaction with multiple MIME types.
#[wasm_bindgen]
#[derive(Debug, Default, Clone)]
pub(crate) struct RdpClipboardTransaction {
contents: Vec<RdpClipboardContent>,
pub(crate) struct ClipboardTransaction {
contents: Vec<ClipboardContent>,
}
impl RdpClipboardTransaction {
pub(crate) fn contents(&self) -> &[RdpClipboardContent] {
impl ClipboardTransaction {
pub(crate) fn contents(&self) -> &[ClipboardContent] {
&self.contents
}
pub(crate) fn clear(&mut self) {
self.contents.clear();
}
pub(crate) fn to_js_value(&self) -> Result<JsValue, IronError> {
let js_object = Object::new();
Reflect::set(
&js_object,
&JsValue::from("contents"),
&iron_remote_desktop::ClipboardTransaction::contents(self)
.map_err(|e| anyhow!("{:?}", e))?
.into(),
)
.map_err(|e| anyhow!("JS error: {:?}", e))?;
Ok(js_object.into())
}
}
impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction {
type ClipboardContent = RdpClipboardContent;
impl iron_remote_desktop::ClipboardTransaction for ClipboardTransaction {
type ClipboardContent = ClipboardContent;
type Error = IronError;
fn init() -> Self {
Self { contents: Vec::new() }
@ -33,17 +50,18 @@ impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction {
self.contents.is_empty()
}
fn contents(&self) -> js_sys::Array {
js_sys::Array::from_iter(
fn contents(&self) -> Result<js_sys::Array, Self::Error> {
Ok(js_sys::Array::from_iter(
self.contents
.iter()
.map(|content: &RdpClipboardContent| JsValue::from(content.clone())),
)
.map(|content| content.to_js_value())
.collect::<Result<Vec<_>, Self::Error>>()?,
))
}
}
impl FromIterator<RdpClipboardContent> for RdpClipboardTransaction {
fn from_iter<T: IntoIterator<Item = RdpClipboardContent>>(iter: T) -> Self {
impl FromIterator<ClipboardContent> for ClipboardTransaction {
fn from_iter<T: IntoIterator<Item = ClipboardContent>>(iter: T) -> Self {
Self {
contents: iter.into_iter().collect(),
}
@ -66,14 +84,13 @@ impl ClipboardContentValue {
}
/// Object which represents single clipboard format represented standard MIME type.
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub(crate) struct RdpClipboardContent {
pub(crate) struct ClipboardContent {
mime_type: String,
value: ClipboardContentValue,
}
impl RdpClipboardContent {
impl ClipboardContent {
pub(crate) fn mime_type(&self) -> &str {
&self.mime_type
}
@ -81,9 +98,20 @@ impl RdpClipboardContent {
pub(crate) fn value(&self) -> &ClipboardContentValue {
&self.value
}
fn to_js_value(&self) -> Result<JsValue, IronError> {
let js_object = Object::new();
Reflect::set(&js_object, &JsValue::from("mime_type"), &JsValue::from(&self.mime_type))
.map_err(|e| anyhow!("JS error: {:?}", e))?;
Reflect::set(&js_object, &JsValue::from("value"), &self.value.value())
.map_err(|e| anyhow!("JS error: {:?}", e))?;
Ok(js_object.into())
}
}
impl iron_remote_desktop::ClipboardContent for RdpClipboardContent {
impl iron_remote_desktop::ClipboardContent for ClipboardContent {
fn new_text(mime_type: &str, text: &str) -> Self {
Self {
mime_type: mime_type.into(),

View file

@ -1,6 +1,7 @@
use iron_remote_desktop::IronErrorKind;
use ironrdp::connector::{self, sspi, ConnectorErrorKind};
#[derive(Debug)]
pub(crate) struct IronError {
kind: IronErrorKind,
source: anyhow::Error,

View file

@ -30,8 +30,8 @@ impl RemoteDesktopApi for Api {
type SessionTerminationInfo = session::SessionTerminationInfo;
type DeviceEvent = input::DeviceEvent;
type InputTransaction = input::InputTransaction;
type ClipboardTransaction = clipboard::RdpClipboardTransaction;
type ClipboardContent = clipboard::RdpClipboardContent;
type ClipboardTransaction = clipboard::ClipboardTransaction;
type ClipboardContent = clipboard::ClipboardContent;
type Error = error::IronError;
}

View file

@ -38,7 +38,7 @@ use web_sys::HtmlCanvasElement;
use crate::canvas::Canvas;
use crate::clipboard;
use crate::clipboard::{RdpClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::clipboard::{ClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::IronError;
use crate::image::extract_partial_image;
use crate::input::InputTransaction;
@ -447,7 +447,7 @@ impl Session {
impl iron_remote_desktop::Session for Session {
type SessionTerminationInfo = SessionTerminationInfo;
type InputTransaction = InputTransaction;
type ClipboardTransaction = RdpClipboardTransaction;
type ClipboardTransaction = ClipboardTransaction;
type Error = IronError;
async fn run(&self) -> Result<Self::SessionTerminationInfo, Self::Error> {