Add sigil-wire: typed binary codec for Sigil values
A length-prefixed, typed binary serialization of the Sigil value model, decoded in one O(n) forward pass with string/bytevector bodies memcpy'd. The transport-neutral wire format for bulk structured payloads (binary sibling to sigil-json).
- Encode/decode for every value type: #f, #t, exact ints (incl. bignum), doubles, string, bytevector, keyword, symbol, char, list, dict, vector (#()) and array (#[]). Non-representable values are encode errors. - Header MAGIC/VERSION/FLAGS; per-value tag + LEB128 varint lengths. Int = single zigzag-LEB128 for all magnitudes. Float = 8-byte IEEE-754 LE (inf/NaN bit-exact) derived via pure float arithmetic. - Security: the decoder is a trust boundary. Every length is checked against the remaining buffer before allocating; configurable caps (body length, collection count, nesting depth, total size, int magnitude); bounds-checked cursor; endless/lying frames error cleanly. - 113 tests: round-trip every type + edge cases, plus a hostile-input fuzz suite (truncation, lying lengths, cap violations, unknown tag/version, varint abuse). README + docs/wire.md specify the format.
Varint and float-bit paths use integer arithmetic (quotient/remainder/*) rather than bitwise/shift, which corrupt fixnum<->bignum boundary values.
.gitignore | 2 +
LICENSE | 28 +++++++++
README.md | 148 +++++++++++++++++++++++++++++++++++++++++++++
dev-redirects.sgl | 6 ++
docs/wire.md | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++
manifest.scm | 10 ++++
package.sgl | 18 ++++++
sigil.lock | 9 +++
src/sigil/wire.sgl | 568 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/test-wire.sgl | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1267 insertions(+).gitignoreadded
build/.sigil/LICENSEadded
BSD 3-Clause LicenseCopyright (c) 2025 David WilsonRedistribution and use in source and binary forms, with or withoutmodification, are permitted provided that the following conditions are met:1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THEIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE AREDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLEFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIALDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS ORSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVERCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USEOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.README.mdadded
# sigil-wireA typed, length-prefixed **binary codec** for the [Sigil](https://codeberg.org/sigil/sigil)value model. The binary sibling to[sigil-json](https://codeberg.org/sigil/sigil-json): where JSON is thehuman-readable format for small control messages, sigil-wire is the compact,fast format for **bulk structured payloads**.Values decode in a single O(n) forward pass, and string/bytevector bodies arecopied wholesale (`memcpy`), avoiding the per-character O(n²) work that JSONencoding of Sigil values incurs. It is the transport-neutral wire format used bythe Slate/Lantern bulk channel and inherited by Familiar over Enclave.## Usage```scheme(import (sigil wire));; Encode any Sigil value to a self-describing bytevector.(define bytes (wire-encode #{ name: "Alice" scores: #[10 20 30] }));; Decode it back.(wire-decode bytes); => #{ name: "Alice" scores: #[10 20 30] }```The decoder is a **trust boundary**: bytes may be attacker-influenced (overEnclave). Pass a caps record to bound the work a hostile frame can trigger:```scheme(wire-decode bytes (make-wire-caps max-depth: 32 max-count: 1000))```## API| Procedure | Purpose ||-----------|---------|| `(wire-encode value)` → bytevector | Serialize a Sigil value. || `(wire-decode bytevector [caps])` → value | Deserialize, bounded by `caps`. || `(make-wire-caps [keys])` → caps | Build a decode-limits record. || `default-wire-caps` | The default caps used when none is passed. |### Caps (decoder limits)`make-wire-caps` accepts keyword arguments; any omitted field takes its default:| Field | Default | Meaning ||-------|---------|---------|| `max-bytes-len:` | 64 MiB | Largest single string / bytevector body. || `max-count:` | 16 M | Largest collection (list/vector/array length, dict pairs). || `max-depth:` | 256 | Deepest nesting of collections. || `max-total:` | 256 MiB | Total body bytes allocated across the whole message. || `max-int-bytes:` | 1024 | Largest integer payload in varint bytes (bounds bignum size). |## Supported value typesEvery value the seam carries: `#f`, `#t`, exact integers (including bignums),doubles, strings, bytevectors, keywords, symbols, characters, lists, dicts,vectors (R7RS `#(...)`) and arrays (Sigil `#[...]`).A value that **cannot** be represented — a procedure, a port, a record without aregistered codec, an improper (dotted) list, or a non-real number — is an**encode error**, never a silent drop. (Registered-tag record extensions are outof scope for v1.)## Wire formatA message is a fixed 4-byte header followed by exactly one root value:```message : MAGIC(2) VERSION(1) FLAGS(1) <value> 0x53 0x57 0x01 0x00value : TAG(1) <payload>```Decoders reject an unknown MAGIC or an unknown major VERSION. `FLAGS` is reserved(0). Trailing bytes after the root value are a decode error.Lengths and counts are unsigned **LEB128** varints (little-endian base-128; thehigh bit of each byte flags continuation). Small collections stay a single byte.### Tag table| TAG | Type | Payload ||------|------------|---------|| 0x00 | `#f` / nil | (none) || 0x01 | `#t` | (none) || 0x02 | int | zigzag LEB128 varint (bignum-safe, single format) || 0x03 | float | 8 bytes, IEEE-754 double, little-endian || 0x04 | string | varint N, then N UTF-8 bytes (memcpy) || 0x05 | bytevector | varint N, then N raw bytes (memcpy) || 0x06 | keyword | varint N, then N UTF-8 bytes || 0x07 | symbol | varint N, then N UTF-8 bytes || 0x08 | char | varint Unicode codepoint || 0x09 | list | varint K, then K values || 0x0A | dict | varint K, then K (key value) pairs || 0x0B | vector | varint K, then K values (R7RS `#(...)`) || 0x0C | array | varint K, then K values (Sigil `#[...]`) |Any tag outside this table is a **hard decode error** (never skip-and-continue —a misparsed length would cascade).### IntegersA single **zigzag LEB128** varint encodes every integer, from small values up toarbitrary-precision bignums — there is no separate bignum form. Zigzag mapssigned to unsigned so small negatives stay short: `n >= 0 → 2n`, `n < 0 →2|n| - 1`. The varint then encodes that non-negative value.### FloatsDoubles are stored as their raw 64-bit IEEE-754 bit pattern, little-endian.`+inf`, `-inf` and `NaN` round-trip bit-exactly. Note that Sigil cannotdistinguish `-0.0` from `+0.0` (they compare equal under every predicate andprint identically), so a negative zero encodes as `+0.0`.## SecurityThe decoder never trusts a length it reads:- Every length prefix is checked against the remaining buffer **before** any allocation or copy — it never pre-allocates N bytes from an untrusted varint.- All caps (body length, collection count, nesting depth, total decoded size, integer magnitude) are enforced, rejecting **before** allocating.- A bounds-checked cursor means a truncated or lying frame errors cleanly and never reads out of bounds; an endless varint is rejected rather than spun on.The test suite includes a hostile-input fuzz battery (truncated frames, lyinglength prefixes, cap violations, unknown tags, unknown version, varint abuse)asserting each errors cleanly.## Build```shsigil deps installsigil build```## Testing```shsigil test```## LicenseBSD-3-Clause. See [LICENSE](LICENSE).dev-redirects.sgladded
;; Development redirects — point dependencies at local Sigil checkout(redirects repos: (list (for-repo url: "codeberg:sigil/sigil" use: (from-path dir: "../sigil"))))docs/wire.mdadded
# The sigil-wire format (v1)This is the normative, byte-level specification of the sigil-wire binary format,for anyone implementing an encoder or decoder in another language. The referenceimplementation is `src/sigil/wire.sgl`.All multi-byte integers are little-endian. Lengths and counts are unsignedLEB128 varints. The format carries no schema: every value is self-describing viaa leading tag byte.## Message framing```message : header valueheader : MAGIC(2) VERSION(1) FLAGS(1)```| Field | Bytes | Value | Notes ||-------|-------|-------|-------|| MAGIC | 2 | `0x53 0x57` (`"SW"`) | Rejected if it does not match. || VERSION | 1 | `0x01` | Decoders reject an unknown major version. || FLAGS | 1 | `0x00` | Reserved; currently ignored on decode. |Exactly one root `value` follows the header. Any bytes remaining after the rootvalue has been fully decoded are an error (a frame carries one value).## Varints (unsigned LEB128)A non-negative integer is encoded as a sequence of 7-bit groups, leastsignificant first. Each byte holds 7 payload bits in its low bits; the high bit(`0x80`) is set on every byte except the last.```encode(u): loop: byte = u mod 128 u = u div 128 if u == 0: emit(byte); break else: emit(byte | 0x80)decode: result = 0; mult = 1 loop: b = next_byte() result += (b mod 128) * mult if b < 0x80: break mult *= 128```Examples: `0 → 00`, `1 → 01`, `127 → 7F`, `128 → 80 01`, `300 → AC 02`.> Implementation note: the reference decoder accumulates with integer> multiply/add rather than shift/or. This is deliberate — Sigil's bitwise-shift> primitives corrupt values crossing the fixnum/bignum boundary. Other languages> may use shift/or freely.A decoder MUST cap varint length. The reference caps length/count varints at 10bytes and integer-payload varints at `max-int-bytes` bytes, and additionallyrange-checks decoded lengths/counts against the remaining buffer before use.## ValuesEach value begins with a 1-byte tag.| TAG | Type | Payload ||-----|------|---------|| `0x00` | `#f` / nil | none || `0x01` | `#t` | none || `0x02` | int | zigzag varint || `0x03` | float | 8 bytes IEEE-754 LE || `0x04` | string | varint N + N UTF-8 bytes || `0x05` | bytevector | varint N + N raw bytes || `0x06` | keyword | varint N + N UTF-8 bytes || `0x07` | symbol | varint N + N UTF-8 bytes || `0x08` | char | varint codepoint || `0x09` | list | varint K + K values || `0x0A` | dict | varint K + K (key value) pairs || `0x0B` | vector | varint K + K values || `0x0C` | array | varint K + K values |A tag not in this table is a hard error. Decoders MUST NOT attempt to skip anunknown tag: without a length the parser cannot know how many bytes to skip, andguessing cascades into misparsing the rest of the frame.### 0x02 int — zigzag varintA single format covers all integers, including arbitrary-precision bignums.Zigzag maps signed to unsigned so small-magnitude negatives stay short:```zigzag(n) = 2n if n >= 0 2|n| - 1 if n < 0unzigzag(u) = u/2 if u even -(u/2) - 1 if u odd (integer division)```The zigzagged value is then written as an unsigned varint.Examples: `0 → 02 00`, `-1 → 02 01`, `1 → 02 02`, `-2 → 02 03`, `63 → 02 7E`.### 0x03 float — IEEE-754 doubleEight bytes holding the raw IEEE-754 binary64 bit pattern, little-endian. Sign inthe top bit of the last (most significant) byte.- `+inf` = `7FF0000000000000`, `-inf` = `FFF0000000000000`.- `NaN` is written canonically as `7FF8000000000000` (any incoming NaN normalizes to this on encode); on decode any payload with exponent field all-1 and non-zero fraction is a NaN.- `-0.0` is not represented distinctly by the reference implementation (Sigil cannot distinguish it from `+0.0`); it encodes as `+0.0` = `0000000000000000`. A decoder that receives `8000000000000000` should still produce `-0.0` where the host supports it.Example: `3.14 → 03 1F 85 EB 51 B8 1E 09 40` (bytes are `0x40091EB851EB851F` LE).### 0x04 string / 0x05 bytevector / 0x06 keyword / 0x07 symbolA varint byte-count `N` followed by `N` bytes copied verbatim. Strings, keywordsand symbols carry UTF-8; bytevectors carry raw bytes. The decoder checks `N`against the remaining buffer before allocating, then copies the body in one`memcpy`. Keywords and symbols are interned from their UTF-8 name on decode.Example: `"hi" → 04 02 68 69`.### 0x08 char — codepoint varintA varint Unicode scalar value. Decoders should reject values above `0x10FFFF`.Example: `#\A → 08 41`.### 0x09 list / 0x0B vector / 0x0C array — counted sequencesA varint element-count `K` followed by `K` encoded values in order. Lists, R7RSvectors (`#(...)`) and Sigil arrays (`#[...]`) share this shape but use distincttags so their type is preserved across a round-trip.Example: `(1 2 3) → 09 03 02 02 02 04 02 06`.### 0x0A dict — counted key/value pairsA varint pair-count `K` followed by `2K` encoded values: `key`, `value`, `key`,`value`, … Keys are ordinary values (usually keywords) and are encoded with theirown tag. Dict equality is order-independent, so the reference implementation doesnot commit to a key order.Example: `#{ a: 1 } → 0A 01 06 01 61 02 02` (keyword `a`, then int `1`).## Decoder safety requirementsAn implementation reading untrusted bytes MUST:1. Validate MAGIC and VERSION before anything else.2. Read every length/count into a bounded varint, then range-check it against the remaining buffer **before** allocating or copying.3. Enforce configurable limits: maximum single-body length, maximum collection count, maximum nesting depth, maximum total decoded size, maximum integer magnitude — rejecting **before** allocation.4. Use a bounds-checked cursor so a truncated or lying frame errors cleanly and never reads out of bounds.5. Treat an unknown tag, unknown version, or trailing bytes as hard errors.## VersioningThe 1-byte VERSION is the format's major version. A decoder rejects a version itdoes not implement rather than guessing. New tags or payload shapes that are notbackward-compatible bump the version; a capability handshake at connect time (forFamiliar/Enclave) negotiates it so a new client and an old node degradegracefully.manifest.scmadded
;; sigil-wire Development Environment;; Use with: guix shell -m manifest.scm;;;; sigil-wire is pure Scheme with no vendored C, so the environment;; only needs the common Sigil build toolchain.(specifications->manifest '("gcc-toolchain" "make" "pkg-config"))package.sgladded
;;; sigil-wire - Typed binary codec for Sigil values;;;;;; A length-prefixed, typed binary serialization of the Sigil value model,;;; decoded in one O(n) forward pass with string/bytevector bodies memcpy'd.;;; The transport-neutral wire format for bulk structured payloads: the binary;;; sibling to sigil-json.(package name: "sigil-wire" version: "0.1.0" sigil: "^0.17" description: "Typed binary codec for Sigil values" url: "https://codeberg.org/sigil/sigil-wire" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>") dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.17")))sigil.lockadded
;; Auto-generated by sigil deps install. Do not edit.(lock (package name: "sigil-stdlib" url: "codeberg:sigil/sigil" ref: "^0.17" sha: "61809566520a392d08c8308ec6217e1ae719eedd" package-selector: "sigil-stdlib" version: "0.17.18"))src/sigil/wire.sgladded
;;; (sigil wire) - Typed binary codec for the Sigil value model;;;;;; A length-prefixed, typed serialization of Sigil values, decoded in one;;; O(n) forward pass with string/bytevector bodies memcpy'd (no per-char;;; work). This is the transport-neutral wire format for bulk structured;;; payloads: the binary sibling to (sigil json).;;;;;; ## Why this exists;;;;;; JSON encoding of Sigil values is O(n^2) on the bulk path because;;; `string-ref` is O(index) over UTF-8 and the encoder walks per-char. This;;; codec keeps the "don't parse, just rehydrate" property that the base64;;; bulk lane already proves: a tag byte says what follows, a varint says how;;; long, and string/bytevector bodies are copied wholesale.;;;;;; JSON stays for tiny CONTROL messages (opens, acks, credits); this codec is;;; for BULK structured payloads.;;;;;; ## Wire format;;;;;; ```;;; message: MAGIC(2)=0x53 0x57 VERSION(1)=1 FLAGS(1)=0 <value>;;; value: TAG(1) <payload>;;; ```;;;;;; Lengths and counts are unsigned LEB128 varints (small collections stay one;;; byte). Value tags:;;;;;; | TAG | type | payload |;;; |------|------------|----------------------------------------------------|;;; | 0x00 | #f / nil | (none) |;;; | 0x01 | #t | (none) |;;; | 0x02 | int | zigzag LEB128 varint (bignum-safe, single format) |;;; | 0x03 | float | 8 bytes IEEE-754 double, little-endian |;;; | 0x04 | string | uvarint N, then N UTF-8 bytes (memcpy) |;;; | 0x05 | bytevector | uvarint N, then N raw bytes (memcpy) |;;; | 0x06 | keyword | uvarint N, then N UTF-8 bytes |;;; | 0x07 | symbol | uvarint N, then N UTF-8 bytes |;;; | 0x08 | char | uvarint Unicode codepoint |;;; | 0x09 | list | uvarint K, then K values |;;; | 0x0A | dict | uvarint K, then K (key value) pairs |;;; | 0x0B | vector | uvarint K, then K values (R7RS #(...)) |;;; | 0x0C | array | uvarint K, then K values (Sigil #[...]) |;;;;;; ## Basic usage;;;;;; ```scheme;;; (import (sigil wire));;;;;; (define bytes (wire-encode #{ name: "Alice" scores: #[10 20 30] }));;; (wire-decode bytes);;; ; => #{ name: "Alice" scores: #[10 20 30] };;; ```;;;;;; ## Security: the decoder is a trust boundary;;;;;; Bytes handed to `wire-decode` may be attacker-influenced. The decoder:;;; - checks every length prefix against the remaining buffer BEFORE allocating;;; or copying (never pre-allocates N from an untrusted varint);;;; - enforces configurable caps (max body length, collection count, nesting;;; depth, total decoded size) and rejects — without allocating — on;;; violation;;;; - uses a bounds-checked cursor, so a truncated or lying frame errors;;; cleanly and never reads out of bounds.;;;;;; Pass a caps record from `make-wire-caps` to tune the limits:;;;;;; ```scheme;;; (wire-decode bytes (make-wire-caps max-depth: 32 max-count: 1000));;; ```(define-library (sigil wire) (import (sigil core) (sigil io) (sigil math)) (export ;; Codec wire-encode wire-decode ;; Security caps make-wire-caps default-wire-caps) (begin ;; ========== Constants ========== (define MAGIC-0 #x53) ; 'S' (define MAGIC-1 #x57) ; 'W' (define WIRE-VERSION 1) (define WIRE-FLAGS 0) (define TAG-FALSE #x00) (define TAG-TRUE #x01) (define TAG-INT #x02) (define TAG-FLOAT #x03) (define TAG-STRING #x04) (define TAG-BYTEVECTOR #x05) (define TAG-KEYWORD #x06) (define TAG-SYMBOL #x07) (define TAG-CHAR #x08) (define TAG-LIST #x09) (define TAG-DICT #x0A) (define TAG-VECTOR #x0B) (define TAG-ARRAY #x0C) ;; A length/count varint indexes into the buffer, so it can never ;; legitimately need more than a handful of bytes. Cap it hard to bound ;; decode CPU regardless of the value it claims (which is then range-checked ;; against the remaining buffer anyway). (define LEN-VARINT-MAX-BYTES 10) ;; ========== Security caps ========== ;;; Build a caps record controlling decode limits. Any omitted field takes ;;; the default from `default-wire-caps`. ;;; ;;; - `max-bytes-len` — largest single string/bytevector body (bytes) ;;; - `max-count` — largest collection (list/vector length, dict pairs) ;;; - `max-depth` — deepest nesting of collections ;;; - `max-total` — total body bytes allocated across the whole message ;;; - `max-int-bytes` — largest int payload (zigzag varint bytes), bounds ;;; bignum size (define (make-wire-caps (keys: (max-bytes-len 67108864) ; 64 MiB (max-count 16777216) ; 16 M (max-depth 256) (max-total 268435456) ; 256 MiB (max-int-bytes 1024))) ; ~8192-bit #{ max-bytes-len: max-bytes-len max-count: max-count max-depth: max-depth max-total: max-total max-int-bytes: max-int-bytes }) (define default-wire-caps (make-wire-caps)) ;; ========== Varint (unsigned LEB128) ========== ;; NOTE: the varint and float-bit paths use only arithmetic (quotient / ;; remainder / * / +), never bitwise-and / bitwise-ior / arithmetic-shift. ;; Sigil's bitwise/shift primitives mis-handle values straddling the ;; fixnum<->bignum boundary (e.g. round-tripping 2^62 or 7^500 through ;; shift+ior corrupts the value), whereas plain integer arithmetic is ;; correct for arbitrary-precision integers. See the wire-format topic note. ;; Write a non-negative integer as unsigned LEB128 to a port. (define (write-uvarint u port) (let loop ((u u)) (let ((b (remainder u 128)) (rest (quotient u 128))) (if (= rest 0) (write-u8 b port) (begin (write-u8 (+ b 128) port) (loop rest)))))) ;; Zigzag-map a signed integer to a non-negative one (bignum-safe). (define (zigzag n) (if (>= n 0) (* n 2) (- (* (- n) 2) 1))) ;; Inverse of `zigzag`. (define (unzigzag u) (if (= 0 (remainder u 2)) (quotient u 2) (- (- (quotient u 2)) 1))) ;; ========== Float <-> IEEE-754 bits ========== ;; ;; Sigil's numeric tower is exact integers (incl. bignums) and inexact ;; doubles — there are NO exact rationals — so `(exact <float>)` errors on a ;; non-integer. We therefore derive the 64-bit IEEE-754 representation with ;; float arithmetic + exact-integer ops, never touching a rational. Every ;; scaling factor stays within the representable double range (|exponent| <= ;; 1022 per step) so no intermediate overflows to infinity. (define POW2-52 (expt 2 52)) ; 4503599627370496 (define POW2-63 (expt 2 63)) ; sign-bit place value ;; Sigil has no infinity/NaN literals, but doubles CAN be inf/NaN (e.g. from ;; overflow). Build them once, by arithmetic, to reconstruct on decode. (define WIRE-POS-INF (expt 2.0 2000)) ; overflows to +inf (define WIRE-NEG-INF (- (expt 2.0 2000))) ; -inf (define WIRE-NAN (- WIRE-POS-INF WIRE-POS-INF)) ; inf - inf = NaN ;; Assemble sign/biased-exponent/fraction into a 64-bit unsigned integer. (define (pack-bits sign biased frac) (+ (* sign POW2-63) (* biased POW2-52) frac)) ;; Encode a double as its unsigned 64-bit IEEE-754 bit pattern. ;; NaN is checked BEFORE zero: Sigil's `(= nan 0.0)` returns #t, so a ;; zero-first test would mis-encode NaN as +0.0. (define (double->bits x) (cond ;; NaN: canonical quiet NaN (0x7FF8000000000000); fraction = 2^51. ((nan? x) (pack-bits 0 2047 (quotient POW2-52 2))) ;; +/- infinity. ((not (finite? x)) (if (< x 0.0) (pack-bits 1 2047 0) (pack-bits 0 2047 0))) ;; Zero (positive and negative zero both encode as +0.0; Sigil does not ;; distinguish them under `=`, `eqv?`, `equal?`, or `number->string`). ((= x 0.0) 0) (else (let* ((neg (< x 0.0)) (sign (if neg 1 0)) (ax (abs x))) (if (< ax (expt 2.0 -1022)) ;; Subnormal: significand = round(ax * 2^1074), scaled in two ;; representable steps (2^1074 itself overflows a double). (let ((frac (exact (round (* (* ax (expt 2.0 1022)) (expt 2.0 52)))))) (pack-bits sign 0 frac)) ;; Normal: find unbiased exponent e with 2^e <= ax < 2^(e+1). (let loop ((e (exact (floor (/ (log ax) (log 2.0)))))) (cond ((<= (expt 2.0 (+ e 1)) ax) (loop (+ e 1))) ((> (expt 2.0 e) ax) (loop (- e 1))) (else ;; ax/2^e is exactly a double in [1,2); *2^52 is an exact ;; integer significand in [2^52, 2^53). (let* ((sig (exact (round (* (/ ax (expt 2.0 e)) (expt 2.0 52))))) (frac (- sig POW2-52)) (biased (+ e 1023))) (pack-bits sign biased frac)))))))))) ;; Decode an unsigned 64-bit IEEE-754 bit pattern to a double. (define (bits->double bits) (let* ((sign (quotient bits POW2-63)) (rest (remainder bits POW2-63)) (biased (quotient rest POW2-52)) (frac (remainder rest POW2-52))) (cond ;; Inf / NaN carry their own sign; return directly. ((= biased 2047) (cond ((not (= frac 0)) WIRE-NAN) ((= sign 1) WIRE-NEG-INF) (else WIRE-POS-INF))) (else (let ((mag (cond ;; Zero / subnormal. ((= biased 0) (if (= frac 0) 0.0 (* frac (expt 2.0 -1074)))) ;; Normal. (else (* (+ frac POW2-52) (expt 2.0 (- biased 1075))))))) (if (= sign 1) (- mag) mag)))))) ;; ========== Encoding ========== ;;; Encode a Sigil value to a self-describing wire bytevector. ;;; ;;; Every value type the seam carries is supported: #f, #t, exact integers ;;; (incl. bignums), doubles, strings, bytevectors, keywords, symbols, ;;; chars, lists, dicts, vectors and arrays. A value that cannot be ;;; represented — ;;; a procedure, a port, a record without a registered codec, an improper ;;; (dotted) list, or a non-real number — is an ENCODE error, never a ;;; silent drop. ;;; ;;; ```scheme ;;; (wire-encode #{ id: 7 tags: #["a" "b"] }) ;;; ; => #<bytevector ...> ;;; ``` (define (wire-encode value) (: any? -> bytevector?) (let ((port (open-output-bytevector))) (write-u8 MAGIC-0 port) (write-u8 MAGIC-1 port) (write-u8 WIRE-VERSION port) (write-u8 WIRE-FLAGS port) (encode-value value port) (get-output-bytevector port))) ;; Write a length-prefixed UTF-8 / raw body (the memcpy path). (define (encode-bytes tag bv port) (write-u8 tag port) (write-uvarint (bytevector-length bv) port) (write-bytevector bv port)) ;; Encode one value (tag + payload) to the port. (define (encode-value value port) (cond ;; Booleans / nil. Order matters: check booleans before numbers so #f ;; and #t never fall through to another branch. ((eq? value #f) (write-u8 TAG-FALSE port)) ((eq? value #t) (write-u8 TAG-TRUE port)) ;; Exact integers (fixnum + bignum) -> zigzag LEB128. ((exact-integer? value) (write-u8 TAG-INT port) (write-uvarint (zigzag value) port)) ;; Any other number is a double (Sigil has no exact rationals; complex ;; numbers, if present, are rejected below). ((and (number? value) (inexact? value)) (write-u8 TAG-FLOAT port) (encode-float value port)) ;; String / bytevector -> memcpy body. ((string? value) (encode-bytes TAG-STRING (string->utf8 value) port)) ((bytevector? value) (encode-bytes TAG-BYTEVECTOR value port)) ;; Keyword / symbol -> UTF-8 name. ((keyword? value) (encode-bytes TAG-KEYWORD (string->utf8 (keyword->string value)) port)) ((symbol? value) (encode-bytes TAG-SYMBOL (string->utf8 (symbol->string value)) port)) ;; Char -> codepoint varint. ((char? value) (write-u8 TAG-CHAR port) (write-uvarint (char->integer value) port)) ;; Empty list and proper lists. ((null? value) (write-u8 TAG-LIST port) (write-uvarint 0 port)) ((pair? value) (encode-list value port)) ;; Vector (R7RS #(...)). ((vector? value) (encode-vector value port)) ;; Array (Sigil #[...], the primary bulk sequence type). ((array? value) (encode-array value port)) ;; Dict. ((dict? value) (encode-dict value port)) (else (error "wire-encode: value is not representable on the wire" value)))) (define (encode-float value port) (let ((bits (double->bits value))) ;; 8 bytes, little-endian (arithmetic, not bitwise). (let loop ((i 0)) (when (< i 8) (write-u8 (remainder (quotient bits (expt 256 i)) 256) port) (loop (+ i 1)))))) (define (encode-list value port) ;; Walk once: verify the list is proper and count length, collecting the ;; elements. An improper (dotted) tail is an encode error. (let loop ((v value) (items '()) (n 0)) (cond ((null? v) (write-u8 TAG-LIST port) (write-uvarint n port) (for-each (lambda (x) (encode-value x port)) (reverse items))) ((pair? v) (loop (cdr v) (cons (car v) items) (+ n 1))) (else (error "wire-encode: improper (dotted) list is not representable" value))))) (define (encode-vector value port) (let ((len (vector-length value))) (write-u8 TAG-VECTOR port) (write-uvarint len port) (let loop ((i 0)) (when (< i len) (encode-value (vector-ref value i) port) (loop (+ i 1)))))) (define (encode-array value port) (let ((len (array-length value))) (write-u8 TAG-ARRAY port) (write-uvarint len port) (let loop ((i 0)) (when (< i len) (encode-value (array-ref value i) port) (loop (+ i 1)))))) (define (encode-dict value port) (let ((entries (dict-entries value))) (write-u8 TAG-DICT port) (write-uvarint (length entries) port) (for-each (lambda (pair) (encode-value (car pair) port) (encode-value (cdr pair) port)) entries))) ;; ========== Decoding cursor (bounds-checked) ========== ;; ;; The cursor is a mutable vector #(bv len pos total) so a lying/truncated ;; frame errors cleanly instead of reading out of bounds. `total` tracks ;; body bytes allocated so far, checked against the max-total cap. (define (cur-make bv) (vector bv (bytevector-length bv) 0 0)) (define (cur-bv c) (vector-ref c 0)) (define (cur-len c) (vector-ref c 1)) (define (cur-pos c) (vector-ref c 2)) (define (cur-remaining c) (- (vector-ref c 1) (vector-ref c 2))) ;; Error unless at least n bytes remain. (define (cur-need! c n) (when (> n (cur-remaining c)) (error "wire-decode: truncated frame (buffer underrun)"))) ;; Read one byte, advancing the cursor. (define (cur-u8! c) (cur-need! c 1) (let ((b (bytevector-u8-ref (cur-bv c) (cur-pos c)))) (vector-set! c 2 (+ (cur-pos c) 1)) b)) ;; Copy and return the next n bytes (the memcpy path), advancing the cursor. (define (cur-take! c n) (cur-need! c n) (let* ((start (cur-pos c)) (slice (bytevector-copy (cur-bv c) start (+ start n)))) (vector-set! c 2 (+ start n)) slice)) ;; Account for n newly-allocated body bytes against the max-total cap. (define (cur-add-total! c n caps) (let ((total (+ (vector-ref c 3) n))) (when (> total (dict-ref caps 'max-total:)) (error "wire-decode: total decoded size exceeds cap")) (vector-set! c 3 total))) ;; Read an unsigned LEB128 varint, rejecting one longer than max-bytes. ;; Arithmetic accumulation (result + low7 * mult) keeps bignums correct. (define (read-uvarint c max-bytes) (let loop ((result 0) (mult 1) (count 0)) (let ((b (cur-u8! c)) (count (+ count 1))) (when (> count max-bytes) (error "wire-decode: varint too long")) (let ((result (+ result (* (remainder b 128) mult)))) (if (< b 128) result (loop result (* mult 128) count)))))) ;; Read a length/count varint (small cap) and range-check it against the ;; remaining buffer BEFORE it is used to allocate. `min-per` is the minimum ;; bytes each counted element consumes (1 for collections), so a count that ;; could not possibly fit in what remains is rejected without allocating. (define (read-length c what min-per) (let ((n (read-uvarint c LEN-VARINT-MAX-BYTES))) (when (> (* n min-per) (cur-remaining c)) (error (string-append "wire-decode: " what " exceeds remaining buffer"))) n)) ;; ========== Decoding ========== ;;; Decode a wire bytevector produced by `wire-encode` back into a Sigil ;;; value. The optional caps record (default `default-wire-caps`) bounds the ;;; work an untrusted frame can trigger. ;;; ;;; Raises a clean error on any malformed input: bad magic, unknown major ;;; version, unknown tag, truncated body, a length prefix that overruns the ;;; buffer, or a cap violation. It never reads out of bounds. ;;; ;;; ```scheme ;;; (wire-decode (wire-encode #[1 2 3])) ;;; ; => #[1 2 3] ;;; ``` (define (wire-decode bv . rest) (: bytevector? any? ... -> any?) (let ((caps (if (null? rest) default-wire-caps (car rest))) (c (cur-make bv))) ;; Header. (unless (and (= (cur-u8! c) MAGIC-0) (= (cur-u8! c) MAGIC-1)) (error "wire-decode: bad magic (not a sigil-wire frame)")) (let ((version (cur-u8! c))) (unless (= version WIRE-VERSION) (error "wire-decode: unsupported wire version" version))) (cur-u8! c) ; FLAGS (reserved, ignored) (let ((value (decode-value c caps 0))) ;; Trailing bytes after a complete root value are malformed. (when (> (cur-remaining c) 0) (error "wire-decode: trailing bytes after root value")) value))) ;; Decode one value at the current cursor. `depth` is the current nesting ;; level, checked against max-depth on every collection. (define (decode-value c caps depth) (let ((tag (cur-u8! c))) (cond ((= tag TAG-FALSE) #f) ((= tag TAG-TRUE) #t) ((= tag TAG-INT) (unzigzag (read-uvarint c (dict-ref caps 'max-int-bytes:)))) ((= tag TAG-FLOAT) (decode-float c)) ((= tag TAG-STRING) (utf8->string (decode-body c caps))) ((= tag TAG-BYTEVECTOR) (decode-body c caps)) ((= tag TAG-KEYWORD) (string->keyword (utf8->string (decode-body c caps)))) ((= tag TAG-SYMBOL) (string->symbol (utf8->string (decode-body c caps)))) ((= tag TAG-CHAR) (decode-char c)) ((= tag TAG-LIST) (decode-list c caps depth)) ((= tag TAG-DICT) (decode-dict c caps depth)) ((= tag TAG-VECTOR) (decode-vector c caps depth)) ((= tag TAG-ARRAY) (decode-array c caps depth)) (elseShowing the first 500 of 569 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
test/test-wire.sgladded
;;; Test suite for (sigil wire);;;;;; Two halves:;;; 1. Round-trip coverage for every value type + edge cases.;;; 2. Hostile-input fuzzing of the decoder (the trust boundary): truncated;;; frames, lying lengths, cap violations, unknown tags/versions. Each must;;; error CLEANLY — no OOB read, no OOM, no spin.(import (sigil test) (sigil wire) (sigil core) (sigil math));; ---------- helpers ----------;; Round-trip: encode then decode.(define (rt v) (wire-decode (wire-encode v)));; Does the thunk raise? (Clean-error probe for hostile inputs.)(define (raises? thunk) (guard (e (#t #t)) (thunk) #f));; Header bytes for a valid frame: MAGIC "SW", VERSION 1, FLAGS 0.(define wire-header (list #x53 #x57 1 0));; Build a bytevector from a list of bytes.(define (bytes->bv lst) (apply bytevector lst));; A valid frame carrying the given raw payload bytes.(define (frame . payload-bytes) (bytes->bv (append wire-header payload-bytes)));; ============================================================;; Round-trip: primitives;; ============================================================(test-group "round-trip - booleans and nil" (test "false" (assert-equal #f (rt #f))) (test "true" (assert-equal #t (rt #t))) (test "empty list is nil-ish" (assert-equal '() (rt '()))))(test-group "round-trip - integers" (test "zero" (assert-equal 0 (rt 0))) (test "one" (assert-equal 1 (rt 1))) (test "small positive" (assert-equal 42 (rt 42))) (test "small negative" (assert-equal -1 (rt -1))) (test "negative" (assert-equal -12345 (rt -12345))) (test "127 boundary" (assert-equal 127 (rt 127))) (test "128 boundary" (assert-equal 128 (rt 128))) (test "large positive bignum" (assert-equal (expt 2 200) (rt (expt 2 200)))) (test "large negative bignum" (assert-equal (- (expt 2 200)) (rt (- (expt 2 200))))) (test "huge bignum" (assert-equal (expt 7 500) (rt (expt 7 500)))) (test "negative huge bignum" (assert-equal (- (expt 7 500)) (rt (- (expt 7 500))))))(test-group "round-trip - floats" (test "pi-ish" (assert-equal 3.14 (rt 3.14))) (test "negative" (assert-equal -2.5 (rt -2.5))) (test "zero" (assert-equal 0.0 (rt 0.0))) (test "one" (assert-equal 1.0 (rt 1.0))) (test "tenth" (assert-equal 0.1 (rt 0.1))) (test "max double" (assert-equal 1.7976931348623157e308 (rt 1.7976931348623157e308))) (test "large" (assert-equal 1e308 (rt 1e308))) (test "small normal" (assert-equal 2.2250738585072014e-308 (rt 2.2250738585072014e-308))) (test "subnormal" (assert-equal 1e-308 (rt 1e-308))) (test "smallest subnormal" (assert-equal 5e-324 (rt 5e-324))) (test "negative small" (assert-equal -0.001 (rt -0.001))))(test-group "round-trip - float special values" ;; Sigil has no inf/NaN literals but doubles can be inf/NaN (overflow etc.); ;; the codec must round-trip them bit-exactly. (test "+inf" (assert-equal (expt 2.0 2000) (rt (expt 2.0 2000)))) (test "-inf" (assert-equal (- (expt 2.0 2000)) (rt (- (expt 2.0 2000))))) (test "+inf stays infinite" (assert-true (infinite? (rt (expt 2.0 2000))))) (test "-inf stays negative" (assert-true (< (rt (- (expt 2.0 2000))) 0.0))) (test "NaN stays NaN" (assert-true (nan? (rt (- (expt 2.0 2000) (expt 2.0 2000)))))))(test-group "round-trip - strings" (test "empty" (assert-equal "" (rt ""))) (test "ascii" (assert-equal "hello" (rt "hello"))) (test "spaces and punct" (assert-equal "a, b. c!" (rt "a, b. c!"))) (test "2-byte utf8" (assert-equal "héllo" (rt "héllo"))) (test "3-byte utf8" (assert-equal "日本語" (rt "日本語"))) (test "4-byte utf8 emoji" (assert-equal "🚀🔥" (rt "🚀🔥"))) (test "mixed" (assert-equal "λ = π · r² 日 🚀" (rt "λ = π · r² 日 🚀"))) (test "embedded null" (let ((s (string #\a (integer->char 0) #\b))) (assert-equal s (rt s)))))(test-group "round-trip - bytevectors" (test "empty" (assert-equal (bytevector) (rt (bytevector)))) (test "bytes" (assert-equal (bytevector 0 1 2 255) (rt (bytevector 0 1 2 255)))) (test "all-zero" (assert-equal (make-bytevector 10 0) (rt (make-bytevector 10 0)))))(test-group "round-trip - keywords and symbols" (test "keyword" (assert-equal 'name: (rt 'name:))) (test "keyword unicode" (assert-equal (string->keyword "café") (rt (string->keyword "café")))) (test "symbol" (assert-equal 'foo-bar (rt 'foo-bar))) (test "symbol unicode" (assert-equal (string->symbol "λ-fn") (rt (string->symbol "λ-fn")))))(test-group "round-trip - chars" ;; Non-ASCII char literals are built via integer->char (Sigil's reader does ;; not accept multi-byte #\<char> literals). (test "ascii" (assert-equal #\a (rt #\a))) (test "space" (assert-equal #\space (rt #\space))) (test "newline" (assert-equal #\newline (rt #\newline))) (test "greek lambda (955)" (assert-equal (integer->char 955) (rt (integer->char 955)))) (test "cjk (26085)" (assert-equal (integer->char 26085) (rt (integer->char 26085)))) (test "emoji codepoint (128640)" (assert-equal (integer->char 128640) (rt (integer->char 128640)))) (test "null char" (assert-equal (integer->char 0) (rt (integer->char 0)))) (test "max codepoint" (assert-equal (integer->char #x10FFFF) (rt (integer->char #x10FFFF)))));; ============================================================;; Round-trip: collections;; ============================================================(test-group "round-trip - lists" (test "empty" (assert-equal '() (rt '()))) (test "ints" (assert-equal '(1 2 3) (rt '(1 2 3)))) (test "mixed" (assert-equal (list 1 "two" 3.0 #\4 'five:) (rt (list 1 "two" 3.0 #\4 'five:)))) (test "nested" (assert-equal '(1 (2 (3 (4)))) (rt '(1 (2 (3 (4))))))) (test "list of strings" (assert-equal '("a" "bb" "ccc") (rt '("a" "bb" "ccc")))))(test-group "round-trip - vectors (R7RS #())" (test "empty" (assert-equal (vector) (rt (vector)))) (test "ints" (assert-equal #(1 2 3) (rt #(1 2 3)))) (test "mixed" (assert-equal (vector 1 "two" 3.0 #t) (rt (vector 1 "two" 3.0 #t)))) (test "nested" (assert-equal #(#(1 2) #(3 4)) (rt #(#(1 2) #(3 4))))) (test "stays a vector, not an array" (assert-true (vector? (rt #(1 2 3))))))(test-group "round-trip - arrays (Sigil #[])" (test "empty" (assert-equal #[] (rt #[]))) (test "ints" (assert-equal #[1 2 3] (rt #[1 2 3]))) (test "mixed" (assert-equal #[1 "two" 3.0 #t] (rt #[1 "two" 3.0 #t]))) (test "nested" (assert-equal #[#[1 2] #[3 4]] (rt #[#[1 2] #[3 4]]))) (test "stays an array, not a vector" (assert-true (array? (rt #[1 2 3])))) (test "array and vector are distinct on the wire" (assert-false (equal? (wire-encode #[1 2 3]) (wire-encode #(1 2 3))))))(test-group "round-trip - dicts" (test "empty" (assert-equal #{} (rt #{}))) (test "simple" (assert-equal #{ a: 1 } (rt #{ a: 1 }))) (test "multi" (assert-equal #{ name: "Alice" age: 30 } (rt #{ name: "Alice" age: 30 }))) (test "nested dict" (assert-equal #{ outer: #{ inner: 42 } } (rt #{ outer: #{ inner: 42 } }))) (test "dict with collections" (assert-equal #{ items: #[1 2 3] tags: (list "x" "y") } (rt #{ items: #[1 2 3] tags: (list "x" "y") }))))(test-group "round-trip - deeply nested mixed" (test "structure like a directory listing" (let ((v #{ entries: #[ #{ name: "a.txt" size: 100 dir: #f } #{ name: "sub" size: 0 dir: #t } ] total: 2 })) (assert-equal v (rt v)))) (test "list/dict/vector interleaved" (let ((v (list #{ k: #[1 (list 2 3) #{ deep: "yes" }] } 'sym #\x (bytevector 9 8 7)))) (assert-equal v (rt v)))));; ============================================================;; Encode errors: non-representable values;; ============================================================(test-group "encode - non-representable is an error, not a drop" (test "procedure" (assert-true (raises? (lambda () (wire-encode car))))) (test "improper list" (assert-true (raises? (lambda () (wire-encode (cons 1 2)))))) (test "procedure nested in a list" (assert-true (raises? (lambda () (wire-encode (list 1 2 car)))))) (test "procedure nested in a dict value" (assert-true (raises? (lambda () (wire-encode #{ f: car }))))));; ============================================================;; Hostile-input fuzzing of the decoder (trust boundary);; ============================================================(test-group "decode - malformed header" (test "empty buffer" (assert-true (raises? (lambda () (wire-decode (bytevector)))))) (test "too short for header" (assert-true (raises? (lambda () (wire-decode (bytevector #x53)))))) (test "bad magic byte 0" (assert-true (raises? (lambda () (wire-decode (bytes->bv (list #x00 #x57 1 0 #x00))))))) (test "bad magic byte 1" (assert-true (raises? (lambda () (wire-decode (bytes->bv (list #x53 #x00 1 0 #x00))))))) (test "unknown version" (assert-true (raises? (lambda () (wire-decode (bytes->bv (list #x53 #x57 99 0 #x00))))))))(test-group "decode - unknown tags" (test "unknown tag 0x7F" (assert-true (raises? (lambda () (wire-decode (frame #x7F)))))) (test "unknown tag 0xFF" (assert-true (raises? (lambda () (wire-decode (frame #xFF)))))) (test "just past known range" (assert-true (raises? (lambda () (wire-decode (frame #x0C)))))))(test-group "decode - truncated frames" (test "tag but no payload (int)" (assert-true (raises? (lambda () (wire-decode (frame #x02)))))) (test "string claims 5 bytes, gives 2" (assert-true (raises? (lambda () (wire-decode (frame #x04 5 #x61 #x62)))))) (test "bytevector claims 10, gives 0" (assert-true (raises? (lambda () (wire-decode (frame #x05 10)))))) (test "float with only 4 of 8 bytes" (assert-true (raises? (lambda () (wire-decode (frame #x03 0 0 0 0)))))) (test "char varint truncated (continuation then EOF)" (assert-true (raises? (lambda () (wire-decode (frame #x08 #x80)))))) (test "list claims 3 elements, gives 1" (assert-true (raises? (lambda () (wire-decode (frame #x09 3 #x01)))))) (test "dict claims 2 pairs, gives nothing" (assert-true (raises? (lambda () (wire-decode (frame #x0A 2)))))))(test-group "decode - lying length prefixes (claim huge, provide few)" (test "string claims ~2 billion bytes" ;; varint for 0xF0F0F0F0: bytes 0xF0 0xE1 0xC3 0x87 0x0F (assert-true (raises? (lambda () (wire-decode (frame #x04 #xF0 #xE1 #xC3 #x87 #x0F #x61)))))) (test "list claims ~2 billion elements" (assert-true (raises? (lambda () (wire-decode (frame #x09 #xF0 #xE1 #xC3 #x87 #x0F)))))) (test "dict claims huge pair count" (assert-true (raises? (lambda () (wire-decode (frame #x0A #xFF #xFF #xFF #xFF #x0F)))))))(test-group "decode - varint abuse" (test "int with endless continuation bytes errors (does not spin/OOM)" ;; 1030 continuation bytes with no terminator, past default max-int-bytes. (assert-true (raises? (lambda () (wire-decode (bytes->bv (append wire-header (list #x02) (make-continuation-bytes 1030)))))))) (test "length varint too long" ;; 12 continuation bytes for a body length; exceeds LEN-VARINT-MAX-BYTES. (assert-true (raises? (lambda () (wire-decode (bytes->bv (append wire-header (list #x04) (make-continuation-bytes 12)))))))))(test-group "decode - trailing garbage" (test "extra byte after a complete value" (assert-true (raises? (lambda () (wire-decode (frame #x01 #x99)))))) (test "second value after root" (assert-true (raises? (lambda () (wire-decode (frame #x00 #x01)))))));; ============================================================;; Cap enforcement;; ============================================================(test-group "caps - body length" (test "oversized string rejected by tiny cap" (let ((bytes (wire-encode "this string is definitely longer than eight bytes"))) (assert-true (raises? (lambda () (wire-decode bytes (make-wire-caps max-bytes-len: 8))))))) (test "within cap decodes fine" (let ((bytes (wire-encode "short"))) (assert-equal "short" (wire-decode bytes (make-wire-caps max-bytes-len: 100))))))(test-group "caps - collection count" (test "too many list elements rejected" (let ((bytes (wire-encode '(1 2 3 4 5 6 7 8 9 10)))) (assert-true (raises? (lambda () (wire-decode bytes (make-wire-caps max-count: 3))))))) (test "too many dict pairs rejected" (let ((bytes (wire-encode #{ a: 1 b: 2 c: 3 }))) (assert-true (raises? (lambda () (wire-decode bytes (make-wire-caps max-count: 2))))))) (test "within count cap decodes" (let ((bytes (wire-encode '(1 2 3)))) (assert-equal '(1 2 3) (wire-decode bytes (make-wire-caps max-count: 10))))))(test-group "caps - nesting depth" (test "too-deep nesting rejected" (let ((deep (build-nested-list 300))) (assert-true (raises? (lambda () (wire-decode (wire-encode deep) (make-wire-caps max-depth: 64))))))) (test "shallow nesting within cap decodes" (let ((shallow (build-nested-list 10))) (assert-equal shallow (wire-decode (wire-encode shallow) (make-wire-caps max-depth: 64))))))(test-group "caps - total decoded size" (test "total body bytes over cap rejected" (let ((bytes (wire-encode (list "aaaa" "bbbb" "cccc" "dddd")))) (assert-true (raises? (lambda () (wire-decode bytes (make-wire-caps max-total: 8))))))) (test "generous total cap decodes" (let ((v (list "aaaa" "bbbb"))) (assert-equal v (wire-decode (wire-encode v) (make-wire-caps max-total: 1000))))))(test-group "caps - int magnitude" (test "bignum over max-int-bytes rejected" (let ((bytes (wire-encode (expt 2 4000)))) (assert-true (raises? (lambda () (wire-decode bytes (make-wire-caps max-int-bytes: 4))))))) (test "bignum within cap decodes" (let ((n (expt 2 200))) (assert-equal n (wire-decode (wire-encode n) (make-wire-caps max-int-bytes: 1024))))));; ---------- helpers used above (defined after; Sigil hoists defines) ----------(define (make-continuation-bytes n) (let loop ((i 0) (acc '())) (if (= i n) acc (loop (+ i 1) (cons #x80 acc)))))(define (build-nested-list depth) (let loop ((n depth) (v '())) (if (= n 0) v (loop (- n 1) (list v)))))