mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Read the native container beyond its preview: the chain of allocation tables (#FAT, #FT2, #FT3, #FT4) with newest-revision selection, CRC-checked extraction of stored, zlib and zstd entries with their byte predictors, and the self-describing tagged object stream in doc.dat as a generic tree of shared, inline and positional objects. Every declared size is checked against per-entry and per-import budgets before allocation; the zstd window is capped at 64 MiB; table chains are cycle-checked; arrays are checked against the remaining bytes; object nesting stops at 384 levels. The header now accepts container versions 8 to 12 and the flag values 4 and 8 that real Affinity 3 files carry (the previous check rejected the public CC0 Affinity 3 documents); the thumbnail record is the same in all those versions. Checked against 176 public documents saved by Affinity 1.x to 3.x: every doc.dat decompresses with a matching CRC and parses with no bytes left over. A test-only `synth` feature builds synthetic containers for unit tests and fuzz seeds; it is not an Affinity exporter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
106 lines
3.3 KiB
Rust
106 lines
3.3 KiB
Rust
//! Typed reads of [`Stream`] objects. Every accessor returns `None` when a field is missing or has
|
|
//! another type, so the document mapping treats odd data as absent rather than failing.
|
|
|
|
use crate::stream::{ObjId, Object, Stream, Tag, Value};
|
|
|
|
impl Stream {
|
|
pub fn object(&self, id: ObjId) -> Option<&Object> {
|
|
self.objects.get(id)
|
|
}
|
|
|
|
pub fn field(&self, id: ObjId, tag: &[u8; 4]) -> Option<&Value> {
|
|
self.object(id)?.get(Tag::of(tag))
|
|
}
|
|
|
|
pub fn class(&self, id: ObjId) -> Option<Tag> {
|
|
self.object(id).map(|o| o.class)
|
|
}
|
|
|
|
pub fn is(&self, id: ObjId, class: &[u8; 4]) -> bool {
|
|
self.class(id) == Some(Tag::of(class))
|
|
}
|
|
|
|
/// A child object (shared, linked or inline); `None` for null or missing.
|
|
pub fn obj(&self, id: ObjId, tag: &[u8; 4]) -> Option<ObjId> {
|
|
match self.field(id, tag)? {
|
|
Value::Obj(o) => *o,
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// An array of objects (nulls skipped).
|
|
pub fn objs(&self, id: ObjId, tag: &[u8; 4]) -> Vec<ObjId> {
|
|
match self.field(id, tag) {
|
|
Some(Value::Array(a)) => a.iter().filter_map(|v| if let Value::Obj(Some(o)) = v { Some(*o) } else { None }).collect(),
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn f64(&self, id: ObjId, tag: &[u8; 4]) -> Option<f64> {
|
|
match self.field(id, tag)? {
|
|
Value::Float(f) if f.is_finite() => Some(*f),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn int(&self, id: ObjId, tag: &[u8; 4]) -> Option<i64> {
|
|
match self.field(id, tag)? {
|
|
Value::Int(i) => Some(*i),
|
|
Value::UInt(u) => i64::try_from(*u).ok(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn bool(&self, id: ObjId, tag: &[u8; 4]) -> Option<bool> {
|
|
match self.field(id, tag)? {
|
|
Value::Bool(b) => Some(*b),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn str(&self, id: ObjId, tag: &[u8; 4]) -> Option<&str> {
|
|
match self.field(id, tag)? {
|
|
Value::Str(s) => Some(s),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn enumeration(&self, id: ObjId, tag: &[u8; 4]) -> Option<(u16, u16)> {
|
|
match self.field(id, tag)? {
|
|
Value::Enum { id, version } => Some((*id, *version)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// A fixed vector of `N` finite floats (points, rectangles, transforms).
|
|
pub fn floats<const N: usize>(&self, id: ObjId, tag: &[u8; 4]) -> Option<[f64; N]> {
|
|
match self.field(id, tag)? {
|
|
Value::Floats(v) if v.len() == N && v.iter().all(|f| f.is_finite()) => {
|
|
let mut a = [0.0; N];
|
|
a.copy_from_slice(v);
|
|
Some(a)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn bytes(&self, id: ObjId, tag: &[u8; 4]) -> Option<&[u8]> {
|
|
match self.field(id, tag)? {
|
|
Value::Bytes(b) | Value::Blob(b) => Some(b),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn entry(&self, id: ObjId, tag: &[u8; 4]) -> Option<&str> {
|
|
match self.field(id, tag)? {
|
|
Value::Entry(s) => Some(s),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Untagged fields of a positional object, in order.
|
|
pub fn positional(&self, id: ObjId) -> impl Iterator<Item = &Value> {
|
|
self.object(id).into_iter().flat_map(|o| o.fields.iter().map(|(_, v)| v))
|
|
}
|
|
}
|