diff --git a/crates/gpui/src/elements/div/inline.rs b/crates/gpui/src/elements/div/inline.rs index 9c55d7594b7..6f9dbcb8980 100644 --- a/crates/gpui/src/elements/div/inline.rs +++ b/crates/gpui/src/elements/div/inline.rs @@ -127,7 +127,11 @@ impl InlineDocument { measurement } + /// Every range is clamped to `candidate.text`'s char boundaries: a source range that ends + /// inside a multi-byte character maps to the same split in the output, and the text system + /// slices the output text by these ranges. fn truncated(&self, candidate: &TruncationCandidate) -> Self { + let text = candidate.text.as_ref(); let text_styles = self .text_styles .iter() @@ -135,6 +139,7 @@ impl InlineDocument { candidate .display_ranges(&style.range) .into_iter() + .filter_map(|range| clamp_to_char_boundaries(text, range)) .map(|range| InlineTextStyle { range, ..style.clone() @@ -148,6 +153,7 @@ impl InlineDocument { candidate .display_ranges(&span.text_range) .into_iter() + .filter_map(|range| clamp_to_char_boundaries(text, range)) .map(|text_range| InlineSpan { layout_id: span.layout_id, text_range, @@ -159,7 +165,7 @@ impl InlineDocument { .collect(); Self { - text: candidate.text.to_string(), + text: text.to_string(), runs: candidate.runs.clone(), text_styles, spans, @@ -168,6 +174,22 @@ impl InlineDocument { } } +/// Moves both ends of `range` down to a char boundary of `text`, never past its end. Returns +/// `None` when nothing is left. +fn clamp_to_char_boundaries(text: &str, range: Range) -> Option> { + let floor = |index: usize| { + let mut index = index.min(text.len()); + while !text.is_char_boundary(index) { + index -= 1; + } + index + }; + let start = floor(range.start); + let end = floor(range.end); + + (start < end).then_some(start..end) +} + struct InlineParagraphMeasurement { truncate_width: Option, options: crate::TextLayoutOptions, @@ -620,3 +642,102 @@ fn merge_fragments(regions: &mut Vec>) { *regions = merged; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{TruncateFrom, px}; + + /// Truncates `text` to at most `max_chars` chars (marker included) and maps `style_ranges` + /// into the result the way a layout pass does. + fn truncate( + text: &str, + style_ranges: &[Range], + direction: TruncateFrom, + max_chars: usize, + ) -> InlineDocument { + let document = InlineDocument { + text: text.to_owned(), + runs: vec![TextRun { + len: text.len(), + ..Default::default() + }], + text_styles: style_ranges + .iter() + .map(|range| InlineTextStyle { + range: range.clone(), + font_size: px(14.), + line_height: px(20.), + }) + .collect(), + ..InlineDocument::default() + }; + let boundaries = text + .grapheme_indices(true) + .map(|(index, _grapheme)| index) + .chain(std::iter::once(text.len())); + let (candidate, ()) = truncate_with_measured_candidates( + text, + boundaries, + None, + "…", + &document.runs, + direction, + |candidate| ((), candidate.text.chars().count() <= max_chars), + ); + + document.truncated(&candidate) + } + + fn assert_ranges_slice(document: &InlineDocument) { + for style in &document.text_styles { + assert!( + document.text.get(style.range.clone()).is_some(), + "{:?} must slice {:?} on char boundaries", + style.range, + document.text + ); + } + } + + #[test] + fn clamps_a_range_past_the_end_of_a_short_string() { + assert_eq!(clamp_to_char_boundaries("abcd", 2..10), Some(2..4)); + assert_eq!(clamp_to_char_boundaries("abcd", 5..9), None); + assert_eq!( + clamp_to_char_boundaries("abcd", 125_780_070_648_069..125_780_070_648_070), + None + ); + assert_eq!(clamp_to_char_boundaries("", 0..1), None); + } + + #[test] + fn clamps_a_range_inside_a_multi_byte_char() { + // "é" is bytes 0..2, "🦀" is bytes 2..6. + assert_eq!(clamp_to_char_boundaries("é🦀", 1..3), Some(0..2)); + assert_eq!(clamp_to_char_boundaries("é🦀", 3..5), None); + assert_eq!(clamp_to_char_boundaries("é🦀", 0..6), Some(0..6)); + } + + #[test] + fn truncating_a_short_string_keeps_style_ranges_inside_the_output() { + for direction in [TruncateFrom::End, TruncateFrom::Start, TruncateFrom::Middle] { + let document = truncate("abcd", &[0..4, 2..10, 1_000_000..1_000_005], direction, 3); + + assert_eq!(document.text.chars().count(), 3, "{direction:?}"); + assert_ranges_slice(&document); + } + } + + #[test] + fn truncating_a_multi_byte_string_keeps_style_ranges_on_char_boundaries() { + // 2..8 starts inside "é" and ends inside "🦀"; 7..12 starts inside "🦀". + let text = "héllo🦀wörld"; + for direction in [TruncateFrom::End, TruncateFrom::Start, TruncateFrom::Middle] { + let document = truncate(text, &[0..2, 2..8, 7..12, 0..text.len()], direction, 8); + + assert!(document.text.chars().count() <= 8, "{direction:?}"); + assert_ranges_slice(&document); + } + } +} diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index e14f9aef48b..453012340ac 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -171,7 +171,7 @@ impl WindowsWindowInner { WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(wparam, lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), - WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), + WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(wparam, lparam), DM_POINTERHITTEST => self.handle_dm_pointer_hit_test(wparam), WM_GETOBJECT => self.handle_wm_getobject(wparam, lparam), _ => None, @@ -1322,7 +1322,14 @@ impl WindowsWindowInner { None } - fn handle_device_lost(&self, lparam: LPARAM) -> Option { + fn handle_device_lost(&self, wparam: WPARAM, lparam: LPARAM) -> Option { + // lparam is read as a `*const DirectXDevices`. `WM_USER + 7` is a common + // number that IMEs, overlays and accessibility tools also send, so only + // trust a message that carries this app's token. + if wparam.0 != self.validation_number { + log::error!("Wrong validation number while processing device lost message"); + return None; + } let devices = lparam.0 as *const DirectXDevices; let devices = unsafe { &*devices }; if let Err(err) = self diff --git a/crates/gpui_windows/src/platform.rs b/crates/gpui_windows/src/platform.rs index 4b96ab993e7..8991b080f0f 100644 --- a/crates/gpui_windows/src/platform.rs +++ b/crates/gpui_windows/src/platform.rs @@ -1933,6 +1933,81 @@ mod tests { Ok(()) } + /// Another process or an injected DLL can send `WM_USER + 7` to a gpui window. The window + /// must not read that message's lparam as its `DirectXDevices` pointer unless it carries the + /// app's validation number: a null lparam faults at the `device` field, a random one corrupts + /// memory. + #[test] + fn a_foreign_device_lost_message_is_ignored() -> anyhow::Result<()> { + use super::WindowsPlatform; + use crate::{WM_GPUI_GPU_DEVICE_LOST, WindowsWindow}; + use gpui::{ + AnyWindowHandle, Bounds, PlatformWindow as _, WindowHandle, WindowId, WindowKind, + WindowParams, point, px, size, + }; + use windows::Win32::Foundation::{LPARAM, WPARAM}; + use windows::Win32::UI::WindowsAndMessaging::{DestroyWindow, SendMessageW}; + + struct Root; + impl gpui::Render for Root { + fn render( + &mut self, + _: &mut gpui::Window, + _: &mut gpui::Context, + ) -> impl gpui::IntoElement { + gpui::Empty + } + } + + let platform = WindowsPlatform::new(false)?; + let validation_number = platform.inner.validation_number; + let handle: AnyWindowHandle = WindowHandle::::new(WindowId::from(1)).into(); + let window = WindowsWindow::new( + handle, + WindowParams { + bounds: Bounds::new(point(px(100.), px(100.)), size(px(400.), px(300.))), + titlebar: None, + kind: WindowKind::Normal, + is_movable: true, + app_owns_titlebar_drag: false, + is_resizable: true, + is_minimizable: true, + focus: false, + show: false, + icon: None, + display_id: None, + app_id: None, + window_min_size: None, + windows_renderer: Default::default(), + }, + platform.generate_creation_info(), + )?; + let hwnd = window.get_raw_handle(); + let size_before = window.0.state.renderer.borrow().drawable_size(); + + // Neither message carries the token, so neither lparam may be dereferenced. Before the + // check, the null one crashed with an access violation at 0x10. + for (wparam, lparam) in [(0, 0), (validation_number.wrapping_add(1), 0x10)] { + unsafe { + SendMessageW( + hwnd, + WM_GPUI_GPU_DEVICE_LOST, + Some(WPARAM(wparam)), + Some(LPARAM(lparam)), + ); + } + } + + assert_eq!( + window.0.state.renderer.borrow().drawable_size(), + size_before, + "the renderer must be untouched by a message without the token" + ); + + unsafe { DestroyWindow(hwnd)? }; + Ok(()) + } + #[test] fn test_clipboard() { let item = ClipboardItem::new_string("你好,我是张小白".to_string());