mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 17:44:02 +00:00
Native files: a document past a read budget is refused after one read
When the pass that reads the document and the saved PDF preference together failed with a data error, open read the whole document a second time without the preference. A document past a read budget, or with any other data error, was then read up to the budget again before it was refused. Open now reads it again only when the first pass stopped at a pdfCompatible that is not a boolean, which it tells by reading the preference alone over the bytes up to the first pass's error position.
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
|
||||
// PDF are skipped without being kept.
|
||||
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::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())) {
|
||||
Ok(read) => read,
|
||||
// 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
|
||||
// the preference a second time, so such a file opens as before.
|
||||
Err(ReadError::Json(e)) if e.is_data() => {
|
||||
// When it stopped at the preference, the file is read again without it, and only then is
|
||||
// its JSON scanned for the preference a second time, so such a file opens as before. Any
|
||||
// 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 pdf_compatible = pdf_member(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
||||
(f, pdf_compatible)
|
||||
@@ -631,6 +634,51 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result
|
||||
thread_local! {
|
||||
/// 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) };
|
||||
/// 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
|
||||
@@ -842,9 +890,44 @@ mod tests {
|
||||
value["pdfCompatible"] = odd.clone();
|
||||
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");
|
||||
DOCUMENT_READS.with(|n| n.set(0));
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user