From 2ac978ce54d32770049d8dc81dbb8ecc01d81c9f Mon Sep 17 00:00:00 2001
From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com>
Date: Wed, 28 Oct 2026 14:49:53 -0500
Subject: [PATCH 1/2] dc5-02: enforce clipboard HTML size cap
---
crates/engine/src/cmd/richclip.rs | 114 ++++++++++++++++++++++++------
1 file changed, 94 insertions(+), 20 deletions(-)
diff --git a/crates/engine/src/cmd/richclip.rs b/crates/engine/src/cmd/richclip.rs
index 5b9eaf6d..063bd392 100644
--- a/crates/engine/src/cmd/richclip.rs
+++ b/crates/engine/src/cmd/richclip.rs
@@ -14,27 +14,97 @@ const MAX_RUNS: usize = 10_000;
pub fn body_to_html(body: &TextBody) -> String {
let mut out = String::new();
for (i, p) in body.paragraphs.iter().enumerate() {
- if i > 0 {
- out.push_str("
");
+ let prefix = if i > 0 { "
" } else { "
" };
+ if !push_if_fits(&mut out, prefix, 4) {
+ return plain_html(body);
}
- out.push_str("
");
for r in &p.runs {
let (open, close) = tags(&r.props);
- out.push_str(&open);
+ if !push_if_fits(&mut out, &open, close.len() + 4) {
+ return plain_html(body);
+ }
match &r.kind {
- deckcraft_model::text::RunKind::Break => out.push_str("
"),
- _ => push_escaped(&mut out, &r.text),
+ deckcraft_model::text::RunKind::Break => {
+ if !push_if_fits(&mut out, "
", close.len() + 4) {
+ return plain_html(body);
+ }
+ }
+ _ => {
+ if !push_escaped_bounded(&mut out, &r.text, close.len() + 4) {
+ return plain_html(body);
+ }
+ }
}
out.push_str(&close);
- if out.len() > MAX_HTML {
- break;
- }
}
out.push_str("
");
}
out
}
+fn plain_html(body: &TextBody) -> String {
+ let mut out = String::new();
+ for (i, p) in body.paragraphs.iter().enumerate() {
+ let prefix = if i > 0 { "
" } else { "
" };
+ if !push_if_fits(&mut out, prefix, 4) {
+ break;
+ }
+ let mut complete = true;
+ for r in &p.runs {
+ if matches!(&r.kind, deckcraft_model::text::RunKind::Break) {
+ complete = push_if_fits(&mut out, "
", 4);
+ } else {
+ complete = push_escaped_bounded(&mut out, &r.text, 4);
+ }
+ if !complete {
+ break;
+ }
+ }
+ out.push_str("
");
+ if !complete {
+ break;
+ }
+ }
+ out
+}
+
+fn push_if_fits(out: &mut String, value: &str, reserve: usize) -> bool {
+ if out.len().checked_add(value.len()).and_then(|n| n.checked_add(reserve)).is_none_or(|n| n > MAX_HTML) {
+ return false;
+ }
+ out.push_str(value);
+ true
+}
+
+fn push_escaped_bounded(out: &mut String, text: &str, reserve: usize) -> bool {
+ for c in text.chars() {
+ match c {
+ '&' => {
+ if !push_if_fits(out, "&", reserve) {
+ return false;
+ }
+ }
+ '<' => {
+ if !push_if_fits(out, "<", reserve) {
+ return false;
+ }
+ }
+ '>' => {
+ if !push_if_fits(out, ">", reserve) {
+ return false;
+ }
+ }
+ _ => {
+ if out.len().checked_add(c.len_utf8()).and_then(|n| n.checked_add(reserve)).is_none_or(|n| n > MAX_HTML) {
+ return false;
+ }
+ out.push(c);
+ }
+ }
+ }
+ true
+}
+
fn tags(props: &RunProps) -> (String, String) {
let mut open = String::new();
let mut close = String::new();
@@ -53,17 +123,6 @@ fn tags(props: &RunProps) -> (String, String) {
(open, close)
}
-fn push_escaped(out: &mut String, text: &str) {
- for c in text.chars() {
- match c {
- '&' => out.push_str("&"),
- '<' => out.push_str("<"),
- '>' => out.push_str(">"),
- _ => out.push(c),
- }
- }
-}
-
/// The formatted text of an HTML fragment, or `None` when it has no text.
///
/// Understands ``, ``, ``, ``, ``, `
`, paragraphs, and a `style` that
@@ -436,6 +495,21 @@ mod tests {
assert!(last.props.underline.is_some());
}
+ #[test]
+ fn oversized_escaped_rich_text_falls_back_to_bounded_plain_html() {
+ let mut body = TextBody::from_text(&"&<🦀".repeat(100_000));
+ body.paragraphs[0].runs[0].props.bold = Some(true);
+ body.paragraphs.push(Paragraph::new("later paragraph"));
+
+ let html = body_to_html(&body);
+ assert!(html.len() <= MAX_HTML);
+ assert!(html.ends_with(""));
+ let parsed = html_to_body(&html).expect("exported fallback must be valid HTML");
+ assert_eq!(parsed.paragraphs[0].runs[0].props.bold, None);
+ assert_eq!(parsed.paragraphs.len(), 1);
+ assert!(parsed.text().starts_with("&<🦀"));
+ }
+
#[test]
fn bare_ampersand_keeps_text_up_to_next_semicolon() {
let text = |html: &str| html_to_body(html).unwrap().text();
From 4d2c1a3bac16b5f96b66305e73f8128ccfbb9f74 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Fri, 30 Oct 2026 16:00:36 -0400
Subject: [PATCH 2/2] Clipboard HTML: put no HTML on the clipboard when even
plain text is over the cap
Co-Authored-By: Claude Opus 5.5
---
crates/engine/src/cmd/richclip.rs | 31 ++++++++++++-------------------
1 file changed, 12 insertions(+), 19 deletions(-)
diff --git a/crates/engine/src/cmd/richclip.rs b/crates/engine/src/cmd/richclip.rs
index 063bd392..6e62b67d 100644
--- a/crates/engine/src/cmd/richclip.rs
+++ b/crates/engine/src/cmd/richclip.rs
@@ -42,28 +42,26 @@ pub fn body_to_html(body: &TextBody) -> String {
out
}
+/// The text without formatting, or "" (no HTML on the clipboard) when even that is too big: a
+/// cut-off prefix would paste as if it were the whole text.
fn plain_html(body: &TextBody) -> String {
let mut out = String::new();
for (i, p) in body.paragraphs.iter().enumerate() {
let prefix = if i > 0 { "
" } else { "
" };
if !push_if_fits(&mut out, prefix, 4) {
- break;
+ return String::new();
}
- let mut complete = true;
for r in &p.runs {
- if matches!(&r.kind, deckcraft_model::text::RunKind::Break) {
- complete = push_if_fits(&mut out, "
", 4);
+ let fits = if matches!(&r.kind, deckcraft_model::text::RunKind::Break) {
+ push_if_fits(&mut out, "
", 4)
} else {
- complete = push_escaped_bounded(&mut out, &r.text, 4);
- }
- if !complete {
- break;
+ push_escaped_bounded(&mut out, &r.text, 4)
+ };
+ if !fits {
+ return String::new();
}
}
out.push_str("
");
- if !complete {
- break;
- }
}
out
}
@@ -496,18 +494,13 @@ mod tests {
}
#[test]
- fn oversized_escaped_rich_text_falls_back_to_bounded_plain_html() {
+ fn oversized_escaped_rich_text_puts_no_html_on_the_clipboard() {
let mut body = TextBody::from_text(&"&<🦀".repeat(100_000));
body.paragraphs[0].runs[0].props.bold = Some(true);
body.paragraphs.push(Paragraph::new("later paragraph"));
- let html = body_to_html(&body);
- assert!(html.len() <= MAX_HTML);
- assert!(html.ends_with(""));
- let parsed = html_to_body(&html).expect("exported fallback must be valid HTML");
- assert_eq!(parsed.paragraphs[0].runs[0].props.bold, None);
- assert_eq!(parsed.paragraphs.len(), 1);
- assert!(parsed.text().starts_with("&<🦀"));
+ // Neither the formatted nor the plain HTML fits, and a truncated prefix would lose text.
+ assert!(body_to_html(&body).is_empty());
}
#[test]