tiny.preserves.packed_reader
Defined in tiny.preserves.
Reads one value back from its binary encoding.
API (6)
Actions
Public operations.
decode: Returns the one value thatbytesencodes, allocated withallocator.readVarint: Reads one varint frombytesatindex.*, advancesindexpast it and returns its value.
Types and contracts
Public types and contracts.
DecodeError: The errorsdecodereturns: everyDecodeFailuretag and running out of memory.DecodeFailure: The waysdecoderefuses its input, one tag per kind of bad or over-limit bytes.Limits: The four boundsdecodeenforces while it reads.Tag: The tag byte that starts each encoded value, as an enum overu8, so code that writes encoded bytes by hand names each tag through it.
Source
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
Audit
| Definitions | 5 |
|---|---|
| Public names | 10 |
| Members | 24 |
| Version | 26.7.0 |
| Revision | daab053ee433 |