mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Security: linear work for shortcut review, gradients, recovery names, include cycles, batch references, DOCX style ids, CAD layouts and long strokes
CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185: - Shortcut import review: example names for each code in one pass over the rows. - Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its increasing positions with one advancing search (`Gradient::sample_increasing_with`). - Recovery: image entry names are looked up by address, and dead ones swept when the map doubles, not on every image. - Script checker: include cycles from strongly connected components (iterative Tarjan), not a reachability row per file. - Batch step references copy at most 256 MB of values for one step. - DOCX: style ids over 256 bytes are ignored where defined and where used. - DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit. - Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep at most 100,000 samples, and the stroke kept is previewed in full before it is committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
bc56851904
commit
20bba9ebae
@@ -77,6 +77,10 @@ const MAX_BASED_ON: usize = 32;
|
||||
const MAX_FONTS: usize = 4096;
|
||||
/// The most bytes of a font name kept.
|
||||
const MAX_NAME: usize = 256;
|
||||
/// The most bytes of a style id: a longer one (Word's are under 256 characters) is ignored, both
|
||||
/// where it is defined and where it is used, since every paragraph and run looks its style up by
|
||||
/// it.
|
||||
const MAX_STYLE_ID: usize = 256;
|
||||
/// The most paragraphs placed type takes from one Word document.
|
||||
const MAX_PARAGRAPHS: usize = 100_000;
|
||||
/// The most runs of one character formatting placed type takes from one Word document.
|
||||
@@ -484,7 +488,7 @@ impl Reader {
|
||||
}
|
||||
}
|
||||
Some("style") if self.styles.len() < MAX_STYLES => {
|
||||
let Some(id) = attr(n, "styleId") else { continue };
|
||||
let Some(id) = attr(n, "styleId").filter(|id| id.len() <= MAX_STYLE_ID) else { continue };
|
||||
let default = match attr(n, "type") {
|
||||
Some("paragraph") => Some(&mut self.default_style),
|
||||
Some("character") => Some(&mut self.default_char_style),
|
||||
@@ -495,7 +499,7 @@ impl Reader {
|
||||
}
|
||||
let run = child(n, "rPr").map(|r| self.run_props(r)).unwrap_or_default();
|
||||
let para = child(n, "pPr").map(para_props).unwrap_or_default();
|
||||
let based_on = child(n, "basedOn").and_then(|b| attr(b, "val")).map(str::to_string);
|
||||
let based_on = child(n, "basedOn").and_then(|b| attr(b, "val")).filter(|v| v.len() <= MAX_STYLE_ID).map(str::to_string);
|
||||
self.styles.entry(id.to_string()).or_insert(Style { based_on, run, para });
|
||||
}
|
||||
_ => {}
|
||||
@@ -637,7 +641,12 @@ impl Reader {
|
||||
fn paragraph(&mut self, p: Node, depth: usize) {
|
||||
self.flush();
|
||||
let ppr = child(p, "pPr");
|
||||
let style = ppr.and_then(|n| child(n, "pStyle")).and_then(|n| attr(n, "val")).map(str::to_string).or_else(|| self.default_style.clone());
|
||||
let style = ppr
|
||||
.and_then(|n| child(n, "pStyle"))
|
||||
.and_then(|n| attr(n, "val"))
|
||||
.filter(|v| v.len() <= MAX_STYLE_ID)
|
||||
.map(str::to_string)
|
||||
.or_else(|| self.default_style.clone());
|
||||
let mut para = Para::default();
|
||||
para.apply(&self.default_para);
|
||||
let mut base = Chars::default();
|
||||
@@ -688,7 +697,11 @@ impl Reader {
|
||||
fn run(&mut self, r: Node) {
|
||||
let rpr = child(r, "rPr");
|
||||
let mut chars = self.base;
|
||||
let style = rpr.and_then(|n| child(n, "rStyle")).and_then(|n| attr(n, "val")).or(self.default_char_style.as_deref());
|
||||
let style = rpr
|
||||
.and_then(|n| child(n, "rStyle"))
|
||||
.and_then(|n| attr(n, "val"))
|
||||
.filter(|v| v.len() <= MAX_STYLE_ID)
|
||||
.or(self.default_char_style.as_deref());
|
||||
for s in self.chain(style) {
|
||||
chars.apply(&s.run, true);
|
||||
}
|
||||
|
||||
@@ -589,17 +589,35 @@ static WRITES: Mutex<()> = Mutex::new(());
|
||||
type Named = (Weak<Vec<u8>>, String);
|
||||
|
||||
/// The entry names of image bytes already hashed ([`blob_name`]), so an image is hashed once,
|
||||
/// not at every interval. Bytes behind a live `Weak` never change (`Arc::make_mut` copies them).
|
||||
static NAMES: Mutex<Vec<Named>> = Mutex::new(Vec::new());
|
||||
/// not at every interval, keyed by the bytes' address. Bytes behind a live `Weak` never change
|
||||
/// (`Arc::make_mut` copies them).
|
||||
static NAMES: Mutex<Names> = Mutex::new(Names { by_address: None, live_after_sweep: 0 });
|
||||
|
||||
struct Names {
|
||||
by_address: Option<std::collections::HashMap<usize, Named>>,
|
||||
/// How many entries were live at the last sweep of dead ones: the next sweep waits until there
|
||||
/// are twice as many, so sweeping costs a constant per name, not a pass over all of them.
|
||||
live_after_sweep: usize,
|
||||
}
|
||||
|
||||
/// The name of the entry holding `bytes`: from their content (128 bits of hash and the length), so
|
||||
/// identical images share one entry and one written before isn't written again.
|
||||
fn blob_name(bytes: &Arc<Vec<u8>>) -> String {
|
||||
let mut names = NAMES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
names.retain(|(w, _)| w.strong_count() > 0);
|
||||
if let Some((_, name)) = names.iter().find(|(w, _)| std::ptr::eq(w.as_ptr(), Arc::as_ptr(bytes))) {
|
||||
let mut guard = NAMES.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let Names { by_address, live_after_sweep } = &mut *guard;
|
||||
let names = by_address.get_or_insert_with(Default::default);
|
||||
let address = Arc::as_ptr(bytes) as usize;
|
||||
// A dead entry at this address named other bytes that were freed there before.
|
||||
if let Some((w, name)) = names.get(&address)
|
||||
&& w.strong_count() > 0
|
||||
&& std::ptr::eq(w.as_ptr(), Arc::as_ptr(bytes))
|
||||
{
|
||||
return name.clone();
|
||||
}
|
||||
if names.len() >= (*live_after_sweep * 2).max(64) {
|
||||
names.retain(|_, (w, _)| w.strong_count() > 0);
|
||||
*live_after_sweep = names.len();
|
||||
}
|
||||
let hash = |seed: u8| {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
seed.hash(&mut h);
|
||||
@@ -607,7 +625,7 @@ fn blob_name(bytes: &Arc<Vec<u8>>) -> String {
|
||||
h.finish()
|
||||
};
|
||||
let name = format!("img-{:016x}{:016x}-{}{BLOB_EXT}", hash(1), hash(2), bytes.len());
|
||||
names.push((Arc::downgrade(bytes), name.clone()));
|
||||
names.insert(address, (Arc::downgrade(bytes), name.clone()));
|
||||
name
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user