Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion crates/gpui/src/elements/div/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,19 @@ 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()
.flat_map(|style| {
candidate
.display_ranges(&style.range)
.into_iter()
.filter_map(|range| clamp_to_char_boundaries(text, range))
.map(|range| InlineTextStyle {
range,
..style.clone()
Expand All @@ -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,
Expand All @@ -159,7 +165,7 @@ impl InlineDocument {
.collect();

Self {
text: candidate.text.to_string(),
text: text.to_string(),
runs: candidate.runs.clone(),
text_styles,
spans,
Expand All @@ -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<usize>) -> Option<Range<usize>> {
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<Pixels>,
options: crate::TextLayoutOptions,
Expand Down Expand Up @@ -620,3 +642,102 @@ fn merge_fragments(regions: &mut Vec<Bounds<Pixels>>) {

*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<usize>],
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);
}
}
}
11 changes: 9 additions & 2 deletions crates/gpui_windows/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1322,7 +1322,14 @@ impl WindowsWindowInner {
None
}

fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
fn handle_device_lost(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
// 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
Expand Down
75 changes: 75 additions & 0 deletions crates/gpui_windows/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>,
) -> impl gpui::IntoElement {
gpui::Empty
}
}

let platform = WindowsPlatform::new(false)?;
let validation_number = platform.inner.validation_number;
let handle: AnyWindowHandle = WindowHandle::<Root>::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());
Expand Down
Loading