mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 17:44:02 +00:00
Merge pull request #6524 from dotCooCoo/fix/native-open-budget-no-reread
Native files: a document past a read budget is refused after one read
This commit is contained in:
@@ -511,6 +511,8 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
|||||||
// fields read before an object's type at `MAX_BUFFERED_VALUES` values. The preview and the
|
// fields read before an object's type at `MAX_BUFFERED_VALUES` values. The preview and the
|
||||||
// PDF are skipped without being kept.
|
// PDF are skipped without being kept.
|
||||||
fn within_budgets<T>(max_nodes: usize, max_values: usize, read: impl FnOnce() -> T) -> T {
|
fn within_budgets<T>(max_nodes: usize, max_values: usize, read: impl FnOnce() -> T) -> T {
|
||||||
|
#[cfg(test)]
|
||||||
|
DOCUMENT_READS.with(|n| n.set(n.get() + 1));
|
||||||
vectorcraft_doc::geom::path::with_anchor_budget(vectorcraft_doc::geom::path::MAX_READ_ANCHORS, || {
|
vectorcraft_doc::geom::path::with_anchor_budget(vectorcraft_doc::geom::path::MAX_READ_ANCHORS, || {
|
||||||
vectorcraft_doc::node::with_node_budget(max_nodes, || vectorcraft_doc::node::with_buffered_value_budget(max_values, read))
|
vectorcraft_doc::node::with_node_budget(max_nodes, || vectorcraft_doc::node::with_buffered_value_budget(max_values, read))
|
||||||
})
|
})
|
||||||
@@ -527,9 +529,10 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
|||||||
let (mut f, pdf_compatible) = match within_budgets(max_nodes, max_values, || Contents::<Document>::read_with_pdf_preference(file.json())) {
|
let (mut f, pdf_compatible) = match within_budgets(max_nodes, max_values, || Contents::<Document>::read_with_pdf_preference(file.json())) {
|
||||||
Ok(read) => read,
|
Ok(read) => read,
|
||||||
// That pass refuses a `pdfCompatible` that is not a boolean, and any other data error.
|
// That pass refuses a `pdfCompatible` that is not a boolean, and any other data error.
|
||||||
// The file is read again without the preference, and only then is its JSON scanned for
|
// When it stopped at the preference, the file is read again without it, and only then is
|
||||||
// the preference a second time, so such a file opens as before.
|
// its JSON scanned for the preference a second time, so such a file opens as before. Any
|
||||||
Err(ReadError::Json(e)) if e.is_data() => {
|
// other error, such as a document past a budget, is returned without a second read.
|
||||||
|
Err(ReadError::Json(e)) if e.is_data() && refused_preference_at(file.json(), &e) => {
|
||||||
let f = within_budgets(max_nodes, max_values, || Contents::<Document>::read(file.json())).map_err(read_error)?;
|
let f = within_budgets(max_nodes, max_values, || Contents::<Document>::read(file.json())).map_err(read_error)?;
|
||||||
let pdf_compatible = pdf_member(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
let pdf_compatible = pdf_member(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
||||||
(f, pdf_compatible)
|
(f, pdf_compatible)
|
||||||
@@ -631,6 +634,51 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
|||||||
thread_local! {
|
thread_local! {
|
||||||
/// Tests: how many times a file's JSON was scanned again for its PDF preference on this thread.
|
/// Tests: how many times a file's JSON was scanned again for its PDF preference on this thread.
|
||||||
static PDF_MEMBER_SCANS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
static PDF_MEMBER_SCANS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
||||||
|
/// Tests: how many times a file's document was read on this thread.
|
||||||
|
static DOCUMENT_READS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether reading `json`'s `pdfCompatible` as a boolean or null fails with a data error at the
|
||||||
|
/// line and column of `e`, that is, whether the read that gave `e` stopped at the preference. Only
|
||||||
|
/// the bytes up to that position, and the one there, are read, and they are skipped without being
|
||||||
|
/// kept.
|
||||||
|
fn refused_preference_at(json: &[u8], e: &serde_json::Error) -> bool {
|
||||||
|
// serde_json gives a 1-based line and the bytes before the position on that line.
|
||||||
|
let Some(line_start) = (match e.line() {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(0),
|
||||||
|
n => json.iter().enumerate().filter(|(_, b)| **b == b'\n').nth(n - 2).map(|(i, _)| i + 1),
|
||||||
|
}) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let end = line_start.saturating_add(e.column()).saturating_add(1).min(json.len());
|
||||||
|
struct StrictPreference;
|
||||||
|
impl<'de> Visitor<'de> for StrictPreference {
|
||||||
|
type Value = ();
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("a native file envelope")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<(), A::Error> {
|
||||||
|
while let Some(key) = map.next_key::<String>()? {
|
||||||
|
if key == "pdfCompatible" {
|
||||||
|
map.next_value::<Option<bool>>()?;
|
||||||
|
} else {
|
||||||
|
map.next_value::<IgnoredAny>()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<(), A::Error> {
|
||||||
|
while seq.next_element::<IgnoredAny>()?.is_some() {}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Deserializer::from_slice(&json[..end])
|
||||||
|
.deserialize_any(StrictPreference)
|
||||||
|
.is_err_and(|s| s.is_data() && s.line() == e.line() && s.column() == e.column())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The saved PDF preference, falling back to the presence of its payload without retaining or
|
/// The saved PDF preference, falling back to the presence of its payload without retaining or
|
||||||
@@ -842,9 +890,44 @@ mod tests {
|
|||||||
value["pdfCompatible"] = odd.clone();
|
value["pdfCompatible"] = odd.clone();
|
||||||
assert_eq!(scans_for(&serde_json::to_vec(&value).unwrap()), (false, 1), "{odd}");
|
assert_eq!(scans_for(&serde_json::to_vec(&value).unwrap()), (false, 1), "{odd}");
|
||||||
}
|
}
|
||||||
// A document that can't be read still fails, after the second read.
|
// A document that can't be read still fails. Read before the preference, it is not read
|
||||||
|
// a second time.
|
||||||
value["document"] = serde_json::json!("not a document");
|
value["document"] = serde_json::json!("not a document");
|
||||||
|
DOCUMENT_READS.with(|n| n.set(0));
|
||||||
assert!(load_info(&serde_json::to_vec(&value).unwrap()).is_err());
|
assert!(load_info(&serde_json::to_vec(&value).unwrap()).is_err());
|
||||||
|
assert_eq!(DOCUMENT_READS.with(|n| n.get()), 1);
|
||||||
|
// Read after a preference that is not a boolean, it fails on the second read, which is
|
||||||
|
// the first to reach it.
|
||||||
|
let mut first = serde_json::to_vec(&value).unwrap();
|
||||||
|
assert!(first.starts_with(br#"{"document""#));
|
||||||
|
first.splice(1..1, br#""pdfCompatible":"yes","#.iter().copied());
|
||||||
|
DOCUMENT_READS.with(|n| n.set(0));
|
||||||
|
assert!(load_info(&first).is_err());
|
||||||
|
assert_eq!(DOCUMENT_READS.with(|n| n.get()), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document past a read budget is refused after one read, also in a file whose preference
|
||||||
|
/// is not a boolean.
|
||||||
|
#[test]
|
||||||
|
fn a_document_past_a_budget_is_read_once() {
|
||||||
|
let mut d = Document::new(100.0, 100.0);
|
||||||
|
let l = d.add_layer(None);
|
||||||
|
for i in 0..20 {
|
||||||
|
let id = d.alloc_id();
|
||||||
|
d.insert(Some(l), i, Node::path(id, shapes::ellipse(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art())).unwrap();
|
||||||
|
}
|
||||||
|
let n = d.node_count();
|
||||||
|
let mut value: serde_json::Value = serde_json::from_slice(&save(&d, false)).unwrap();
|
||||||
|
for preference in [serde_json::Value::Null, serde_json::json!(true), serde_json::json!("yes")] {
|
||||||
|
value["pdfCompatible"] = preference.clone();
|
||||||
|
let bytes = serde_json::to_vec(&value).unwrap();
|
||||||
|
DOCUMENT_READS.with(|r| r.set(0));
|
||||||
|
let Err(FormatError::NotVectorcraft(why)) = load_file_within(&bytes, n - 1, vectorcraft_doc::node::MAX_BUFFERED_VALUES) else {
|
||||||
|
panic!("one object past the budget must be refused")
|
||||||
|
};
|
||||||
|
assert!(why.contains(&format!("more than {} objects", n - 1)), "{why}");
|
||||||
|
assert_eq!(DOCUMENT_READS.with(|r| r.get()), 1, "{preference}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user