From 84aa89891cdb278e14b6a8022fd8890be3afae86 Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Fri, 27 Mar 2026 21:14:56 +0700 Subject: [PATCH 1/7] =?UTF-8?q?tdmanh1=2027/03/2026,=20b=E1=BA=ADt=20c?= =?UTF-8?q?=E1=BB=9D=20enable=5Fserver=5Fpointer=20l=C3=AAn=20=C4=91?= =?UTF-8?q?=E1=BB=83=20khi=20render=20tr=C3=AAn=20tr=C3=ACnh=20duy?= =?UTF-8?q?=E1=BB=87t=20mong=20mu=E1=BB=91n=20render=20c=E1=BA=A3=20con=20?= =?UTF-8?q?tr=E1=BB=8F=20chu=E1=BB=99t=20cho=20ti=E1=BB=87n=20thao=20t?= =?UTF-8?q?=C3=A1c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ironrdp-web/src/session.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 13da806174..40e0df378c 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -911,11 +911,13 @@ fn build_config( client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), platform: ironrdp::pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED, compression_type: None, - enable_server_pointer: false, + // tdmanh1 27/03/2026, bật cờ này lên để khi render trên trình duyệt mong muốn render cả con trỏ chuột cho tiện thao tác + enable_server_pointer: true, autologon: false, enable_audio_playback: false, request_data: None, - pointer_software_rendering: false, + // tdmanh1 27/03/2026, bật cờ này lên để khi render trên trình duyệt mong muốn render cả con trỏ chuột cho tiện thao tác + pointer_software_rendering: true, multitransport_flags: None, performance_flags: PerformanceFlags::default(), desktop_scale_factor: 0, From c00b59f36a94e9e42337df25415ad4d89e6f075c Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Sat, 28 Mar 2026 07:45:43 +0700 Subject: [PATCH 2/7] =?UTF-8?q?expose=20th=C3=AAm=201=20s=E1=BB=91=20optio?= =?UTF-8?q?n=20cho=20wasm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/iron-remote-desktop/src/lib.rs | 32 +++++++++++++++++++++++ crates/iron-remote-desktop/src/session.rs | 13 +++++++++ 2 files changed, 45 insertions(+) diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs index a4d309bd37..b29e2d1447 100644 --- a/crates/iron-remote-desktop/src/lib.rs +++ b/crates/iron-remote-desktop/src/lib.rs @@ -243,6 +243,38 @@ macro_rules! make_bridge { Self($crate::SessionBuilder::render_canvas(&self.0, canvas)) } + // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + #[wasm_bindgen(js_name = setEnableServerPointer)] + pub fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { + Self($crate::SessionBuilder::set_enable_server_pointer( + &self.0, + enable_server_pointer, + )) + } + + #[wasm_bindgen(js_name = setPointerSoftwareRendering)] + pub fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self { + Self($crate::SessionBuilder::set_pointer_software_rendering( + &self.0, + pointer_software_rendering, + )) + } + + #[wasm_bindgen(js_name = setEnableAudioPlayback)] + pub fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self { + Self($crate::SessionBuilder::set_enable_audio_playback( + &self.0, + enable_audio_playback, + )) + } + #[wasm_bindgen(js_name = setDesktopScaleFactor)] + pub fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self { + Self($crate::SessionBuilder::set_desktop_scale_factor( + &self.0, + desktop_scale_factor, + )) + } + #[wasm_bindgen(js_name = setCursorStyleCallback)] pub fn set_cursor_style_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self { Self($crate::SessionBuilder::set_cursor_style_callback( diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs index db347f0e70..227fe24025 100644 --- a/crates/iron-remote-desktop/src/session.rs +++ b/crates/iron-remote-desktop/src/session.rs @@ -36,6 +36,19 @@ pub trait SessionBuilder { #[must_use] fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self; + // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + #[must_use] + fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self; + + #[must_use] + fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self; + + #[must_use] + fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self; + + #[must_use] + fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self; + #[must_use] fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self; From a31f5071df6b9d7b4c2b67068ce0319a00f85e50 Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Sat, 28 Mar 2026 07:45:51 +0700 Subject: [PATCH 3/7] =?UTF-8?q?expose=20th=C3=AAm=201=20s=E1=BB=91=20optio?= =?UTF-8?q?n=20cho=20wasm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ironrdp-web/src/session.rs | 79 +++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 40e0df378c..98d2bf59db 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -70,6 +70,11 @@ struct SessionBuilderInner { use_display_control: bool, enable_credssp: bool, outbound_message_size_limit: Option, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer: bool, + pointer_software_rendering: bool, + enable_audio_playback: bool, + desktop_scale_factor: u32, } impl Default for SessionBuilderInner { @@ -98,6 +103,12 @@ impl Default for SessionBuilderInner { use_display_control: false, enable_credssp: true, outbound_message_size_limit: None, + + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer: false, + pointer_software_rendering: false, + enable_audio_playback: false, + desktop_scale_factor: 0, } } } @@ -162,6 +173,31 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { self.clone() } + // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + /// Optional + fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { + self.0.borrow_mut().enable_server_pointer = enable_server_pointer; + self.clone() + } + + /// Optional + fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self { + self.0.borrow_mut().pointer_software_rendering = pointer_software_rendering; + self.clone() + } + + /// Optional + fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self { + self.0.borrow_mut().enable_audio_playback = enable_audio_playback; + self.clone() + } + + /// Optional + fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self { + self.0.borrow_mut().desktop_scale_factor = desktop_scale_factor; + self.clone() + } + /// Required. /// /// # Callback signature: @@ -247,6 +283,11 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { remote_clipboard_changed_callback, force_clipboard_update_callback, outbound_message_size_limit, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer, + pointer_software_rendering, + enable_audio_playback, + desktop_scale_factor, ); { @@ -262,7 +303,11 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { kdc_proxy_url = inner.kdc_proxy_url.clone(); client_name = inner.client_name.clone(); desktop_size = inner.desktop_size; - + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer = inner.enable_server_pointer; + pointer_software_rendering = inner.pointer_software_rendering; + enable_audio_playback = inner.enable_audio_playback; + desktop_scale_factor = inner.desktop_scale_factor; render_canvas = inner.render_canvas.clone().context("render_canvas missing")?; set_cursor_style_callback = inner @@ -280,7 +325,18 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { info!("Connect to RDP host"); - let mut config = build_config(username, password, server_domain, client_name, desktop_size); + let mut config = build_config( + username, + password, + server_domain, + client_name, + desktop_size, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer, + pointer_software_rendering, + enable_audio_playback, + desktop_scale_factor, + ); let enable_credssp = self.0.borrow().enable_credssp; config.enable_credssp = enable_credssp; @@ -875,6 +931,11 @@ fn build_config( domain: Option, client_name: String, desktop_size: DesktopSize, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer: bool, + pointer_software_rendering: bool, + enable_audio_playback: bool, + desktop_scale_factor: u32, ) -> connector::Config { connector::Config { credentials: Credentials::UsernamePassword { username, password }, @@ -911,16 +972,18 @@ fn build_config( client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), platform: ironrdp::pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED, compression_type: None, - // tdmanh1 27/03/2026, bật cờ này lên để khi render trên trình duyệt mong muốn render cả con trỏ chuột cho tiện thao tác - enable_server_pointer: true, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_server_pointer: enable_server_pointer, autologon: false, - enable_audio_playback: false, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + enable_audio_playback: enable_audio_playback, request_data: None, - // tdmanh1 27/03/2026, bật cờ này lên để khi render trên trình duyệt mong muốn render cả con trỏ chuột cho tiện thao tác - pointer_software_rendering: true, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + pointer_software_rendering: pointer_software_rendering, multitransport_flags: None, performance_flags: PerformanceFlags::default(), - desktop_scale_factor: 0, + // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + desktop_scale_factor: desktop_scale_factor, hardware_id: None, license_cache: None, timezone_info: TimezoneInfo::default(), From f666a2b348e2cf3b05d26323c6a7aad9ba31b0ea Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Mon, 24 Aug 2026 06:23:43 +0700 Subject: [PATCH 4/7] change comment to english --- crates/iron-remote-desktop/src/lib.rs | 2 +- crates/iron-remote-desktop/src/session.rs | 2 +- crates/ironrdp-web/src/session.rs | 20 +++++++++----------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs index cef8b6be7d..7f74086ae4 100644 --- a/crates/iron-remote-desktop/src/lib.rs +++ b/crates/iron-remote-desktop/src/lib.rs @@ -243,7 +243,7 @@ macro_rules! make_bridge { Self($crate::SessionBuilder::render_canvas(&self.0, canvas)) } - // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + // tdmanh1 24/08/2026 expose more function for wasm app #[wasm_bindgen(js_name = setEnableServerPointer)] pub fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { Self($crate::SessionBuilder::set_enable_server_pointer( diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs index 227fe24025..9a0e4e2ddc 100644 --- a/crates/iron-remote-desktop/src/session.rs +++ b/crates/iron-remote-desktop/src/session.rs @@ -36,7 +36,7 @@ pub trait SessionBuilder { #[must_use] fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self; - // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + // tdmanh1 24/08/2026 expose more function for wasm app #[must_use] fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self; diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 1ca20f800c..0d904eff48 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -92,7 +92,7 @@ struct SessionBuilderInner { use_display_control: bool, enable_credssp: bool, outbound_message_size_limit: Option, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer: bool, pointer_software_rendering: bool, enable_audio_playback: bool, @@ -140,7 +140,7 @@ impl Default for SessionBuilderInner { enable_credssp: true, outbound_message_size_limit: None, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer: false, pointer_software_rendering: false, enable_audio_playback: false, @@ -209,7 +209,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { self.clone() } - // tdmanh1 28/03/2026 expose thêm 1 số hàm để set các thông tin từ web + // tdmanh1 24/08/2026 expose more function for wasm app /// Optional fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { self.0.borrow_mut().enable_server_pointer = enable_server_pointer; @@ -393,7 +393,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { printer_device_id, printer_driver_name, outbound_message_size_limit, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer, pointer_software_rendering, enable_audio_playback, @@ -414,7 +414,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { kdc_proxy_url = inner.kdc_proxy_url.clone(); client_name = inner.client_name.clone(); desktop_size = inner.desktop_size; - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer = inner.enable_server_pointer; pointer_software_rendering = inner.pointer_software_rendering; enable_audio_playback = inner.enable_audio_playback; @@ -458,7 +458,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { server_domain, client_name.clone(), desktop_size, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer, pointer_software_rendering, enable_audio_playback, @@ -1496,7 +1496,7 @@ fn build_config( domain: Option, client_name: String, desktop_size: DesktopSize, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer: bool, pointer_software_rendering: bool, enable_audio_playback: bool, @@ -1540,18 +1540,16 @@ fn build_config( client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), platform: ironrdp::pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED, compression_type: None, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer: enable_server_pointer, autologon: false, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập enable_audio_playback: enable_audio_playback, enable_audio_capture: false, request_data: None, - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập pointer_software_rendering: pointer_software_rendering, multitransport_flags: None, performance_flags: PerformanceFlags::default(), - // tdmanh1 28/03/2026 expose các option này cho bên ngoài thiết lập + // tdmanh1 24/08/2026 expose more flag for wasm app desktop_scale_factor: desktop_scale_factor, hardware_id: None, license_cache: None, From 2a2919627046b4ba34f3b15a9f862cc03861c107 Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Sat, 26 Sep 2026 16:11:07 +0700 Subject: [PATCH 5/7] change flag extension follow iron-remote-desktop#design-philosophy --- crates/iron-remote-desktop/src/lib.rs | 32 ------------ crates/iron-remote-desktop/src/session.rs | 13 ----- crates/ironrdp-web/src/session.rs | 62 +++++++++-------------- 3 files changed, 25 insertions(+), 82 deletions(-) diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs index 7f74086ae4..d5fb282963 100644 --- a/crates/iron-remote-desktop/src/lib.rs +++ b/crates/iron-remote-desktop/src/lib.rs @@ -243,38 +243,6 @@ macro_rules! make_bridge { Self($crate::SessionBuilder::render_canvas(&self.0, canvas)) } - // tdmanh1 24/08/2026 expose more function for wasm app - #[wasm_bindgen(js_name = setEnableServerPointer)] - pub fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { - Self($crate::SessionBuilder::set_enable_server_pointer( - &self.0, - enable_server_pointer, - )) - } - - #[wasm_bindgen(js_name = setPointerSoftwareRendering)] - pub fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self { - Self($crate::SessionBuilder::set_pointer_software_rendering( - &self.0, - pointer_software_rendering, - )) - } - - #[wasm_bindgen(js_name = setEnableAudioPlayback)] - pub fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self { - Self($crate::SessionBuilder::set_enable_audio_playback( - &self.0, - enable_audio_playback, - )) - } - #[wasm_bindgen(js_name = setDesktopScaleFactor)] - pub fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self { - Self($crate::SessionBuilder::set_desktop_scale_factor( - &self.0, - desktop_scale_factor, - )) - } - #[wasm_bindgen(js_name = setCursorStyleCallback)] pub fn set_cursor_style_callback(&self, callback: $crate::internal::web_sys::js_sys::Function) -> Self { Self($crate::SessionBuilder::set_cursor_style_callback( diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs index 9a0e4e2ddc..db347f0e70 100644 --- a/crates/iron-remote-desktop/src/session.rs +++ b/crates/iron-remote-desktop/src/session.rs @@ -36,19 +36,6 @@ pub trait SessionBuilder { #[must_use] fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self; - // tdmanh1 24/08/2026 expose more function for wasm app - #[must_use] - fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self; - - #[must_use] - fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self; - - #[must_use] - fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self; - - #[must_use] - fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self; - #[must_use] fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self; diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 0d904eff48..e33a4ef1b9 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -209,31 +209,6 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { self.clone() } - // tdmanh1 24/08/2026 expose more function for wasm app - /// Optional - fn set_enable_server_pointer(&self, enable_server_pointer: bool) -> Self { - self.0.borrow_mut().enable_server_pointer = enable_server_pointer; - self.clone() - } - - /// Optional - fn set_pointer_software_rendering(&self, pointer_software_rendering: bool) -> Self { - self.0.borrow_mut().pointer_software_rendering = pointer_software_rendering; - self.clone() - } - - /// Optional - fn set_enable_audio_playback(&self, enable_audio_playback: bool) -> Self { - self.0.borrow_mut().enable_audio_playback = enable_audio_playback; - self.clone() - } - - /// Optional - fn set_desktop_scale_factor(&self, desktop_scale_factor: u32) -> Self { - self.0.borrow_mut().desktop_scale_factor = desktop_scale_factor; - self.clone() - } - /// Required. /// /// # Callback signature: @@ -297,6 +272,21 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { }; self.0.borrow_mut().outbound_message_size_limit = if limit > 0 { Some(limit) } else { None }; }; + // tdmanh1 24/08/2026 expose more flag for wasm app + |enable_server_pointer: bool| { self.0.borrow_mut().enable_server_pointer = enable_server_pointer }; + |pointer_software_rendering: bool| { self.0.borrow_mut().pointer_software_rendering = pointer_software_rendering }; + |enable_audio_playback: bool| { self.0.borrow_mut().enable_audio_playback = enable_audio_playback }; + |desktop_scale_factor: f64| { + // Zero means the server picks a factor matching the requested desktop size. + let scale_factor = if desktop_scale_factor >= 0.0 && desktop_scale_factor <= f64::from(u32::MAX) { + #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { desktop_scale_factor as u32 } + } else { + warn!(desktop_scale_factor, "Invalid desktop scale factor; falling back to default"); + 0 + }; + self.0.borrow_mut().desktop_scale_factor = scale_factor; + }; // File transfer callbacks - protocol-specific, routed through extension() // rather than dedicated trait methods to keep iron-remote-desktop protocol-agnostic. |files_available_callback: JsValue| { @@ -453,16 +443,16 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { info!("Connect to RDP host"); let mut config = build_config( - username, - password, - server_domain, - client_name.clone(), + username, + password, + server_domain, + client_name.clone(), desktop_size, // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer, pointer_software_rendering, enable_audio_playback, - desktop_scale_factor + desktop_scale_factor, ); let enable_credssp = self.0.borrow().enable_credssp; @@ -1490,13 +1480,13 @@ fn parse_file_metadata_array(files: JsValue) -> Result, IronEr Ok(file_list) } +#[expect(clippy::too_many_arguments)] fn build_config( username: String, password: String, domain: Option, client_name: String, desktop_size: DesktopSize, - // tdmanh1 24/08/2026 expose more flag for wasm app enable_server_pointer: bool, pointer_software_rendering: bool, enable_audio_playback: bool, @@ -1540,17 +1530,15 @@ fn build_config( client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), platform: ironrdp::pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED, compression_type: None, - // tdmanh1 24/08/2026 expose more flag for wasm app - enable_server_pointer: enable_server_pointer, + enable_server_pointer, autologon: false, - enable_audio_playback: enable_audio_playback, + enable_audio_playback, enable_audio_capture: false, request_data: None, - pointer_software_rendering: pointer_software_rendering, + pointer_software_rendering, multitransport_flags: None, performance_flags: PerformanceFlags::default(), - // tdmanh1 24/08/2026 expose more flag for wasm app - desktop_scale_factor: desktop_scale_factor, + desktop_scale_factor, hardware_id: None, license_cache: None, timezone_info: TimezoneInfo::default(), From aa87f5d27e6b6f1c45a1a01500e39b96f0eff7b1 Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Sat, 26 Sep 2026 17:11:28 +0700 Subject: [PATCH 6/7] add more flag for rdp --- crates/ironrdp-web/src/session.rs | 93 +++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 75570502d8..6da6d486de 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -25,7 +25,7 @@ use ironrdp::dvc::DrdynvcClient; use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; use ironrdp::pdu::rdp::capability_sets::{BitmapCodecs, client_codecs_capabilities}; -use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use ironrdp::pdu::rdp::client_info::{OptionalSystemTime, PerformanceFlags, TimezoneInfo}; use ironrdp::rdpdr::Rdpdr; use ironrdp::rdpdr::pdu::efs::{DEFAULT_PRINTER_DRIVER_NAME, MICROSOFT_PRINT_TO_PDF_DRIVER_NAME}; use ironrdp::rdpsnd::client::{NoopRdpsndBackend, Rdpsnd}; @@ -95,8 +95,9 @@ struct SessionBuilderInner { legacy_graphics: bool, outbound_message_size_limit: Option, pointer_software_rendering: bool, - enable_audio_playback: bool, desktop_scale_factor: u32, + performance_flags: PerformanceFlags, + timezone_info: TimezoneInfo, } impl Default for SessionBuilderInner { @@ -142,8 +143,9 @@ impl Default for SessionBuilderInner { legacy_graphics: false, outbound_message_size_limit: None, pointer_software_rendering: false, - enable_audio_playback: false, desktop_scale_factor: 0, + performance_flags: PerformanceFlags::default(), + timezone_info: TimezoneInfo::default(), } } } @@ -274,7 +276,6 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { self.0.borrow_mut().outbound_message_size_limit = if limit > 0 { Some(limit) } else { None }; }; |pointer_software_rendering: bool| { self.0.borrow_mut().pointer_software_rendering = pointer_software_rendering }; - |enable_audio_playback: bool| { self.0.borrow_mut().enable_audio_playback = enable_audio_playback }; |desktop_scale_factor: f64| { // Zero means the server picks a factor matching the requested desktop size. let scale_factor = if desktop_scale_factor >= 0.0 && desktop_scale_factor <= f64::from(u32::MAX) { @@ -286,6 +287,30 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { }; self.0.borrow_mut().desktop_scale_factor = scale_factor; }; + // Bitmask of `PerformanceFlags`. Disabling wallpaper, theming, font smoothing and + // desktop composition cuts both bandwidth and CPU, which is what a browser client + // over a slow link needs. + |performance_flags: f64| { + let bits = if performance_flags >= 0.0 && performance_flags <= f64::from(u32::MAX) { + #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { performance_flags as u32 } + } else { + warn!(performance_flags, "Invalid performance_flags; falling back to default"); + 0 + }; + self.0.borrow_mut().performance_flags = PerformanceFlags::from_bits_truncate(bits); + }; + // Timezone of the browser user, so clocks shown by the remote session match local time. + // DST transition dates are left unset: the browser only exposes the current UTC offset. + |timezone_info: JsValue| { + match parse_timezone_info(timezone_info) { + Ok(info) => self.0.borrow_mut().timezone_info = info, + Err(error) => { + let reason = iron_remote_desktop::IronError::backtrace(&error); + warn!(%reason, "Invalid timezone_info; falling back to default"); + } + } + }; // File transfer callbacks - protocol-specific, routed through extension() // rather than dedicated trait methods to keep iron-remote-desktop protocol-agnostic. |files_available_callback: JsValue| { @@ -383,8 +408,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { printer_driver_name, outbound_message_size_limit, pointer_software_rendering, - enable_audio_playback, desktop_scale_factor, + performance_flags, + timezone_info, legacy_graphics, ); @@ -403,8 +429,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { client_name = inner.client_name.clone(); desktop_size = inner.desktop_size; pointer_software_rendering = inner.pointer_software_rendering; - enable_audio_playback = inner.enable_audio_playback; desktop_scale_factor = inner.desktop_scale_factor; + performance_flags = inner.performance_flags; + timezone_info = inner.timezone_info.clone(); render_canvas = inner.render_canvas.clone().context("render_canvas missing")?; set_cursor_style_callback = inner @@ -446,8 +473,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { client_name.clone(), desktop_size, pointer_software_rendering, - enable_audio_playback, desktop_scale_factor, + performance_flags, + timezone_info, legacy_graphics, ); @@ -1348,6 +1376,17 @@ fn get_u64(obj: &js_sys::Object, key: &str) -> Result { Ok(f as u64) } +fn get_str(obj: &js_sys::Object, key: &str) -> Result { + let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) + .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; + if val.is_undefined() || val.is_null() { + return Ok(String::new()); + } + val.as_string() + .with_context(|| format!("invalid type for property `{key}`")) + .map_err(Into::into) +} + fn get_bool(obj: &js_sys::Object, key: &str) -> Result { let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; @@ -1368,6 +1407,35 @@ fn get_u32_opt(obj: &js_sys::Object, key: &str) -> Result, IronError Ok(Some(f64_to_u32_saturating_cast(f))) } +/// Builds a `TimezoneInfo` from the browser timezone reported through the extension. +/// +/// `bias` follows the MS-RDPBCGR convention and matches `Date::getTimezoneOffset()`: +/// a client ahead of UTC sends a negative value. DST transition dates stay unset +/// because the browser only exposes the offset in effect right now. +fn parse_timezone_info(value: JsValue) -> Result { + // The name fields are 64 bytes of UTF-16, so 32 code units fit. + const MAX_NAME_CHARS: usize = 32; + + let obj = into_object(value)?; + let bias = get_i32(&obj, "bias")?; + + if !(-1440..=1440).contains(&bias) { + return Err(anyhow::anyhow!("timezone_info bias {bias} is out of range").into()); + } + + let truncate = |name: String| -> String { name.chars().take(MAX_NAME_CHARS).collect() }; + + Ok(TimezoneInfo { + bias, + standard_name: truncate(get_str(&obj, "standard_name")?), + standard_date: OptionalSystemTime(None), + standard_bias: 0, + daylight_name: truncate(get_str(&obj, "daylight_name")?), + daylight_date: OptionalSystemTime(None), + daylight_bias: 0, + }) +} + fn parse_print_job_stream_callbacks(callbacks: JsValue) -> anyhow::Result { let callbacks = callbacks .dyn_into::() @@ -1487,8 +1555,9 @@ fn build_config( client_name: String, desktop_size: DesktopSize, pointer_software_rendering: bool, - enable_audio_playback: bool, desktop_scale_factor: u32, + performance_flags: PerformanceFlags, + timezone_info: TimezoneInfo, legacy_graphics: bool, ) -> connector::Config { // Win7-class servers need 32-bpp lossless bitmaps and no advertised codecs. @@ -1542,17 +1611,19 @@ fn build_config( compression_type: None, enable_server_pointer: true, autologon: false, - enable_audio_playback, + // Audio is not played by the web client: RDPSND is attached with a no-op backend, so + // advertising playback would only make the server send audio that is then discarded. + enable_audio_playback: false, enable_audio_capture: false, request_data: None, pointer_software_rendering, multitransport_flags: None, support_dyn_vc_gfx_protocol: false, - performance_flags: PerformanceFlags::default(), + performance_flags, desktop_scale_factor, hardware_id: None, license_cache: None, - timezone_info: TimezoneInfo::default(), + timezone_info, alternate_shell: String::new(), work_dir: String::new(), remote_application_mode: false, From c78a1f624e2b0c545ca308e3aefb6a7c2db0c13d Mon Sep 17 00:00:00 2001 From: tonguyenducmanh Date: Sun, 11 Oct 2026 19:46:44 +0700 Subject: [PATCH 7/7] fix(web): validate WASM session extension values The desktop_scale_factor, performance_flags and timezone_info extensions accepted any JS number and truncated it on the way to the wire. - desktop_scale_factor now honours the MS-RDPBCGR 2.2.1.13.1.2 window of 100% to 500%, keeps 0 as the "server picks" default, and rejects a fractional devicePixelRatio instead of flooring 1.5 to the out-of-range value 1. - performance_flags falls back to PerformanceFlags::default() rather than an empty set, so a type-invalid value keeps the behaviour the client advertised before the extension existed. - Timezone names are truncated on UTF-16 code units, not char scalars. StandardName and DaylightName are fixed at 64 bytes and the encoder only pads them, so an astral-plane name overflowed the field and shifted the rest of the Client Info PDU. build_config keeps its original signature and the four values are assigned on the returned config, which drops the too_many_arguments suppression and unbreaks the test helper that was still calling the old signature. Also states the DST trade-off accurately and covers the new pure helpers with tests. --- crates/ironrdp-web/src/session.rs | 249 +++++++++++++++++++++++------- 1 file changed, 190 insertions(+), 59 deletions(-) diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 3f5d3a4feb..e7373d4de7 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -288,31 +288,16 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { }; |pointer_software_rendering: bool| { self.0.borrow_mut().pointer_software_rendering = pointer_software_rendering }; |desktop_scale_factor: f64| { - // Zero means the server picks a factor matching the requested desktop size. - let scale_factor = if desktop_scale_factor >= 0.0 && desktop_scale_factor <= f64::from(u32::MAX) { - #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] - { desktop_scale_factor as u32 } - } else { - warn!(desktop_scale_factor, "Invalid desktop scale factor; falling back to default"); - 0 - }; - self.0.borrow_mut().desktop_scale_factor = scale_factor; + self.0.borrow_mut().desktop_scale_factor = parse_desktop_scale_factor(desktop_scale_factor); }; // Bitmask of `PerformanceFlags`. Disabling wallpaper, theming, font smoothing and // desktop composition cuts both bandwidth and CPU, which is what a browser client // over a slow link needs. |performance_flags: f64| { - let bits = if performance_flags >= 0.0 && performance_flags <= f64::from(u32::MAX) { - #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] - { performance_flags as u32 } - } else { - warn!(performance_flags, "Invalid performance_flags; falling back to default"); - 0 - }; - self.0.borrow_mut().performance_flags = PerformanceFlags::from_bits_truncate(bits); + self.0.borrow_mut().performance_flags = parse_performance_flags(performance_flags); }; // Timezone of the browser user, so clocks shown by the remote session match local time. - // DST transition dates are left unset: the browser only exposes the current UTC offset. + // The browser reports the offset in effect right now, not the zone's DST rules. |timezone_info: JsValue| { match parse_timezone_info(timezone_info) { Ok(info) => self.0.borrow_mut().timezone_info = info, @@ -492,16 +477,16 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { server_domain, client_name.clone(), desktop_size, - pointer_software_rendering, - desktop_scale_factor, - performance_flags, - timezone_info, legacy_graphics, security, ); let enable_server_pointer = self.0.borrow().enable_server_pointer; config.enable_server_pointer = enable_server_pointer; + config.pointer_software_rendering = pointer_software_rendering; + config.desktop_scale_factor = desktop_scale_factor; + config.performance_flags = performance_flags; + config.timezone_info = timezone_info; let (input_events_tx, input_events_rx) = mpsc::unbounded(); @@ -1358,28 +1343,26 @@ fn into_object(val: JsValue) -> Result { .map_err(|_| anyhow::anyhow!("expected object").into()) } +fn get_property(obj: &js_sys::Object, key: &str) -> anyhow::Result { + js_sys::Reflect::get(obj, &JsValue::from_str(key)).map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}")) +} + fn get_u32(obj: &js_sys::Object, key: &str) -> Result { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; - let f = val + let f = get_property(obj, key)? .as_f64() .with_context(|| format!("invalid type for property `{key}`"))?; Ok(f64_to_u32_saturating_cast(f)) } fn get_i32(obj: &js_sys::Object, key: &str) -> Result { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; - let f = val + let f = get_property(obj, key)? .as_f64() .with_context(|| format!("invalid type for property `{key}`"))?; Ok(f64_to_i32_saturating_cast(f)) } fn get_u64(obj: &js_sys::Object, key: &str) -> Result { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; - let f = val + let f = get_property(obj, key)? .as_f64() .with_context(|| format!("invalid type for property `{key}`"))?; // Validate integer precision before casting @@ -1395,8 +1378,7 @@ fn get_u64(obj: &js_sys::Object, key: &str) -> Result { } fn get_str(obj: &js_sys::Object, key: &str) -> Result { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; + let val = get_property(obj, key)?; if val.is_undefined() || val.is_null() { return Ok(String::new()); } @@ -1406,16 +1388,14 @@ fn get_str(obj: &js_sys::Object, key: &str) -> Result { } fn get_bool(obj: &js_sys::Object, key: &str) -> Result { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; - val.as_bool() + get_property(obj, key)? + .as_bool() .with_context(|| format!("invalid type for property `{key}`")) .map_err(Into::into) } fn get_u32_opt(obj: &js_sys::Object, key: &str) -> Result, IronError> { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; + let val = get_property(obj, key)?; if val.is_undefined() || val.is_null() { return Ok(None); } @@ -1425,35 +1405,113 @@ fn get_u32_opt(obj: &js_sys::Object, key: &str) -> Result, IronError Ok(Some(f64_to_u32_saturating_cast(f))) } +/// Reads the `desktop_scale_factor` extension value as a percentage. +/// +/// [MS-RDPBCGR] 2.2.1.13.1.2 stores the factor in the `desktopScaleFactor` +/// field of the `TS_UD_CS_CORE` structure and states that the server ignores it +/// below 100% or above 500%, so forwarding a value outside that window would be +/// a silent no-op. Zero is not a percentage: it is the “let the server pick the +/// factor matching the requested desktop size” default. +/// +/// Ratios such as `devicePixelRatio` (1.5) are rejected rather than floored, +/// because 1.5 truncated to 1 is outside the window the server honours anyway. +/// +/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 +fn parse_desktop_scale_factor(value: f64) -> u32 { + const MIN_PERCENT: f64 = 100.0; + const MAX_PERCENT: f64 = 500.0; + + let is_percentage = value.is_finite() && value.fract() == 0.0 && (MIN_PERCENT..=MAX_PERCENT).contains(&value); + + if value == 0.0 || is_percentage { + #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let percentage = value as u32; + + percentage + } else { + warn!(value, "Invalid desktop scale factor; using default"); + 0 + } +} + +/// Reads the `performance_flags` extension value as a `PerformanceFlags` bitmask. +/// +/// JS numbers are doubles, so a value that is not a whole number in +/// `0..=u32::MAX` (a ratio, a negative number, or something past `u32::MAX`) +/// cannot be a bitmask, and casting it would silently truncate it. Such a value +/// falls back to `PerformanceFlags::default()` rather than to an empty set, so +/// a type-invalid value keeps advertising the behaviour the client had before +/// this extension existed. +/// +/// Bits outside the known set are dropped rather than rejected, matching how +/// the connector treats the flags it forwards. +fn parse_performance_flags(value: f64) -> PerformanceFlags { + let is_bitmask = value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= f64::from(u32::MAX); + + if !is_bitmask { + warn!(value, "Invalid performance flags; using default"); + return PerformanceFlags::default(); + } + + #[expect(clippy::as_conversions, clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let bits = value as u32; + + PerformanceFlags::from_bits_truncate(bits) +} + /// Builds a `TimezoneInfo` from the browser timezone reported through the extension. /// -/// `bias` follows the MS-RDPBCGR convention and matches `Date::getTimezoneOffset()`: -/// a client ahead of UTC sends a negative value. DST transition dates stay unset -/// because the browser only exposes the offset in effect right now. +/// `bias` follows [MS-RDPBCGR] 2.2.1.11.1.1.1.1 (`UTC = local time + bias`) and +/// matches `Date::getTimezoneOffset()`: a client ahead of UTC sends a negative +/// value. +/// +/// Both transition dates stay zeroed because the browser exposes the offset in +/// effect right now, not the zone's DST rules. [MS-RDPBCGR] reads a zeroed +/// `StandardDate` or `DaylightDate` as “this client does not support daylight +/// saving time” and makes the server ignore `standard_bias` and `daylight_bias`, +/// so the remote clock stays correct until the zone's next transition rather +/// than until the session ends. +/// +/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/526ed635-d7a9-4d3c-bbe1-4e3fb17585f4 fn parse_timezone_info(value: JsValue) -> Result { - // The name fields are 64 bytes of UTF-16, so 32 code units fit. - const MAX_NAME_CHARS: usize = 32; - let obj = into_object(value)?; let bias = get_i32(&obj, "bias")?; + // The bias is a number of minutes, so no time zone is more than a day away. if !(-1440..=1440).contains(&bias) { return Err(anyhow::anyhow!("timezone_info bias {bias} is out of range").into()); } - let truncate = |name: String| -> String { name.chars().take(MAX_NAME_CHARS).collect() }; - Ok(TimezoneInfo { bias, - standard_name: truncate(get_str(&obj, "standard_name")?), + standard_name: truncate_timezone_name(&get_str(&obj, "standard_name")?), standard_date: OptionalSystemTime(None), standard_bias: 0, - daylight_name: truncate(get_str(&obj, "daylight_name")?), + daylight_name: truncate_timezone_name(&get_str(&obj, "daylight_name")?), daylight_date: OptionalSystemTime(None), daylight_bias: 0, }) } +/// Truncates a time zone name so that it fits the `TS_TIME_ZONE_INFORMATION` name fields. +/// +/// [MS-RDPBCGR] 2.2.1.11.1.1.1.1 fixes `StandardName` and `DaylightName` at 64 +/// bytes, that is 32 UTF-16 code units. The encoder only pads that field, it +/// never truncates it, so a longer name would overrun the boundary and shift +/// the rest of the Client Info PDU. +/// +/// Truncation counts code units rather than `char`s because a character +/// outside the basic multilingual plane encodes to two code units, and a +/// truncation that splits a surrogate pair decodes to a replacement character +/// without growing past the limit. +fn truncate_timezone_name(name: &str) -> String { + const MAX_NAME_CODE_UNITS: usize = 32; + + let code_units: Vec = name.encode_utf16().take(MAX_NAME_CODE_UNITS).collect(); + + String::from_utf16_lossy(&code_units) +} + fn parse_print_job_stream_callbacks(callbacks: JsValue) -> anyhow::Result { let callbacks = callbacks .dyn_into::() @@ -1472,8 +1530,7 @@ fn get_required_function(obj: &js_sys::Object, key: &str) -> anyhow::Result anyhow::Result> { - let val = js_sys::Reflect::get(obj, &JsValue::from_str(key)) - .map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?; + let val = get_property(obj, key)?; if val.is_undefined() || val.is_null() { return Ok(None); @@ -1565,17 +1622,12 @@ fn parse_file_metadata_array(files: JsValue) -> Result, IronEr Ok(file_list) } -#[expect(clippy::too_many_arguments)] fn build_config( username: String, password: String, domain: Option, client_name: String, desktop_size: DesktopSize, - pointer_software_rendering: bool, - desktop_scale_factor: u32, - performance_flags: PerformanceFlags, - timezone_info: TimezoneInfo, legacy_graphics: bool, security: SecurityConfig, ) -> connector::Config { @@ -1634,14 +1686,14 @@ fn build_config( enable_audio_playback: false, enable_audio_capture: false, request_data: None, - pointer_software_rendering, + pointer_software_rendering: false, multitransport_flags: None, support_dyn_vc_gfx_protocol: false, - performance_flags, - desktop_scale_factor, + performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, hardware_id: None, license_cache: None, - timezone_info, + timezone_info: TimezoneInfo::default(), alternate_shell: String::new(), work_dir: String::new(), remote_application_mode: false, @@ -2624,4 +2676,83 @@ mod tests { _ => panic!("Expected correct flags conversion"), } } + + #[test] + fn desktop_scale_factor_keeps_the_server_default_for_zero() { + assert_eq!(parse_desktop_scale_factor(0.0), 0); + } + + #[test] + fn desktop_scale_factor_accepts_the_ms_rdpbcgr_percentages() { + for (percentage, expected) in [(100.0, 100), (150.0, 150), (175.0, 175), (500.0, 500)] { + assert_eq!(parse_desktop_scale_factor(percentage), expected); + } + } + + #[test] + fn desktop_scale_factor_rejects_ratios_and_out_of_range_percentages() { + // A `devicePixelRatio`-style ratio must not be floored: 1.5 truncated to + // 1 is outside the window the server honours, so it would be a silent + // no-op. + for invalid in [1.5, 99.0, 501.0, -100.0, f64::NAN, f64::INFINITY] { + assert_eq!(parse_desktop_scale_factor(invalid), 0, "unexpected scale for {invalid}"); + } + } + + #[test] + fn performance_flags_drops_unknown_bits() { + let known = PerformanceFlags::DISABLE_WALLPAPER; + // A bit the `PerformanceFlags` set does not define. + let unknown = 0x0000_0200; + + let flags = parse_performance_flags(f64::from(known.bits() | unknown)); + + assert_eq!(flags, known); + } + + #[test] + fn performance_flags_fall_back_to_the_default_on_a_type_invalid_value() { + // The log says "falling back to the default", so the stored value has to + // be that default rather than an empty set. + for invalid in [-1.0, 1.5, f64::from(u32::MAX) + 1.0, f64::NAN] { + assert_eq!( + parse_performance_flags(invalid), + PerformanceFlags::default(), + "unexpected flags for {invalid}" + ); + } + } + + #[test] + fn timezone_names_are_truncated_on_utf16_code_units() { + // An emoji: encoded as a surrogate pair, so two UTF-16 code units. + const ASTRAL: char = '\u{1f980}'; + + // 32 characters of the basic multilingual plane fill the 64-byte field + // exactly. + let exact = "a".repeat(32); + + assert_eq!(truncate_timezone_name(&exact), exact); + + // Each character below needs two UTF-16 code units, so only half of them + // fit. Counting `char`s instead would emit 128 bytes and overrun the + // field. + let astral = ASTRAL.to_string().repeat(20); + let truncated = truncate_timezone_name(&astral); + + assert_eq!(truncated, ASTRAL.to_string().repeat(16)); + assert_eq!(truncated.encode_utf16().count(), 32); + } + + #[test] + fn timezone_names_that_split_a_surrogate_pair_stay_within_the_field() { + // 31 code units, then one half of a surrogate pair: the lossy decode + // turns it into a single replacement character instead of growing. + let name = format!("{}\u{1f980}", "a".repeat(31)); + + let truncated = truncate_timezone_name(&name); + + assert_eq!(truncated.encode_utf16().count(), 32); + assert_eq!(truncated, format!("{}\u{fffd}", "a".repeat(31))); + } }