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`][fn@to_wxf] (compression optional), [`from_wxf`][fn@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(crate) mod errors;
25// `from_wxf`, `numeric_in`, and `strategy` stay `pub`: the derive macros emit
26// fully-qualified calls into them (`wolfram_serialize::from_wxf::err_at`,
27// `wolfram_serialize::numeric_in::read_fixed`, `wolfram_serialize::strategy::*`)
28// from *downstream* crates, so those paths must resolve outside this crate.
29pub mod from_wxf;
30pub mod numeric_in;
31pub(crate) mod reader;
32pub mod strategy;
33pub(crate) mod to_wxf;
34pub(crate) mod writer;
35pub(crate) mod wxf;
36
37pub use crate::errors::Error;
38
39/// Upper bound on container capacity pre-allocated from an untrusted
40/// length/count prefix. Deserialization reads counts (array rank, association
41/// size, function arity) straight from the input; a malformed prefix could
42/// otherwise request a multi-gigabyte allocation before any bytes are validated.
43/// We cap the `with_capacity` *hint* — the container still grows to the real
44/// size as elements are read, but a bogus count can no longer OOM us up front.
45pub(crate) const PREALLOC_CAP: usize = 4096;
46
47/// Clamp a capacity hint that came from an untrusted length prefix to
48/// [`PREALLOC_CAP`]. Use this for every `with_capacity` driven by wire data.
49pub(crate) fn capped_capacity(hint: usize) -> usize {
50 hint.min(PREALLOC_CAP)
51}
52
53pub use crate::complex::{Complex, Complex32, Complex64};
54
55pub use crate::constants::{
56 ExpressionEnum, HeaderEnum, NumericArrayEnum, PackedArrayEnum,
57};
58pub use crate::from_wxf::FromWXF;
59pub use crate::reader::{Reader, SliceReader};
60pub use crate::to_wxf::{ToWXF, WxfStruct};
61pub use crate::writer::Writer;
62pub use crate::wxf::reader::WxfReader;
63pub use crate::wxf::writer::WxfWriter;
64// Procedural derives — same names as the traits, resolved by Rust's separate
65// macro / type namespaces.
66pub use wolfram_serialize_macros::{Failure, FromWXF, ToWXF};
67
68/// zlib compression level passed to [`to_wxf`][fn@to_wxf].
69#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
70pub enum CompressionLevel {
71 /// zlib level 1 — fastest, lowest ratio.
72 Fastest,
73 /// zlib level 6 — balanced (zlib default; matches `BinarySerialize[…, PerformanceGoal -> "Size"]`).
74 Default,
75 /// zlib level 9 — slowest, highest ratio.
76 Best,
77 /// Explicit zlib level. Values above 9 are clamped to 9.
78 Level(u8),
79}
80
81impl CompressionLevel {
82 pub(crate) fn to_u8(self) -> u8 {
83 match self {
84 CompressionLevel::Fastest => 1,
85 CompressionLevel::Default => 6,
86 CompressionLevel::Best => 9,
87 CompressionLevel::Level(n) => n.min(9),
88 }
89 }
90}
91
92//==============================================================================
93// Top-level API
94//==============================================================================
95
96/// Serialize `value` to WXF.
97///
98/// `compression` is `impl Into<Option<CompressionLevel>>`: pass `None` for plain
99/// uncompressed WXF (`8:` header), or a [`CompressionLevel`] for zlib-compressed
100/// WXF (`8C:` header) — e.g. `to_wxf(&v, None)` or
101/// `to_wxf(&v, CompressionLevel::Default)`.
102///
103/// The compressed path streams the token body directly through the
104/// [`ZlibEncoder`][flate2::write::ZlibEncoder] — no intermediate uncompressed
105/// buffer.
106///
107/// ```
108/// use wolfram_serialize::{to_wxf, from_wxf, CompressionLevel};
109///
110/// let bytes = to_wxf(&vec![1_i64, 2, 3], None).unwrap();
111/// assert_eq!(&bytes[..2], b"8:"); // uncompressed header
112///
113/// let compressed = to_wxf(&vec![1_i64, 2, 3], CompressionLevel::Default).unwrap();
114/// assert_eq!(&compressed[..3], b"8C:"); // zlib-compressed header
115///
116/// // Both forms decode the same way — `from_wxf` auto-detects the header.
117/// assert_eq!(from_wxf::<Vec<i64>>(&bytes).unwrap(), vec![1, 2, 3]);
118/// assert_eq!(from_wxf::<Vec<i64>>(&compressed).unwrap(), vec![1, 2, 3]);
119/// ```
120pub fn to_wxf<T: ToWXF + ?Sized>(
121 value: &T,
122 compression: impl Into<Option<CompressionLevel>>,
123) -> Result<Vec<u8>, Error> {
124 use crate::constants::HeaderEnum;
125
126 // The header (`8:` / `8C:`) is framing, written here — uncompressed and at
127 // the front — mirroring `strip_header` on the read side. The token body is
128 // then written through the appropriate sink (the Vec directly, or a
129 // streaming ZlibEncoder over it for `8C:`).
130 let ver = HeaderEnum::Version as u8;
131 let sep = HeaderEnum::Separator as u8;
132 match compression.into() {
133 None => {
134 let out = vec![ver, sep];
135 let mut w = WxfWriter::new(out);
136 value.to_wxf(&mut w)?;
137 Ok(w.into_inner())
138 },
139 Some(level) => {
140 use flate2::write::ZlibEncoder;
141 use flate2::Compression;
142
143 let out = vec![ver, HeaderEnum::Compress as u8, sep];
144 let encoder =
145 ZlibEncoder::new(out, Compression::new(u32::from(level.to_u8())));
146 let mut w = WxfWriter::new(encoder);
147 value.to_wxf(&mut w)?;
148 Ok(w.into_inner().finish()?)
149 },
150 }
151}
152
153/// Serialize `value` as **uncompressed** WXF (`8:` header) into an arbitrary
154/// [`Writer`] sink, without materializing an intermediate `Vec<u8>`.
155///
156/// Combined with [`wxf_byte_len`] this allows serializing straight into a
157/// pre-sized caller-owned buffer (e.g. a LibraryLink `NumericArray<u8>`),
158/// avoiding the allocate-then-copy round trip of [`to_wxf`].
159pub fn to_wxf_into<T: ToWXF + ?Sized, W: Writer>(
160 value: &T,
161 mut sink: W,
162) -> Result<(), Error> {
163 use crate::constants::HeaderEnum;
164
165 sink.write_byte(HeaderEnum::Version as u8)?;
166 sink.write_byte(HeaderEnum::Separator as u8)?;
167 let mut w = WxfWriter::new(sink);
168 value.to_wxf(&mut w)
169}
170
171/// Exact byte length of the **uncompressed** WXF encoding of `value` (i.e.
172/// of `to_wxf(value, None)`), computed by a counting pass over the token
173/// stream — no bytes are buffered, so this is cheap even for large payloads
174/// (bulk `write_bytes` calls just add their length).
175pub fn wxf_byte_len<T: ToWXF + ?Sized>(value: &T) -> Result<usize, Error> {
176 struct ByteCounter(usize);
177
178 impl std::io::Write for ByteCounter {
179 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
180 self.0 += buf.len();
181 Ok(buf.len())
182 }
183
184 fn flush(&mut self) -> std::io::Result<()> {
185 Ok(())
186 }
187 }
188
189 let mut counter = ByteCounter(2); // `8:` header
190 let mut w = WxfWriter::new(&mut counter);
191 value.to_wxf(&mut w)?;
192 Ok(counter.0)
193}
194
195/// Strip the WXF header, returning the raw token stream. `8:` payloads are
196/// borrowed; `8C:` payloads are zlib-decompressed into an owned buffer.
197fn strip_header(bytes: &[u8]) -> Result<std::borrow::Cow<'_, [u8]>, Error> {
198 use std::io::Read;
199
200 use crate::constants::HeaderEnum;
201
202 if bytes.len() < 2 {
203 return Err(Error::invalid(
204 "byte stream too short for WXF header".into(),
205 ));
206 }
207 if bytes[0] != HeaderEnum::Version as u8 {
208 return Err(Error::invalid(format!(
209 "WXF header version mismatch: expected {:?}, got {:?}",
210 HeaderEnum::Version as u8 as char,
211 bytes[0] as char
212 )));
213 }
214 if bytes[1] == HeaderEnum::Compress as u8 {
215 if bytes.len() < 3 || bytes[2] != HeaderEnum::Separator as u8 {
216 return Err(Error::invalid("WXF compressed header truncated".into()));
217 }
218 let mut decoded = Vec::new();
219 flate2::read::ZlibDecoder::new(&bytes[3..])
220 .read_to_end(&mut decoded)
221 .map_err(|e| Error::invalid(format!("zlib decompress failed: {}", e)))?;
222 Ok(std::borrow::Cow::Owned(decoded))
223 } else if bytes[1] == HeaderEnum::Separator as u8 {
224 Ok(std::borrow::Cow::Borrowed(&bytes[2..]))
225 } else {
226 Err(Error::invalid(format!(
227 "WXF header separator mismatch: expected ':' or 'C', got {:?}",
228 bytes[1] as char
229 )))
230 }
231}
232
233/// Strip the WXF header (`8:` / `8C:` auto-detected, decompressing if needed)
234/// and hand the closure a [`WxfReader`] positioned at the start of the token
235/// stream, so it can drive the cursor directly.
236///
237/// [`from_wxf`][fn@from_wxf] only fits when the *entire* wire value decodes as
238/// one [`FromWXF`] type. Reach for `read_wxf` instead when you need to:
239///
240/// * decode several **positional** values off one cursor — e.g. a LibraryLink
241/// argument list arrives as `Function[<head>, arg0, arg1, …]`, where each
242/// argument has its own Rust type and must be read in order (this is exactly
243/// how `#[export(wxf)]` unpacks its arguments);
244/// * inspect a token (via [`WxfReader::read_expr_token`]) before deciding how
245/// to decode the rest, since [`from_wxf`][fn@from_wxf] commits to a single
246/// `T` up front;
247/// * read **borrowed** (`&str` / `&[u8]`) data — the borrow is tied to the
248/// input buffer, so it must be consumed *inside* the closure instead of
249/// escaping the call (see [`FromWXF`] for the zero-copy story).
250///
251/// ```
252/// use wolfram_serialize::{read_wxf, ExpressionEnum, FromWXF, WxfWriter};
253///
254/// // Hand-build the wire form of `{1, "two", 3.0}`:
255/// // `Function[System`List, 1, "two", 3.0]`.
256/// let mut w = WxfWriter::new(vec![b'8', b':']);
257/// w.write_function(3).unwrap();
258/// w.write_symbol("System`List").unwrap();
259/// w.write_integer(1).unwrap();
260/// w.write_string("two").unwrap();
261/// w.write_real(3.0).unwrap();
262/// let bytes = w.into_inner();
263///
264/// // Decode the three arguments positionally, each with its own Rust type —
265/// // there is no single `FromWXF` type spanning all three, so `from_wxf`
266/// // alone can't do this.
267/// let (a, b, c) = read_wxf(&bytes, |r| {
268/// assert_eq!(r.read_expr_token()?, ExpressionEnum::Function);
269/// let arity = r.read_varint()?;
270/// r.skip()?; // discard the head (`System`List`)
271/// assert_eq!(arity, 3);
272/// Ok((i64::from_wxf(r)?, String::from_wxf(r)?, f64::from_wxf(r)?))
273/// })
274/// .unwrap();
275///
276/// assert_eq!((a, b, c), (1, "two".to_string(), 3.0));
277/// ```
278pub fn read_wxf<T>(
279 bytes: &[u8],
280 f: impl for<'a> FnOnce(&mut WxfReader<SliceReader<'a>>) -> Result<T, Error>,
281) -> Result<T, Error> {
282 let payload = strip_header(bytes)?;
283 let mut r = WxfReader::new(SliceReader::new(&payload));
284 f(&mut r)
285}
286
287/// Deserialize `bytes` (WXF; `8:` or `8C:` auto-detected) into a typed `T`.
288///
289/// Use `T = Expr` for an untyped tree, or any [`FromWXF`] type — including those
290/// produced by `#[derive(FromWXF)]` — for typed deserialization with no
291/// intermediate `Expr`.
292///
293/// ```
294/// use wolfram_serialize::{to_wxf, from_wxf, FromWXF, ToWXF};
295///
296/// #[derive(ToWXF, FromWXF, Debug, PartialEq)]
297/// struct Point { x: f64, y: f64 }
298///
299/// let bytes = to_wxf(&Point { x: 1.0, y: 2.0 }, None).unwrap();
300/// let point: Point = from_wxf(&bytes).unwrap();
301/// assert_eq!(point, Point { x: 1.0, y: 2.0 });
302/// ```
303///
304/// Downstream, `wolfram_expr::Expr` also implements [`FromWXF`], so `T = Expr`
305/// decodes into an untyped tree when the shape isn't known ahead of time.
306pub fn from_wxf<T: for<'de> FromWXF<'de>>(bytes: &[u8]) -> Result<T, Error> {
307 read_wxf(bytes, |r| T::from_wxf(r))
308}