Releases: ponylang/msgpack
Release list
0.4.0
0.3.1
Add decoding size limits to streaming decoder
MessagePackStreamingDecoder now enforces size limits on variable-length values to protect against denial-of-service attacks. By default, conservative limits are applied: 1 MB for str/bin/ext data and 131,072 for array/map element counts.
When a value exceeds its limit, next() returns LimitExceeded with no bytes consumed.
// Default limits (1 MB str/bin/ext, 131K array/map):
let sd = MessagePackStreamingDecoder
// Custom limits:
let limits = MessagePackDecodeLimits(
where max_str_len' = 4096)
let sd = MessagePackStreamingDecoder(limits)
// No limits:
let sd = MessagePackStreamingDecoder(
MessagePackDecodeLimits.unlimited())Existing code using MessagePackStreamingDecoder with no arguments gets limit protection automatically. The DecodeResult type now includes LimitExceeded, so match expressions on DecodeResult need a new branch:
// Before:
match decoder.next()
| let v: U32 => // handle value
| NotEnoughData => // wait for more data
| InvalidData => // abort
end
// After:
match decoder.next()
| let v: U32 => // handle value
| NotEnoughData => // wait for more data
| LimitExceeded => // value too large, reject
| InvalidData => // abort
endAdd container depth limits to streaming decoder
MessagePackStreamingDecoder now tracks container nesting depth and enforces a configurable limit. When decoding nested arrays or maps would exceed the limit, next() returns LimitExceeded with no bytes consumed. This protects against stack overflow and exponential work from deeply nested container structures.
The default limit is 512 levels of nesting. Depth tracking is automatic — the decoder increments depth when returning MessagePackArray or MessagePackMap headers and decrements as elements are consumed.
// Default limits include max_depth=512:
let sd = MessagePackStreamingDecoder
// Custom depth limit:
let limits = MessagePackDecodeLimits(
where max_depth' = 16)
let sd = MessagePackStreamingDecoder(limits)
// Query current nesting depth:
sd.depth()Add skip method for advancing past values without decoding
MessagePackDecoder and MessagePackStreamingDecoder now support
skipping values without decoding them. This enables forward-compatible
protocols where consumers can gracefully handle unknown fields.
MessagePackDecoder.skip advances the reader past one complete
value, including nested containers:
let count = MessagePackDecoder.map(reader)?
var i: U32 = 0
while i < count do
let key = MessagePackDecoder.str(reader)?
match consume key
| "name" => name = MessagePackDecoder.str(reader)?
else
MessagePackDecoder.skip(reader)?
end
i = i + 1
endMessagePackStreamingDecoder.skip provides the streaming-safe
variant, returning SkipResult (None on success, NotEnoughData,
InvalidData, or LimitExceeded). A new max_skip_values field
on MessagePackDecodeLimits bounds the number of values traversed
during a single skip (default: 1,048,576).
Add opt-in UTF-8 validation for str format values
The MessagePack spec defines str format values as UTF-8 strings, but previously this library treated them as opaque byte sequences without validation. Opt-in UTF-8 validation is now available at every layer.
Validating _utf8 method variants are available on all encoders and decoders. They work identically to their non-validating counterparts but error when the bytes are not valid UTF-8:
// Encode — errors if bytes are not valid UTF-8
MessagePackEncoder.str_utf8(w, value)?
// Decode — errors if decoded bytes are not valid UTF-8
let s = MessagePackDecoder.str_utf8(reader)?
let s = MessagePackZeroCopyDecoder.str_utf8(reader)?Format-specific variants are also available: fixstr_utf8, str_8_utf8, str_16_utf8, and str_32_utf8.
The streaming decoder accepts a validate_utf8 constructor option. When enabled, str values with invalid UTF-8 return InvalidUtf8 — a new member of the DecodeResult union, distinct from InvalidData. The MessagePack framing is valid and decoding can continue:
let sd = MessagePackStreamingDecoder(
where validate_utf8' = true)
match sd.next()
| let s: String val => // valid UTF-8 string
| InvalidUtf8 => // invalid UTF-8, stream is fine
endA public MessagePackValidateUTF8 primitive supports the "decode then validate" pattern for callers who need access to the raw bytes on validation failure:
let s = MessagePackDecoder.str(reader)?
if not MessagePackValidateUTF8(s) then
// s still available — log, reject, or use as raw bytes
endExisting non-validating methods are unchanged. The default behavior is preserved for backward compatibility.
[0.3.1] - 2026-02-08
Added
0.3.0
Add streaming-safe MessagePack decoder
Add MessagePackStreamingDecoder, a new decoder class designed for use with streaming data sources. Unlike MessagePackDecoder, which assumes all data is available and will corrupt the reader on partial reads, the streaming decoder peeks at format bytes and length fields before consuming any data. If insufficient data is available, it returns NotEnoughData with zero bytes consumed, allowing the caller to append more data and retry.
Example Usage
// Decode values as chunks of data arrive.
// Call append() with each chunk, then next() to attempt decoding.
// next() returns NotEnoughData if more bytes are needed,
// InvalidData if the stream is corrupt, or a decoded value.
let sd = MessagePackStreamingDecoder
sd.append(chunk)
match sd.next()
| let v: U32 => // got a value
| let s: String val => // got a string
| let t: MessagePackTimestamp => // got a timestamp
| let a: MessagePackArray => // array header; read a.size elements
| let m: MessagePackMap => // map header; read m.size key-value pairs
| NotEnoughData => // append more data and retry
| InvalidData => // stream is corrupt
endChange timestamp nsec type from I64 to U32
The nanoseconds component of decoded timestamps is now U32 instead of I64. This affects MessagePackDecoder.timestamp() (return type changed from (I64, I64) to (I64, U32)) and MessagePackTimestamp.nsec (field type changed from I64 to U32).
This is a breaking change. Code that destructures the decoder's return value or reads the nsec field will need to account for the new type. The encoder already accepted U32 for nanoseconds, so encoding code is unaffected.
Before
// MessagePackDecoder.timestamp()
(let sec: I64, let nsec: I64) =
MessagePackDecoder.timestamp(reader)?
// MessagePackTimestamp.nsec
let ts: MessagePackTimestamp = ...
let nsec: I64 = ts.nsecAfter
// MessagePackDecoder.timestamp()
(let sec: I64, let nsec: U32) =
MessagePackDecoder.timestamp(reader)?
// MessagePackTimestamp.nsec
let ts: MessagePackTimestamp = ...
let nsec: U32 = ts.nsecAdd compact encoding and decoding methods
MessagePackEncoder and MessagePackDecoder now provide compact methods that automatically select the smallest wire format for a given value, per the MessagePack spec recommendation. The format-specific methods remain available for explicit control.
Encoder
let w: Writer ref = Writer
// Integer — picks positive_fixint, uint_8, ..., or uint_64
MessagePackEncoder.uint(w, 42)
// Signed — uses unsigned formats for positive values
MessagePackEncoder.int(w, -100)
// String — picks fixstr, str_8, str_16, or str_32
MessagePackEncoder.str(w, "hello")?
// Binary — picks bin_8, bin_16, or bin_32
MessagePackEncoder.bin(w, data)?
// Array header — picks fixarray, array_16, or array_32
MessagePackEncoder.array(w, 3)
// Map header — picks fixmap, map_16, or map_32
MessagePackEncoder.map(w, 2)
// Extension — prefers fixext for sizes 1/2/4/8/16
MessagePackEncoder.ext(w, ext_type, data)?
// Timestamp — picks timestamp_32, _64, or _96
MessagePackEncoder.timestamp(w, seconds, nanoseconds)?Decoder
let b: Reader ref = Reader
// Reads any unsigned integer format
let n: U64 = MessagePackDecoder.uint(b)?
// Reads any signed or unsigned integer format
let i: I64 = MessagePackDecoder.int(b)?
// str() now handles fixstr in addition to str_8/str_16/str_32
// (previously, fixstr required calling fixstr() directly)
let s: String iso^ = MessagePackDecoder.str(b)?
// Reads any array header format
let count: U32 = MessagePackDecoder.array(b)?
// Reads any map header format
let pairs: U32 = MessagePackDecoder.map(b)?[0.3.0] - 2026-02-08
Added
Changed
- Change timestamp nsec type from I64 to U32 (PR #56)
0.2.5
Update to work with Pony 0.49.0
Pony 0.49.0 introduced a lot of different breaking changes. We've updated to account for them all.
[0.2.5] - 2022-02-26
0.2.3
[0.2.3] - 2019-09-02
Added
- Automated release process
0.2.1
0.2.0
[0.2] - 2017-12-02
Added
- Added low level decoding methods
Initial version
Alpha software. Low-level API support for encoding.
See README for full details.