mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 12:00:50 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c1b9a3f54 |
@@ -157,7 +157,7 @@ fn write_audit_log(_: &Session, _: &crate::audit::Report) {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn read_path(cmd: &str, path: &str) -> Result<Vec<u8>> {
|
||||
crate::capped_read::read_capped(std::path::Path::new(path), file::MAX_DRAWING_BYTES).map_err(|e| bad(cmd, format!("{path}: {e}")))
|
||||
std::fs::read(path).map_err(|e| bad(cmd, format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -85,8 +85,7 @@ fn read_drawing(path: &str) -> Result<Drawing> {
|
||||
fn read_file(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), crate::cmd::file::MAX_DRAWING_BYTES)
|
||||
.map_err(|e| bad("insert", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("insert", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("insert", "paths are not available on the web"))
|
||||
|
||||
@@ -204,8 +204,7 @@ fn import_drawing(s: &mut Session, src: &Drawing, at: Vec2, scale: f64, file: &s
|
||||
fn read_file(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), crate::cmd::file::MAX_DRAWING_BYTES)
|
||||
.map_err(|e| bad("import", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("import", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("import", "paths are not available on the web; pass `data`"))
|
||||
|
||||
@@ -24,7 +24,7 @@ pub use template::{qnew_template, set_qnew_template};
|
||||
/// endless device must be rejected before it exhausts memory (CWE-400), so native opens read
|
||||
/// through [`crate::capped_read`] rather than buffering to EOF.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) const MAX_DRAWING_BYTES: usize = 512 << 20;
|
||||
const MAX_DRAWING_BYTES: usize = 512 << 20;
|
||||
/// Largest `.scr` script accepted from a local path.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_SCRIPT_BYTES: usize = 64 << 20;
|
||||
|
||||
@@ -110,7 +110,7 @@ fn other_drawing(s: &Session, p: &Value) -> Result<(Arc<Drawing>, String)> {
|
||||
fn read_path(_path: &str) -> Result<Vec<u8>> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::capped_read::read_capped(std::path::Path::new(_path), super::MAX_DRAWING_BYTES).map_err(|e| bad("compare", format!("{_path}: {e}")))
|
||||
std::fs::read(_path).map_err(|e| bad("compare", format!("{_path}: {e}")))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Err(bad("compare", "paths are not available on the web: open the other drawing and compare with it by name (`document`)"))
|
||||
|
||||
@@ -73,7 +73,7 @@ fn read_template(path: &str) -> Result<Drawing> {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn read_bytes(path: &str) -> Result<Vec<u8>> {
|
||||
crate::capped_read::read_capped(std::path::Path::new(path), super::MAX_DRAWING_BYTES).map_err(|e| bad("new", format!("template {path}: {e}")))
|
||||
std::fs::read(path).map_err(|e| bad("new", format!("template {path}: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -303,9 +303,11 @@ fn run_stylesmanager(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(None, Some(_path)) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let bytes = crate::capped_read::read_capped(std::path::Path::new(_path), cadcraft_doc::MAX_TABLE_FILE)
|
||||
.map_err(|e| bad(cmd, format!("{_path}: {e}")))?;
|
||||
String::from_utf8(bytes).map_err(|e| bad(cmd, format!("{_path}: {e}")))?
|
||||
let meta = std::fs::metadata(_path).map_err(|e| bad(cmd, format!("{_path}: {e}")))?;
|
||||
if meta.len() > cadcraft_doc::MAX_TABLE_FILE as u64 {
|
||||
return Err(bad(cmd, format!("{_path}: the plot style table file is too large")));
|
||||
}
|
||||
std::fs::read_to_string(_path).map_err(|e| bad(cmd, format!("{_path}: {e}")))?
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Err(bad(cmd, "files can't be read here: pass `text`"));
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
/// Largest linked file read (bytes).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_LINK_BYTES: usize = 8 << 20;
|
||||
const MAX_LINK_BYTES: u64 = 8 << 20;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -238,9 +238,11 @@ fn read_link(id: &str, file: &std::path::Path) -> Result<String> {
|
||||
let path = file.display();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// The limit is enforced on the bytes read, not on reported metadata, so a device or FIFO
|
||||
// that misreports its length can't be buffered to EOF.
|
||||
let bytes = crate::capped_read::read_capped(file, MAX_LINK_BYTES).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
let meta = std::fs::metadata(file).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
if meta.len() > MAX_LINK_BYTES {
|
||||
return Err(bad(id, format!("{path}: larger than {} MB", MAX_LINK_BYTES >> 20)));
|
||||
}
|
||||
let bytes = std::fs::read(file).map_err(|e| bad(id, format!("{path}: {e}")))?;
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
Ok(text.strip_prefix('\u{feff}').unwrap_or(&text).to_owned())
|
||||
}
|
||||
@@ -368,16 +370,6 @@ fn run_update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A linked path naming an endless device is refused instead of buffered to EOF.
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
#[test]
|
||||
fn link_to_endless_device_is_rejected() {
|
||||
let dev = std::path::Path::new("/dev/zero");
|
||||
if dev.exists() {
|
||||
assert!(read_link("datalink", dev).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_parsing() {
|
||||
assert_eq!(parse_csv("a,b\r\n\"c,d\",\"e\"\"f\"\n", ','), vec![vec!["a", "b"], vec!["c,d", "e\"f"]]);
|
||||
|
||||
@@ -844,15 +844,29 @@ pub(crate) fn apply_anno(d: &mut cadcraft_doc::Drawing, p: AnnoPayload) {
|
||||
std::sync::Arc::make_mut(b).annotative = true;
|
||||
}
|
||||
}
|
||||
// Which layout holds each paper space object, found once: `modify_entity` searches every
|
||||
// layout for a handle model space lacks, which a long list of missing handles made quadratic.
|
||||
let in_layout: std::collections::HashMap<Handle, usize> = if p.objects.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
d.layouts.iter().enumerate().flat_map(|(i, l)| l.entities.handles().into_iter().map(move |h| (h, i))).collect()
|
||||
};
|
||||
for o in p.objects {
|
||||
let Some(h) = Handle::parse_hex(&o.handle) else { continue };
|
||||
let store = if d.model.contains(h) {
|
||||
&mut d.model
|
||||
} else if let Some(l) = in_layout.get(&h).and_then(|&i| d.layouts.get_mut(i)) {
|
||||
&mut l.entities
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut scales: Vec<cadcraft_doc::AnnoScale> = Vec::new();
|
||||
for s in o.scales.into_iter().filter(|s| s.is_valid() && !s.name.trim().is_empty()) {
|
||||
if scales.len() < cadcraft_doc::MAX_SCALES && !scales.iter().any(|x| x.name.eq_ignore_ascii_case(&s.name)) {
|
||||
scales.push(s);
|
||||
}
|
||||
}
|
||||
let _ = d.modify_entity(h, |e| {
|
||||
store.modify(h, |e| {
|
||||
if cadcraft_doc::can_be_annotative(&e.kind) {
|
||||
e.common.anno_scales = scales;
|
||||
}
|
||||
|
||||
@@ -339,7 +339,8 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
|
||||
}
|
||||
let root = objects.first().filter(|(k, _, _)| is_dictionary(k)).and_then(|(_, _, t)| head(t).handle);
|
||||
let root_entries = objects.first().filter(|(k, _, _)| is_dictionary(k)).map(|(_, _, t)| entries(t)).unwrap_or_default();
|
||||
let root_key = |h: &str| root_entries.iter().find(|(_, eh)| eh == h).map(|(k, _)| k.clone());
|
||||
let root_keys = root_key_index(&root_entries);
|
||||
let root_key = |h: &str| root_keys.get(h).map(|k| (*k).to_string());
|
||||
// An extension dictionary of an entity or record (not of another object).
|
||||
let is_xdict = |k: usize| {
|
||||
let Some((_, _, t)) = objects.get(k) else { return false };
|
||||
@@ -482,6 +483,16 @@ fn extension_dictionaries(
|
||||
dictionaries
|
||||
}
|
||||
|
||||
/// The root dictionary's key for each entry handle (the first key when several name one handle),
|
||||
/// built once: a lookup per object scanning the entries made a large dictionary quadratic.
|
||||
fn root_key_index(root_entries: &[(String, String)]) -> HashMap<&str, &str> {
|
||||
let mut index = HashMap::with_capacity(root_entries.len());
|
||||
for (key, handle) in root_entries {
|
||||
index.entry(handle.as_str()).or_insert(key.as_str());
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
/// Determine which objects belong to unmodelled dictionary entries or owners.
|
||||
fn retained_objects(
|
||||
objects: &[(String, usize, &[Tag])],
|
||||
@@ -492,7 +503,8 @@ fn retained_objects(
|
||||
root_entries: &[(String, String)],
|
||||
xdict_of: &HashMap<String, String>,
|
||||
) -> HashMap<usize, bool> {
|
||||
let root_key = |handle: &str| root_entries.iter().find(|(_, entry_handle)| entry_handle == handle).map(|(key, _)| key);
|
||||
let root_keys = root_key_index(root_entries);
|
||||
let root_key = |handle: &str| root_keys.get(handle).copied();
|
||||
let is_xdict = |index: usize| {
|
||||
let Some((_, _, tags)) = objects.get(index) else { return false };
|
||||
let header = head(tags);
|
||||
|
||||
@@ -1170,26 +1170,37 @@ fn add_class_instance_counts(tags: &mut Vec<Tag>, target: Version) {
|
||||
}
|
||||
}
|
||||
|
||||
// One pass into a new list: inserting each missing count in place shifted every later tag,
|
||||
// quadratic in the number of CLASS records.
|
||||
let old = std::mem::take(tags);
|
||||
tags.reserve(old.len());
|
||||
let mut start = 0;
|
||||
while start < tags.len() {
|
||||
if tags.get(start).is_none_or(|t| t.code != 0 || t.str() != "CLASS") {
|
||||
while let Some(t) = old.get(start) {
|
||||
if t.code != 0 || t.str() != "CLASS" {
|
||||
tags.push(t.clone());
|
||||
start += 1;
|
||||
continue;
|
||||
}
|
||||
let end = tags[start + 1..].iter().position(|t| t.code == 0).map_or(tags.len(), |i| start + 1 + i);
|
||||
let name = tags[start + 1..end].iter().find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase();
|
||||
let end = old[start + 1..].iter().position(|t| t.code == 0).map_or(old.len(), |i| start + 1 + i);
|
||||
let record = &old[start..end];
|
||||
let name = record.iter().skip(1).find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase();
|
||||
let count = counts.get(&name).copied().unwrap_or(0);
|
||||
let inserted = if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 91) {
|
||||
tags[start + 1 + i] = Tag::i(91, count);
|
||||
false
|
||||
} else if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 90) {
|
||||
tags.insert(start + 2 + i, Tag::i(91, count));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
// Inserting the group shifts the next record's start by one.
|
||||
start = end + usize::from(inserted);
|
||||
// The first 91 gets the count; without one, a 91 goes in after the first 90.
|
||||
let mut added = record.iter().skip(1).any(|t| t.code == 91);
|
||||
let mut replaced = false;
|
||||
for (i, t) in record.iter().enumerate() {
|
||||
if i > 0 && t.code == 91 && !replaced {
|
||||
tags.push(Tag::i(91, count));
|
||||
replaced = true;
|
||||
continue;
|
||||
}
|
||||
tags.push(t.clone());
|
||||
if i > 0 && t.code == 90 && !added {
|
||||
tags.push(Tag::i(91, count));
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,3 +1228,31 @@ fn mark_xrefs(tags: &mut [Tag], d: &Drawing) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod class_count_tests {
|
||||
use super::*;
|
||||
|
||||
fn record(kind: &str, name: &str, extra: &[Tag]) -> Vec<Tag> {
|
||||
let mut r = vec![Tag::s(0, kind), Tag::s(1, name), Tag::i(90, 0)];
|
||||
r.extend_from_slice(extra);
|
||||
r
|
||||
}
|
||||
|
||||
/// The first 91 gets the count, a CLASS without one gets it after its 90, in one pass over many records.
|
||||
#[test]
|
||||
fn counts_are_set_and_added_in_one_pass() {
|
||||
let mut tags = record("CLASS", "FOO", &[]);
|
||||
tags.extend(record("CLASS", "BAR", &[Tag::i(91, 7), Tag::i(91, 8)]));
|
||||
for i in 0..50_000 {
|
||||
tags.extend(record("CLASS", &format!("X{i}"), &[]));
|
||||
}
|
||||
tags.push(Tag::s(0, "FOO"));
|
||||
let started = std::time::Instant::now();
|
||||
add_class_instance_counts(&mut tags, Version::R2018);
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(5));
|
||||
assert_eq!(&tags[..4], &[Tag::s(0, "CLASS"), Tag::s(1, "FOO"), Tag::i(90, 0), Tag::i(91, 1)]);
|
||||
assert_eq!(&tags[4..9], &[Tag::s(0, "CLASS"), Tag::s(1, "BAR"), Tag::i(90, 0), Tag::i(91, 0), Tag::i(91, 8)]);
|
||||
assert_eq!(tags.iter().filter(|t| t.code == 91).count(), 50_003);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +590,10 @@ fn hidden(e: &BytesStart) -> bool {
|
||||
}
|
||||
|
||||
fn attr(e: &BytesStart, name: &[u8]) -> Option<String> {
|
||||
for a in e.attributes().flatten() {
|
||||
// Stop at the first malformed attribute: skipping errors resumed the parser's duplicate check,
|
||||
// which rescans the attributes before it each time, so many duplicates were quadratic.
|
||||
for a in e.attributes() {
|
||||
let Ok(a) = a else { return None };
|
||||
if a.key.into_inner().as_bytes() == name {
|
||||
return a.normalized_value(XmlVersion::Explicit1_0).ok().map(|v| v.into_owned());
|
||||
}
|
||||
|
||||
@@ -337,6 +337,16 @@ fn entity_transparency_roundtrips_as_group_440() {
|
||||
assert_eq!(d.model.iter().map(|e| e.common.transparency).collect::<Vec<_>>(), [Transparency::ByLayer, Transparency::Percent(90)]);
|
||||
}
|
||||
|
||||
/// Elements with many duplicate attributes are read in bounded time.
|
||||
#[test]
|
||||
fn svg_duplicate_attributes_are_not_rescanned() {
|
||||
let dup = " x=\"1\"".repeat(20_000);
|
||||
let svg = format!("<svg xmlns=\"http://www.w3.org/2000/svg\">{}</svg>", format!("<rect{dup} width=\"1\" height=\"1\"/>").repeat(20));
|
||||
let started = std::time::Instant::now();
|
||||
let _ = read(svg.as_bytes(), "dup.svg");
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(10), "took {:?}", started.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_dxf_does_not_panic() {
|
||||
for t in [
|
||||
|
||||
Reference in New Issue
Block a user