Merge pull request #7607 from dotCooCoo/fix/paste-copied-file-size-limit

Paste: a copied file over 64 MiB is skipped before it is read
This commit is contained in:
BFlat
2026-11-15 03:19:47 +07:00
committed by GitHub
2 changed files with 76 additions and 8 deletions
+75 -8
View File
@@ -17,7 +17,8 @@
//! that copied to send its data (a bitmap 10 s), and until that app answers, later reads fail at
//! once; a paste then waits for that read's answer off the UI thread.
//! - Everywhere: files copied in a file manager paste as the first one that is art (SVG, PDF, EPS,
//! a metafile or a bitmap), before the clipboard's other formats (the files' paths as text).
//! a metafile or a bitmap), before the clipboard's other formats (the files' paths as text). A
//! file of more than 64 MiB is skipped before it is read.
use std::io::{Cursor, Read};
use std::path::PathBuf;
@@ -144,9 +145,18 @@ fn dib_image(dib: &[u8]) -> Option<image::RgbaImage> {
Some(img)
}
/// The most bytes a paste takes from another app (untrusted data), VectorCraft's own limit: a copied
/// file of more is skipped, and on Wayland a read of more fails.
const MAX_BYTES: usize = 64 << 20;
/// The first of `paths` (files copied in a file manager) that pastes as one of `mimes`, read
/// whole. Only its first bytes are read to tell, so other files cost little.
/// whole, at most [`MAX_BYTES`]. Only its first bytes are read to tell, so other files cost little.
fn copied_file(paths: Vec<PathBuf>, mimes: &[&'static str]) -> Option<Flavour> {
copied_file_within(paths, mimes, MAX_BYTES)
}
/// [`copied_file`] with a file of more than `limit` bytes skipped.
fn copied_file_within(paths: Vec<PathBuf>, mimes: &[&'static str], limit: usize) -> Option<Flavour> {
paths.into_iter().find_map(|path| {
// text/uri-list lines end in CRLF (RFC 2483), but arboard splits them on LF only.
let path = match path.to_str() {
@@ -164,14 +174,29 @@ fn copied_file(paths: Vec<PathBuf>, mimes: &[&'static str]) -> Option<Flavour> {
if !std::fs::metadata(&path).ok()?.is_file() {
return None;
}
let mut file = std::fs::File::open(&path).ok()?;
// The file opened may not be the one looked at: its size is checked before any read.
let meta = file.metadata().ok()?;
if !meta.is_file() || meta.len() > limit as u64 {
return None;
}
let mut head = Vec::with_capacity(FILE_HEAD);
std::fs::File::open(&path).ok()?.take(FILE_HEAD as u64).read_to_end(&mut head).ok()?;
(&mut file).take(FILE_HEAD as u64).read_to_end(&mut head).ok()?;
let mime = file_flavour(&path.to_string_lossy(), &head, mimes)?;
let data = std::fs::read(&path).ok()?;
let data = read_rest(file, head, limit)?;
(!data.is_empty()).then_some(Flavour { mime, data })
})
}
/// `data` (the first bytes read of `file`) followed by the rest of `file`, or `None` when they come
/// to more than `limit` bytes, as when the file grew after its size was checked. At most one byte
/// past `limit` is read.
fn read_rest(file: impl Read, mut data: Vec<u8>, limit: usize) -> Option<Vec<u8>> {
let more = (limit + 1).saturating_sub(data.len());
file.take(more as u64).read_to_end(&mut data).ok()?;
(data.len() <= limit).then_some(data)
}
/// The most URIs a copied file list may hold, VectorCraft's own limit: a longer list (untrusted
/// data) names no files.
const MAX_URIS: usize = 1024;
@@ -1900,7 +1925,7 @@ mod wayland {
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum};
use super::loads::{Asker, GONE, LOAD_WAIT, Loads, wait_for_answer};
use super::{BITMAP, Flavour, PNG, PathBuf, SVG, TEXT, copied_file, gnome_copied_paths, uri_list_paths};
use super::{BITMAP, Flavour, MAX_BYTES, PNG, PathBuf, SVG, TEXT, copied_file, gnome_copied_paths, uri_list_paths};
/// The types text is offered under and read from, best first: `text/plain;charset=utf-8` (GTK
/// and Qt apps), `UTF8_STRING` (X11 apps through XWayland) and `text/plain`.
@@ -1911,9 +1936,6 @@ mod wayland {
/// best first: `text/uri-list` (RFC 2483), and GNOME Files' own list, read only when the first
/// names no file.
const FILE_LISTS: [(&str, PathsOf); 2] = [("text/uri-list", uri_list_paths), ("x-special/gnome-copied-files", gnome_copied_paths)];
/// The most bytes a read takes from the app that copied (untrusted data), VectorCraft's own
/// limit: a read of more fails.
const MAX_BYTES: usize = 64 << 20;
/// The most pastes of a Copy's data that are sent at once; a paste beyond them gets nothing.
const MAX_SENDS: usize = 8;
/// The most types of an offer that are kept (untrusted data).
@@ -3176,6 +3198,51 @@ mod tests {
assert_eq!(copied_file(vec![with_cr], &PASTE_ORDER).map(|f| f.mime), Some(PNG));
}
/// A copied file of more than 64 MiB is skipped before it is read, and the next one pastes.
#[test]
fn an_oversized_copied_file_is_skipped_before_it_is_read() {
let d = dir("oversized");
let (big, small) = (d.path().join("big.png"), d.path().join("small.png"));
std::fs::write(&big, png(2, 2)).unwrap();
// Sparse where the file system allows: no 64 MiB is written.
std::fs::OpenOptions::new().write(true).open(&big).unwrap().set_len(MAX_BYTES as u64 + 1).unwrap();
std::fs::write(&small, png(3, 2)).unwrap();
assert!(copied_file(vec![big.clone()], &PASTE_ORDER).is_none());
let f = copied_file(vec![big, small], &PASTE_ORDER).unwrap();
assert_eq!((f.mime, f.data), (PNG, png(3, 2)));
}
/// A copied file of exactly the limit pastes; one byte more and it is skipped.
#[test]
fn a_copied_file_pastes_up_to_the_limit() {
let d = dir("limit");
let art = d.path().join("art.svg");
let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\"/>";
std::fs::write(&art, svg).unwrap();
let read = |limit| copied_file_within(vec![art.clone()], &PASTE_ORDER, limit).map(|f| f.data);
assert_eq!(read(svg.len()), Some(svg.as_bytes().to_vec()));
assert_eq!(read(svg.len() - 1), None);
}
/// A file that grows past the limit after its size was checked is read to one byte past the
/// limit and refused.
#[test]
fn the_rest_of_a_file_is_read_to_the_limit_at_most() {
let read = |len: usize, head: usize| {
let bytes = vec![7u8; len];
let mut file = Cursor::new(bytes);
let mut first = vec![0; head];
file.read_exact(&mut first).unwrap();
(read_rest(&mut file, first, 16).map(|d| d.len()), file.position())
};
assert_eq!(read(16, 4), (Some(16), 16));
assert_eq!(read(0, 0), (Some(0), 0));
assert_eq!(read(17, 4), (None, 17));
assert_eq!(read(1000, 4), (None, 17));
// First bytes already past the limit: nothing more is read.
assert_eq!(read(1000, 20), (None, 20));
}
fn uri_paths(list: &[u8]) -> Vec<String> {
uri_list_paths(list).into_iter().map(|p| p.to_string_lossy().into_owned()).collect()
}
+1
View File
@@ -1859,6 +1859,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- A look at the linked files whose worker thread ends without a result, such as after a panic, no longer stops later looks: the app clears it, stops asking for a frame every 100 ms, and the next look starts on schedule (`crates/engine/src/tests/linkwatch.rs`, `crates/ui-egui/src/tests/place.rs`).
- Import and paste, 2026-10-13: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499).
- A `.ai` or `.ait` file copied in a file manager pastes as its art: one that holds a PDF pastes the objects of its editing data, and one saved as PostScript pastes as a copied `.eps` file does.
- Paste checks the size of a file copied in a file manager before reading it: a file of more than 64 MiB, VectorCraft's own limit, is skipped and the next copied file that is art pastes. A file that grows past the limit while it is read is skipped too, after at most one byte more than the limit is read.
- A copied `.ai` or `.ait` file that holds a PDF pastes the objects of its editing data as File › Open reads them: every layer's objects go into the active layer in their stacking order, with their names, hidden objects and type objects, instead of the PDF page's art with its type as outlines. When the editing data can't be read, the file pastes its page as before (`clipboard.importPdf {editingData}`). The objects of a hidden layer come in hidden, from a pasted EPS too.
- Import and export, later 2026-10-13 merges: SVGs that move objects with a CSS transform (`translate(100px, 0)`, `rotate(45deg)`) or style type with the `font` shorthand import as drawn (#1565); SVG symbols whose shapes use percentage lengths import at the size and place they are drawn, the percentages being of the symbol's `viewBox` (#1596); Inkscape's flowed text (`flowRoot`) opens as area type in its rectangle, a paragraph per `flowPara`, instead of being dropped, and a region that can't be read is left out with a warning (#1569, fixes #1568); the warning for an unsupported SVG filter names the filter, its primitives and the object, and says the object is drawn without it (#1599, fixes #1598); the pages of an Inkscape multi-page SVG become artboards, named by their labels (#1603, fixes #1601); SVG export with CSS classes or style entities stays fast in documents with many distinct styles (#1567); PDF and `.ai` import: evenly letter-spaced or tightened type on a path keeps its length along the path instead of closing up to the font's advances (#1586), and type on a path whose stroke is written glyph by glyph comes in as one text object with its stroke, not one object per glyph (#1585); the ICC profiles embedded in JPEG, TIFF and PSD exports have every tag on a 4-byte boundary (ICC.1 §7.1.2), so strict readers accept them (#1589, fixes #1584); GIF, PNG-8 and other indexed exports find each pixel's palette colour through an index instead of a scan, with identical output (#1576); a zero-length subpath draws its round cap as a dot and its projecting cap as a square, so dots in imported `.ai` and SVG files show (#1619, fixes #1613).
- Files and untrusted input, 2026-10-14 merges: EPS import: a stitched shading keeps its hard colour changes (#1654, #1653). WMF/EMF import refuses an oversized embedded PNG or JPEG from its header, before any pixels are decoded (#1776). PDF open no longer takes exponential time on a crafted name tree with shared nodes when it looks for the editing data (#1781). The size limits on a native file's embedded data also apply to files read from memory, not only streamed ones (#1783). Saving over a private file on Unix no longer leaves a readable temporary copy beside it while the file is written (#1778). An Export for Screens suffix can no longer place files outside the export folder (#1777). Package refuses a folder where it would overwrite the open document or one of its linked files (#1786). ruzstd 0.9.1 fixes a decompression bomb (RUSTSEC-2026-0344) reachable from EPS/`.ai` and Affinity import (#1807). File Info dates are stamped only after a save succeeds, so a failed save no longer changes them (#1774). Linked files that change during a drag are updated after the drag instead of being missed (#1773).