Skip to main content

wolfram_serialize/
lib.rs

1//! Serialize and deserialize Wolfram Language expressions
2//! to and from the WXF binary wire format.
3//!
4//! Two layers:
5//!
6//! * Byte level — [`Reader`] / [`Writer`]. [`Reader`] lends zero-copy
7//!   buffer-lifetime views (`&'de`), so the default [`SliceReader`] reads
8//!   straight out of an in-memory buffer; the default writer is `Vec<u8>`.
9//! * WXF level — [`WxfReader`] / [`WxfWriter`], typed sugar over the byte layer
10//!   built on the WXF token enums.
11//!
12//! Per-Rust-type encoding/decoding is [`ToWXF`] / [`FromWXF`], both generic over
13//! the byte layer (monomorphized, no `dyn`, streaming). Top-level entry points:
14//! [`to_wxf`] (compression optional), [`from_wxf`], [`read_wxf`].
15
16#![warn(missing_docs)]
17
18// Lets the derive macros' absolute `::wolfram_serialize::…` paths resolve while
19// compiling this crate itself — so `#[derive(ToWXF)]` works on our own types.
20extern crate self as wolfram_serialize;
21
22pub mod complex;
23pub mod constants;
24pub mod errors;
25pub mod from_wxf;
26pub mod numeric_in;
27pub mod reader;
28pub mod strategy;
29pub mod to_wxf;
30pub mod writer;
31pub mod wxf;
32
33pub use crate::errors::Error;
34
35pub use crate::complex::{Complex, Complex32, Complex64};
36
37pub use crate::constants::{
38    ExpressionEnum, HeaderEnum, NumericArrayEnum, PackedArrayEnum,
39};
40pub use crate::from_wxf::FromWXF;
41pub use crate::reader::{Reader, SliceReader};
42pub use crate::to_wxf::{ToWXF, WxfStruct};
43pub use crate::writer::Writer;
44pub use crate::wxf::reader::WxfReader;
45pub use crate::wxf::writer::WxfWriter;
46// Procedural derives — same names as the traits, resolved by Rust's separate
47// macro / type namespaces.
48pub use wolfram_serialize_macros::{Failure, FromWXF, ToWXF};
49
50/// zlib compression level passed to [`to_wxf`].
51#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
52pub enum CompressionLevel {
53    /// zlib level 1 — fastest, lowest ratio.
54    Fastest,
55    /// zlib level 6 — balanced (zlib default; matches `BinarySerialize[…, PerformanceGoal -> "Size"]`).
56    Default,
57    /// zlib level 9 — slowest, highest ratio.
58    Best,
59    /// Explicit zlib level. Values above 9 are clamped to 9.
60    Level(u8),
61}
62
63impl CompressionLevel {
64    pub(crate) fn to_u8(self) -> u8 {
65        match self {
66            CompressionLevel::Fastest => 1,
67            CompressionLevel::Default => 6,
68            CompressionLevel::Best => 9,
69            CompressionLevel::Level(n) => n.min(9),
70        }
71    }
72}
73
74//==============================================================================
75// Top-level API
76//==============================================================================
77
78/// Serialize `value` to WXF.
79///
80/// `compression` is `impl Into<Option<CompressionLevel>>`: pass `None` for plain
81/// uncompressed WXF (`8:` header), or a [`CompressionLevel`] for zlib-compressed
82/// WXF (`8C:` header) — e.g. `to_wxf(&v, None)` or
83/// `to_wxf(&v, CompressionLevel::Default)`.
84///
85/// The compressed path streams the token body directly through the
86/// [`ZlibEncoder`][flate2::write::ZlibEncoder] — no intermediate uncompressed
87/// buffer.
88pub fn to_wxf<T: ToWXF + ?Sized>(
89    value: &T,
90    compression: impl Into<Option<CompressionLevel>>,
91) -> Result<Vec<u8>, Error> {
92    use crate::constants::HeaderEnum;
93
94    // The header (`8:` / `8C:`) is framing, written here — uncompressed and at
95    // the front — mirroring `strip_header` on the read side. The token body is
96    // then written through the appropriate sink (the Vec directly, or a
97    // streaming ZlibEncoder over it for `8C:`).
98    let ver = HeaderEnum::Version as u8;
99    let sep = HeaderEnum::Separator as u8;
100    match compression.into() {
101        None => {
102            let out = vec![ver, sep];
103            let mut w = WxfWriter::new(out);
104            value.to_wxf(&mut w)?;
105            Ok(w.into_inner())
106        },
107        Some(level) => {
108            use flate2::write::ZlibEncoder;
109            use flate2::Compression;
110
111            let out = vec![ver, HeaderEnum::Compress as u8, sep];
112            let encoder =
113                ZlibEncoder::new(out, Compression::new(u32::from(level.to_u8())));
114            let mut w = WxfWriter::new(encoder);
115            value.to_wxf(&mut w)?;
116            Ok(w.into_inner().finish()?)
117        },
118    }
119}
120
121/// Strip the WXF header, returning the raw token stream. `8:` payloads are
122/// borrowed; `8C:` payloads are zlib-decompressed into an owned buffer.
123fn strip_header(bytes: &[u8]) -> Result<std::borrow::Cow<'_, [u8]>, Error> {
124    use std::io::Read;
125
126    use crate::constants::HeaderEnum;
127
128    if bytes.len() < 2 {
129        return Err(Error::invalid(
130            "byte stream too short for WXF header".into(),
131        ));
132    }
133    if bytes[0] != HeaderEnum::Version as u8 {
134        return Err(Error::invalid(format!(
135            "WXF header version mismatch: expected {:?}, got {:?}",
136            HeaderEnum::Version as u8 as char,
137            bytes[0] as char
138        )));
139    }
140    if bytes[1] == HeaderEnum::Compress as u8 {
141        if bytes.len() < 3 || bytes[2] != HeaderEnum::Separator as u8 {
142            return Err(Error::invalid("WXF compressed header truncated".into()));
143        }
144        let mut decoded = Vec::new();
145        flate2::read::ZlibDecoder::new(&bytes[3..])
146            .read_to_end(&mut decoded)
147            .map_err(|e| Error::invalid(format!("zlib decompress failed: {}", e)))?;
148        Ok(std::borrow::Cow::Owned(decoded))
149    } else if bytes[1] == HeaderEnum::Separator as u8 {
150        Ok(std::borrow::Cow::Borrowed(&bytes[2..]))
151    } else {
152        Err(Error::invalid(format!(
153            "WXF header separator mismatch: expected ':' or 'C', got {:?}",
154            bytes[1] as char
155        )))
156    }
157}
158
159/// Read from a WXF blob (`8:` / `8C:` auto-detected) via a [`WxfReader`]. The
160/// closure can pull one or more top-level values — e.g. a `Function[List, …]`
161/// wrapper around several arguments. For a single value, prefer [`from_wxf`].
162pub fn read_wxf<T>(
163    bytes: &[u8],
164    f: impl for<'a> FnOnce(&mut WxfReader<SliceReader<'a>>) -> Result<T, Error>,
165) -> Result<T, Error> {
166    let payload = strip_header(bytes)?;
167    let mut r = WxfReader::new(SliceReader::new(&payload));
168    f(&mut r)
169}
170
171/// Deserialize `bytes` (WXF; `8:` or `8C:` auto-detected) into a typed `T`.
172///
173/// Use `T = Expr` for an untyped tree, or any [`FromWXF`] type — including those
174/// produced by `#[derive(FromWXF)]` — for typed deserialization with no
175/// intermediate `Expr`.
176pub fn from_wxf<T: for<'de> FromWXF<'de>>(bytes: &[u8]) -> Result<T, Error> {
177    read_wxf(bytes, |r| T::from_wxf(r))
178}
179
180/// Deserialize `bytes` into a **borrowed** `T` whose `&str` / `&[u8]` fields
181/// point straight into `bytes` (zero-copy). The result borrows `bytes`, so the
182/// input must be **uncompressed** (`8:`) — a `8C:` payload would have to be
183/// decompressed into a temporary the borrow couldn't outlive (use [`from_wxf`]
184/// for the owned form, or [`read_wxf`] to borrow within a closure).
185pub fn from_wxf_ref<'de, T: FromWXF<'de>>(bytes: &'de [u8]) -> Result<T, Error> {
186    use crate::constants::HeaderEnum;
187
188    if bytes.len() < 2 || bytes[0] != HeaderEnum::Version as u8 {
189        return Err(Error::invalid("not a WXF stream".into()));
190    }
191    if bytes[1] == HeaderEnum::Compress as u8 {
192        return Err(Error::invalid(
193            "from_wxf_ref requires uncompressed (8:) WXF — borrowed views can't \
194             point into a decompressed buffer"
195                .into(),
196        ));
197    }
198    if bytes[1] != HeaderEnum::Separator as u8 {
199        return Err(Error::invalid("malformed WXF header".into()));
200    }
201    let payload: &'de [u8] = &bytes[2..];
202    let mut r = WxfReader::new(SliceReader::new(payload));
203    T::from_wxf(&mut r)
204}