mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
M7: #1360 review: cap the clipboard PNG's bytes before copying it
The pixel cap reads only the PNG's header, so a small image followed by a large tail of data passed it and was copied and base64-encoded whole. MAX_CLIPBOARD_PNG_BYTES (256 MB, the same cap as design data read from the clipboard) is checked on the NSData length before the copy on macOS, and again in the paste path so every Services implementation is held to it. A PNG past the cap is treated like an unreadable one: paste falls back to the decoded image.
This commit is contained in:
@@ -445,7 +445,7 @@ impl Services for Desktop {
|
||||
fn clipboard_get_png(&mut self) -> Option<Vec<u8>> {
|
||||
let board = objc2_app_kit::NSPasteboard::generalPasteboard();
|
||||
let png = board.dataForType(&objc2_foundation::NSString::from_str(MACOS_PASTEBOARD_PNG))?;
|
||||
Some(png.to_vec())
|
||||
(png.len() <= figcraft_ui_egui::MAX_CLIPBOARD_PNG_BYTES).then(|| png.to_vec())
|
||||
}
|
||||
fn open_url(&mut self, url: &str) {
|
||||
let _ = webbrowser::open(url);
|
||||
|
||||
@@ -284,7 +284,10 @@ mod state;
|
||||
pub(crate) use clock::repaint_in;
|
||||
pub use clock::{AUTO_SAVE_SECS, AUTOSAVE_SECS, FrameClock, ToastClock};
|
||||
pub(crate) use open::{drop_overlay, dropped_bytes, is_document, opens};
|
||||
pub use services::{ContentsFuture, DialogFuture, NoServices, ReadFuture, Services, browser_can_read, egui_copy_png, egui_copy_text, unreadable};
|
||||
pub use services::{
|
||||
ContentsFuture, DialogFuture, MAX_CLIPBOARD_PNG_BYTES, NoServices, ReadFuture, Services, browser_can_read, egui_copy_png, egui_copy_text,
|
||||
unreadable,
|
||||
};
|
||||
use services::{DialogDone, PickFuture, Picked, RepaintWaker};
|
||||
pub use state::{
|
||||
AutoSaveEncoding, CANVAS_MIN, LEFT_PANEL, LeftTab, MobilePanel, NARROW_WIDTH, PANEL_DEFAULT, RIGHT_PANEL, RightTab, TitleBar, fit_panels,
|
||||
|
||||
@@ -101,7 +101,8 @@ fn paste_text(app: &mut FigApp, text: &str, (cx, cy): (f64, f64), center: bool)
|
||||
|
||||
/// The system clipboard's image as PNG bytes, with its size.
|
||||
fn clipboard_png(app: &mut FigApp) -> Option<(Vec<u8>, f64, f64)> {
|
||||
let (png, (w, h)) = match app.services.clipboard_get_png().and_then(with_pixel_size) {
|
||||
let clipboard_png = app.services.clipboard_get_png().filter(|png| png.len() <= crate::MAX_CLIPBOARD_PNG_BYTES);
|
||||
let (png, (w, h)) = match clipboard_png.and_then(with_pixel_size) {
|
||||
Some(readable) => readable,
|
||||
None => with_pixel_size(encoded_clipboard_image(app)?)?,
|
||||
};
|
||||
|
||||
@@ -534,3 +534,17 @@ fn an_unreadable_png_on_the_clipboard_falls_back_to_the_decoded_image() {
|
||||
let pasted = doc.nodes().find(|n| n.name == "Pasted image").expect("pasted from the decoded image");
|
||||
assert_eq!((pasted.width, pasted.height), (2.0, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipboard_png_past_the_byte_limit_falls_back_to_the_decoded_image() {
|
||||
let mut w = Win::new();
|
||||
let mut png = std::io::Cursor::new(Vec::new());
|
||||
image::RgbaImage::new(4, 4).write_to(&mut png, image::ImageFormat::Png).unwrap();
|
||||
let mut png = png.into_inner();
|
||||
png.resize(crate::MAX_CLIPBOARD_PNG_BYTES + 1, 0);
|
||||
*w.clip.borrow_mut() = Clip { png: Some(png), image: Some((2, 2, vec![255; 16])), ..Clip::default() };
|
||||
w.cmd_v(CMD);
|
||||
let doc = w.app.session.doc().unwrap();
|
||||
let pasted = doc.nodes().find(|n| n.name == "Pasted image").expect("pasted from the decoded image");
|
||||
assert_eq!((pasted.width, pasted.height), (2.0, 2.0));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
pub const MAX_CLIPBOARD_PNG_BYTES: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// Platform services the app crate provides (file dialogs, clipboard, URLs).
|
||||
pub trait Services {
|
||||
fn pick_open(&mut self, _title: &str, _exts: &[&str]) -> DialogFuture {
|
||||
|
||||
Reference in New Issue
Block a user