tiny.preserves.text_reader
Defined in tiny.preserves.
Reads values from text, whatever the type of their embedded values.
API (6)
Actions
Public operations.
decode: Returns the one valuetextholds, allocated withallocator.looksLikeFloat: Accepts an optional+or-, one or more ASCII decimal digits, and a fraction or exponent or both, so.5fails.looksLikeInteger: The predicate accepts an input consisting of an optional+or-followed by one or more ASCII decimal digits.parseDecimalIntoSignedInteger: Returns the integer the decimal stringsspells, with an optional leading+or-.readValue: Reads one value fromtextatindex.*and advancesindexpast it.
Types and contracts
Public types and contracts.
DecodeError: The errorsdecodeandreadValuereturn.
Source
Source: lib/preserves/src/root.zig:124
zig
pub const text_reader = text.reader;Source: lib/preserves/src/text/reader.zig
zig
//! Reads values from text, whatever the type of their embedded values. A caller needs a text reader//! for its own value type, whatever type its embedded values have. The caller also needs a reader//! that checks every string and symbol as UTF-8 and reads JSON-style surrogate-pair escapes. The//! type of the embedded values decides how an embedded value is spelled in text, and the reader//! knows only what that type tells it. Annotations can hold any value, including embedded values//! the caller's type does not read.//!//! `#:` reads an embedded value through its type's `decodeText`, and fails with//! `EmbeddedNotSupported` when the type lacks one, as `NoEmbedded` and `AnyEmbedded` both do. The//! reader reads each annotation with its own private type for embedded values, checks it like any//! value, then frees it, so `@a v` returns `v`. Sets and dictionaries keep the order the text lists//! them in, after the repeated-item check. The reader copies every string and symbol it keeps, so//! the value owns all its memory and borrows nothing from the text.const std = @import("std");const Allocator = std.mem.Allocator;const preserves = @import("../root.zig");const nesting = @import("nesting.zig");const value_mod = preserves.value;const integer_mod = preserves.integer_mod;/// The errors `decode` and `readValue` return. Code that calls `decode` or `readValue` switches on/// these errors. `UnexpectedEof`: the text ends before a value, inside a string, escape or byte/// string, or before a closing bracket. `UnexpectedTrailingBytes`: `decode` finds anything other/// than whitespace and commas after the value. `BadSyntax`: a delimiter at the start of a value, an/// unknown `#` form, a dictionary key followed by something other than `:`, or a `#xd"…"` double/// not closed after 16 digits. `BadEscape`: an unknown backslash escape, or a `\u` escape that/// names a lone surrogate. `BadHex`: a character other than a hexadecimal digit in `\u`, `\x`,/// `#x"…"` or `#xd"…"`. `BadBase64`: a `#[…]` byte string with invalid base64. `BadNumber`: a token/// shaped like a double that the standard parser rejects. `InvalidUtf8`: a string, quoted symbol or/// bare symbol with invalid UTF-8. `EmbeddedNotSupported`: a `#:` value outside an annotation when/// the type of the embedded values lacks `decodeText`. `NonCanonicalInteger` is listed, and the/// reader never returns it. `DuplicateSetElement` and `DuplicateDictionaryKey`: a set with two/// equal elements, or a dictionary with two equal keys. `NestingLimitExceeded`: values nested more/// than 256 levels deep.pub const DecodeError = Allocator.Error || error{ UnexpectedEof, UnexpectedTrailingBytes, BadSyntax, BadEscape, BadHex, BadBase64, BadNumber, InvalidUtf8, EmbeddedNotSupported, NonCanonicalInteger, DuplicateSetElement, DuplicateDictionaryKey, NestingLimitExceeded,};const AnnotationEmbedded = struct { value: *value_mod.Value(AnnotationEmbedded), pub fn eql(a: AnnotationEmbedded, b: AnnotationEmbedded) bool { return a.value.*.eql(b.value.*); } pub fn hash(self: AnnotationEmbedded) u64 { return self.value.*.hash(); } pub fn order(a: AnnotationEmbedded, b: AnnotationEmbedded) std.math.Order { return a.value.*.compare(b.value.*); } pub fn deinit(self: *AnnotationEmbedded, allocator: Allocator) void { self.value.deinit(allocator); allocator.destroy(self.value); } pub fn clone( self: AnnotationEmbedded, allocator: Allocator, ) Allocator.Error!AnnotationEmbedded { const V = value_mod.Value(AnnotationEmbedded); const cloned = try allocator.create(V); errdefer allocator.destroy(cloned); cloned.* = try preserves.ownership.cloneValueDeep( AnnotationEmbedded, allocator, self.value.*, ); return .{ .value = cloned }; }};fn decodeAnnotationEmbedded( allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!AnnotationEmbedded { const V = value_mod.Value(AnnotationEmbedded); var value = try readValueAt(AnnotationEmbedded, allocator, text, index, level); errdefer value.deinit(allocator); const owned = try allocator.create(V); owned.* = value; return .{ .value = owned };}/// Returns the one value `text` holds, allocated with `allocator`. Code that reads a whole data/// file calls it, so each file yields one owned value. The value's embedded values have the type/// `D`. Whitespace and commas separate values. `decode` reads no comments, so `# ` fails as an/// unknown `#` form. A bare token that is all digits, with an optional sign, becomes an integer of/// any size, a token in decimal or exponent form becomes a double, and any other token becomes a/// symbol.////// The value owns all its memory, so `text` may be freed at once, and the caller frees the value/// with `deinit`. On any error the call frees everything it allocated. Its repeated-item checks/// compare each element or key with every earlier one, so their cost grows with the square of the/// count.pub fn decode(comptime D: type, allocator: Allocator, text: []const u8) DecodeError!value_mod.Value(D) { var index: usize = 0; var v = try readValueAt(D, allocator, text, &index, nesting.root); errdefer v.deinit(allocator); skipTrailing(text, &index); if (index != text.len) return error.UnexpectedTrailingBytes; return v;}/// Reads one value from `text` at `index.*` and advances `index` past it. Code reading more than/// one value from one text calls it once per value, for each value and the position after it. The/// call leaves any text after the value unread and reports no trailing content. On/// `NestingLimitExceeded`, `index` stops at the opening bracket it rejected. Its value, errors and/// ownership match `decode`.pub fn readValue( comptime D: type, allocator: Allocator, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { return readValueAt(D, allocator, text, index, nesting.root);}fn readValueAt( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!value_mod.Value(D) { const value_level = try skipAnnotations(allocator, text, index, level); skipWhitespaceAndCommas(text, index); if (index.* >= text.len) return error.UnexpectedEof; const c = text[index.*]; return switch (c) { '"' => blk: { index.* += 1; const bytes = try readStringLiteral(allocator, text, index, '"'); if (!std.unicode.utf8ValidateSlice(bytes)) { allocator.free(bytes); break :blk error.InvalidUtf8; } break :blk .{ .string = bytes }; }, '\'' => blk: { index.* += 1; const bytes = try readStringLiteral(allocator, text, index, '\''); if (!std.unicode.utf8ValidateSlice(bytes)) { allocator.free(bytes); break :blk error.InvalidUtf8; } break :blk .{ .symbol = bytes }; }, '<' => blk: { const nested = try nesting.descend(value_level); index.* += 1; break :blk try readRecord( D, allocator, text, index, nested, ); }, '[' => blk: { const nested = try nesting.descend(value_level); index.* += 1; break :blk try readSequence( D, allocator, text, index, nested, ); }, '{' => blk: { const nested = try nesting.descend(value_level); index.* += 1; break :blk try readDictionary( D, allocator, text, index, nested, ); }, '#' => try readHashForm(D, allocator, text, index, value_level), else => try readBareToken(D, allocator, text, index), };}fn readHashForm( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!value_mod.Value(D) { if (index.* + 1 >= text.len) return error.UnexpectedEof; const next = text[index.* + 1]; switch (next) { 't' => { index.* += 2; return value_mod.Value(D).initBoolean(true); }, 'f' => { index.* += 2; return value_mod.Value(D).initBoolean(false); }, '{' => { const nested = try nesting.descend(level); index.* += 2; return try readSet( D, allocator, text, index, nested, ); }, '"' => { index.* += 2; return try readLiteralByteString(D, allocator, text, index); }, '[' => { index.* += 2; return try readBase64ByteString(D, allocator, text, index); }, ':' => { const embedded_level = try nesting.descend(level); index.* += 2; if (D == AnnotationEmbedded) { const d = try decodeAnnotationEmbedded( allocator, text, index, embedded_level, ); return value_mod.Value(D).initEmbedded(d); } if (!@hasDecl(D, "decodeText")) return error.EmbeddedNotSupported; const d = try D.decodeText(allocator, text, index); return value_mod.Value(D).initEmbedded(d); }, 'x' => { if (index.* + 2 >= text.len) return error.UnexpectedEof; const follow = text[index.* + 2]; if (follow == '"') { index.* += 3; return try readHexByteString(D, allocator, text, index); } else if (follow == 'd') { if (index.* + 3 >= text.len or text[index.* + 3] != '"') return error.BadSyntax; index.* += 4; return try readHexDouble(D, text, index); } else return error.BadSyntax; }, else => return error.BadSyntax, }}fn readRecord( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!value_mod.Value(D) { const V = value_mod.Value(D); skipWhitespaceAndCommas(text, index); var label_value = try readValueAt(D, allocator, text, index, level); errdefer label_value.deinit(allocator); var fields: std.ArrayListUnmanaged(V) = .empty; errdefer { for (fields.items) |*f| f.deinit(allocator); fields.deinit(allocator); } while (true) { skipWhitespaceAndCommas(text, index); if (index.* >= text.len) return error.UnexpectedEof; if (text[index.*] == '>') { index.* += 1; break; } var f = try readValueAt(D, allocator, text, index, level); errdefer f.deinit(allocator); try fields.append(allocator, f); } const owned = try fields.toOwnedSlice(allocator); errdefer { for (owned) |*field| field.deinit(allocator); allocator.free(owned); } return try V.initRecord(allocator, label_value, owned);}fn readSequence( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) 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) { skipWhitespaceAndCommas(text, index); if (index.* >= text.len) return error.UnexpectedEof; if (text[index.*] == ']') { index.* += 1; break; } var it = try readValueAt(D, allocator, text, index, level); errdefer it.deinit(allocator); try items.append(allocator, it); } return .{ .sequence = try items.toOwnedSlice(allocator) };}fn readSet( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) 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) { skipWhitespaceAndCommas(text, index); if (index.* >= text.len) return error.UnexpectedEof; if (text[index.*] == '}') { index.* += 1; break; } var it = try readValueAt(D, allocator, text, index, level); errdefer it.deinit(allocator); if (V.setContainsElement(items.items, it)) return error.DuplicateSetElement; try items.append(allocator, it); } return .{ .set = try items.toOwnedSlice(allocator) };}fn readDictionary( comptime D: type, allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!value_mod.Value(D) { const V = value_mod.Value(D); var entries: std.ArrayListUnmanaged(V.DictionaryEntry) = .empty; errdefer { for (entries.items) |*e| { e.key.deinit(allocator); e.value.deinit(allocator); } entries.deinit(allocator); } while (true) { skipWhitespaceAndCommas(text, index); if (index.* >= text.len) return error.UnexpectedEof; if (text[index.*] == '}') { index.* += 1; break; } var k = try readValueAt(D, allocator, text, index, level); errdefer k.deinit(allocator); skipWhitespaceAndCommas(text, index); if (index.* >= text.len or text[index.*] != ':') return error.BadSyntax; index.* += 1; skipWhitespaceAndCommas(text, index); var v = try readValueAt(D, allocator, text, index, level); errdefer v.deinit(allocator); if (V.dictionaryContainsKey(entries.items, k)) { return error.DuplicateDictionaryKey; } try entries.append(allocator, .{ .key = k, .value = v }); } return .{ .dictionary = try entries.toOwnedSlice(allocator) };}fn readStringLiteral( allocator: Allocator, text: []const u8, index: *usize, terminator: u8,) DecodeError![]u8 { var buf: std.ArrayListUnmanaged(u8) = .empty; errdefer buf.deinit(allocator); while (true) { if (index.* >= text.len) return error.UnexpectedEof; const c = text[index.*]; index.* += 1; if (c == terminator) break; if (c == '\\') { if (index.* >= text.len) return error.UnexpectedEof; const esc = text[index.*]; index.* += 1; switch (esc) { '\\' => try buf.append(allocator, '\\'), '/' => try buf.append(allocator, '/'), 'b' => try buf.append(allocator, 0x08), 'f' => try buf.append(allocator, 0x0c), 'n' => try buf.append(allocator, 0x0a), 'r' => try buf.append(allocator, 0x0d), 't' => try buf.append(allocator, 0x09), 'u' => try readUnicodeEscape(allocator, &buf, text, index), else => { if (esc == terminator) { try buf.append(allocator, esc); } else return error.BadEscape; }, } } else { try buf.append(allocator, c); } } return try buf.toOwnedSlice(allocator);}fn readUnicodeEscape( allocator: Allocator, buf: *std.ArrayListUnmanaged(u8), text: []const u8, index: *usize,) DecodeError!void { const n1 = try readHex4(text, index); var codepoint: u32 = n1; if (n1 >= 0xD800 and n1 <= 0xDBFF) { if (index.* + 2 > text.len or text[index.*] != '\\' or text[index.* + 1] != 'u') { return error.BadEscape; } index.* += 2; const n2 = try readHex4(text, index); if (n2 < 0xDC00 or n2 > 0xDFFF) return error.BadEscape; codepoint = ((n1 - 0xD800) << 10) + (n2 - 0xDC00) + 0x10000; } else if (n1 >= 0xDC00 and n1 <= 0xDFFF) { return error.BadEscape; } var utf8: [4]u8 = undefined; const written = std.unicode.utf8Encode(@intCast(codepoint), &utf8) catch return error.BadEscape; try buf.appendSlice(allocator, utf8[0..written]);}fn readHex4(text: []const u8, index: *usize) DecodeError!u32 { if (index.* + 4 > text.len) return error.UnexpectedEof; var v: u32 = 0; var i: usize = 0; while (i < 4) : (i += 1) { const d = hexDigit(text[index.*]) orelse return error.BadHex; v = (v << 4) | d; index.* += 1; } return v;}fn readHexByteString( comptime D: type, allocator: Allocator, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { var buf: std.ArrayListUnmanaged(u8) = .empty; errdefer buf.deinit(allocator); while (true) { skipWhitespaceOnly(text, index); if (index.* >= text.len) return error.UnexpectedEof; const c = text[index.*]; if (c == '"') { index.* += 1; break; } index.* += 1; if (index.* >= text.len) return error.UnexpectedEof; const c2 = text[index.*]; index.* += 1; const h1 = hexDigit(c) orelse return error.BadHex; const h2 = hexDigit(c2) orelse return error.BadHex; try buf.append(allocator, @intCast((h1 << 4) | h2)); } return .{ .byte_string = try buf.toOwnedSlice(allocator) };}fn readHexDouble( comptime D: type, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { var bytes: [8]u8 = undefined; var filled: usize = 0; while (filled < 8) { skipWhitespaceOnly(text, index); if (index.* + 1 >= text.len) return error.UnexpectedEof; const c1 = text[index.*]; const c2 = text[index.* + 1]; const h1 = hexDigit(c1) orelse return error.BadHex; const h2 = hexDigit(c2) orelse return error.BadHex; bytes[filled] = @intCast((h1 << 4) | h2); filled += 1; index.* += 2; } skipWhitespaceOnly(text, index); if (index.* >= text.len or text[index.*] != '"') return error.BadSyntax; index.* += 1; const bits = std.mem.readInt(u64, &bytes, .big); return value_mod.Value(D).initDouble(@bitCast(bits));}fn readLiteralByteString( comptime D: type, allocator: Allocator, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { var buf: std.ArrayListUnmanaged(u8) = .empty; errdefer buf.deinit(allocator); while (true) { if (index.* >= text.len) return error.UnexpectedEof; const c = text[index.*]; index.* += 1; if (c == '"') break; if (c == '\\') { if (index.* >= text.len) return error.UnexpectedEof; const esc = text[index.*]; index.* += 1; switch (esc) { '\\' => try buf.append(allocator, '\\'), '"' => try buf.append(allocator, '"'), '/' => try buf.append(allocator, '/'), 'b' => try buf.append(allocator, 0x08), 'f' => try buf.append(allocator, 0x0c), 'n' => try buf.append(allocator, 0x0a), 'r' => try buf.append(allocator, 0x0d), 't' => try buf.append(allocator, 0x09), 'x' => { if (index.* + 2 > text.len) return error.UnexpectedEof; const h1 = hexDigit(text[index.*]) orelse return error.BadHex; const h2 = hexDigit(text[index.* + 1]) orelse return error.BadHex; index.* += 2; try buf.append(allocator, @intCast((h1 << 4) | h2)); }, else => return error.BadEscape, } } else { try buf.append(allocator, c); } } return .{ .byte_string = try buf.toOwnedSlice(allocator) };}fn readBase64ByteString( comptime D: type, allocator: Allocator, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { var enc_buf: std.ArrayListUnmanaged(u8) = .empty; defer enc_buf.deinit(allocator); while (true) { skipWhitespaceOnly(text, index); if (index.* >= text.len) return error.UnexpectedEof; var c = text[index.*]; if (c == ']') { index.* += 1; break; } index.* += 1; if (c == '=') continue; if (c == '-') c = '+'; if (c == '_') c = '/'; try enc_buf.append(allocator, c); } const decoder = std.base64.standard_no_pad.Decoder; const expected = decoder.calcSizeForSlice(enc_buf.items) catch return error.BadBase64; const dst = try allocator.alloc(u8, expected); errdefer allocator.free(dst); decoder.decode(dst, enc_buf.items) catch { return error.BadBase64; }; return .{ .byte_string = dst };}fn readBareToken( comptime D: type, allocator: Allocator, text: []const u8, index: *usize,) DecodeError!value_mod.Value(D) { const start = index.*; while (index.* < text.len and !isDelimiter(text[index.*])) { index.* += 1; } const tok = text[start..index.*]; if (tok.len == 0) return error.BadSyntax; if (looksLikeFloat(tok)) { const f = std.fmt.parseFloat(f64, tok) catch return error.BadNumber; return value_mod.Value(D).initDouble(f); } if (looksLikeInteger(tok)) { const si = try parseDecimalIntoSignedInteger(allocator, tok); return value_mod.Value(D).initSignedInteger(si); } if (!std.unicode.utf8ValidateSlice(tok)) return error.InvalidUtf8; const owned = try allocator.dupe(u8, tok); return .{ .symbol = owned };}fn isDelimiter(c: u8) bool { return switch (c) { ' ', '\t', '\r', '\n', ',', '(', ')', '{', '}', '[', ']', '<', '>', '"', '\'', ';', '@', '#', ':' => true, else => false, };}fn skipWhitespaceOnly(text: []const u8, index: *usize) void { while (index.* < text.len) { switch (text[index.*]) { ' ', '\t', '\r', '\n' => index.* += 1, else => return, } }}fn skipWhitespaceAndCommas(text: []const u8, index: *usize) void { while (index.* < text.len) { switch (text[index.*]) { ' ', '\t', '\r', '\n', ',' => index.* += 1, else => return, } }}fn skipTrailing(text: []const u8, index: *usize) void { skipWhitespaceAndCommas(text, index);}fn skipAnnotations( allocator: Allocator, text: []const u8, index: *usize, level: nesting.Level,) DecodeError!nesting.Level { var value_level = level; while (true) { skipWhitespaceAndCommas(text, index); if (index.* >= text.len or text[index.*] != '@') return value_level; const nested = try nesting.descend(value_level); index.* += 1; value_level = nested; var annotation = try readValueAt( AnnotationEmbedded, allocator, text, index, value_level, ); annotation.deinit(allocator); }}fn hexDigit(c: u8) ?u32 { return switch (c) { '0'...'9' => @as(u32, c - '0'), 'a'...'f' => @as(u32, c - 'a' + 10), 'A'...'F' => @as(u32, c - 'A' + 10), else => null, };}/// The predicate accepts an input consisting of an optional `+` or `-` followed by one or more/// ASCII decimal digits. It rejects an empty input, a sign alone, and underscores. The public/// `parse` and `decode` readers call this predicate before decimal integer conversion. The text/// writer quotes a symbol that the predicate accepts, because a matching symbol takes the quoted/// branch when `looksLikeNumber` recognizes it.pub fn looksLikeInteger(s: []const u8) bool { if (s.len == 0) return false; var i: usize = 0; if (s[i] == '-' or s[i] == '+') i += 1; if (i >= s.len) return false; const digit_start = i; while (i < s.len) : (i += 1) { if (s[i] < '0' or s[i] > '9') return false; } return i > digit_start;}/// Accepts an optional `+` or `-`, one or more ASCII decimal digits, and a fraction or exponent or/// both, so `.5` fails. A dot requires at least one following digit, so `5.` fails. An `e` or `E`/// exponent accepts an optional sign and requires at least one digit. The predicate consumes the/// whole token, so `1_2e3` fails.////// The public `parse` reader calls this predicate before trying `std.fmt.parseFloat`, then falls/// through to a symbol if conversion fails. The public `decode` reader calls it before float/// conversion and reports `BadNumber` if conversion fails.////// The text writer quotes a symbol that this predicate accepts, because a matching symbol takes the/// quoted branch when `looksLikeNumber` recognizes it.pub fn looksLikeFloat(s: []const u8) bool { if (s.len == 0) return false; var i: usize = 0; if (s[i] == '-' or s[i] == '+') i += 1; if (i >= s.len or s[i] < '0' or s[i] > '9') return false; while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1; var has_decimal_part = false; if (i < s.len and s[i] == '.') { i += 1; const frac_start = i; while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1; if (i > frac_start) has_decimal_part = true else return false; } var has_exp = false; if (i < s.len and (s[i] == 'e' or s[i] == 'E')) { i += 1; if (i < s.len and (s[i] == '-' or s[i] == '+')) i += 1; const exp_start = i; while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1; if (i > exp_start) has_exp = true else return false; } return i == s.len and (has_decimal_part or has_exp);}/// Returns the integer the decimal string `s` spells, with an optional leading `+` or `-`. `parse`/// and `decode` call it only for tokens accepted by `looksLikeInteger`, which permits an optional/// sign followed by digits. A value that fits 128 bits signed or unsigned is stored inline, and a/// wider one as big-endian two's-complement bytes allocated with `allocator`. Inside the 128-bit/// range, underscores between digits are skipped, so `1_000` returns 1000. The call returns/// `error.BadNumber` for an empty string, a sign alone, or a wide value holding a character other/// than a digit.pub fn parseDecimalIntoSignedInteger(allocator: Allocator, s: []const u8) DecodeError!integer_mod.SignedInteger { if (s.len == 0) return error.BadNumber; if (std.fmt.parseInt(i128, s, 10)) |v| { return integer_mod.SignedInteger.fromI128(v); } else |_| {} if (std.fmt.parseInt(u128, s, 10)) |v| { return integer_mod.SignedInteger.fromU128(v); } else |_| {} return try parseDecimalIntoBig(allocator, s);}fn parseDecimalIntoBig(allocator: Allocator, s: []const u8) DecodeError!integer_mod.SignedInteger { var i: usize = 0; var is_negative = false; if (s[i] == '-') { is_negative = true; i += 1; } else if (s[i] == '+') i += 1; if (i >= s.len) return error.BadNumber; var mag: std.ArrayListUnmanaged(u8) = .empty; defer mag.deinit(allocator); try mag.append(allocator, 0); while (i < s.len) : (i += 1) { const d = s[i]; if (d < '0' or d > '9') return error.BadNumber; const digit: u8 = @intCast(d - '0'); var carry: u16 = digit; var j: usize = mag.items.len; while (j > 0) { j -= 1; const product = @as(u16, mag.items[j]) * 10 + carry; mag.items[j] = @intCast(product & 0xff); carry = product >> 8; } while (carry > 0) { try mag.insert(allocator, 0, @intCast(carry & 0xff)); carry >>= 8; } } var start: usize = 0; while (start < mag.items.len - 1 and mag.items[start] == 0) start += 1; const stripped = mag.items[start..]; if (is_negative) { const buf = try allocator.alloc(u8, stripped.len + 1); defer allocator.free(buf); buf[0] = 0x00; @memcpy(buf[1..], stripped); var k: usize = buf.len; var carry: u16 = 1; while (k > 0) { k -= 1; const inv: u16 = @as(u16, ~buf[k]) & 0xff; const sum = inv + carry; buf[k] = @intCast(sum & 0xff); carry = sum >> 8; } const canonical = try canonicalizeBytes(allocator, buf); defer allocator.free(canonical); return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, canonical); } else { if (stripped.len > 0 and (stripped[0] & 0x80) != 0) { const buf = try allocator.alloc(u8, stripped.len + 1); defer allocator.free(buf); buf[0] = 0x00; @memcpy(buf[1..], stripped); return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, buf); } return try integer_mod.SignedInteger.fromCanonicalBytes(allocator, stripped); }}fn canonicalizeBytes(allocator: Allocator, bytes: []const u8) DecodeError![]const u8 { var start: usize = 0; while (start + 1 < bytes.len) { const first = bytes[start]; const second = bytes[start + 1]; if (first == 0xff and (second & 0x80) != 0) { start += 1; continue; } if (first == 0x00 and (second & 0x80) == 0) { start += 1; continue; } break; } return try allocator.dupe(u8, bytes[start..]);}fn expectDuplicateWithAllocator( allocator: Allocator, text: []const u8, expected: DecodeError,) !void { const NE = preserves.domain.NoEmbedded; if (decode(NE, allocator, text)) |decoded| { var value = decoded; value.deinit(allocator); return error.ExpectedDuplicate; } else |err| { if (err == error.OutOfMemory) return err; if (err == expected) return; return err; }}fn checkDuplicateAllocationFailures(allocator: Allocator) !void { try expectDuplicateWithAllocator( allocator, "#{<\"label\" 'quoted'> <\"label\" 'quoted'>}", error.DuplicateSetElement, ); try expectDuplicateWithAllocator( allocator, "{\"key\": <\"label\" 'first'>, \"key\": <\"label\" 'second'>}", error.DuplicateDictionaryKey, ); try expectDuplicateWithAllocator( allocator, "@#:#{[\"owned\"] [\"owned\"]} 3", error.DuplicateSetElement, );}test "parse integer" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var v = try decode(NE, allocator, "42"); defer v.deinit(allocator); try std.testing.expectEqual(@as(i128, 42), try v.signed_integer.toI128());}test "parse negative big integer" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var v = try decode(NE, allocator, "-170141183460469231731687303715884105729"); defer v.deinit(allocator); try std.testing.expectEqual(integer_mod.Tier.big, @as(integer_mod.Tier, v.signed_integer.repr));}test "parse sequence with commas" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var v = try decode(NE, allocator, "[1, 2, 3]"); defer v.deinit(allocator); try std.testing.expectEqual(@as(usize, 3), v.sequence.len);}test "parse sequence preserves duplicate values" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var v = try decode(NE, allocator, "[\"same\", \"same\"]"); defer v.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), v.sequence.len); try std.testing.expect(v.sequence[0].eql(v.sequence[1]));}test "parse quoted symbol" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var v = try decode(NE, allocator, "'hello world'"); defer v.deinit(allocator); try std.testing.expectEqualStrings("hello world", v.symbol);}test "parse rejects duplicate set elements and releases owned values" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; try std.testing.expectError( error.DuplicateSetElement, decode(NE, allocator, "#{[\"owned\"] [\"owned\"]}"), );}test "parse rejects duplicate dictionary keys and releases owned entries" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; try std.testing.expectError( error.DuplicateDictionaryKey, decode(NE, allocator, "{\"key\": [\"first\"], \"key\": [\"second\"]}"), );}test "parse duplicate errors release every allocation failure path" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkDuplicateAllocationFailures, .{}, );}test "parse skips structurally nested annotations" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; const cases = [_][]const u8{ "@[[1] 2] 3", "@{a: {b: c}} 3", "@<r <s>> 3", "@#{[1] {a: 2}} 3", "@{a: @b c} 3", "@\"closing ] } >\" 3", "@#xd\"3ff0000000000000\" 3", "@#[AQI=] 3", }; for (cases) |text| { var value = try decode(NE, allocator, text); defer value.deinit(allocator); try std.testing.expectEqual(@as(i128, 3), try value.signed_integer.toI128()); }}test "parse validates discarded annotations" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; try std.testing.expectError( error.DuplicateSetElement, decode(NE, allocator, "@#{1 1} 2"), ); try std.testing.expectError( error.DuplicateDictionaryKey, decode(NE, allocator, "@{a: 1 a: 2} 3"), ); try std.testing.expectError(error.BadSyntax, decode(NE, allocator, "@{a} 3")); try std.testing.expectError(error.BadEscape, decode(NE, allocator, "@\"\\q\" 3")); try std.testing.expectError(error.BadHex, decode(NE, allocator, "@#x\"zz\" 3")); try std.testing.expectError(error.BadBase64, decode(NE, allocator, "@#[%%%] 3"));}test "discarded annotations do not enter the application embedded domain" { const allocator = std.testing.allocator; const NE = preserves.domain.NoEmbedded; var value = try decode(NE, allocator, "@#:foo 3"); defer value.deinit(allocator); try std.testing.expectEqual(@as(i128, 3), try value.signed_integer.toI128()); try std.testing.expectError( error.DuplicateSetElement, decode(NE, allocator, "@#:#{1 1} 3"), );}Also reachable as
Audit
| Definitions | 6 |
|---|---|
| Public names | 12 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |