main
Matthew Butterick 7 years ago
parent 6008236b1e
commit b61161ba36

@ -1,79 +1,3 @@
#lang restructure/racket
(require "number.rkt" "utils.rkt" "stream.rkt")
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Array.coffee
|#
(define-subclass Streamcoder (ArrayT type [len #f] [length-type 'count])
(define/augride (decode stream [parent #f])
(define ctx (if (NumberT? len)
(mhasheq 'parent parent
'_startOffset (· stream pos)
'_currentOffset 0
'_length len)
parent))
(define decoded-len (resolve-length len stream parent))
(cond
[(or (not decoded-len) (eq? length-type 'bytes))
(define end-pos (cond
;; decoded-len is byte length
[decoded-len (+ (· stream pos) decoded-len)]
;; no decoded-len, but parent has length
[(and parent (not (zero? (· parent _length)))) (+ (· parent _startOffset) (· parent _length))]
;; no decoded-len or parent, so consume whole stream
[else (· stream length_)]))
(for/list ([i (in-naturals)]
#:break (= (· stream pos) end-pos))
(send type decode stream ctx))]
;; we have decoded-len, which is treated as count of items
[else (for/list ([i (in-range decoded-len)])
(send type decode stream ctx))]))
(define/override (size [val #f] [ctx #f])
(when val (unless (countable? val)
(raise-argument-error 'Array:size "list or countable" val)))
(cond
[val (let-values ([(ctx len-size) (if (NumberT? len)
(values (mhasheq 'parent ctx) (send len size))
(values ctx 0))])
(+ len-size (for/sum ([item (in-list (countable->list val))])
(send type size item ctx))))]
[else (let ([item-count (resolve-length len #f ctx)]
[item-size (send type size #f ctx)])
(* item-size item-count))]))
(define/augride (encode stream array [parent #f])
(when array (unless (countable? array)
(raise-argument-error 'Array:encode "list or countable" array)))
(define (encode-items ctx)
(for ([item (in-list (countable->list array))])
(send type encode stream item ctx)))
(cond
[(NumberT? len) (define ctx (mhash 'pointers null
'startOffset (· stream pos)
'parent parent))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size array ctx)))
(send len encode stream (length array)) ; encode length at front
(encode-items ctx)
(for ([ptr (in-list (· ctx pointers))]) ; encode pointer data at end
(send (· ptr type) encode stream (· ptr val)))]
[else (encode-items parent)])))
(define-values (Array Array? +Array) (values ArrayT ArrayT? +ArrayT))
(test-module
(define stream (+DecodeStream #"ABCDEFG"))
(define A (+Array uint16be 3))
(check-equal? (send A decode stream) '(16706 17220 17734))
(check-equal? (send A encode #f '(16706 17220 17734)) #"ABCDEF")
(check-equal? (send (+Array uint16be) size '(1 2 3)) 6)
(check-equal? (send (+Array doublebe) size '(1 2 3 4 5)) 40))
(r+p "private/array.rkt")

@ -1,45 +1,3 @@
#lang restructure/racket
(require "stream.rkt")
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Bitfield.coffee
|#
(define-subclass Streamcoder (Bitfield type [flags empty])
(unless (andmap (λ (f) (or (key? f) (not f))) flags)
(raise-argument-error 'Bitfield "list of keys" flags))
(define/augment (decode stream . _)
(define flag-hash (mhasheq))
(for* ([val (in-value (send type decode stream))]
[(flag i) (in-indexed flags)]
#:when flag)
(hash-set! flag-hash flag (bitwise-bit-set? val i)))
flag-hash)
(define/override (size . _) (send type size))
(define/augment (encode stream flag-hash [ctx #f])
(define bitfield-integer (for/sum ([(flag i) (in-indexed flags)]
#:when (and flag (ref flag-hash flag)))
(arithmetic-shift 1 i)))
(send type encode stream bitfield-integer)))
(test-module
(require "number.rkt" "stream.rkt")
(define bfer (+Bitfield uint16be '(bold italic underline #f shadow condensed extended)))
(define bf (send bfer decode (+DecodeStream #"\0\25")))
(check-equal? (length (ref-keys bf)) 6) ; omits #f flag
(check-true (ref bf 'bold))
(check-true (ref bf 'underline))
(check-true (ref bf 'shadow))
(check-false (ref bf 'italic))
(check-false (ref bf 'condensed))
(check-false (ref bf 'extended))
(define os (+EncodeStream))
(send bfer encode os bf)
(check-equal? (send os dump) #"\0\25"))
(r+p "private/bitfield.rkt")

@ -1,53 +1,3 @@
#lang restructure/racket
(require "number.rkt" "utils.rkt")
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Buffer.coffee
|#
#|
A Buffer is a container object for any data object that supports random access
A Node Buffer object is basically a byte string.
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
A Restructure RBuffer object is separate.
|#
(define (+Buffer xs [type #f])
((if (string? xs)
string->bytes/utf-8
list->bytes) xs))
(define-subclass RestructureBase (RBuffer [len #xffff])
(define/override (decode stream [parent #f])
(define decoded-len (resolve-length len stream parent))
(send stream readBuffer decoded-len))
(define/override (size [val #f] [parent #f])
(when val (unless (bytes? val)
(raise-argument-error 'Buffer:size "bytes" val)))
(if val
(bytes-length val)
(resolve-length len val parent)))
(define/override (encode stream buf [parent #f])
(unless (bytes? buf)
(raise-argument-error 'Buffer:encode "bytes" buf))
(when (NumberT? len)
(send len encode stream (length buf)))
(send stream writeBuffer buf)))
(define-subclass RBuffer (BufferT))
#;(test-module
(require "stream.rkt")
(define stream (+DecodeStream #"\2BCDEF"))
(define S (+String uint8 'utf8))
(check-equal? (send S decode stream) "BC")
(define os (+EncodeStream))
(send S encode os "Mike")
(check-equal? (send os dump) #"\4Mike")
(check-equal? (send (+String) size "foobar") 6))
(r+p "private/buffer.rkt")

@ -1,23 +1,3 @@
#lang restructure/racket
(require "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Enum.coffee
|#
(define-subclass Streamcoder (Enum type [options empty])
(define/augment (decode stream . _)
(define index (send type decode stream))
(or (list-ref options index) index))
(define/override (size . _) (send type size))
(define/augment (encode stream val [ctx #f])
(define index (index-of options val))
(unless index
(raise-argument-error 'Enum:encode "valid option" val))
(send type encode stream index)))
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/enum.rkt")

@ -1,74 +1,3 @@
#lang restructure/racket
(require "utils.rkt" "array.rkt" "number.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/LazyArray.coffee
|#
(define-subclass object% (InnerLazyArray type [len #f] [stream #f] [ctx #f])
(unless stream (raise-argument-error 'LazyArray "stream" stream))
(define starting-pos (· stream pos))
(define item-cache (mhasheqv)) ; integer-keyed hash, rather than list
(define/public-final (get index)
(unless (<= 0 index (sub1 len))
#;(raise-argument-error 'LazyArray:get (format "index in range 0 to ~a" len) index)
(void))
(ref! item-cache index (λ ()
(define orig-pos (· stream pos))
(send stream pos (+ starting-pos (* (send type size #f ctx) index)))
(define new-item (send type decode stream ctx))
(send stream pos orig-pos)
new-item)))
(define/public-final (to-list)
(for/list ([i (in-range len)])
(get i))))
(define-subclass ArrayT (LazyArray)
(inherit-field len type)
(define/override (decode stream [parent #f])
(define pos (· stream pos)) ; ! placement matters. `resolve-length` will change `pos`
(define decoded-len (resolve-length len stream parent))
(let ([parent (if (NumberT? len)
(mhasheq 'parent parent
'_startOffset pos
'_currentOffset 0
'_length len)
parent)])
(define res (+InnerLazyArray type decoded-len stream parent))
(send stream pos (+ (· stream pos) (* decoded-len (send type size #f parent))))
res))
(define/override (size [val #f] [ctx #f])
(super size (if (InnerLazyArray? val)
(send val to-list)
val) ctx))
(define/override (encode stream val [ctx #f])
(super encode stream (if (InnerLazyArray? val)
(send val to-list)
val) ctx)))
(test-module
(require "stream.rkt")
(define bstr #"ABCD1234")
(define ds (+DecodeStream bstr))
(define la (+LazyArray uint8 4))
(define ila (send la decode ds))
(check-equal? (send ds pos) 4)
(check-equal? (send ila get 1) 66)
(check-equal? (send ila get 3) 68)
(check-equal? (send ds pos) 4)
(check-equal? (send ila to-list) '(65 66 67 68))
(define la2 (+LazyArray int16be (λ (t) 4)))
(define es (+EncodeStream))
(send la2 encode es '(1 2 3 4))
(check-equal? (send es dump) #"\0\1\0\2\0\3\0\4")
(check-equal? (send (send la2 decode (+DecodeStream #"\0\1\0\2\0\3\0\4")) to-list) '(1 2 3 4)))
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/lazy-array.rkt")

@ -1,7 +1,6 @@
#lang restructure/racket
#lang reader (submod "private/racket.rkt" reader)
(r+p "base.rkt"
"array.rkt"
(r+p "array.rkt"
"bitfield.rkt"
"buffer.rkt"
"enum.rkt"

@ -1,188 +1,3 @@
#lang restructure/racket
(require "stream.rkt" "sizes.rkt" (for-syntax "sizes.rkt" racket/match))
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Number.coffee
|#
(define (ends-with-8? type)
(define str (symbol->string type))
(equal? (substring str (sub1 (string-length str))) "8"))
(define (signed-type? type)
(not (equal? "u" (substring (symbol->string type) 0 1))))
(test-module
(check-false (signed-type? 'uint16))
(check-true (signed-type? 'int16)))
(define (exact-if-possible x) (if (integer? x) (inexact->exact x) x))
(define system-endian (if (system-big-endian?) 'be 'le))
(define-subclass Streamcoder (Integer [type 'uint16] [endian system-endian])
(getter-field [number-type (string->symbol (format "~a~a" type (if (ends-with-8? type) "" endian)))])
(define _signed? (signed-type? type))
;; `get-type-size` will raise error if number-type is invalid: use this as check of input
;; size of a number doesn't change, so we can stash it as `_size`
(define _size (with-handlers ([exn:fail:contract?
(λ (exn)
(raise-argument-error 'Integer "valid type and endian" (format "~v ~v" type endian)))])
(get-type-size number-type)))
(define bits (* _size 8))
(define/override (size . args) _size)
(define-values (bound-min bound-max)
;; if a signed integer has n bits, it can contain a number between - (expt 2 (sub1 n)) and (sub1 (expt 2 (sub1 n)).
(let* ([signed-max (sub1 (arithmetic-shift 1 (sub1 bits)))]
[signed-min (sub1 (- signed-max))]
[delta (if _signed? 0 signed-min)])
(values (- signed-min delta) (- signed-max delta))))
(define/augment (decode stream . args)
(define bstr (send stream readBuffer _size))
(define bs ((if (eq? endian system-endian) identity reverse) (bytes->list bstr)))
(define unsigned-int (for/sum ([(b i) (in-indexed bs)])
(arithmetic-shift b (* 8 i))))
(post-decode unsigned-int))
(define/public (post-decode unsigned-int)
(if _signed? (unsigned->signed unsigned-int bits) unsigned-int))
(define/public (pre-encode val-in)
(exact-if-possible val-in))
(define/augment (encode stream val-in [parent #f])
(define val (pre-encode val-in))
(unless (<= bound-min val bound-max)
(raise-argument-error 'Integer:encode (format "value within range of ~a ~a-byte int (~a to ~a)" (if _signed? "signed" "unsigned") _size bound-min bound-max) val))
(define-values (bs _) (for/fold ([bs empty] [n val])
([i (in-range _size)])
(values (cons (bitwise-and n #xff) bs) (arithmetic-shift n -8))))
(define bstr (apply bytes ((if (eq? endian 'be) identity reverse) bs)))
(send stream write bstr)))
(define-values (NumberT NumberT? +NumberT) (values Integer Integer? +Integer))
(define-values (Number Number? +Number) (values Integer Integer? +Integer))
(define-subclass Streamcoder (Float _size [endian system-endian])
(define byte-size (/ _size 8))
(define/augment (decode stream . args) ; convert int to float
(define bs (send stream readBuffer byte-size))
(floating-point-bytes->real bs (eq? endian 'be)))
(define/augment (encode stream val-in [parent #f]) ; convert float to int
(define bs (real->floating-point-bytes val-in byte-size (eq? endian 'be)))
(send stream write bs))
(define/override (size . args) byte-size))
(define-instance float (make-object Float 32))
(define-instance floatbe (make-object Float 32 'be))
(define-instance floatle (make-object Float 32 'le))
(define-instance double (make-object Float 64))
(define-instance doublebe (make-object Float 64 'be))
(define-instance doublele (make-object Float 64 'le))
(define-subclass* Integer (Fixed size [fixed-endian system-endian] [fracBits (floor (/ size 2))])
(super-make-object (string->symbol (format "int~a" size)) fixed-endian)
(define _point (arithmetic-shift 1 fracBits))
(define/override (post-decode int)
(exact-if-possible (/ int _point 1.0)))
(define/override (pre-encode fixed)
(exact-if-possible (floor (* fixed _point)))))
(define-instance fixed16 (make-object Fixed 16))
(define-instance fixed16be (make-object Fixed 16 'be))
(define-instance fixed16le (make-object Fixed 16 'le))
(define-instance fixed32 (make-object Fixed 32))
(define-instance fixed32be (make-object Fixed 32 'be))
(define-instance fixed32le (make-object Fixed 32 'le))
(test-module
(check-exn exn:fail:contract? (λ () (+Integer 'not-a-valid-type)))
(check-exn exn:fail:contract? (λ () (send uint8 encode (+EncodeStream) 256)))
(check-not-exn (λ () (send uint8 encode (+EncodeStream) 255)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) 256)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) 255)))
(check-not-exn (λ () (send int8 encode (+EncodeStream) 127)))
(check-not-exn (λ () (send int8 encode (+EncodeStream) -128)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) -129)))
(check-exn exn:fail:contract? (λ () (send uint16 encode (+EncodeStream) (add1 #xffff))))
(check-not-exn (λ () (send uint16 encode (+EncodeStream) #xffff)))
(let ([o (+Integer 'uint16 'le)]
[ip (+DecodeStream (bytes 1 2 3 4))]
[op (open-output-bytes)])
(check-equal? (send o decode ip) 513) ;; 1000 0000 0100 0000
(check-equal? (send o decode ip) 1027) ;; 1100 0000 0010 0000
(send o encode op 513)
(check-equal? (get-output-bytes op) (bytes 1 2))
(send o encode op 1027)
(check-equal? (get-output-bytes op) (bytes 1 2 3 4)))
(let ([o (+Integer 'uint16 'be)]
[ip (+DecodeStream (bytes 1 2 3 4))]
[op (open-output-bytes)])
(check-equal? (send o decode ip) 258) ;; 0100 0000 1000 0000
(check-equal? (send o decode ip) 772) ;; 0010 0000 1100 0000
(send o encode op 258)
(check-equal? (get-output-bytes op) (bytes 1 2))
(send o encode op 772)
(check-equal? (get-output-bytes op) (bytes 1 2 3 4))))
(test-module
(check-equal? (send (+Integer 'uint8) size) 1)
(check-equal? (send (+Integer) size) 2)
(check-equal? (send (+Integer 'uint32) size) 4)
(check-equal? (send (+Integer 'double) size) 8)
(check-equal? (send (+Number 'uint8) size) 1)
(check-equal? (send (+Number) size) 2)
(check-equal? (send (+Number 'uint32) size) 4)
(check-equal? (send (+Number 'double) size) 8))
;; use keys of type-sizes hash to generate corresponding number definitions
(define-macro (make-int-types)
(with-pattern ([((ID BASE ENDIAN) ...) (for*/list ([k (in-hash-keys type-sizes)]
[kstr (in-value (format "~a" k))]
#:unless (regexp-match #rx"^(float|double)" kstr))
(match-define (list* prefix suffix _)
(regexp-split #rx"(?=[bl]e|$)" kstr))
(map string->symbol
(list (string-downcase kstr)
prefix
(if (positive? (string-length suffix))
suffix
(if (system-big-endian?) "be" "le")))))]
[(ID ...) (suffix-id #'(ID ...) #:context caller-stx)])
#'(begin (define-instance ID (make-object Integer 'BASE 'ENDIAN)) ...)))
(make-int-types)
(test-module
(check-equal? (send uint8 size) 1)
(check-equal? (send uint16 size) 2)
(check-equal? (send uint32 size) 4)
(check-equal? (send double size) 8)
(define bs (send fixed16be encode #f 123.45))
(check-equal? bs #"{s")
(check-equal? (ceiling (* (send fixed16be decode bs) 100)) 12345.0)
(check-equal? (send int8 decode (bytes 127)) 127)
(check-equal? (send int8 decode (bytes 255)) -1)
(check-equal? (send int8 encode #f -1) (bytes 255))
(check-equal? (send int8 encode #f 127) (bytes 127)))
(r+p "private/number.rkt")

@ -1,29 +1,3 @@
#lang restructure/racket
(require "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Optional.coffee
|#
(define-subclass Streamcoder (Optional type [condition #t])
(define (resolve-condition parent)
(if (procedure? condition)
(condition parent)
condition))
(define/augment (decode stream parent)
(when (resolve-condition parent)
(send type decode stream parent)))
(define/override (size [val #f] [parent #f])
(if (resolve-condition parent)
(send type size val parent)
0))
(define/augment (encode stream val parent)
(when (resolve-condition parent)
(send type encode stream val parent))))
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/optional.rkt")

@ -1,98 +1,3 @@
#lang restructure/racket
(require racket/undefined)
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Pointer.coffee
|#
(define (resolve-void-pointer type val)
(cond
[type (values type val)]
[(VoidPointer? val) (values (· val type) (· val value))]
[else (raise-argument-error 'Pointer:size "VoidPointer" val)]))
(define (find-top-ctx ctx)
(cond
[(· ctx parent) => find-top-ctx]
[else ctx]))
(define-subclass object% (Pointer offset-type type-in [options (mhasheq)])
(field [type (and (not (eq? type-in 'void)) type-in)])
(define pointer-style (or (· options type) 'local))
(define allow-null (or (· options allowNull) #t))
(define null-value (or (· options nullValue) 0))
(define lazy (· options lazy))
(define relative-getter-or-0 (or (· options relativeTo) (λ (ctx) 0))) ; changed this to a simple lambda
(define/public (decode stream [ctx #f])
(define offset (send offset-type decode stream ctx))
(cond
[(and allow-null (= offset null-value)) #f] ; handle null pointers
[else
(define relative (+ (caseq pointer-style
[(local) (· ctx _startOffset)]
[(immediate) (- (· stream pos) (send offset-type size))]
[(parent) (· ctx parent _startOffset)]
[(global) (or (· (find-top-ctx ctx) _startOffset) 0)]
[else (error 'unknown-pointer-style)])
(relative-getter-or-0 ctx)))
(define ptr (+ offset relative))
(cond
[type (define val (void))
(define (decode-value)
(cond
[(not (void? val)) val]
[else
(define orig-pos (· stream pos))
(send stream pos ptr)
(set! val (send type decode stream ctx))
(send stream pos orig-pos)
val]))
(if lazy
(LazyThunk decode-value)
(decode-value))]
[else ptr])]))
(define/public (size [val #f] [ctx #f])
(let*-values ([(parent) ctx]
[(ctx) (caseq pointer-style
[(local immediate) ctx]
[(parent) (· ctx parent)]
[(global) (find-top-ctx ctx)]
[else (error 'unknown-pointer-style)])]
[(type val) (resolve-void-pointer type val)])
(when (and val ctx)
(ref-set! ctx 'pointerSize (and (· ctx pointerSize)
(+ (· ctx pointerSize) (send type size val parent)))))
(send offset-type size)))
(define/public (encode stream val [ctx #f])
(if (not val)
(send offset-type encode stream null-value)
(let* ([parent ctx]
[ctx (caseq pointer-style
[(local immediate) ctx]
[(parent) (· ctx parent)]
[(global) (find-top-ctx ctx)]
[else (error 'unknown-pointer-style)])]
[relative (+ (caseq pointer-style
[(local parent) (· ctx startOffset)]
[(immediate) (+ (· stream pos) (send offset-type size val parent))]
[(global) 0])
(relative-getter-or-0 (· parent val)))])
(send offset-type encode stream (- (· ctx pointerOffset) relative))
(let-values ([(type val) (resolve-void-pointer type val)])
(ref-set! ctx 'pointers (append (· ctx pointers) (list (mhasheq 'type type
'val val
'parent parent))))
(ref-set! ctx 'pointerOffset (+ (· ctx pointerOffset) (send type size val parent))))))))
;; A pointer whose type is determined at decode time
(define-subclass object% (VoidPointer type value))
(r+p "private/pointer.rkt")

@ -0,0 +1,79 @@
#lang reader (submod "racket.rkt" reader)
(require "number.rkt" "utils.rkt" "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Array.coffee
|#
(define-subclass Streamcoder (ArrayT type [len #f] [length-type 'count])
(define/augride (decode stream [parent #f])
(define ctx (if (NumberT? len)
(mhasheq 'parent parent
'_startOffset (· stream pos)
'_currentOffset 0
'_length len)
parent))
(define decoded-len (resolve-length len stream parent))
(cond
[(or (not decoded-len) (eq? length-type 'bytes))
(define end-pos (cond
;; decoded-len is byte length
[decoded-len (+ (· stream pos) decoded-len)]
;; no decoded-len, but parent has length
[(and parent (not (zero? (· parent _length)))) (+ (· parent _startOffset) (· parent _length))]
;; no decoded-len or parent, so consume whole stream
[else (· stream length_)]))
(for/list ([i (in-naturals)]
#:break (= (· stream pos) end-pos))
(send type decode stream ctx))]
;; we have decoded-len, which is treated as count of items
[else (for/list ([i (in-range decoded-len)])
(send type decode stream ctx))]))
(define/override (size [val #f] [ctx #f])
(when val (unless (countable? val)
(raise-argument-error 'Array:size "list or countable" val)))
(cond
[val (let-values ([(ctx len-size) (if (NumberT? len)
(values (mhasheq 'parent ctx) (send len size))
(values ctx 0))])
(+ len-size (for/sum ([item (in-list (countable->list val))])
(send type size item ctx))))]
[else (let ([item-count (resolve-length len #f ctx)]
[item-size (send type size #f ctx)])
(* item-size item-count))]))
(define/augride (encode stream array [parent #f])
(when array (unless (countable? array)
(raise-argument-error 'Array:encode "list or countable" array)))
(define (encode-items ctx)
(for ([item (in-list (countable->list array))])
(send type encode stream item ctx)))
(cond
[(NumberT? len) (define ctx (mhash 'pointers null
'startOffset (· stream pos)
'parent parent))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size array ctx)))
(send len encode stream (length array)) ; encode length at front
(encode-items ctx)
(for ([ptr (in-list (· ctx pointers))]) ; encode pointer data at end
(send (· ptr type) encode stream (· ptr val)))]
[else (encode-items parent)])))
(define-values (Array Array? +Array) (values ArrayT ArrayT? +ArrayT))
(test-module
(define stream (+DecodeStream #"ABCDEFG"))
(define A (+Array uint16be 3))
(check-equal? (send A decode stream) '(16706 17220 17734))
(check-equal? (send A encode #f '(16706 17220 17734)) #"ABCDEF")
(check-equal? (send (+Array uint16be) size '(1 2 3)) 6)
(check-equal? (send (+Array doublebe) size '(1 2 3 4 5)) 40))

@ -0,0 +1,45 @@
#lang reader (submod "racket.rkt" reader)
(require "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Bitfield.coffee
|#
(define-subclass Streamcoder (Bitfield type [flags empty])
(unless (andmap (λ (f) (or (key? f) (not f))) flags)
(raise-argument-error 'Bitfield "list of keys" flags))
(define/augment (decode stream . _)
(define flag-hash (mhasheq))
(for* ([val (in-value (send type decode stream))]
[(flag i) (in-indexed flags)]
#:when flag)
(hash-set! flag-hash flag (bitwise-bit-set? val i)))
flag-hash)
(define/override (size . _) (send type size))
(define/augment (encode stream flag-hash [ctx #f])
(define bitfield-integer (for/sum ([(flag i) (in-indexed flags)]
#:when (and flag (ref flag-hash flag)))
(arithmetic-shift 1 i)))
(send type encode stream bitfield-integer)))
(test-module
(require "number.rkt" "stream.rkt")
(define bfer (+Bitfield uint16be '(bold italic underline #f shadow condensed extended)))
(define bf (send bfer decode (+DecodeStream #"\0\25")))
(check-equal? (length (ref-keys bf)) 6) ; omits #f flag
(check-true (ref bf 'bold))
(check-true (ref bf 'underline))
(check-true (ref bf 'shadow))
(check-false (ref bf 'italic))
(check-false (ref bf 'condensed))
(check-false (ref bf 'extended))
(define os (+EncodeStream))
(send bfer encode os bf)
(check-equal? (send os dump) #"\0\25"))

@ -0,0 +1,53 @@
#lang reader (submod "racket.rkt" reader)
(require "number.rkt" "utils.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Buffer.coffee
|#
#|
A Buffer is a container object for any data object that supports random access
A Node Buffer object is basically a byte string.
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
A Restructure RBuffer object is separate.
|#
(define (+Buffer xs [type #f])
((if (string? xs)
string->bytes/utf-8
list->bytes) xs))
(define-subclass RestructureBase (RBuffer [len #xffff])
(define/override (decode stream [parent #f])
(define decoded-len (resolve-length len stream parent))
(send stream readBuffer decoded-len))
(define/override (size [val #f] [parent #f])
(when val (unless (bytes? val)
(raise-argument-error 'Buffer:size "bytes" val)))
(if val
(bytes-length val)
(resolve-length len val parent)))
(define/override (encode stream buf [parent #f])
(unless (bytes? buf)
(raise-argument-error 'Buffer:encode "bytes" buf))
(when (NumberT? len)
(send len encode stream (length buf)))
(send stream writeBuffer buf)))
(define-subclass RBuffer (BufferT))
#;(test-module
(require "stream.rkt")
(define stream (+DecodeStream #"\2BCDEF"))
(define S (+String uint8 'utf8))
(check-equal? (send S decode stream) "BC")
(define os (+EncodeStream))
(send S encode os "Mike")
(check-equal? (send os dump) #"\4Mike")
(check-equal? (send (+String) size "foobar") 6))

@ -0,0 +1,23 @@
#lang reader (submod "racket.rkt" reader)
(require "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Enum.coffee
|#
(define-subclass Streamcoder (Enum type [options empty])
(define/augment (decode stream . _)
(define index (send type decode stream))
(or (list-ref options index) index))
(define/override (size . _) (send type size))
(define/augment (encode stream val [ctx #f])
(define index (index-of options val))
(unless index
(raise-argument-error 'Enum:encode "valid option" val))
(send type encode stream index)))

@ -0,0 +1,74 @@
#lang reader (submod "racket.rkt" reader)
(require "utils.rkt" "array.rkt" "number.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/LazyArray.coffee
|#
(define-subclass object% (InnerLazyArray type [len #f] [stream #f] [ctx #f])
(unless stream (raise-argument-error 'LazyArray "stream" stream))
(define starting-pos (· stream pos))
(define item-cache (mhasheqv)) ; integer-keyed hash, rather than list
(define/public-final (get index)
(unless (<= 0 index (sub1 len))
#;(raise-argument-error 'LazyArray:get (format "index in range 0 to ~a" len) index)
(void))
(ref! item-cache index (λ ()
(define orig-pos (· stream pos))
(send stream pos (+ starting-pos (* (send type size #f ctx) index)))
(define new-item (send type decode stream ctx))
(send stream pos orig-pos)
new-item)))
(define/public-final (to-list)
(for/list ([i (in-range len)])
(get i))))
(define-subclass ArrayT (LazyArray)
(inherit-field len type)
(define/override (decode stream [parent #f])
(define pos (· stream pos)) ; ! placement matters. `resolve-length` will change `pos`
(define decoded-len (resolve-length len stream parent))
(let ([parent (if (NumberT? len)
(mhasheq 'parent parent
'_startOffset pos
'_currentOffset 0
'_length len)
parent)])
(define res (+InnerLazyArray type decoded-len stream parent))
(send stream pos (+ (· stream pos) (* decoded-len (send type size #f parent))))
res))
(define/override (size [val #f] [ctx #f])
(super size (if (InnerLazyArray? val)
(send val to-list)
val) ctx))
(define/override (encode stream val [ctx #f])
(super encode stream (if (InnerLazyArray? val)
(send val to-list)
val) ctx)))
(test-module
(require "stream.rkt")
(define bstr #"ABCD1234")
(define ds (+DecodeStream bstr))
(define la (+LazyArray uint8 4))
(define ila (send la decode ds))
(check-equal? (send ds pos) 4)
(check-equal? (send ila get 1) 66)
(check-equal? (send ila get 3) 68)
(check-equal? (send ds pos) 4)
(check-equal? (send ila to-list) '(65 66 67 68))
(define la2 (+LazyArray int16be (λ (t) 4)))
(define es (+EncodeStream))
(send la2 encode es '(1 2 3 4))
(check-equal? (send es dump) #"\0\1\0\2\0\3\0\4")
(check-equal? (send (send la2 decode (+DecodeStream #"\0\1\0\2\0\3\0\4")) to-list) '(1 2 3 4)))

@ -0,0 +1,188 @@
#lang reader (submod "racket.rkt" reader)
(require "stream.rkt" "sizes.rkt" (for-syntax "sizes.rkt" racket/match))
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Number.coffee
|#
(define (ends-with-8? type)
(define str (symbol->string type))
(equal? (substring str (sub1 (string-length str))) "8"))
(define (signed-type? type)
(not (equal? "u" (substring (symbol->string type) 0 1))))
(test-module
(check-false (signed-type? 'uint16))
(check-true (signed-type? 'int16)))
(define (exact-if-possible x) (if (integer? x) (inexact->exact x) x))
(define system-endian (if (system-big-endian?) 'be 'le))
(define-subclass Streamcoder (Integer [type 'uint16] [endian system-endian])
(getter-field [number-type (string->symbol (format "~a~a" type (if (ends-with-8? type) "" endian)))])
(define _signed? (signed-type? type))
;; `get-type-size` will raise error if number-type is invalid: use this as check of input
;; size of a number doesn't change, so we can stash it as `_size`
(define _size (with-handlers ([exn:fail:contract?
(λ (exn)
(raise-argument-error 'Integer "valid type and endian" (format "~v ~v" type endian)))])
(get-type-size number-type)))
(define bits (* _size 8))
(define/override (size . args) _size)
(define-values (bound-min bound-max)
;; if a signed integer has n bits, it can contain a number between - (expt 2 (sub1 n)) and (sub1 (expt 2 (sub1 n)).
(let* ([signed-max (sub1 (arithmetic-shift 1 (sub1 bits)))]
[signed-min (sub1 (- signed-max))]
[delta (if _signed? 0 signed-min)])
(values (- signed-min delta) (- signed-max delta))))
(define/augment (decode stream . args)
(define bstr (send stream readBuffer _size))
(define bs ((if (eq? endian system-endian) identity reverse) (bytes->list bstr)))
(define unsigned-int (for/sum ([(b i) (in-indexed bs)])
(arithmetic-shift b (* 8 i))))
(post-decode unsigned-int))
(define/public (post-decode unsigned-int)
(if _signed? (unsigned->signed unsigned-int bits) unsigned-int))
(define/public (pre-encode val-in)
(exact-if-possible val-in))
(define/augment (encode stream val-in [parent #f])
(define val (pre-encode val-in))
(unless (<= bound-min val bound-max)
(raise-argument-error 'Integer:encode (format "value within range of ~a ~a-byte int (~a to ~a)" (if _signed? "signed" "unsigned") _size bound-min bound-max) val))
(define-values (bs _) (for/fold ([bs empty] [n val])
([i (in-range _size)])
(values (cons (bitwise-and n #xff) bs) (arithmetic-shift n -8))))
(define bstr (apply bytes ((if (eq? endian 'be) identity reverse) bs)))
(send stream write bstr)))
(define-values (NumberT NumberT? +NumberT) (values Integer Integer? +Integer))
(define-values (Number Number? +Number) (values Integer Integer? +Integer))
(define-subclass Streamcoder (Float _size [endian system-endian])
(define byte-size (/ _size 8))
(define/augment (decode stream . args) ; convert int to float
(define bs (send stream readBuffer byte-size))
(floating-point-bytes->real bs (eq? endian 'be)))
(define/augment (encode stream val-in [parent #f]) ; convert float to int
(define bs (real->floating-point-bytes val-in byte-size (eq? endian 'be)))
(send stream write bs))
(define/override (size . args) byte-size))
(define-instance float (make-object Float 32))
(define-instance floatbe (make-object Float 32 'be))
(define-instance floatle (make-object Float 32 'le))
(define-instance double (make-object Float 64))
(define-instance doublebe (make-object Float 64 'be))
(define-instance doublele (make-object Float 64 'le))
(define-subclass* Integer (Fixed size [fixed-endian system-endian] [fracBits (floor (/ size 2))])
(super-make-object (string->symbol (format "int~a" size)) fixed-endian)
(define _point (arithmetic-shift 1 fracBits))
(define/override (post-decode int)
(exact-if-possible (/ int _point 1.0)))
(define/override (pre-encode fixed)
(exact-if-possible (floor (* fixed _point)))))
(define-instance fixed16 (make-object Fixed 16))
(define-instance fixed16be (make-object Fixed 16 'be))
(define-instance fixed16le (make-object Fixed 16 'le))
(define-instance fixed32 (make-object Fixed 32))
(define-instance fixed32be (make-object Fixed 32 'be))
(define-instance fixed32le (make-object Fixed 32 'le))
(test-module
(check-exn exn:fail:contract? (λ () (+Integer 'not-a-valid-type)))
(check-exn exn:fail:contract? (λ () (send uint8 encode (+EncodeStream) 256)))
(check-not-exn (λ () (send uint8 encode (+EncodeStream) 255)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) 256)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) 255)))
(check-not-exn (λ () (send int8 encode (+EncodeStream) 127)))
(check-not-exn (λ () (send int8 encode (+EncodeStream) -128)))
(check-exn exn:fail:contract? (λ () (send int8 encode (+EncodeStream) -129)))
(check-exn exn:fail:contract? (λ () (send uint16 encode (+EncodeStream) (add1 #xffff))))
(check-not-exn (λ () (send uint16 encode (+EncodeStream) #xffff)))
(let ([o (+Integer 'uint16 'le)]
[ip (+DecodeStream (bytes 1 2 3 4))]
[op (open-output-bytes)])
(check-equal? (send o decode ip) 513) ;; 1000 0000 0100 0000
(check-equal? (send o decode ip) 1027) ;; 1100 0000 0010 0000
(send o encode op 513)
(check-equal? (get-output-bytes op) (bytes 1 2))
(send o encode op 1027)
(check-equal? (get-output-bytes op) (bytes 1 2 3 4)))
(let ([o (+Integer 'uint16 'be)]
[ip (+DecodeStream (bytes 1 2 3 4))]
[op (open-output-bytes)])
(check-equal? (send o decode ip) 258) ;; 0100 0000 1000 0000
(check-equal? (send o decode ip) 772) ;; 0010 0000 1100 0000
(send o encode op 258)
(check-equal? (get-output-bytes op) (bytes 1 2))
(send o encode op 772)
(check-equal? (get-output-bytes op) (bytes 1 2 3 4))))
(test-module
(check-equal? (send (+Integer 'uint8) size) 1)
(check-equal? (send (+Integer) size) 2)
(check-equal? (send (+Integer 'uint32) size) 4)
(check-equal? (send (+Integer 'double) size) 8)
(check-equal? (send (+Number 'uint8) size) 1)
(check-equal? (send (+Number) size) 2)
(check-equal? (send (+Number 'uint32) size) 4)
(check-equal? (send (+Number 'double) size) 8))
;; use keys of type-sizes hash to generate corresponding number definitions
(define-macro (make-int-types)
(with-pattern ([((ID BASE ENDIAN) ...) (for*/list ([k (in-hash-keys type-sizes)]
[kstr (in-value (format "~a" k))]
#:unless (regexp-match #rx"^(float|double)" kstr))
(match-define (list* prefix suffix _)
(regexp-split #rx"(?=[bl]e|$)" kstr))
(map string->symbol
(list (string-downcase kstr)
prefix
(if (positive? (string-length suffix))
suffix
(if (system-big-endian?) "be" "le")))))]
[(ID ...) (suffix-id #'(ID ...) #:context caller-stx)])
#'(begin (define-instance ID (make-object Integer 'BASE 'ENDIAN)) ...)))
(make-int-types)
(test-module
(check-equal? (send uint8 size) 1)
(check-equal? (send uint16 size) 2)
(check-equal? (send uint32 size) 4)
(check-equal? (send double size) 8)
(define bs (send fixed16be encode #f 123.45))
(check-equal? bs #"{s")
(check-equal? (ceiling (* (send fixed16be decode bs) 100)) 12345.0)
(check-equal? (send int8 decode (bytes 127)) 127)
(check-equal? (send int8 decode (bytes 255)) -1)
(check-equal? (send int8 encode #f -1) (bytes 255))
(check-equal? (send int8 encode #f 127) (bytes 127)))

@ -0,0 +1,29 @@
#lang reader (submod "racket.rkt" reader)
(require "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Optional.coffee
|#
(define-subclass Streamcoder (Optional type [condition #t])
(define (resolve-condition parent)
(if (procedure? condition)
(condition parent)
condition))
(define/augment (decode stream parent)
(when (resolve-condition parent)
(send type decode stream parent)))
(define/override (size [val #f] [parent #f])
(if (resolve-condition parent)
(send type size val parent)
0))
(define/augment (encode stream val parent)
(when (resolve-condition parent)
(send type encode stream val parent))))

@ -0,0 +1,98 @@
#lang reader (submod "racket.rkt" reader)
(require racket/undefined)
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Pointer.coffee
|#
(define (resolve-void-pointer type val)
(cond
[type (values type val)]
[(VoidPointer? val) (values (· val type) (· val value))]
[else (raise-argument-error 'Pointer:size "VoidPointer" val)]))
(define (find-top-ctx ctx)
(cond
[(· ctx parent) => find-top-ctx]
[else ctx]))
(define-subclass object% (Pointer offset-type type-in [options (mhasheq)])
(field [type (and (not (eq? type-in 'void)) type-in)])
(define pointer-style (or (· options type) 'local))
(define allow-null (or (· options allowNull) #t))
(define null-value (or (· options nullValue) 0))
(define lazy (· options lazy))
(define relative-getter-or-0 (or (· options relativeTo) (λ (ctx) 0))) ; changed this to a simple lambda
(define/public (decode stream [ctx #f])
(define offset (send offset-type decode stream ctx))
(cond
[(and allow-null (= offset null-value)) #f] ; handle null pointers
[else
(define relative (+ (caseq pointer-style
[(local) (· ctx _startOffset)]
[(immediate) (- (· stream pos) (send offset-type size))]
[(parent) (· ctx parent _startOffset)]
[(global) (or (· (find-top-ctx ctx) _startOffset) 0)]
[else (error 'unknown-pointer-style)])
(relative-getter-or-0 ctx)))
(define ptr (+ offset relative))
(cond
[type (define val (void))
(define (decode-value)
(cond
[(not (void? val)) val]
[else
(define orig-pos (· stream pos))
(send stream pos ptr)
(set! val (send type decode stream ctx))
(send stream pos orig-pos)
val]))
(if lazy
(LazyThunk decode-value)
(decode-value))]
[else ptr])]))
(define/public (size [val #f] [ctx #f])
(let*-values ([(parent) ctx]
[(ctx) (caseq pointer-style
[(local immediate) ctx]
[(parent) (· ctx parent)]
[(global) (find-top-ctx ctx)]
[else (error 'unknown-pointer-style)])]
[(type val) (resolve-void-pointer type val)])
(when (and val ctx)
(ref-set! ctx 'pointerSize (and (· ctx pointerSize)
(+ (· ctx pointerSize) (send type size val parent)))))
(send offset-type size)))
(define/public (encode stream val [ctx #f])
(if (not val)
(send offset-type encode stream null-value)
(let* ([parent ctx]
[ctx (caseq pointer-style
[(local immediate) ctx]
[(parent) (· ctx parent)]
[(global) (find-top-ctx ctx)]
[else (error 'unknown-pointer-style)])]
[relative (+ (caseq pointer-style
[(local parent) (· ctx startOffset)]
[(immediate) (+ (· stream pos) (send offset-type size val parent))]
[(global) 0])
(relative-getter-or-0 (· parent val)))])
(send offset-type encode stream (- (· ctx pointerOffset) relative))
(let-values ([(type val) (resolve-void-pointer type val)])
(ref-set! ctx 'pointers (append (· ctx pointers) (list (mhasheq 'type type
'val val
'parent parent))))
(ref-set! ctx 'pointerOffset (+ (· ctx pointerOffset) (send type size val parent))))))))
;; A pointer whose type is determined at decode time
(define-subclass object% (VoidPointer type value))

@ -24,7 +24,4 @@
(module reader syntax/module-reader
#:language 'restructure/racket
#:read @-read
#:read-syntax @-read-syntax
(require (prefix-in @- scribble/reader)))
#:language 'restructure/private/racket)

@ -0,0 +1,21 @@
#lang reader (submod "racket.rkt" reader)
(require "stream.rkt" "utils.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Reserved.coffee
|#
(define-subclass Streamcoder (Reserved type [count 1])
(define/augment (decode stream parent)
(send stream pos (+ (· stream pos) (size #f parent)))
(void))
(define/override (size [val #f] [parent #f])
(* (send type size) (resolve-length count #f parent)))
(define/augment (encode stream val [parent #f])
(send stream fill 0 (size val parent))))

@ -1,4 +1,4 @@
#lang restructure/racket
#lang reader (submod "racket.rkt" reader)
(provide type-sizes get-type-size)
(define-values (int-keys byte-values) (for*/lists (int-keys byte-values)

@ -0,0 +1,215 @@
#lang reader (submod "racket.rkt" reader)
(require racket/private/generic-methods)
(provide (all-defined-out))
;; helper class
(define-subclass object% (PortWrapper _port)
(unless (port? _port)
(raise-argument-error 'PortWrapper:constructor "port" _port))
(define/public (pos [where #f])
(when where (file-position _port where))
(file-position _port))
(define/public (dump) (void)))
(test-module
(check-not-exn (λ () (make-object PortWrapper (open-input-bytes #"Foo"))))
(check-not-exn (λ () (make-object PortWrapper (open-output-bytes))))
(check-exn exn:fail? (λ () (make-object PortWrapper -42))))
#| approximates
https://github.com/mbutterick/restructure/blob/master/src/EncodeStream.coffee
|#
;; basically just a wrapper for a Racket output port
(define-subclass* PortWrapper (EncodeStream [maybe-output-port (open-output-bytes)])
(unless (output-port? maybe-output-port)
(raise-argument-error 'EncodeStream:constructor "output port" maybe-output-port))
(super-make-object maybe-output-port)
(inherit-field _port)
(define/override-final (dump) (get-output-bytes _port))
(define/public-final (write val)
(unless (bytes? val)
(raise-argument-error 'EncodeStream:write "bytes" val))
(write-bytes val _port)
(void))
(define/public-final (writeBuffer buffer)
(write buffer))
(define/public-final (writeUInt8 int)
(write (bytes int)))
(define/public (writeString string [encoding 'ascii])
;; todo: handle encodings correctly.
;; right now just utf8 and ascii are correct
(caseq encoding
[(utf16le ucs2 utf8 ascii) (writeBuffer (string->bytes/utf-8 string))
(when (eq? encoding 'utf16le)
(error 'swap-bytes-unimplemented))]
[else (error 'unsupported-string-encoding)]))
(define/public (fill val len)
(write (make-bytes len val))))
(test-module
(define es (+EncodeStream))
(check-true (EncodeStream? es))
(send es write #"AB")
(check-equal? (· es pos) 2)
(send es write #"C")
(check-equal? (· es pos) 3)
(send es write #"D")
(check-equal? (· es pos) 4)
(check-exn exn:fail? (λ () (send es write -42)))
(check-exn exn:fail? (λ () (send es write 1)))
(define op (open-output-bytes))
(define es2 (+EncodeStream op))
(send es2 write #"FOOBAR")
(check-equal? (send es2 dump) #"FOOBAR")
(check-equal? (send es2 dump) #"FOOBAR") ; dump can repeat
(check-equal? (get-output-bytes op) #"FOOBAR")
(define es3 (+EncodeStream))
(send es3 fill 0 10)
(check-equal? (send es3 dump) (make-bytes 10 0)))
#| approximates
https://github.com/mbutterick/restructure/blob/master/src/DecodeStream.coffee
|#
;; basically just a wrapper for a Racket port
;; but needs to start with a buffer so length can be found
(require "sizes.rkt")
(define-macro (define-reader ID)
#'(define/public (ID)
(define bs (*ref type-sizes (string->symbol (string-downcase (string-replace (symbol->string 'ID) "read" "")))))
(readBuffer bs)))
(define countable<%>
(interface* ()
([(generic-property gen:countable)
(generic-method-table gen:countable
(define (length o) (get-field length_ o)))])))
(define DecodeStreamT
(class* PortWrapper
(countable<%>)
(init-field [buffer #""])
(unless (bytes? buffer) ; corresponds to a Node Buffer, not a restructure BufferT object
(raise-argument-error 'DecodeStream:constructor "bytes" buffer))
(super-make-object (open-input-bytes buffer))
(inherit-field _port)
(field [_pos 0]
[length_ (length buffer)])
(define/override (pos [where #f])
(when where
(set! _pos (super pos where)))
_pos)
(define/public (count-nonzero-chars)
;; helper function for String
;; counts nonzero chars from current position
(length (car (regexp-match-peek "[^\u0]*" _port))))
(public [-length length])
(define (-length) length_)
(define/public (readString length__ [encoding 'ascii])
(define proc (caseq encoding
[(utf16le) (error 'bah)]
[(ucs2) (error 'bleh)]
[(utf8) bytes->string/utf-8]
[(ascii) bytes->string/latin-1]
[else identity]))
(define start (pos))
(define stop (+ start length__))
(proc (subbytes buffer start (pos stop))))
(define/public-final (readBuffer count)
(unless (index? count)
(raise-argument-error 'DecodeStream:read "positive integer" count))
(define bytes-remaining (- length_ (pos)))
(when (> count bytes-remaining)
(raise-argument-error 'DecodeStream:read (format "byte count not more than bytes remaining = ~a" bytes-remaining) count))
(increment-field! _pos this count) ; don't use `pos` method here because `read-bytes` will increment the port position
(define bs (read-bytes count _port))
(unless (= _pos (file-position _port)) (raise-result-error 'DecodeStream "positions askew" (list _pos (file-position _port))))
bs)
(define/public (read count) (readBuffer count))
(define/public (readUInt8) (bytes-ref (readBuffer 1) 0))
(define/public (readUInt16BE) (+ (arithmetic-shift (readUInt8) 8) (readUInt8)))
(define/public (readInt16BE) (unsigned->signed (readUInt16BE) 16))
(define/public (readUInt16LE) (+ (readUInt8) (arithmetic-shift (readUInt8) 8)))
(define/public (readUInt24BE) (+ (arithmetic-shift (readUInt16BE) 8) (readUInt8)))
(define/public (readUInt24LE) (+ (readUInt16LE) (arithmetic-shift (readUInt8) 16)))
(define/public (readInt24BE) (unsigned->signed (readUInt24BE) 24))
(define/public (readInt24LE) (unsigned->signed (readUInt24LE) 24))
(define/override-final (dump)
(define current-position (port-position _port))
(set-port-position! _port 0)
(define bs (port->bytes _port))
(set-port-position! _port current-position)
bs)))
(define-subclass DecodeStreamT (DecodeStream))
(test-module
(define ds (+DecodeStream #"ABCD"))
(check-true (DecodeStream? ds))
(check-equal? (length ds) 4)
(check-equal? (send ds dump) #"ABCD")
(check-equal? (send ds dump) #"ABCD") ; dump can repeat
(check-equal? (send ds readUInt16BE) 16706)
(check-equal? (send ds dump) #"ABCD")
(check-equal? (· ds pos) 2)
(check-equal? (send ds readUInt8) 67)
(check-equal? (· ds pos) 3)
(check-equal? (send ds readUInt8) 68)
(check-equal? (· ds pos) 4)
(check-exn exn:fail? (λ () (send ds read -42)))
(check-exn exn:fail? (λ () (send ds read 1))))
;; Streamcoder is a helper class that checks / converts stream arguments before decode / encode
;; not a subclass of DecodeStream or EncodeStream, however.
(define-subclass RestructureBase (Streamcoder)
(define/overment (decode x [parent #f])
(when parent (unless (indexable? parent)
(raise-argument-error 'Streamcoder:decode "hash or indexable" x)))
(define stream (if (bytes? x) (+DecodeStream x) x))
(unless (DecodeStream? stream)
(raise-argument-error 'Streamcoder:decode "bytes or DecodeStream" x))
(inner (void) decode stream parent))
(define/overment (encode x [val #f] [parent #f])
(define stream (cond
[(output-port? x) (+EncodeStream x)]
[(not x) (+EncodeStream)]
[else x]))
(unless (EncodeStream? stream)
(raise-argument-error 'Streamcoder:encode "output port or EncodeStream" x))
(inner (void) encode stream val parent)
(when (not x) (send stream dump))))
(test-module
(define-subclass Streamcoder (Dummy)
(define/augment (decode stream . args) "foo")
(define/augment (encode stream val parent) "bar")
(define/override (size) 42))
(define d (+Dummy))
(check-true (Dummy? d))
(check-exn exn:fail:contract? (λ () (send d decode 42)))
(check-not-exn (λ () (send d decode #"foo")))
(check-exn exn:fail:contract? (λ () (send d encode 42 21)))
(check-not-exn (λ () (send d encode (open-output-bytes) 42))))

@ -0,0 +1,61 @@
#lang reader (submod "racket.rkt" reader)
(require "number.rkt" "utils.rkt" "stream.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/String.coffee
|#
(define (byte-length val encoding)
(define encoder
(caseq encoding
[(ascii utf8) string->bytes/utf-8]))
(bytes-length (encoder (format "~a" val))))
(define-subclass Streamcoder (StringT [len #f] [encoding 'ascii])
(define/augment (decode stream [parent #f])
(let ([len (or (resolve-length len stream parent) (send stream count-nonzero-chars))]
[encoding (if (procedure? encoding)
(or (encoding parent) 'ascii)
encoding)]
[adjustment (if (and (not len) (< (· stream pos) (· stream length))) 1 0)])
(define string (send stream readString len encoding))
(send stream pos (+ (· stream pos) adjustment))
string))
(define/augment (encode stream val [parent #f])
(let* ([val (format "~a" val)]
[encoding (if (procedure? encoding)
(or (encoding (and parent (· parent val)) 'ascii))
encoding)])
(when (NumberT? len)
(send len encode stream (byte-length val encoding)))
(send stream writeString val encoding)
(when (not len) (send stream writeUInt8 #x00)))) ; null terminated when no len
(define/override (size [val #f] [parent #f])
(if (not val)
(resolve-length len #f parent)
(let* ([encoding (if (procedure? encoding)
(or (encoding (and parent (· parent val)) 'ascii))
encoding)]
[encoding (if (eq? encoding 'utf16be) 'utf16le encoding)])
(+ (byte-length val encoding) (cond
[(not len) 1]
[(NumberT? len) (send len size)]
[else 0]))))))
(define-values (String? +String) (values StringT? +StringT))
(test-module
(require "stream.rkt")
(define stream (+DecodeStream #"\2BCDEF"))
(define S (+String uint8 'utf8))
(check-equal? (send S decode stream) "BC")
(check-equal? (send S encode #f "Mike") #"\4Mike")
(check-equal? (send (+String) size "foobar") 7)) ; null terminated when no len

@ -0,0 +1,139 @@
#lang reader (submod "racket.rkt" reader)
(require racket/dict "stream.rkt" racket/private/generic-methods racket/struct)
(provide (all-defined-out) ref* ref*-set! (all-from-out racket/dict))
(require (prefix-in d: racket/dict))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Struct.coffee
|#
(define private-keys '(parent _startOffset _currentOffset _length))
(define (choose-dict d k)
(if (memq k private-keys)
(get-field pvt d)
(get-field kv d)))
(define dictable<%>
(interface* ()
([(generic-property gen:dict)
(generic-method-table gen:dict
(define (dict-set! d k v) (d:dict-set! (choose-dict d k) k v))
(define (dict-ref d k [thunk #f])
(define res (d:dict-ref (choose-dict d k) k thunk))
(if (LazyThunk? res) ((LazyThunk-proc res)) res))
(define (dict-remove! d k) (d:dict-remove! (choose-dict d k) k))
;; public keys only
(define (dict-keys d) (d:dict-keys (get-field kv d))))]
[(generic-property gen:custom-write)
(generic-method-table gen:custom-write
(define (write-proc o port mode)
(define proc (case mode
[(#t) write]
[(#f) display]
[else (λ (p port) (print p port mode))]))
(proc (get-field kv o) port)))])))
(define StructDictRes (class* RestructureBase (dictable<%>)
(super-make-object)
(field [kv (mhasheq)]
[pvt (mhasheq)])
(public [_kv kv])
(define (_kv) kv)))
(define-subclass Streamcoder (Struct [fields (dictify)])
(field [[_process process] (λ (res stream ctx) res)]
[[_preEncode preEncode] void]) ; store as field so it can be mutated from outside
(define/overment (process res stream [ctx #f])
(let* ([res (_process res stream ctx)]
[res (inner res process res stream ctx)])
(unless (dict? res) (raise-result-error 'Struct:process "dict" res))
res))
(define/override (preEncode . args) (apply _preEncode args))
(unless ((disjoin assocs? Struct?) fields) ; should be Versioned Struct but whatever
(raise-argument-error 'Struct "assocs or Versioned Struct" fields))
(define/augride (decode stream [parent #f] [len 0])
;; _setup and _parse-fields are separate to cooperate with VersionedStruct
(let* ([res (_setup stream parent len)]
[res (_parse-fields stream res fields)]
[res (process res stream)])
res))
(define/public-final (_setup stream parent len)
(define res (make-object StructDictRes)) ; not mere hash
(dict-set*! res 'parent parent
'_startOffset (· stream pos)
'_currentOffset 0
'_length len)
res)
(define/public-final (_parse-fields stream res fields)
(unless (assocs? fields)
(raise-argument-error '_parse-fields "assocs" fields))
(for/fold ([res res])
([(key type) (in-dict fields)])
(define val (if (procedure? type)
(type res)
(send type decode stream res)))
(unless (void? val)
(ref-set! res key val))
(ref-set! res '_currentOffset (- (· stream pos) (· res _startOffset)))
res))
(define/override (size [val (mhash)] [parent #f] [include-pointers #t])
(define ctx (mhasheq 'parent parent
'val val
'pointerSize 0))
(+ (for/sum ([(key type) (in-dict fields)]
#:when val)
(send type size (ref val key) ctx))
(if include-pointers (· ctx pointerSize) 0)))
(define/augride (encode stream val [parent #f])
(unless (dict? val)
(raise-argument-error 'Struct:encode "dict" val))
(send this preEncode val stream) ; preEncode goes first, because it might bring input dict into compliance
(define ctx (mhash 'pointers empty
'startOffset (· stream pos)
'parent parent
'val val
'pointerSize 0))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size val ctx #f)))
(unless (andmap (λ (key) (memq key (dict-keys val))) (dict-keys fields))
(raise-argument-error 'Struct:encode
(format "dict that contains superset of Struct keys: ~a" (dict-keys fields)) (dict-keys val)))
(for ([(key type) (in-dict fields)])
(send type encode stream (ref val key) ctx))
(for ([ptr (in-list (· ctx pointers))])
(send (· ptr type) encode stream (· ptr val) (· ptr parent)))))
(test-module
(require "number.rkt")
(define (random-pick xs) (list-ref xs (random (length xs))))
(check-exn exn:fail:contract? (λ () (+Struct 42)))
;; make random structs and make sure we can round trip
(for ([i (in-range 20)])
(define field-types (for/list ([i (in-range 40)])
(random-pick (list uint8 uint16be uint16le uint32be uint32le double))))
(define size-num-types (for/sum ([num-type (in-list field-types)])
(send num-type size)))
(define s (+Struct (for/list ([num-type (in-list field-types)])
(cons (gensym) num-type))))
(define bs (apply bytes (for/list ([i (in-range size-num-types)])
(random 256))))
(check-equal? (send s encode #f (send s decode bs)) bs)))

@ -1,4 +1,4 @@
#lang restructure/racket
#lang reader (submod "racket.rkt" reader)
(provide (all-defined-out) (rename-out [resolveLength resolve-length]))
(require "number.rkt")

@ -0,0 +1,155 @@
#lang reader (submod "racket.rkt" reader)
(require racket/dict "struct.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbuttrackerick/restructure/blob/master/src/VersionedStruct.coffee
|#
(define-subclass Struct (VersionedStruct type [versions (dictify)])
(unless ((disjoin integer? procedure? RestructureBase? symbol?) type)
(raise-argument-error 'VersionedStruct "integer, function, symbol, or Restructure object" type))
(unless (and (dict? versions) (andmap (λ (val) (or (dict? val) (Struct? val))) (map cdr versions)))
(raise-argument-error 'VersionedStruct "dict of dicts or Structs" versions))
(inherit _setup _parse-fields process)
(inherit-field fields)
(field [forced-version #f]
[versionGetter void]
[versionSetter void])
(when (or (key? type) (procedure? type))
(set-field! versionGetter this (if (procedure? type)
type
(λ (parent) (ref parent type))))
(set-field! versionSetter this (if (procedure? type)
type
(λ (parent version) (ref-set! parent type version)))))
(define/override (decode stream [parent #f] [length 0])
(define res (_setup stream parent length))
(ref-set! res 'version
(cond
[forced-version] ; for testing purposes: pass an explicit version
[(or (key? type) (procedure? type))
(unless parent
(raise-argument-error 'VersionedStruct:decode "valid parent" parent))
(versionGetter parent)]
[else (send type decode stream)]))
(when (ref versions 'header)
(_parse-fields stream res (ref versions 'header)))
(define fields (or (ref versions (ref res 'version)) (raise-argument-error 'VersionedStruct:decode "valid version key" (cons version (· this versions)))))
(cond
[(VersionedStruct? fields) (send fields decode stream parent)]
[else
(_parse-fields stream res fields)
(process res stream)
res]))
(define/public-final (force-version! version)
(set! forced-version version))
(define/override (encode stream val [parent #f])
(unless (hash? val)
(raise-argument-error 'Struct:encode "hash" val))
(send this preEncode val stream) ; preEncode goes first, because it might bring input hash into compliance
(define ctx (mhash 'pointers empty
'startOffset (· stream pos)
'parent parent
'val val
'pointerSize 0))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size val ctx #f)))
(when (not (or (key? type) (procedure? type)))
(send type encode stream (or forced-version (· val version))))
(when (ref versions 'header)
(for ([(key type) (in-dict (ref versions 'header))])
(send type encode stream (ref val key) ctx)))
(define fields (or (ref versions (or forced-version (· val version))) (raise-argument-error 'VersionedStruct:encode "valid version key" version)))
(unless (andmap (λ (key) (member key (ref-keys val))) (ref-keys fields))
(raise-argument-error 'VersionedStruct:encode (format "hash that contains superset of Struct keys: ~a" (dict-keys fields)) (hash-keys val)))
(for ([(key type) (in-dict fields)])
(send type encode stream (ref val key) ctx))
(for ([ptr (in-list (ref ctx 'pointers))])
(send (ref ptr 'type) encode stream (ref ptr 'val) (ref ptr 'parent))))
(define/override (size [val (mhash)] [parent #f] [includePointers #t])
(unless (or val forced-version)
(error 'VersionedStruct-cannot-compute-size))
(define ctx (mhash 'parent parent
'val val
'pointerSize 0))
(define size 0)
(when (not (or (key? type) (procedure? type)))
(increment! size (send type size (or forced-version (ref val 'version)) ctx)))
(when (ref versions 'header)
(increment! size
(for/sum ([(key type) (in-dict (ref versions 'header))])
(send type size (ref val key) ctx))))
(define fields (or (ref versions (or forced-version (ref val 'version))) (raise-argument-error 'VersionedStruct:encode "valid version key" version)))
(increment! size
(for/sum ([(key type) (in-dict fields)])
(send type size (ref val key) ctx)))
(when includePointers
(increment! size (ref ctx 'pointerSize)))
size))
#;(test-module
(require "number.rkt")
(define (random-pick xs) (list-ref xs (random (length xs))))
(check-exn exn:fail:contract? (λ () (+VersionedStruct 42 42)))
;; make random versioned structs and make sure we can round trip
#;(for ([i (in-range 1)])
(define field-types (for/list ([i (in-range 1)])
(random-pick (list uint8 uint16be uint16le uint32be uint32le double))))
(define num-versions 20)
(define which-struct (random num-versions))
(define struct-versions (for/list ([v (in-range num-versions)])
(cons v (for/list ([num-type (in-list field-types)])
(cons (gensym) num-type)))))
(define vs (+VersionedStruct which-struct struct-versions))
(define struct-size (for/sum ([num-type (in-list (map cdr (ref struct-versions which-struct)))])
(send num-type size)))
(define bs (apply bytes (for/list ([i (in-range struct-size)])
(random 256))))
(check-equal? (send vs encode #f (send vs decode bs)) bs))
(define s (+Struct (dictify 'a uint8 'b uint8 'c uint8)))
(check-equal? (send s size) 3)
(define vs (+VersionedStruct uint8 (dictify 1 (dictify 'd s) 2 (dictify 'e s 'f s))))
(send vs force-version! 1)
(check-equal? (send vs size) 6)
#|
(define s2 (+Struct (dictify 'a vs)))
(check-equal? (send s2 size) 6)
(define vs2 (+VersionedStruct (λ (p) 2) (dictify 1 vs 2 vs)))
(check-equal? (send vs2 size) 6)
|#
)

@ -1,21 +1,3 @@
#lang restructure/racket
(require "stream.rkt" "utils.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Reserved.coffee
|#
(define-subclass Streamcoder (Reserved type [count 1])
(define/augment (decode stream parent)
(send stream pos (+ (· stream pos) (size #f parent)))
(void))
(define/override (size [val #f] [parent #f])
(* (send type size) (resolve-length count #f parent)))
(define/augment (encode stream val [parent #f])
(send stream fill 0 (size val parent))))
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/reserved.rkt")

@ -1,215 +1,3 @@
#lang restructure/racket
(require racket/private/generic-methods)
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
;; helper class
(define-subclass object% (PortWrapper _port)
(unless (port? _port)
(raise-argument-error 'PortWrapper:constructor "port" _port))
(define/public (pos [where #f])
(when where (file-position _port where))
(file-position _port))
(define/public (dump) (void)))
(test-module
(check-not-exn (λ () (make-object PortWrapper (open-input-bytes #"Foo"))))
(check-not-exn (λ () (make-object PortWrapper (open-output-bytes))))
(check-exn exn:fail? (λ () (make-object PortWrapper -42))))
#| approximates
https://github.com/mbutterick/restructure/blob/master/src/EncodeStream.coffee
|#
;; basically just a wrapper for a Racket output port
(define-subclass* PortWrapper (EncodeStream [maybe-output-port (open-output-bytes)])
(unless (output-port? maybe-output-port)
(raise-argument-error 'EncodeStream:constructor "output port" maybe-output-port))
(super-make-object maybe-output-port)
(inherit-field _port)
(define/override-final (dump) (get-output-bytes _port))
(define/public-final (write val)
(unless (bytes? val)
(raise-argument-error 'EncodeStream:write "bytes" val))
(write-bytes val _port)
(void))
(define/public-final (writeBuffer buffer)
(write buffer))
(define/public-final (writeUInt8 int)
(write (bytes int)))
(define/public (writeString string [encoding 'ascii])
;; todo: handle encodings correctly.
;; right now just utf8 and ascii are correct
(caseq encoding
[(utf16le ucs2 utf8 ascii) (writeBuffer (string->bytes/utf-8 string))
(when (eq? encoding 'utf16le)
(error 'swap-bytes-unimplemented))]
[else (error 'unsupported-string-encoding)]))
(define/public (fill val len)
(write (make-bytes len val))))
(test-module
(define es (+EncodeStream))
(check-true (EncodeStream? es))
(send es write #"AB")
(check-equal? (· es pos) 2)
(send es write #"C")
(check-equal? (· es pos) 3)
(send es write #"D")
(check-equal? (· es pos) 4)
(check-exn exn:fail? (λ () (send es write -42)))
(check-exn exn:fail? (λ () (send es write 1)))
(define op (open-output-bytes))
(define es2 (+EncodeStream op))
(send es2 write #"FOOBAR")
(check-equal? (send es2 dump) #"FOOBAR")
(check-equal? (send es2 dump) #"FOOBAR") ; dump can repeat
(check-equal? (get-output-bytes op) #"FOOBAR")
(define es3 (+EncodeStream))
(send es3 fill 0 10)
(check-equal? (send es3 dump) (make-bytes 10 0)))
#| approximates
https://github.com/mbutterick/restructure/blob/master/src/DecodeStream.coffee
|#
;; basically just a wrapper for a Racket port
;; but needs to start with a buffer so length can be found
(require "sizes.rkt")
(define-macro (define-reader ID)
#'(define/public (ID)
(define bs (*ref type-sizes (string->symbol (string-downcase (string-replace (symbol->string 'ID) "read" "")))))
(readBuffer bs)))
(define countable<%>
(interface* ()
([(generic-property gen:countable)
(generic-method-table gen:countable
(define (length o) (get-field length_ o)))])))
(define DecodeStreamT
(class* PortWrapper
(countable<%>)
(init-field [buffer #""])
(unless (bytes? buffer) ; corresponds to a Node Buffer, not a restructure BufferT object
(raise-argument-error 'DecodeStream:constructor "bytes" buffer))
(super-make-object (open-input-bytes buffer))
(inherit-field _port)
(field [_pos 0]
[length_ (length buffer)])
(define/override (pos [where #f])
(when where
(set! _pos (super pos where)))
_pos)
(define/public (count-nonzero-chars)
;; helper function for String
;; counts nonzero chars from current position
(length (car (regexp-match-peek "[^\u0]*" _port))))
(public [-length length])
(define (-length) length_)
(define/public (readString length__ [encoding 'ascii])
(define proc (caseq encoding
[(utf16le) (error 'bah)]
[(ucs2) (error 'bleh)]
[(utf8) bytes->string/utf-8]
[(ascii) bytes->string/latin-1]
[else identity]))
(define start (pos))
(define stop (+ start length__))
(proc (subbytes buffer start (pos stop))))
(define/public-final (readBuffer count)
(unless (index? count)
(raise-argument-error 'DecodeStream:read "positive integer" count))
(define bytes-remaining (- length_ (pos)))
(when (> count bytes-remaining)
(raise-argument-error 'DecodeStream:read (format "byte count not more than bytes remaining = ~a" bytes-remaining) count))
(increment-field! _pos this count) ; don't use `pos` method here because `read-bytes` will increment the port position
(define bs (read-bytes count _port))
(unless (= _pos (file-position _port)) (raise-result-error 'DecodeStream "positions askew" (list _pos (file-position _port))))
bs)
(define/public (read count) (readBuffer count))
(define/public (readUInt8) (bytes-ref (readBuffer 1) 0))
(define/public (readUInt16BE) (+ (arithmetic-shift (readUInt8) 8) (readUInt8)))
(define/public (readInt16BE) (unsigned->signed (readUInt16BE) 16))
(define/public (readUInt16LE) (+ (readUInt8) (arithmetic-shift (readUInt8) 8)))
(define/public (readUInt24BE) (+ (arithmetic-shift (readUInt16BE) 8) (readUInt8)))
(define/public (readUInt24LE) (+ (readUInt16LE) (arithmetic-shift (readUInt8) 16)))
(define/public (readInt24BE) (unsigned->signed (readUInt24BE) 24))
(define/public (readInt24LE) (unsigned->signed (readUInt24LE) 24))
(define/override-final (dump)
(define current-position (port-position _port))
(set-port-position! _port 0)
(define bs (port->bytes _port))
(set-port-position! _port current-position)
bs)))
(define-subclass DecodeStreamT (DecodeStream))
(test-module
(define ds (+DecodeStream #"ABCD"))
(check-true (DecodeStream? ds))
(check-equal? (length ds) 4)
(check-equal? (send ds dump) #"ABCD")
(check-equal? (send ds dump) #"ABCD") ; dump can repeat
(check-equal? (send ds readUInt16BE) 16706)
(check-equal? (send ds dump) #"ABCD")
(check-equal? (· ds pos) 2)
(check-equal? (send ds readUInt8) 67)
(check-equal? (· ds pos) 3)
(check-equal? (send ds readUInt8) 68)
(check-equal? (· ds pos) 4)
(check-exn exn:fail? (λ () (send ds read -42)))
(check-exn exn:fail? (λ () (send ds read 1))))
;; Streamcoder is a helper class that checks / converts stream arguments before decode / encode
;; not a subclass of DecodeStream or EncodeStream, however.
(define-subclass RestructureBase (Streamcoder)
(define/overment (decode x [parent #f])
(when parent (unless (indexable? parent)
(raise-argument-error 'Streamcoder:decode "hash or indexable" x)))
(define stream (if (bytes? x) (+DecodeStream x) x))
(unless (DecodeStream? stream)
(raise-argument-error 'Streamcoder:decode "bytes or DecodeStream" x))
(inner (void) decode stream parent))
(define/overment (encode x [val #f] [parent #f])
(define stream (cond
[(output-port? x) (+EncodeStream x)]
[(not x) (+EncodeStream)]
[else x]))
(unless (EncodeStream? stream)
(raise-argument-error 'Streamcoder:encode "output port or EncodeStream" x))
(inner (void) encode stream val parent)
(when (not x) (send stream dump))))
(test-module
(define-subclass Streamcoder (Dummy)
(define/augment (decode stream . args) "foo")
(define/augment (encode stream val parent) "bar")
(define/override (size) 42))
(define d (+Dummy))
(check-true (Dummy? d))
(check-exn exn:fail:contract? (λ () (send d decode 42)))
(check-not-exn (λ () (send d decode #"foo")))
(check-exn exn:fail:contract? (λ () (send d encode 42 21)))
(check-not-exn (λ () (send d encode (open-output-bytes) 42))))
(r+p "private/stream.rkt")

@ -1,61 +1,3 @@
#lang restructure/racket
(require "number.rkt" "utils.rkt" "stream.rkt")
(provide (all-defined-out))
#lang reader (submod "private/racket.rkt" reader)
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/String.coffee
|#
(define (byte-length val encoding)
(define encoder
(caseq encoding
[(ascii utf8) string->bytes/utf-8]))
(bytes-length (encoder (format "~a" val))))
(define-subclass Streamcoder (StringT [len #f] [encoding 'ascii])
(define/augment (decode stream [parent #f])
(let ([len (or (resolve-length len stream parent) (send stream count-nonzero-chars))]
[encoding (if (procedure? encoding)
(or (encoding parent) 'ascii)
encoding)]
[adjustment (if (and (not len) (< (· stream pos) (· stream length))) 1 0)])
(define string (send stream readString len encoding))
(send stream pos (+ (· stream pos) adjustment))
string))
(define/augment (encode stream val [parent #f])
(let* ([val (format "~a" val)]
[encoding (if (procedure? encoding)
(or (encoding (and parent (· parent val)) 'ascii))
encoding)])
(when (NumberT? len)
(send len encode stream (byte-length val encoding)))
(send stream writeString val encoding)
(when (not len) (send stream writeUInt8 #x00)))) ; null terminated when no len
(define/override (size [val #f] [parent #f])
(if (not val)
(resolve-length len #f parent)
(let* ([encoding (if (procedure? encoding)
(or (encoding (and parent (· parent val)) 'ascii))
encoding)]
[encoding (if (eq? encoding 'utf16be) 'utf16le encoding)])
(+ (byte-length val encoding) (cond
[(not len) 1]
[(NumberT? len) (send len size)]
[else 0]))))))
(define-values (String? +String) (values StringT? +StringT))
(test-module
(require "stream.rkt")
(define stream (+DecodeStream #"\2BCDEF"))
(define S (+String uint8 'utf8))
(check-equal? (send S decode stream) "BC")
(check-equal? (send S encode #f "Mike") #"\4Mike")
(check-equal? (send (+String) size "foobar") 7)) ; null terminated when no len
(r+p "private/string.rkt")

@ -1,139 +1,3 @@
#lang restructure/racket
(require racket/dict "stream.rkt" racket/private/generic-methods racket/struct)
(provide (all-defined-out) ref* ref*-set! (all-from-out racket/dict))
(require (prefix-in d: racket/dict))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Struct.coffee
|#
(define private-keys '(parent _startOffset _currentOffset _length))
(define (choose-dict d k)
(if (memq k private-keys)
(get-field pvt d)
(get-field kv d)))
(define dictable<%>
(interface* ()
([(generic-property gen:dict)
(generic-method-table gen:dict
(define (dict-set! d k v) (d:dict-set! (choose-dict d k) k v))
(define (dict-ref d k [thunk #f])
(define res (d:dict-ref (choose-dict d k) k thunk))
(if (LazyThunk? res) ((LazyThunk-proc res)) res))
(define (dict-remove! d k) (d:dict-remove! (choose-dict d k) k))
;; public keys only
(define (dict-keys d) (d:dict-keys (get-field kv d))))]
[(generic-property gen:custom-write)
(generic-method-table gen:custom-write
(define (write-proc o port mode)
(define proc (case mode
[(#t) write]
[(#f) display]
[else (λ (p port) (print p port mode))]))
(proc (get-field kv o) port)))])))
(define StructDictRes (class* RestructureBase (dictable<%>)
(super-make-object)
(field [kv (mhasheq)]
[pvt (mhasheq)])
(public [_kv kv])
(define (_kv) kv)))
(define-subclass Streamcoder (Struct [fields (dictify)])
(field [[_process process] (λ (res stream ctx) res)]
[[_preEncode preEncode] void]) ; store as field so it can be mutated from outside
(define/overment (process res stream [ctx #f])
(let* ([res (_process res stream ctx)]
[res (inner res process res stream ctx)])
(unless (dict? res) (raise-result-error 'Struct:process "dict" res))
res))
(define/override (preEncode . args) (apply _preEncode args))
(unless ((disjoin assocs? Struct?) fields) ; should be Versioned Struct but whatever
(raise-argument-error 'Struct "assocs or Versioned Struct" fields))
(define/augride (decode stream [parent #f] [len 0])
;; _setup and _parse-fields are separate to cooperate with VersionedStruct
(let* ([res (_setup stream parent len)]
[res (_parse-fields stream res fields)]
[res (process res stream)])
res))
(define/public-final (_setup stream parent len)
(define res (make-object StructDictRes)) ; not mere hash
(dict-set*! res 'parent parent
'_startOffset (· stream pos)
'_currentOffset 0
'_length len)
res)
(define/public-final (_parse-fields stream res fields)
(unless (assocs? fields)
(raise-argument-error '_parse-fields "assocs" fields))
(for/fold ([res res])
([(key type) (in-dict fields)])
(define val (if (procedure? type)
(type res)
(send type decode stream res)))
(unless (void? val)
(ref-set! res key val))
(ref-set! res '_currentOffset (- (· stream pos) (· res _startOffset)))
res))
(define/override (size [val (mhash)] [parent #f] [include-pointers #t])
(define ctx (mhasheq 'parent parent
'val val
'pointerSize 0))
(+ (for/sum ([(key type) (in-dict fields)]
#:when val)
(send type size (ref val key) ctx))
(if include-pointers (· ctx pointerSize) 0)))
(define/augride (encode stream val [parent #f])
(unless (dict? val)
(raise-argument-error 'Struct:encode "dict" val))
(send this preEncode val stream) ; preEncode goes first, because it might bring input dict into compliance
(define ctx (mhash 'pointers empty
'startOffset (· stream pos)
'parent parent
'val val
'pointerSize 0))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size val ctx #f)))
(unless (andmap (λ (key) (memq key (dict-keys val))) (dict-keys fields))
(raise-argument-error 'Struct:encode
(format "dict that contains superset of Struct keys: ~a" (dict-keys fields)) (dict-keys val)))
(for ([(key type) (in-dict fields)])
(send type encode stream (ref val key) ctx))
(for ([ptr (in-list (· ctx pointers))])
(send (· ptr type) encode stream (· ptr val) (· ptr parent)))))
(test-module
(require "number.rkt")
(define (random-pick xs) (list-ref xs (random (length xs))))
(check-exn exn:fail:contract? (λ () (+Struct 42)))
;; make random structs and make sure we can round trip
(for ([i (in-range 20)])
(define field-types (for/list ([i (in-range 40)])
(random-pick (list uint8 uint16be uint16le uint32be uint32le double))))
(define size-num-types (for/sum ([num-type (in-list field-types)])
(send num-type size)))
(define s (+Struct (for/list ([num-type (in-list field-types)])
(cons (gensym) num-type))))
(define bs (apply bytes (for/list ([i (in-range size-num-types)])
(random 256))))
(check-equal? (send s encode #f (send s decode bs)) bs)))
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/struct.rkt")

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
(require racket/match)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,4 +1,4 @@
#lang restructure/racket
#lang reader (submod "racket.rkt" reader)
(require "array-test.rkt"
"bitfield-test.rkt"

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,6 +1,6 @@
#lang racket/base
(require rackunit restructure restructure/racket)
(provide (all-from-out rackunit restructure restructure/racket))
(require rackunit restructure "../private/racket.rkt")
(provide (all-from-out rackunit restructure "../private/racket.rkt"))
(module reader syntax/module-reader
#:language 'restructure/test/racket)

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
(define Person
(make-object Struct

@ -1,4 +1,4 @@
#lang restructure/test/racket
#lang reader (submod "racket.rkt" reader)
#|
approximates

@ -1,155 +1,3 @@
#lang restructure/racket
(require racket/dict "struct.rkt")
(provide (all-defined-out))
#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/VersionedStruct.coffee
|#
(define-subclass Struct (VersionedStruct type [versions (dictify)])
(unless ((disjoin integer? procedure? RestructureBase? symbol?) type)
(raise-argument-error 'VersionedStruct "integer, function, symbol, or Restructure object" type))
(unless (and (dict? versions) (andmap (λ (val) (or (dict? val) (Struct? val))) (map cdr versions)))
(raise-argument-error 'VersionedStruct "dict of dicts or Structs" versions))
(inherit _setup _parse-fields process)
(inherit-field fields)
(field [forced-version #f]
[versionGetter void]
[versionSetter void])
(when (or (key? type) (procedure? type))
(set-field! versionGetter this (if (procedure? type)
type
(λ (parent) (ref parent type))))
(set-field! versionSetter this (if (procedure? type)
type
(λ (parent version) (ref-set! parent type version)))))
(define/override (decode stream [parent #f] [length 0])
(define res (_setup stream parent length))
(ref-set! res 'version
(cond
[forced-version] ; for testing purposes: pass an explicit version
[(or (key? type) (procedure? type))
(unless parent
(raise-argument-error 'VersionedStruct:decode "valid parent" parent))
(versionGetter parent)]
[else (send type decode stream)]))
(when (ref versions 'header)
(_parse-fields stream res (ref versions 'header)))
(define fields (or (ref versions (ref res 'version)) (raise-argument-error 'VersionedStruct:decode "valid version key" (cons version (· this versions)))))
(cond
[(VersionedStruct? fields) (send fields decode stream parent)]
[else
(_parse-fields stream res fields)
(process res stream)
res]))
(define/public-final (force-version! version)
(set! forced-version version))
(define/override (encode stream val [parent #f])
(unless (hash? val)
(raise-argument-error 'Struct:encode "hash" val))
(send this preEncode val stream) ; preEncode goes first, because it might bring input hash into compliance
(define ctx (mhash 'pointers empty
'startOffset (· stream pos)
'parent parent
'val val
'pointerSize 0))
(ref-set! ctx 'pointerOffset (+ (· stream pos) (size val ctx #f)))
(when (not (or (key? type) (procedure? type)))
(send type encode stream (or forced-version (· val version))))
(when (ref versions 'header)
(for ([(key type) (in-dict (ref versions 'header))])
(send type encode stream (ref val key) ctx)))
(define fields (or (ref versions (or forced-version (· val version))) (raise-argument-error 'VersionedStruct:encode "valid version key" version)))
(unless (andmap (λ (key) (member key (ref-keys val))) (ref-keys fields))
(raise-argument-error 'VersionedStruct:encode (format "hash that contains superset of Struct keys: ~a" (dict-keys fields)) (hash-keys val)))
(for ([(key type) (in-dict fields)])
(send type encode stream (ref val key) ctx))
(for ([ptr (in-list (ref ctx 'pointers))])
(send (ref ptr 'type) encode stream (ref ptr 'val) (ref ptr 'parent))))
(define/override (size [val (mhash)] [parent #f] [includePointers #t])
(unless (or val forced-version)
(error 'VersionedStruct-cannot-compute-size))
(define ctx (mhash 'parent parent
'val val
'pointerSize 0))
(define size 0)
(when (not (or (key? type) (procedure? type)))
(increment! size (send type size (or forced-version (ref val 'version)) ctx)))
(when (ref versions 'header)
(increment! size
(for/sum ([(key type) (in-dict (ref versions 'header))])
(send type size (ref val key) ctx))))
(define fields (or (ref versions (or forced-version (ref val 'version))) (raise-argument-error 'VersionedStruct:encode "valid version key" version)))
(increment! size
(for/sum ([(key type) (in-dict fields)])
(send type size (ref val key) ctx)))
(when includePointers
(increment! size (ref ctx 'pointerSize)))
size))
#;(test-module
(require "number.rkt")
(define (random-pick xs) (list-ref xs (random (length xs))))
(check-exn exn:fail:contract? (λ () (+VersionedStruct 42 42)))
;; make random versioned structs and make sure we can round trip
#;(for ([i (in-range 1)])
(define field-types (for/list ([i (in-range 1)])
(random-pick (list uint8 uint16be uint16le uint32be uint32le double))))
(define num-versions 20)
(define which-struct (random num-versions))
(define struct-versions (for/list ([v (in-range num-versions)])
(cons v (for/list ([num-type (in-list field-types)])
(cons (gensym) num-type)))))
(define vs (+VersionedStruct which-struct struct-versions))
(define struct-size (for/sum ([num-type (in-list (map cdr (ref struct-versions which-struct)))])
(send num-type size)))
(define bs (apply bytes (for/list ([i (in-range struct-size)])
(random 256))))
(check-equal? (send vs encode #f (send vs decode bs)) bs))
(define s (+Struct (dictify 'a uint8 'b uint8 'c uint8)))
(check-equal? (send s size) 3)
(define vs (+VersionedStruct uint8 (dictify 1 (dictify 'd s) 2 (dictify 'e s 'f s))))
(send vs force-version! 1)
(check-equal? (send vs size) 6)
#|
(define s2 (+Struct (dictify 'a vs)))
(check-equal? (send s2 size) 6)
(define vs2 (+VersionedStruct (λ (p) 2) (dictify 1 vs 2 vs)))
(check-equal? (send vs2 size) 6)
|#
)
#lang reader (submod "private/racket.rkt" reader)
(r+p "private/versioned-struct.rkt")
Loading…
Cancel
Save