mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 10:44:50 +00:00
779 lines
30 KiB
Rust
779 lines
30 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
//! The envelope of VectorCraft's `.vectorcraft` file, shared by VectorCraft and by the apps that
|
|
//! read its files. The document is a type parameter: VectorCraft supplies its own document type,
|
|
//! and a reader that needs only the envelope uses [`serde::de::IgnoredAny`].
|
|
//!
|
|
//! A `.vectorcraft` file is UTF-8 JSON, gzip-compressed when saved with Use Compression:
|
|
//!
|
|
//! ```text
|
|
//! { "format": "vectorcraft", "version": 4, "generator": "VectorCraft 0.17.0",
|
|
//! "preview": { "mime": "image/png", "data": "<base64>" },
|
|
//! "profiles": { "<profile name>": { "mime": "application/vnd.iccprofile", "blob": [0, 9000] } },
|
|
//! "pdf": { "mime": "application/pdf", "blob": [9000, 52000] },
|
|
//! "document": { ... },
|
|
//! "images": { "<key>": { "mime": "image/png", "data": "<base64>" } } }
|
|
//! ```
|
|
//!
|
|
//! Since version 4, entries of [`INLINE_MAX`] bytes or more follow the JSON after [`BLOB_MAGIC`],
|
|
//! and such an entry gives `"blob": [offset, length]` (from the end of the marker) instead of
|
|
//! `data`. In a compressed file, the marker and the data after it are inside the gzip stream.
|
|
//! Files written under the former name say `"format": "drawcraft"`.
|
|
//!
|
|
//! - [`File`], [`Head`] and [`Image`] are the members, in the order VectorCraft writes them.
|
|
//! [`File::read`] reads all of them, and [`Contents::read`] reads a file's document, images and
|
|
//! profiles and skips the preview and PDF. Both refuse another format and a newer version.
|
|
//! - [`split_tail`] separates a file's JSON from the data after it, and [`marker_at`] finds the
|
|
//! marker in a file's first bytes; [`Entries`] reads a file's entries from either place, copying
|
|
//! at most the size of that data in all; [`Tail`] places entries when writing.
|
|
//! - [`sniff`] and [`is_compressed`] recognize a file from its first bytes.
|
|
//! - [`base64`] encodes and decodes the `data` members.
|
|
//!
|
|
//! The crate takes and returns bytes. It does not open files or decompress past the first bytes
|
|
//! [`sniff`] reads: a reader of a compressed file decompresses it with its own limits first.
|
|
|
|
pub mod base64;
|
|
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::fmt;
|
|
use std::io::{self, Read, Write};
|
|
use std::marker::PhantomData;
|
|
use std::sync::Arc;
|
|
|
|
use serde::de::value::SeqAccessDeserializer;
|
|
use serde::de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// The newest version: data after the JSON. Versions 1 to 3 still load.
|
|
pub const VERSION: u32 = 4;
|
|
/// The oldest version VectorCraft writes.
|
|
pub const MIN_VERSION: u32 = 1;
|
|
/// The first version whose readers open compressed files.
|
|
pub const COMPRESSED_SINCE: u32 = 3;
|
|
/// The first version that keeps entries after the JSON.
|
|
pub const TAIL_SINCE: u32 = 4;
|
|
/// Entries this large or larger go after the JSON (from [`TAIL_SINCE`]), not into it as base64.
|
|
pub const INLINE_MAX: usize = 8 << 10;
|
|
/// What separates the JSON from the data after it.
|
|
pub const BLOB_MAGIC: &[u8] = b"\n\0VCBLOBS\0";
|
|
/// The most pixels on the longer side of the embedded `preview`.
|
|
pub const PREVIEW_MAX: u32 = 256;
|
|
/// The file extension, and the `format` name since version 3.
|
|
pub const EXTENSION: &str = "vectorcraft";
|
|
/// The extension and `format` name from before the rename (DrawCraft), still read; VectorCraft
|
|
/// writes it for versions 1 and 2.
|
|
pub const LEGACY_EXTENSION: &str = "drawcraft";
|
|
/// The MIME type of embedded ICC profiles.
|
|
pub const ICC_MIME: &str = "application/vnd.iccprofile";
|
|
/// The most bytes [`Image::bytes`] gives for one entry: VectorCraft's limit on one entry of the
|
|
/// data after the JSON of a compressed file (1 GiB).
|
|
pub const MAX_ENTRY: u64 = 1 << 30;
|
|
|
|
/// Why a file's envelope is refused.
|
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
|
#[non_exhaustive]
|
|
pub enum Error {
|
|
#[error("not a VectorCraft file: format is `{0}`")]
|
|
Format(String),
|
|
#[error("file version {0} is newer than this reader supports ({VERSION})")]
|
|
TooNew(u32),
|
|
#[error("the entries name more bytes than the data after the JSON holds")]
|
|
TooBig,
|
|
}
|
|
|
|
/// [`File::read`] or [`Contents::read`] could not read a file's JSON.
|
|
///
|
|
/// Other crates match it with a wildcard arm:
|
|
///
|
|
/// ```compile_fail
|
|
/// fn name(e: craft_vectorcraft_format::ReadError) -> &'static str {
|
|
/// match e {
|
|
/// craft_vectorcraft_format::ReadError::Json(_) => "json",
|
|
/// craft_vectorcraft_format::ReadError::Envelope(_) => "envelope",
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, thiserror::Error)]
|
|
#[non_exhaustive]
|
|
pub enum ReadError {
|
|
/// The JSON is not a file's top-level object, or the document `D` does not read.
|
|
#[error(transparent)]
|
|
Json(#[from] serde_json::Error),
|
|
/// The JSON reads, but the format is another one ([`Error::Format`]) or the version is newer
|
|
/// than [`VERSION`] ([`Error::TooNew`]).
|
|
#[error(transparent)]
|
|
Envelope(#[from] Error),
|
|
}
|
|
|
|
/// The top-level object of a file, with the document as `D`. Serialized with `serde_json`, it
|
|
/// gives the bytes VectorCraft writes, compact or pretty: the members in this order, `generator`,
|
|
/// `preview`, `profiles` and `pdf` left out when absent, `images` always written. [`File::read`]
|
|
/// reads one. A reader that does not use `generator`, `preview` and `pdf` reads [`Contents`]
|
|
/// instead.
|
|
///
|
|
/// Other crates start one with [`File::new`] and set its members:
|
|
///
|
|
/// ```compile_fail
|
|
/// let _ = craft_vectorcraft_format::File {
|
|
/// format: "vectorcraft".into(), version: 4, generator: None, preview: None,
|
|
/// profiles: Default::default(), pdf: None, document: 0, images: Default::default(),
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
|
#[non_exhaustive]
|
|
pub struct File<D> {
|
|
/// [`EXTENSION`], or [`LEGACY_EXTENSION`] in versions 1 and 2.
|
|
pub format: String,
|
|
pub version: u32,
|
|
/// `"VectorCraft <app version>"`.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub generator: Option<String>,
|
|
/// A PNG of the first artboard, at most [`PREVIEW_MAX`] pixels on its longer side.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub preview: Option<Image>,
|
|
/// ICC files by profile name (Embed ICC Profiles).
|
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
|
pub profiles: BTreeMap<String, Image>,
|
|
/// A PDF of every artboard (Create PDF-Compatible File).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub pdf: Option<Image>,
|
|
pub document: D,
|
|
/// The images the document uses, by key.
|
|
pub images: BTreeMap<String, Image>,
|
|
}
|
|
|
|
impl<D> File<D> {
|
|
/// A file of the current [`VERSION`] holding `document`, with no other members.
|
|
pub fn new(document: D) -> Self {
|
|
Self {
|
|
format: EXTENSION.into(),
|
|
version: VERSION,
|
|
generator: None,
|
|
preview: None,
|
|
profiles: BTreeMap::new(),
|
|
pdf: None,
|
|
document,
|
|
images: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Is `format` a VectorCraft name, and `version` one this crate reads?
|
|
pub fn check(&self) -> Result<(), Error> {
|
|
check(&self.format, self.version)
|
|
}
|
|
|
|
/// Written under the former name (`"format": "drawcraft"`).
|
|
pub fn is_legacy(&self) -> bool {
|
|
self.format == LEGACY_EXTENSION
|
|
}
|
|
|
|
/// An empty [`Tail`] for this file's `version`. Set `version` before calling it.
|
|
pub fn tail<'a>(&self) -> Tail<'a> {
|
|
Tail::new(self.version)
|
|
}
|
|
|
|
/// Reads a file's JSON (from [`split_tail`]), and refuses another format or a version newer
|
|
/// than [`VERSION`] ([`ReadError::Envelope`]), as VectorCraft does after reading it. A file
|
|
/// reads as VectorCraft opens it:
|
|
///
|
|
/// - A `generator`, `preview` or `pdf` of the wrong shape reads as `None`. One that appears
|
|
/// more than once reads as its last value, as `serde_json` reads a repeated key into a
|
|
/// `serde_json::Value`.
|
|
/// - A missing or repeated `format`, `version` or `document`, or a repeated `images` or
|
|
/// `profiles`, is refused. Other members are skipped.
|
|
/// - A JSON array holds `format`, `version`, `document`, `images` and `profiles` in that
|
|
/// order; the last two may be left out.
|
|
pub fn read<'de>(json: &'de [u8]) -> Result<Self, ReadError>
|
|
where
|
|
D: Deserialize<'de>,
|
|
{
|
|
read(json, true, false).map(|(file, _)| file)
|
|
}
|
|
}
|
|
|
|
/// [`File::read`] and [`Contents::read`]; `keep` is false for [`Contents`], which skips
|
|
/// `generator`, `preview` and `pdf` without keeping them.
|
|
fn read<'de, D: Deserialize<'de>>(json: &'de [u8], keep: bool, read_pdf: bool) -> Result<(File<D>, bool), ReadError> {
|
|
let mut de = serde_json::Deserializer::from_slice(json);
|
|
let mut pdf = PdfPreference::default();
|
|
let file = serde::Deserializer::deserialize_struct(
|
|
&mut de,
|
|
"File",
|
|
MEMBERS,
|
|
FileVisitor { keep, pdf: read_pdf.then_some(&mut pdf), document: PhantomData },
|
|
)?;
|
|
de.end()?;
|
|
file.check()?;
|
|
Ok((file, pdf.preference.unwrap_or(pdf.present)))
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PdfPreference {
|
|
preference: Option<bool>,
|
|
present: bool,
|
|
}
|
|
|
|
/// The members of a file, in the order of the array form.
|
|
const MEMBERS: &[&str] = &["format", "version", "document", "images", "profiles"];
|
|
|
|
/// The array form of a file, read as VectorCraft reads it.
|
|
#[derive(Deserialize)]
|
|
#[serde(rename = "File")]
|
|
struct Members<D> {
|
|
format: String,
|
|
version: u32,
|
|
document: D,
|
|
#[serde(default)]
|
|
images: BTreeMap<String, Image>,
|
|
#[serde(default)]
|
|
profiles: BTreeMap<String, Image>,
|
|
}
|
|
|
|
/// Reads a [`File`] from a JSON object or array.
|
|
struct FileVisitor<'a, D> {
|
|
keep: bool,
|
|
pdf: Option<&'a mut PdfPreference>,
|
|
document: PhantomData<D>,
|
|
}
|
|
|
|
impl<'de, D: Deserialize<'de>> Visitor<'de> for FileVisitor<'_, D> {
|
|
type Value = File<D>;
|
|
|
|
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.write_str("struct File")
|
|
}
|
|
|
|
fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<File<D>, A::Error> {
|
|
let Members { format, version, document, images, profiles } = Members::deserialize(SeqAccessDeserializer::new(seq))?;
|
|
Ok(File { format, version, generator: None, preview: None, profiles, pdf: None, document, images })
|
|
}
|
|
|
|
fn visit_map<A: MapAccess<'de>>(mut self, mut map: A) -> Result<File<D>, A::Error> {
|
|
let (mut format, mut version, mut document, mut images, mut profiles) = (None, None, None, None, None);
|
|
let (mut generator, mut preview, mut pdf) = (None, None, None);
|
|
while let Some(key) = map.next_key::<String>()? {
|
|
match key.as_str() {
|
|
"format" => once(&mut map, &mut format, "format")?,
|
|
"version" => once(&mut map, &mut version, "version")?,
|
|
"document" => once(&mut map, &mut document, "document")?,
|
|
"images" => once(&mut map, &mut images, "images")?,
|
|
"profiles" => once(&mut map, &mut profiles, "profiles")?,
|
|
"generator" if self.keep => generator = map.next_value::<Lenient<String>>()?.0,
|
|
"preview" if self.keep => preview = map.next_value::<Lenient<Image>>()?.0,
|
|
"pdf" if self.keep => pdf = map.next_value::<Lenient<Image>>()?.0,
|
|
"pdfCompatible" if self.pdf.is_some() => {
|
|
if let Some(state) = self.pdf.as_mut() {
|
|
state.preference = map.next_value::<Option<bool>>()?;
|
|
}
|
|
}
|
|
"pdf" if self.pdf.is_some() => {
|
|
if let Some(state) = self.pdf.as_mut() {
|
|
state.present = map.next_value::<Option<IgnoredAny>>()?.is_some();
|
|
}
|
|
}
|
|
_ => {
|
|
map.next_value::<IgnoredAny>()?;
|
|
}
|
|
}
|
|
}
|
|
Ok(File {
|
|
format: format.ok_or_else(|| de::Error::missing_field("format"))?,
|
|
version: version.ok_or_else(|| de::Error::missing_field("version"))?,
|
|
generator,
|
|
preview,
|
|
profiles: profiles.unwrap_or_default(),
|
|
pdf,
|
|
document: document.ok_or_else(|| de::Error::missing_field("document"))?,
|
|
images: images.unwrap_or_default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Reads the value of member `name` into `slot`, refusing a second one.
|
|
fn once<'de, A: MapAccess<'de>, T: Deserialize<'de>>(map: &mut A, slot: &mut Option<T>, name: &'static str) -> Result<(), A::Error> {
|
|
if slot.is_some() {
|
|
return Err(de::Error::duplicate_field(name));
|
|
}
|
|
*slot = Some(map.next_value()?);
|
|
Ok(())
|
|
}
|
|
|
|
/// An optional member read as `None` when its value has the wrong shape.
|
|
struct Lenient<T>(Option<T>);
|
|
|
|
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Lenient<T> {
|
|
fn deserialize<De: serde::Deserializer<'de>>(d: De) -> Result<Self, De::Error> {
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum Shape<T> {
|
|
Value(T),
|
|
Other(IgnoredAny),
|
|
}
|
|
Ok(Lenient(match Shape::deserialize(d)? {
|
|
Shape::Value(v) => Some(v),
|
|
Shape::Other(_) => None,
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// [`Lenient`] as a `deserialize_with` function.
|
|
fn lenient<'de, De: serde::Deserializer<'de>, T: Deserialize<'de>>(d: De) -> Result<Option<T>, De::Error> {
|
|
Lenient::deserialize(d).map(|l| l.0)
|
|
}
|
|
|
|
/// [`File::check`] for a `format` and `version` read on their own. Every version up to
|
|
/// [`VERSION`] passes, 0 included, as VectorCraft opens them; [`MIN_VERSION`] limits only the
|
|
/// versions VectorCraft writes.
|
|
pub fn check(format: &str, version: u32) -> Result<(), Error> {
|
|
if format != EXTENSION && format != LEGACY_EXTENSION {
|
|
return Err(Error::Format(format.into()));
|
|
}
|
|
if version > VERSION {
|
|
return Err(Error::TooNew(version));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The members of a file that a reader of its document needs: [`File`] without `generator`,
|
|
/// `preview` and `pdf`, in the order of the array form. Only [`Contents::read`] makes one:
|
|
///
|
|
/// ```compile_fail
|
|
/// let _ = craft_vectorcraft_format::Contents {
|
|
/// format: "vectorcraft".into(), version: 4, document: 0,
|
|
/// images: Default::default(), profiles: Default::default(),
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub struct Contents<D> {
|
|
pub format: String,
|
|
pub version: u32,
|
|
pub document: D,
|
|
pub images: BTreeMap<String, Image>,
|
|
pub profiles: BTreeMap<String, Image>,
|
|
}
|
|
|
|
impl<D> Contents<D> {
|
|
/// Reads a file's JSON as [`File::read`] does, and skips `generator`, `preview` and `pdf` as
|
|
/// it reads them, so their data is neither buffered nor kept. Any value of those three is
|
|
/// accepted, however many times it appears.
|
|
pub fn read<'de>(json: &'de [u8]) -> Result<Self, ReadError>
|
|
where
|
|
D: Deserialize<'de>,
|
|
{
|
|
let (file, _) = read(json, false, false)?;
|
|
let File { format, version, document, images, profiles, .. } = file;
|
|
Ok(Self { format, version, document, images, profiles })
|
|
}
|
|
|
|
/// Reads the document and its saved PDF preference in one envelope pass, without
|
|
/// keeping or decoding the PDF payload. A non-null `pdf` supplies the fallback when
|
|
/// `pdfCompatible` is absent or null. Repeated members use their last value; arrays
|
|
/// have no PDF preference and return false. A non-boolean preference is an error.
|
|
/// The existing [`Self::read`] continues to ignore that optional preference.
|
|
pub fn read_with_pdf_preference<'de>(json: &'de [u8]) -> Result<(Self, bool), ReadError>
|
|
where
|
|
D: Deserialize<'de>,
|
|
{
|
|
let (file, compatible) = read(json, false, true)?;
|
|
let File { format, version, document, images, profiles, .. } = file;
|
|
Ok((Self { format, version, document, images, profiles }, compatible))
|
|
}
|
|
|
|
/// [`File::is_legacy`].
|
|
pub fn is_legacy(&self) -> bool {
|
|
self.format == LEGACY_EXTENSION
|
|
}
|
|
}
|
|
|
|
/// The members of a file besides its document, which is skipped without being decoded. A
|
|
/// `preview` or `pdf` of the wrong shape reads as `None`, as in [`File`]; a repeated `preview` or
|
|
/// `pdf` is refused.
|
|
///
|
|
/// ```compile_fail
|
|
/// let _ = craft_vectorcraft_format::Head { preview: None, pdf: None };
|
|
/// ```
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
|
|
#[non_exhaustive]
|
|
pub struct Head {
|
|
#[serde(default, deserialize_with = "lenient")]
|
|
pub preview: Option<Image>,
|
|
#[serde(default, deserialize_with = "lenient")]
|
|
pub pdf: Option<Image>,
|
|
}
|
|
|
|
/// An entry holding bytes: an image, an ICC profile, the preview or the PDF. Other crates make
|
|
/// one with [`Image::inline`], [`Tail::image`] or `Image::default()`:
|
|
///
|
|
/// ```compile_fail
|
|
/// let _ = craft_vectorcraft_format::Image { mime: String::new(), data: String::new(), blob: None, proxy: false };
|
|
/// ```
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[non_exhaustive]
|
|
pub struct Image {
|
|
pub mime: String,
|
|
/// The bytes as base64, unless they come after the JSON (`blob`).
|
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
|
pub data: String,
|
|
/// Where the bytes are in the data after the JSON: `[offset, length]`.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub blob: Option<[u64; 2]>,
|
|
/// `data` is a linked image's low-resolution preview, not the linked file's bytes.
|
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
pub proxy: bool,
|
|
}
|
|
|
|
impl Image {
|
|
/// An entry with `bytes` in the JSON, as base64.
|
|
pub fn inline(mime: &str, bytes: &[u8]) -> Self {
|
|
Self { mime: mime.into(), data: base64::encode(bytes), blob: None, proxy: false }
|
|
}
|
|
|
|
/// The number of bytes the entry states it holds, without decoding it: the `blob` length, or
|
|
/// the length `data` decodes to.
|
|
pub fn len(&self) -> u64 {
|
|
match self.blob {
|
|
Some([_, len]) => len,
|
|
None => base64::decoded_len(&self.data),
|
|
}
|
|
}
|
|
|
|
/// Whether [`Image::len`] is 0.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
|
|
/// [`Image::len`] with fallible checkpoints while scanning inline data. The checkpoint's
|
|
/// error is returned unchanged.
|
|
pub fn len_with_check<E>(&self, mut check: impl FnMut() -> Result<(), E>) -> Result<u64, E> {
|
|
match self.blob {
|
|
Some([_, len]) => {
|
|
check()?;
|
|
Ok(len)
|
|
}
|
|
None => base64::decoded_len_with_check(&self.data, check),
|
|
}
|
|
}
|
|
|
|
/// The entry's bytes: `data` decoded, or the range `blob` names in `tail` (the data after the
|
|
/// JSON, from [`split_tail`]). `None` when the entry states more than [`MAX_ENTRY`] bytes,
|
|
/// when `data` is not valid base64, or when `tail` ends before the range does. Each call
|
|
/// copies the range again; read the entries of one file through [`Entries`], which limits the
|
|
/// bytes copied in all.
|
|
pub fn bytes(&self, tail: &[u8]) -> Option<Vec<u8>> {
|
|
self.bytes_at_most(tail, MAX_ENTRY)
|
|
}
|
|
|
|
/// [`Image::bytes`] with a limit of `max` bytes instead of [`MAX_ENTRY`]: `None` when
|
|
/// [`Image::len`] is more than `max`, checked before anything is decoded or copied.
|
|
pub fn bytes_at_most(&self, tail: &[u8], max: u64) -> Option<Vec<u8>> {
|
|
if self.len() > max {
|
|
return None;
|
|
}
|
|
match self.blob {
|
|
Some(at) => entry(tail, at).map(<[u8]>::to_vec),
|
|
None => base64::decode(&self.data),
|
|
}
|
|
}
|
|
|
|
/// [`Image::bytes_at_most`] with fallible checkpoints during size checks, base64 decoding
|
|
/// and copying. `Ok(None)` has the same meaning as [`Image::bytes_at_most`].
|
|
pub fn bytes_at_most_with_check<E>(&self, tail: &[u8], max: u64, mut check: impl FnMut() -> Result<(), E>) -> Result<Option<Vec<u8>>, E> {
|
|
if self.len_with_check(&mut check)? > max {
|
|
return Ok(None);
|
|
}
|
|
match self.blob {
|
|
Some(at) => {
|
|
let Some(bytes) = entry(tail, at) else { return Ok(None) };
|
|
let mut out = Vec::with_capacity(bytes.len());
|
|
for chunk in bytes.chunks(base64::CHECK_CHUNK) {
|
|
check()?;
|
|
out.extend_from_slice(chunk);
|
|
}
|
|
Ok(Some(out))
|
|
}
|
|
None => base64::decode_with_check(&self.data, check),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The bytes `[offset, length]` names in `tail`; `None` when `tail` ends before them.
|
|
pub fn entry(tail: &[u8], [offset, len]: [u64; 2]) -> Option<&[u8]> {
|
|
let start = usize::try_from(offset).ok()?;
|
|
let end = start.checked_add(usize::try_from(len).ok()?)?;
|
|
tail.get(start..end)
|
|
}
|
|
|
|
/// Reads the entries of one file, copying at most as many bytes from the data after the JSON as
|
|
/// that data holds, in all. A file VectorCraft writes names each of those bytes once, so all of
|
|
/// its entries read. Entries that name the same `[offset, length]` share one copy, which counts
|
|
/// once. Entries whose different ranges overlap copy the shared bytes once each, and are refused
|
|
/// once their copies pass that size, as VectorCraft's reader refuses them. Unlike
|
|
/// [`Image::bytes`], `Entries` puts no limit on one entry: VectorCraft opens an uncompressed file
|
|
/// whose entry is larger than [`MAX_ENTRY`].
|
|
#[derive(Debug, Clone)]
|
|
pub struct Entries<'a> {
|
|
tail: &'a [u8],
|
|
copied: u64,
|
|
done: HashMap<[u64; 2], Arc<Vec<u8>>>,
|
|
}
|
|
|
|
impl<'a> Entries<'a> {
|
|
/// A reader of entries over `tail`, the data after the JSON (from [`split_tail`]).
|
|
pub fn new(tail: &'a [u8]) -> Self {
|
|
Self { tail, copied: 0, done: HashMap::new() }
|
|
}
|
|
|
|
/// The entry's bytes: `data` decoded (`None` when it is not valid base64), or
|
|
/// [`Entries::blob`] of its range.
|
|
pub fn bytes(&mut self, image: &Image) -> Result<Option<Arc<Vec<u8>>>, Error> {
|
|
match image.blob {
|
|
Some(at) => self.blob(at),
|
|
None => Ok(base64::decode(&image.data).map(Arc::new)),
|
|
}
|
|
}
|
|
|
|
/// The bytes `[offset, length]` names in `tail` (`None` when `tail` ends before them). A range
|
|
/// this reader has already read gets the same buffer. [`Error::TooBig`] when the bytes this
|
|
/// reader has copied from `tail`, these included, would be more than `tail` holds; a refused
|
|
/// range is not counted.
|
|
pub fn blob(&mut self, at: [u64; 2]) -> Result<Option<Arc<Vec<u8>>>, Error> {
|
|
if let Some(bytes) = self.done.get(&at) {
|
|
return Ok(Some(Arc::clone(bytes)));
|
|
}
|
|
let Some(bytes) = entry(self.tail, at) else {
|
|
return Ok(None);
|
|
};
|
|
let copied = self.copied.saturating_add(bytes.len() as u64);
|
|
if copied > self.tail.len() as u64 {
|
|
return Err(Error::TooBig);
|
|
}
|
|
self.copied = copied;
|
|
let bytes = Arc::new(bytes.to_vec());
|
|
self.done.insert(at, Arc::clone(&bytes));
|
|
Ok(Some(bytes))
|
|
}
|
|
|
|
/// The bytes copied from `tail` so far.
|
|
pub fn copied(&self) -> u64 {
|
|
self.copied
|
|
}
|
|
}
|
|
|
|
/// Is this gzip data (a file saved with Use Compression)?
|
|
pub fn is_compressed(bytes: &[u8]) -> bool {
|
|
bytes.starts_with(&[0x1f, 0x8b])
|
|
}
|
|
|
|
/// Does `head`, the first bytes of a file (compressed or not), start a `.vectorcraft` file? It
|
|
/// must hold a JSON object whose top-level `format` member is `"vectorcraft"` or `"drawcraft"`
|
|
/// within the first 256 bytes after decompression. VectorCraft writes `format` first.
|
|
pub fn sniff(head: &[u8]) -> bool {
|
|
const HEAD: usize = 256;
|
|
let mut unpacked = Vec::new();
|
|
let head = if is_compressed(head) {
|
|
// A head cut short still decompresses its start.
|
|
let _ = flate2::read::GzDecoder::new(head).take(HEAD as u64).read_to_end(&mut unpacked);
|
|
unpacked.as_slice()
|
|
} else {
|
|
head.get(..HEAD).unwrap_or(head)
|
|
};
|
|
format_member(head).is_some_and(|f| f == EXTENSION || f == LEGACY_EXTENSION)
|
|
}
|
|
|
|
/// The string value of the top-level `format` member of the object `text` starts, with keys and
|
|
/// values compared as JSON reads them (escapes decoded). `None` when `text` ends before it, or the
|
|
/// value is not a string.
|
|
fn format_member(text: &[u8]) -> Option<String> {
|
|
let mut s = Scan { text, at: 0 };
|
|
s.space();
|
|
s.expect(b'{')?;
|
|
loop {
|
|
s.space();
|
|
let key = s.decoded()?;
|
|
s.space();
|
|
s.expect(b':')?;
|
|
s.space();
|
|
if key == "format" {
|
|
return s.decoded();
|
|
}
|
|
s.skip_value()?;
|
|
s.space();
|
|
s.expect(b',')?;
|
|
}
|
|
}
|
|
|
|
/// A position in the first bytes of a JSON text.
|
|
struct Scan<'a> {
|
|
text: &'a [u8],
|
|
at: usize,
|
|
}
|
|
|
|
impl<'a> Scan<'a> {
|
|
fn space(&mut self) {
|
|
while self.text.get(self.at).is_some_and(u8::is_ascii_whitespace) {
|
|
self.at += 1;
|
|
}
|
|
}
|
|
|
|
fn expect(&mut self, b: u8) -> Option<()> {
|
|
(*self.text.get(self.at)? == b).then(|| self.at += 1)
|
|
}
|
|
|
|
/// The contents of the string that starts here, between its quotes.
|
|
fn string(&mut self) -> Option<&'a [u8]> {
|
|
self.expect(b'"')?;
|
|
let start = self.at;
|
|
loop {
|
|
match *self.text.get(self.at)? {
|
|
b'"' => {
|
|
self.at += 1;
|
|
return self.text.get(start..self.at - 1);
|
|
}
|
|
b'\\' => self.at += 2,
|
|
_ => self.at += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The string that starts here, as JSON reads it.
|
|
fn decoded(&mut self) -> Option<String> {
|
|
let start = self.at;
|
|
self.string()?;
|
|
serde_json::from_slice(self.text.get(start..self.at)?).ok()
|
|
}
|
|
|
|
/// Move past the value that starts here: a string, an object or array with everything in it,
|
|
/// or a number or literal up to the `,` or `}` after it.
|
|
fn skip_value(&mut self) -> Option<()> {
|
|
let mut depth = 0usize;
|
|
loop {
|
|
match *self.text.get(self.at)? {
|
|
b'"' => {
|
|
self.string()?;
|
|
if depth == 0 {
|
|
return Some(());
|
|
}
|
|
}
|
|
b'{' | b'[' => {
|
|
depth += 1;
|
|
self.at += 1;
|
|
}
|
|
b'}' | b']' if depth == 0 => return Some(()),
|
|
b'}' | b']' => {
|
|
depth -= 1;
|
|
self.at += 1;
|
|
if depth == 0 {
|
|
return Some(());
|
|
}
|
|
}
|
|
b',' if depth == 0 => return Some(()),
|
|
_ => self.at += 1,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The JSON of an uncompressed (or decompressed) file, without the data after it.
|
|
pub fn json_part(file: &[u8]) -> &[u8] {
|
|
split_tail(file).0
|
|
}
|
|
|
|
/// A file's JSON and the data after it, split at [`BLOB_MAGIC`] where it follows the first JSON
|
|
/// value. Without the marker there, all of `file` is the JSON and the data after it is empty.
|
|
pub fn split_tail(file: &[u8]) -> (&[u8], &[u8]) {
|
|
if !file.contains(&0) {
|
|
return (file, &[]);
|
|
}
|
|
if let Some(at) = marker_at(file) {
|
|
return (file.get(..at).unwrap_or(file), file.get(at + BLOB_MAGIC.len()..).unwrap_or_default());
|
|
}
|
|
let mut values = serde_json::Deserializer::from_slice(file).into_iter::<serde::de::IgnoredAny>();
|
|
if values.next().is_some_and(|v| v.is_ok()) {
|
|
let end = values.byte_offset();
|
|
if let Some(rest) = file.get(end..)
|
|
&& rest.starts_with(BLOB_MAGIC)
|
|
{
|
|
return (file.get(..end).unwrap_or(file), rest.get(BLOB_MAGIC.len()..).unwrap_or_default());
|
|
}
|
|
}
|
|
(file, &[])
|
|
}
|
|
|
|
/// Where [`BLOB_MAGIC`] starts in `file` when it is where VectorCraft writes it: right after a
|
|
/// JSON object, array or string, and holding the first NUL of `file`, which JSON never contains.
|
|
/// `file` may be the first bytes of a file, up to the end of the marker. [`split_tail`] splits
|
|
/// there first; a reader that unpacks a compressed file as a stream finds the end of its JSON with
|
|
/// it.
|
|
pub fn marker_at(file: &[u8]) -> Option<usize> {
|
|
let at = file.iter().position(|&b| b == 0)?.checked_sub(1)?;
|
|
(file.get(at..)?.starts_with(BLOB_MAGIC) && matches!(file.get(at.checked_sub(1)?)?, b'}' | b']' | b'"')).then_some(at)
|
|
}
|
|
|
|
/// One entry placed after the JSON by [`Tail::image`].
|
|
///
|
|
/// ```compile_fail
|
|
/// let _ = craft_vectorcraft_format::Part { mime: String::new(), bytes: &[] };
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
#[non_exhaustive]
|
|
pub struct Part<'a> {
|
|
pub mime: String,
|
|
pub bytes: &'a [u8],
|
|
}
|
|
|
|
/// The data after the JSON of a file being written. Entries go after the JSON, each at the
|
|
/// offset where the one before ends, when the version keeps them there and they are at least
|
|
/// [`INLINE_MAX`] bytes; other entries go into the JSON as base64.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Tail<'a> {
|
|
parts: Vec<Part<'a>>,
|
|
len: u64,
|
|
after: bool,
|
|
}
|
|
|
|
impl<'a> Tail<'a> {
|
|
/// An empty tail for a file of `version`, which is the file's own `version`; [`File::tail`]
|
|
/// takes it from the file.
|
|
pub fn new(version: u32) -> Self {
|
|
Self { parts: vec![], len: 0, after: version >= TAIL_SINCE }
|
|
}
|
|
|
|
/// The entry for `bytes`, placed after the JSON or inline.
|
|
pub fn image(&mut self, mime: &str, bytes: &'a [u8], proxy: bool) -> Image {
|
|
if self.after && bytes.len() >= INLINE_MAX {
|
|
let len = bytes.len() as u64;
|
|
let at = self.len;
|
|
self.len = self.len.saturating_add(len);
|
|
self.parts.push(Part { mime: mime.into(), bytes });
|
|
return Image { mime: mime.into(), data: String::new(), blob: Some([at, len]), proxy };
|
|
}
|
|
Image { proxy, ..Image::inline(mime, bytes) }
|
|
}
|
|
|
|
/// The entries placed after the JSON, in order.
|
|
pub fn parts(&self) -> &[Part<'a>] {
|
|
&self.parts
|
|
}
|
|
|
|
/// The bytes of the entries placed after the JSON, together.
|
|
pub fn len(&self) -> u64 {
|
|
self.len
|
|
}
|
|
|
|
/// No entry is placed after the JSON.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.parts.is_empty()
|
|
}
|
|
|
|
/// Write what follows the JSON: [`BLOB_MAGIC`] and the entries, or nothing when there are none.
|
|
pub fn write_to(&self, mut w: impl Write) -> io::Result<()> {
|
|
if self.parts.is_empty() {
|
|
return Ok(());
|
|
}
|
|
w.write_all(BLOB_MAGIC)?;
|
|
self.parts.iter().try_for_each(|p| w.write_all(p.bytes))
|
|
}
|
|
}
|