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] =
|
static mut S_SCRATCH: [release::Arena; 2] =
|
||||||
const { [release::Arena::empty(), release::Arena::empty()] };
|
const { [release::Arena::empty(), release::Arena::empty()] };
|
||||||
|
|
||||||
|
/// Initialize the scratch arenas with a given capacity.
|
||||||
/// Call this before using [`scratch_arena`].
|
/// Call this before using [`scratch_arena`].
|
||||||
pub fn init() -> apperr::Result<()> {
|
pub fn init(capacity: usize) -> apperr::Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
for s in &mut S_SCRATCH[..] {
|
for s in &mut S_SCRATCH[..] {
|
||||||
*s = release::Arena::new(128 * MEBI)?;
|
*s = release::Arena::new(capacity)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,11 @@ impl<'a> ArenaString<'a> {
|
||||||
self.vec.reserve(additional)
|
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!
|
/// Now it's small! Alarming!
|
||||||
///
|
///
|
||||||
/// *Do not* call this unless this string is the last thing on the arena.
|
/// *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+/";
|
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.
|
/// Encodes the given bytes as base64 and appends them to the destination string.
|
||||||
pub fn encode(dst: &mut ArenaString, src: &[u8]) {
|
pub fn encode(dst: &mut ArenaString, src: &[u8]) {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
@ -11,8 +18,7 @@ pub fn encode(dst: &mut ArenaString, src: &[u8]) {
|
||||||
let mut remaining = src.len();
|
let mut remaining = src.len();
|
||||||
let dst = dst.as_mut_vec();
|
let dst = dst.as_mut_vec();
|
||||||
|
|
||||||
// One aspect of base64 is that the encoded length can be calculated accurately in advance.
|
let out_len = encode_len(src.len());
|
||||||
let out_len = src.len().div_ceil(3) * 4;
|
|
||||||
// ... we can then use this fact to reserve space all at once.
|
// ... we can then use this fact to reserve space all at once.
|
||||||
dst.reserve(out_len);
|
dst.reserve(out_len);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,10 @@ pub enum LocId {
|
||||||
AboutDialogVersion,
|
AboutDialogVersion,
|
||||||
|
|
||||||
// Shown when the clipboard size exceeds the limit for OSC 52
|
// Shown when the clipboard size exceeds the limit for OSC 52
|
||||||
LargeClipboardWarningLine1, // "Text you copy is shared with the terminal clipboard."
|
LargeClipboardWarningLine1,
|
||||||
LargeClipboardWarningLine2, // "You copied {size} which may take a long time to share."
|
LargeClipboardWarningLine2,
|
||||||
LargeClipboardWarningLine3, // "Do you want to send it anyway?"
|
LargeClipboardWarningLine3,
|
||||||
|
SuperLargeClipboardWarning,
|
||||||
|
|
||||||
// Warning dialog
|
// Warning dialog
|
||||||
WarningDialogTitle,
|
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?",
|
/* en */ "Do you want to send it anyway?",
|
||||||
/* de */ "Möchten Sie es trotzdem senden?",
|
/* de */ "Möchten Sie es trotzdem senden?",
|
||||||
/* es */ "¿Desea enviarlo de todas formas?",
|
/* 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?",
|
/* it */ "Vuoi inviarlo comunque?",
|
||||||
/* ja */ "それでも送信しますか?",
|
/* ja */ "それでも送信しますか?",
|
||||||
/* ko */ "그래도 전송하시겠습니까?",
|
/* ko */ "그래도 전송하시겠습니까?",
|
||||||
|
|
@ -636,6 +637,20 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
||||||
/* zh_hans */ "仍要发送吗?",
|
/* zh_hans */ "仍要发送吗?",
|
||||||
/* zh_hant */ "仍要傳送嗎?",
|
/* 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
|
// WarningDialogTitle
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use draw_menubar::*;
|
||||||
use draw_statusbar::*;
|
use draw_statusbar::*;
|
||||||
use edit::arena::{self, ArenaString, scratch_arena};
|
use edit::arena::{self, ArenaString, scratch_arena};
|
||||||
use edit::framebuffer::{self, IndexedColor};
|
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::input::{self, kbmod, vk};
|
||||||
use edit::oklab::oklab_blend;
|
use edit::oklab::oklab_blend;
|
||||||
use edit::tui::*;
|
use edit::tui::*;
|
||||||
|
|
@ -29,6 +29,11 @@ use edit::{apperr, arena_format, base64, icu, path, sys};
|
||||||
use localization::*;
|
use localization::*;
|
||||||
use state::*;
|
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 {
|
fn main() -> process::ExitCode {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
let hook = std::panic::take_hook();
|
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.).
|
// Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
|
||||||
let _sys_deinit = sys::init()?;
|
let _sys_deinit = sys::init()?;
|
||||||
// Next init `arena`, so that `scratch_arena` works. `loc` depends on it.
|
// 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.
|
// Init the `loc` module, so that error messages are localized.
|
||||||
localization::init();
|
localization::init();
|
||||||
|
|
||||||
|
|
@ -186,8 +191,10 @@ fn run() -> apperr::Result<()> {
|
||||||
// Print the number of passes and latency in the top right corner.
|
// Print the number of passes and latency in the top right corner.
|
||||||
let time_end = std::time::Instant::now();
|
let time_end = std::time::Instant::now();
|
||||||
let status = time_end - time_beg;
|
let status = time_end - time_beg;
|
||||||
|
|
||||||
|
let scratch_alt = scratch_arena(Some(&scratch));
|
||||||
let status = arena_format!(
|
let status = arena_format!(
|
||||||
&scratch,
|
&scratch_alt,
|
||||||
"{}P {}B {:.3}μs",
|
"{}P {}B {:.3}μs",
|
||||||
passes,
|
passes,
|
||||||
output.len(),
|
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.
|
// 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);
|
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.
|
// To avoid moving the cursor, push and pop it onto the VT cursor stack.
|
||||||
_ = write!(
|
_ = write!(
|
||||||
output,
|
output,
|
||||||
|
|
@ -376,11 +388,19 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
|
||||||
|
|
||||||
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
|
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
|
||||||
{
|
{
|
||||||
ctx.block_begin("description");
|
ctx.block_begin("description");
|
||||||
ctx.attr_padding(Rect::three(1, 2, 1));
|
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 label2 = {
|
||||||
let template = loc(LocId::LargeClipboardWarningLine2);
|
let template = loc(LocId::LargeClipboardWarningLine2);
|
||||||
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
|
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.table_next_row();
|
||||||
ctx.inherit_focus();
|
ctx.inherit_focus();
|
||||||
|
|
||||||
if ctx.button("always", loc(LocId::Always)) {
|
if over_limit {
|
||||||
state.osc_clipboard_always_send = true;
|
if ctx.button("ok", loc(LocId::Ok)) {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
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 {
|
|
||||||
ctx.inherit_focus();
|
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)) {
|
if ctx.button("yes", loc(LocId::Yes)) {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
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.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();
|
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) {
|
fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
|
||||||
let clipboard = tui.clipboard();
|
let clipboard = tui.clipboard();
|
||||||
if !clipboard.is_empty() {
|
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;");
|
output.push_str("\x1b]52;c;");
|
||||||
base64::encode(output, clipboard);
|
base64::encode(output, clipboard);
|
||||||
output.push_str("\x1b\\");
|
output.push_str("\x1b\\");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue