Avoid going OOM for super large clipboard contents

This commit is contained in:
Leonard Hecker 2025-05-16 23:35:33 +02:00
parent 10f2bf9481
commit 03d5d19f67
5 changed files with 88 additions and 29 deletions

View File

@ -9,11 +9,12 @@ use crate::helpers::*;
static mut S_SCRATCH: [release::Arena; 2] =
const { [release::Arena::empty(), release::Arena::empty()] };
/// Initialize the scratch arenas with a given capacity.
/// Call this before using [`scratch_arena`].
pub fn init() -> apperr::Result<()> {
pub fn init(capacity: usize) -> apperr::Result<()> {
unsafe {
for s in &mut S_SCRATCH[..] {
*s = release::Arena::new(128 * MEBI)?;
*s = release::Arena::new(capacity)?;
}
}
Ok(())

View File

@ -135,6 +135,11 @@ impl<'a> ArenaString<'a> {
self.vec.reserve(additional)
}
/// Just like [`reserve`], but it doesn't overallocate.
pub fn reserve_exact(&mut self, additional: usize) {
self.vec.reserve_exact(additional)
}
/// Now it's small! Alarming!
///
/// *Do not* call this unless this string is the last thing on the arena.

View File

@ -4,6 +4,13 @@ use crate::arena::ArenaString;
const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// One aspect of base64 is that the encoded length can be
/// calculated accurately in advance, which is what this returns.
#[inline]
pub fn encode_len(src_len: usize) -> usize {
src_len.div_ceil(3) * 4
}
/// Encodes the given bytes as base64 and appends them to the destination string.
pub fn encode(dst: &mut ArenaString, src: &[u8]) {
unsafe {
@ -11,8 +18,7 @@ pub fn encode(dst: &mut ArenaString, src: &[u8]) {
let mut remaining = src.len();
let dst = dst.as_mut_vec();
// One aspect of base64 is that the encoded length can be calculated accurately in advance.
let out_len = src.len().div_ceil(3) * 4;
let out_len = encode_len(src.len());
// ... we can then use this fact to reserve space all at once.
dst.reserve(out_len);

View File

@ -52,9 +52,10 @@ pub enum LocId {
AboutDialogVersion,
// Shown when the clipboard size exceeds the limit for OSC 52
LargeClipboardWarningLine1, // "Text you copy is shared with the terminal clipboard."
LargeClipboardWarningLine2, // "You copied {size} which may take a long time to share."
LargeClipboardWarningLine3, // "Do you want to send it anyway?"
LargeClipboardWarningLine1,
LargeClipboardWarningLine2,
LargeClipboardWarningLine3,
SuperLargeClipboardWarning,
// Warning dialog
WarningDialogTitle,
@ -627,7 +628,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
/* en */ "Do you want to send it anyway?",
/* de */ "Möchten Sie es trotzdem senden?",
/* es */ "¿Desea enviarlo de todas formas?",
/* fr */ "Voulez-vous quand même lenvoyer ?",
/* fr */ "Voulez-vous quand même lenvoyer?",
/* it */ "Vuoi inviarlo comunque?",
/* ja */ "それでも送信しますか?",
/* ko */ "그래도 전송하시겠습니까?",
@ -636,6 +637,20 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
/* zh_hans */ "仍要发送吗?",
/* zh_hant */ "仍要傳送嗎?",
],
// SuperLargeClipboardWarning (as an alternative to LargeClipboardWarningLine2 and 3)
[
/* en */ "The text you copied is too large to be shared.",
/* de */ "Der kopierte Text ist zu groß, um geteilt zu werden.",
/* es */ "El texto que copiaste es demasiado grande para compartirse.",
/* fr */ "Le texte que vous avez copié est trop volumineux pour être partagé.",
/* it */ "Il testo copiato è troppo grande per essere condiviso.",
/* ja */ "コピーしたテキストは大きすぎて共有できません。",
/* ko */ "복사한 텍스트가 너무 커서 공유할 수 없습니다.",
/* pt_br */ "O texto copiado é grande demais para ser compartilhado.",
/* ru */ "Скопированный текст слишком велик для передачи.",
/* zh_hans */ "你复制的文本过大,无法共享。",
/* zh_hant */ "您複製的文字過大,無法分享。",
],
// WarningDialogTitle
[

View File

@ -20,7 +20,7 @@ use draw_menubar::*;
use draw_statusbar::*;
use edit::arena::{self, ArenaString, scratch_arena};
use edit::framebuffer::{self, IndexedColor};
use edit::helpers::{KIBI, MetricFormatter, Rect, Size};
use edit::helpers::{KIBI, MEBI, MetricFormatter, Rect, Size};
use edit::input::{self, kbmod, vk};
use edit::oklab::oklab_blend;
use edit::tui::*;
@ -29,6 +29,11 @@ use edit::{apperr, arena_format, base64, icu, path, sys};
use localization::*;
use state::*;
#[cfg(target_pointer_width = "32")]
const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI;
fn main() -> process::ExitCode {
if cfg!(debug_assertions) {
let hook = std::panic::take_hook();
@ -56,7 +61,7 @@ fn run() -> apperr::Result<()> {
// Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
let _sys_deinit = sys::init()?;
// Next init `arena`, so that `scratch_arena` works. `loc` depends on it.
arena::init()?;
arena::init(SCRATCH_ARENA_CAPACITY)?;
// Init the `loc` module, so that error messages are localized.
localization::init();
@ -186,8 +191,10 @@ fn run() -> apperr::Result<()> {
// Print the number of passes and latency in the top right corner.
let time_end = std::time::Instant::now();
let status = time_end - time_beg;
let scratch_alt = scratch_arena(Some(&scratch));
let status = arena_format!(
&scratch,
&scratch_alt,
"{}P {}B {:.3}μs",
passes,
output.len(),
@ -200,6 +207,11 @@ fn run() -> apperr::Result<()> {
// Since the status may shrink and grow, we may have to overwrite the previous one with whitespace.
let padding = (last_latency_width - cols).max(0);
// If the `output` is already very large,
// Rust may double the size during the write below.
// Let's avoid that by reserving the needed size in advance.
output.reserve_exact(128);
// To avoid moving the cursor, push and pop it onto the VT cursor stack.
_ = write!(
output,
@ -376,11 +388,19 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
return;
}
let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
{
ctx.block_begin("description");
ctx.attr_padding(Rect::three(1, 2, 1));
{
if over_limit {
ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
ctx.attr_position(Position::Center);
ctx.label("line2", loc(LocId::SuperLargeClipboardWarning));
ctx.attr_position(Position::Center);
} else {
let label2 = {
let template = loc(LocId::LargeClipboardWarningLine2);
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
@ -410,25 +430,32 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
ctx.table_next_row();
ctx.inherit_focus();
if ctx.button("always", loc(LocId::Always)) {
state.osc_clipboard_always_send = true;
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.button("yes", loc(LocId::Yes)) {
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
if over_limit {
if ctx.button("ok", loc(LocId::Ok)) {
state.osc_clipboard_seen_generation = generation;
}
ctx.inherit_focus();
}
} else {
if ctx.button("always", loc(LocId::Always)) {
state.osc_clipboard_always_send = true;
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.button("no", loc(LocId::No)) {
state.osc_clipboard_seen_generation = generation;
}
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
ctx.inherit_focus();
if ctx.button("yes", loc(LocId::Yes)) {
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
ctx.inherit_focus();
}
if ctx.button("no", loc(LocId::No)) {
state.osc_clipboard_seen_generation = generation;
}
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
ctx.inherit_focus();
}
}
}
ctx.table_end();
@ -442,6 +469,11 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
let clipboard = tui.clipboard();
if !clipboard.is_empty() {
// Rust doubles the size of a string when it needs to grow it.
// If `clipboard` is *really* large, this may then double
// the size of the `output` from e.g. 100MB to 200MB. Not good.
// We can avoid that by reserving the needed size in advance.
output.reserve_exact(base64::encode_len(clipboard.len()) + 16);
output.push_str("\x1b]52;c;");
base64::encode(output, clipboard);
output.push_str("\x1b\\");