mirror of https://github.com/microsoft/edit
Avoid going OOM for super large clipboard contents
This commit is contained in:
parent
10f2bf9481
commit
03d5d19f67
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 l’envoyer ?",
|
||||
/* fr */ "Voulez-vous quand même l’envoyer?",
|
||||
/* 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
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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\\");
|
||||
|
|
|
|||
Loading…
Reference in New Issue