Skip to documentation
SLOP

tiny.preserves.packed_reader

Reference tiny.preserves packed_reader

Defined in tiny.preserves.

Reads one value back from its binary encoding.

API (6)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.preservespacked reader
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate sourcelib.preserves.src.packed.readerreadDoubleprivate sourcelib.preserves.src.packed.readerreadSignedIntegerprivate sourcelib.preserves.src.packed.readerreadStringLiketest sourcelib.preserves.src.packed.readertest: readVarint parses multi-bytetest sourcelib.preserves.src.packed.readertest: readVarint parses single-bytetest sourcelib.preserves.src.packed.readertest: readVarint rejects overlongpacked_readerreadVarint
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/preserves/src/packed/reader.zig

zig
//! Reads one value back from its binary encoding. A caller decoding bytes from a peer needs the//! decoder's memory, recursion and work bounded by numbers it chooses, whatever the bytes claim. A//! caller that fingerprints encoded bytes needs every accepted input to be the one encoding of its//! value.//!//! Encoded bytes declare their own lengths and nesting, so a short input can claim a huge string or//! nest thousands of levels deep. The encoding is the binary syntax of the//! [Preserves](https://preserves.dev/) data language, which the package keeps.//!//! `decode` takes four limits (`Limits`) and fails as soon as the input would pass one: nesting//! depth, total values, items per collection and retained bytes. It accepts only the one encoding//! of each value: shortest integers and varints, and set elements and dictionary keys in ascending//! byte order with no repeats. It rejects annotations and embedded values. The decoded value copies//! every byte it keeps, so it owns all its memory and borrows nothing from the input.const std = @import("std");const Allocator = std.mem.Allocator;const preserves = @import("../root.zig");const value_mod = preserves.value;const integer_mod = preserves.integer_mod;const constants = @import("constants.zig");pub const Tag = constants.Tag;/// The ways `decode` refuses its input, one tag per kind of bad or over-limit bytes. Code that/// reports why bytes were refused switches on these tags, and `DecodeError` adds running out of/// memory.pub const DecodeFailure = error{    /// Values nested deeper than `max_depth`, counting the top value as depth 1.    DepthLimitExceeded,    /// Input holding more than `max_nodes` values in all, counting every nested value, record label    /// and dictionary key.    NodeLimitExceeded,    /// A record, sequence, set or dictionary with more than `max_collection_items` fields, items or    /// entries.    CollectionLimitExceeded,    /// Input whose decoded value would keep more than `max_retained_bytes` bytes.    RetainedBytesLimitExceeded,    /// Input that ends inside a value: before a tag byte, inside a varint or payload, or before a    /// collection's end marker.    UnexpectedEof,    /// Bytes left over after the first complete value.    UnexpectedTrailingBytes,    /// A tag byte outside the fourteen tags in `Tag`.    UnknownTag,    /// The end marker 0x84 at the start of a value, such as right after a record's opening tag or    /// at the start of the input.    UnexpectedEndMarker,    /// A varint of two or more bytes whose last byte is zero.    OverlongVarint,    /// A varint whose value needs more than 64 bits: its tenth byte is greater than 1.    VarintTooWide,    /// A length that exceeds `usize`. It can happen only on targets whose `usize` has fewer than 64    /// bits, since a varint holds at most 64.    LengthOutOfRange,    /// A double, tag 0x87, whose length differs from 8.    InvalidDoubleLength,    /// An integer payload with a redundant leading byte: 0x00 before a byte below 0x80, 0xff before    /// a byte of 0x80 or above, or a lone 0x00.    NonCanonicalInteger,    /// A set whose element encodings fall outside strictly ascending byte order.    NonCanonicalSetOrder,    /// A dictionary whose key encodings fall outside strictly ascending byte order.    NonCanonicalDictionaryOrder,    /// A string or symbol whose payload is invalid UTF-8.    InvalidUtf8,    /// An annotation, tag 0x85, anywhere in the input.    AnnotationNotSupported,    /// An embedded value, tag 0x86, anywhere in the input, whatever the type of the embedded    /// values.    EmbeddedNotSupported,    /// A set holding two equal elements. The check runs before the order check, so repeated bytes    /// report this tag.    DuplicateSetElement,    /// A dictionary holding two equal keys. The check runs before the order check, so a repeated    /// key reports this tag.    DuplicateDictionaryKey,};/// The errors `decode` returns: every `DecodeFailure` tag and running out of memory. Code that/// decodes packed bytes names this set, for its own error sets.pub const DecodeError = Allocator.Error || DecodeFailure;/// The four bounds `decode` enforces while it reads. Code that decodes bytes from a peer sets one/// for each kind of payload it admits. The struct has no defaults, so the caller picks every bound.pub const Limits = struct {    /// The deepest nesting `decode` accepts, counting the top value as depth 1. Each record label,    /// field, item, key and value sits one level below its container.    max_depth: u16,    /// The most values `decode` reads in all, counting every nested value.    max_nodes: u32,    /// The most fields, items or entries one record, sequence, set or dictionary may hold. A    /// record's label does not count, and a dictionary entry counts once.    max_collection_items: u32,    /// The most bytes the decoded value may keep, counted as it is built. The decoder counts every    /// string, byte string and symbol payload. It counts an integer's payload only when it is    /// longer than 16 bytes and exceeds 128 bits unsigned. It counts each collection's slice at the    /// size of its items, and one more value for each record's label.    max_retained_bytes: usize,};const Admission = struct {    limits: Limits,    nodes: u32 = 0,    retained_bytes: usize = 0,    fn enter(self: *Admission, depth: u32) DecodeError!void {        if (depth > self.limits.max_depth) return error.DepthLimitExceeded;        if (self.nodes >= self.limits.max_nodes) return error.NodeLimitExceeded;        self.nodes += 1;    }    fn retain(self: *Admission, bytes: usize) DecodeError!void {        if (bytes > self.limits.max_retained_bytes -| self.retained_bytes) {            return error.RetainedBytesLimitExceeded;        }        self.retained_bytes += bytes;    }    fn retainItems(self: *Admission, comptime T: type, count: usize) DecodeError!void {        const bytes = std.math.mul(usize, @sizeOf(T), count) catch            return error.RetainedBytesLimitExceeded;        return self.retain(bytes);    }    fn admitItem(self: *Admission, count: usize) DecodeError!void {        if (count >= self.limits.max_collection_items) {            return error.CollectionLimitExceeded;        }    }};/// Returns the one value that `bytes` encodes, allocated with `allocator`. Code that admits packed/// bytes from a peer calls it, for one owned value within the caller's limits. `D` is the type of/// the result's embedded values, and the decoder rejects embedded values whatever `D` is. The value/// owns all its memory, so `bytes` may be freed at once, and the caller frees the value with/// `deinit`. On any error the call frees everything it allocated. Its duplicate checks compare each/// set element or dictionary key with every earlier one, so their cost grows with the square of the/// count.pub fn decode(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    limits: Limits,) DecodeError!value_mod.Value(D) {    var index: usize = 0;    var admission = Admission{ .limits = limits };    var value = try readValue(D, allocator, bytes, &index, &admission, 1);    errdefer value.deinit(allocator);    if (index != bytes.len) return error.UnexpectedTrailingBytes;    return value;}fn readValue(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    depth: u32,) DecodeError!value_mod.Value(D) {    try admission.enter(depth);    if (index.* >= bytes.len) return error.UnexpectedEof;    const tag_byte = bytes[index.*];    index.* += 1;    const tag = Tag.fromByte(tag_byte) orelse return error.UnknownTag;    return switch (tag) {        .false_ => value_mod.Value(D).initBoolean(false),        .true_ => value_mod.Value(D).initBoolean(true),        .end => error.UnexpectedEndMarker,        .annotation => error.AnnotationNotSupported,        .embedded => error.EmbeddedNotSupported,        .ieee754 => try readDouble(D, bytes, index),        .signed_integer => try readSignedInteger(D, allocator, bytes, index, admission),        .string => try readStringLike(D, allocator, bytes, index, admission, .string),        .byte_string => try readStringLike(D, allocator, bytes, index, admission, .byte_string),        .symbol => try readStringLike(D, allocator, bytes, index, admission, .symbol),        .record => try readRecord(D, allocator, bytes, index, admission, depth),        .sequence => try readSequence(D, allocator, bytes, index, admission, depth),        .set => try readSet(D, allocator, bytes, index, admission, depth),        .dictionary => try readDictionary(D, allocator, bytes, index, admission, depth),    };}/// Reads one varint from `bytes` at `index.*`, advances `index` past it and returns its value. The/// reader calls it for every length prefix, and code that walks packed bytes by hand can call it/// too. The varint holds seven bits per byte, low bits first, with the high bit set on every byte/// but the last. The call returns `error.UnexpectedEof` when the input ends first,/// `error.OverlongVarint` when a later byte is zero, and `error.VarintTooWide` past 64 bits.pub fn readVarint(bytes: []const u8, index: *usize) DecodeError!u64 {    var result: u64 = 0;    var shift: u6 = 0;    for (0..10) |byte_index| {        if (index.* >= bytes.len) return error.UnexpectedEof;        const b = bytes[index.*];        index.* += 1;        if (byte_index == 9 and b > 1) return error.VarintTooWide;        result |= @as(u64, b & 0x7f) << shift;        if ((b & 0x80) == 0) {            if (byte_index > 0 and b == 0) return error.OverlongVarint;            return result;        }        if (byte_index == 9) return error.VarintTooWide;        shift += 7;    }    unreachable;}fn readDouble(comptime D: type, bytes: []const u8, index: *usize) DecodeError!value_mod.Value(D) {    const len = try readVarint(bytes, index);    if (len != 8) return error.InvalidDoubleLength;    if (bytes.len - index.* < 8) return error.UnexpectedEof;    const raw = std.mem.readInt(u64, bytes[index.*..][0..8], .big);    index.* += 8;    return value_mod.Value(D).initDouble(@bitCast(raw));}fn readSignedInteger(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,) DecodeError!value_mod.Value(D) {    const len = try readVarint(bytes, index);    const n = std.math.cast(usize, len) orelse return error.LengthOutOfRange;    if (bytes.len - index.* < n) return error.UnexpectedEof;    const payload = bytes[index.* .. index.* + n];    index.* += n;    if (!integer_mod.SignedInteger.isCanonicalBytes(payload)) {        return error.NonCanonicalInteger;    }    try admission.retain(integerRetainedBytes(payload));    const si = try integer_mod.SignedInteger.fromCanonicalBytes(allocator, payload);    return value_mod.Value(D).initSignedInteger(si);}fn integerRetainedBytes(payload: []const u8) usize {    if (payload.len <= 16) return 0;    if (payload.len == 17 and payload[0] == 0x00 and        (payload[1] & 0x80) != 0)    {        return 0;    }    return payload.len;}fn readStringLike(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    comptime kind: enum { string, byte_string, symbol },) DecodeError!value_mod.Value(D) {    const len = try readVarint(bytes, index);    const n = std.math.cast(usize, len) orelse return error.LengthOutOfRange;    if (bytes.len - index.* < n) return error.UnexpectedEof;    const payload = bytes[index.* .. index.* + n];    index.* += n;    switch (kind) {        .string, .symbol => if (!std.unicode.utf8ValidateSlice(payload)) return error.InvalidUtf8,        .byte_string => {},    }    try admission.retain(payload.len);    const owned = try allocator.dupe(u8, payload);    return switch (kind) {        .string => .{ .string = owned },        .byte_string => .{ .byte_string = owned },        .symbol => .{ .symbol = owned },    };}fn readRecord(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    depth: u32,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var label_value = try readValue(D, allocator, bytes, index, admission, depth + 1);    errdefer label_value.deinit(allocator);    var fields: std.ArrayListUnmanaged(V) = .empty;    errdefer {        for (fields.items) |*f| f.deinit(allocator);        fields.deinit(allocator);    }    while (true) {        if (index.* >= bytes.len) return error.UnexpectedEof;        if (bytes[index.*] == Tag.end.byte()) {            index.* += 1;            break;        }        try admission.admitItem(fields.items.len);        var field = try readValue(D, allocator, bytes, index, admission, depth + 1);        errdefer field.deinit(allocator);        try fields.append(allocator, field);    }    try admission.retainItems(V, fields.items.len);    const owned = try fields.toOwnedSlice(allocator);    errdefer {        for (owned) |*field| field.deinit(allocator);        allocator.free(owned);    }    try admission.retainItems(V, 1);    return try V.initRecord(allocator, label_value, owned);}fn readSequence(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    depth: u32,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var items: std.ArrayListUnmanaged(V) = .empty;    errdefer {        for (items.items) |*it| it.deinit(allocator);        items.deinit(allocator);    }    while (true) {        if (index.* >= bytes.len) return error.UnexpectedEof;        if (bytes[index.*] == Tag.end.byte()) {            index.* += 1;            break;        }        try admission.admitItem(items.items.len);        var it = try readValue(D, allocator, bytes, index, admission, depth + 1);        errdefer it.deinit(allocator);        try items.append(allocator, it);    }    try admission.retainItems(V, items.items.len);    return .{ .sequence = try items.toOwnedSlice(allocator) };}fn readSet(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    depth: u32,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var items: std.ArrayListUnmanaged(V) = .empty;    var previous_encoding: ?[]const u8 = null;    errdefer {        for (items.items) |*it| it.deinit(allocator);        items.deinit(allocator);    }    while (true) {        if (index.* >= bytes.len) return error.UnexpectedEof;        if (bytes[index.*] == Tag.end.byte()) {            index.* += 1;            break;        }        try admission.admitItem(items.items.len);        const start = index.*;        var it = try readValue(D, allocator, bytes, index, admission, depth + 1);        errdefer it.deinit(allocator);        if (V.setContainsElement(items.items, it)) return error.DuplicateSetElement;        const encoding = bytes[start..index.*];        if (previous_encoding) |previous| {            if (std.mem.order(u8, previous, encoding) != .lt) {                return error.NonCanonicalSetOrder;            }        }        try items.append(allocator, it);        previous_encoding = encoding;    }    try admission.retainItems(V, items.items.len);    return .{ .set = try items.toOwnedSlice(allocator) };}fn readDictionary(    comptime D: type,    allocator: Allocator,    bytes: []const u8,    index: *usize,    admission: *Admission,    depth: u32,) DecodeError!value_mod.Value(D) {    const V = value_mod.Value(D);    var entries: std.ArrayListUnmanaged(V.DictionaryEntry) = .empty;    var previous_key_encoding: ?[]const u8 = null;    errdefer {        for (entries.items) |*e| {            e.key.deinit(allocator);            e.value.deinit(allocator);        }        entries.deinit(allocator);    }    while (true) {        if (index.* >= bytes.len) return error.UnexpectedEof;        if (bytes[index.*] == Tag.end.byte()) {            index.* += 1;            break;        }        try admission.admitItem(entries.items.len);        const key_start = index.*;        var key = try readValue(D, allocator, bytes, index, admission, depth + 1);        errdefer key.deinit(allocator);        if (V.dictionaryContainsKey(entries.items, key)) return error.DuplicateDictionaryKey;        const key_encoding = bytes[key_start..index.*];        if (previous_key_encoding) |previous| {            if (std.mem.order(u8, previous, key_encoding) != .lt) {                return error.NonCanonicalDictionaryOrder;            }        }        var val = try readValue(D, allocator, bytes, index, admission, depth + 1);        errdefer val.deinit(allocator);        try entries.append(allocator, .{ .key = key, .value = val });        previous_key_encoding = key_encoding;    }    try admission.retainItems(V.DictionaryEntry, entries.items.len);    return .{ .dictionary = try entries.toOwnedSlice(allocator) };}const test_limits: Limits = .{    .max_depth = 64,    .max_nodes = 1024,    .max_collection_items = 256,    .max_retained_bytes = 1024 * 1024,};test "readVarint parses single-byte" {    var idx: usize = 0;    const got = try readVarint(&[_]u8{0x7f}, &idx);    try std.testing.expectEqual(@as(u64, 0x7f), got);    try std.testing.expectEqual(@as(usize, 1), idx);}test "readVarint parses multi-byte" {    var idx: usize = 0;    const got = try readVarint(&[_]u8{ 0x80, 0x01 }, &idx);    try std.testing.expectEqual(@as(u64, 128), got);    try std.testing.expectEqual(@as(usize, 2), idx);}test "readVarint rejects overlong" {    var idx: usize = 0;    try std.testing.expectError(error.OverlongVarint, readVarint(&[_]u8{ 0x80, 0x00 }, &idx));}test "decode boolean" {    const allocator = std.testing.allocator;    const V = value_mod.Value(preserves.domain.NoEmbedded);    var t = try decode(preserves.domain.NoEmbedded, allocator, &[_]u8{0x81}, test_limits);    defer t.deinit(allocator);    try std.testing.expectEqual(V.initBoolean(true), t);    var f = try decode(preserves.domain.NoEmbedded, allocator, &[_]u8{0x80}, test_limits);    defer f.deinit(allocator);    try std.testing.expectEqual(V.initBoolean(false), f);}test "decode rejects trailing bytes" {    const allocator = std.testing.allocator;    try std.testing.expectError(        error.UnexpectedTrailingBytes,        decode(preserves.domain.NoEmbedded, allocator, &[_]u8{ 0x80, 0x80 }, test_limits),    );}fn checkRecordAllocationFailures(allocator: Allocator) !void {    const bytes = [_]u8{        0xb4,        0xb1,        0x05,        'l',        'a',        'b',        'e',        'l',        0xb1,        0x05,        'o',        'w',        'n',        'e',        'd',        0x84,    };    var value = try decode(preserves.domain.NoEmbedded, allocator, &bytes, test_limits);    defer value.deinit(allocator);    try std.testing.expect(value == .record);}test "packed record releases every allocation failure path" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkRecordAllocationFailures,        .{},    );}test "packed admission rejects every exhausted budget" {    const allocator = std.testing.allocator;    const nested = [_]u8{ 0xb5, 0xb5, 0x80, 0x84, 0x84 };    const nodes = [_]u8{ 0xb5, 0x80, 0x81, 0x84 };    const retained = [_]u8{ 0xb1, 0x02, 'a', 'b' };    try std.testing.expectError(error.DepthLimitExceeded, decode(        preserves.NoEmbedded,        allocator,        &nested,        .{ .max_depth = 2, .max_nodes = 8, .max_collection_items = 8, .max_retained_bytes = 8 },    ));    try std.testing.expectError(error.NodeLimitExceeded, decode(        preserves.NoEmbedded,        allocator,        &nodes,        .{ .max_depth = 4, .max_nodes = 2, .max_collection_items = 8, .max_retained_bytes = 8 },    ));    try std.testing.expectError(error.CollectionLimitExceeded, decode(        preserves.NoEmbedded,        allocator,        &nodes,        .{ .max_depth = 4, .max_nodes = 8, .max_collection_items = 1, .max_retained_bytes = 128 },    ));    try std.testing.expectError(error.RetainedBytesLimitExceeded, decode(        preserves.NoEmbedded,        allocator,        &retained,        .{ .max_depth = 1, .max_nodes = 1, .max_collection_items = 0, .max_retained_bytes = 1 },    ));}test "packed admission accepts every exact budget boundary" {    const allocator = std.testing.allocator;    const nested = [_]u8{ 0xb5, 0xb5, 0x80, 0x84, 0x84 };    const nodes = [_]u8{ 0xb5, 0x80, 0x81, 0x84 };    const retained = [_]u8{ 0xb1, 0x02, 'a', 'b' };    var depth_value = try decode(        preserves.NoEmbedded,        allocator,        &nested,        .{ .max_depth = 3, .max_nodes = 8, .max_collection_items = 8, .max_retained_bytes = 256 },    );    defer depth_value.deinit(allocator);    var node_value = try decode(        preserves.NoEmbedded,        allocator,        &nodes,        .{ .max_depth = 2, .max_nodes = 3, .max_collection_items = 2, .max_retained_bytes = 256 },    );    defer node_value.deinit(allocator);    var retained_value = try decode(        preserves.NoEmbedded,        allocator,        &retained,        .{ .max_depth = 1, .max_nodes = 1, .max_collection_items = 0, .max_retained_bytes = 2 },    );    defer retained_value.deinit(allocator);}test "packed admission rejects noncanonical unordered collections" {    const allocator = std.testing.allocator;    const set = [_]u8{ 0xb6, 0xb1, 0x01, 'b', 0xb1, 0x01, 'a', 0x84 };    const dictionary = [_]u8{        0xb7, 0xb1, 0x01, 'b', 0x80, 0xb1, 0x01, 'a', 0x81, 0x84,    };    try std.testing.expectError(        error.NonCanonicalSetOrder,        decode(preserves.NoEmbedded, allocator, &set, test_limits),    );    try std.testing.expectError(        error.NonCanonicalDictionaryOrder,        decode(preserves.NoEmbedded, allocator, &dictionary, test_limits),    );}test "packed admission rejects annotations and embedded values" {    const allocator = std.testing.allocator;    try std.testing.expectError(        error.AnnotationNotSupported,        decode(preserves.NoEmbedded, allocator, &.{ 0x85, 0x80, 0x81 }, test_limits),    );    try std.testing.expectError(        error.EmbeddedNotSupported,        decode(preserves.NoEmbedded, allocator, &.{ 0x86, 0x80 }, test_limits),    );}

Source: lib/preserves/src/root.zig:121

zig
pub const packed_reader = @"packed".reader;

Also reachable as

@"packed".reader.

Audit

Definitions5
Public names10
Members24
Version26.7.0
Revisiondaab053ee433