123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- ;; This file is part of scheme-GNUnet, a partial Scheme port of GNUnet.
- ;; Copyright (C) 2021 Maxime Devos <maximedevos@telenet.be>
- ;;
- ;; scheme-GNUnet is free software: you can redistribute it and/or modify it
- ;; under the terms of the GNU Affero General Public License as published
- ;; by the Free Software Foundation, either version 3 of the License,
- ;; or (at your option) any later version.
- ;;
- ;; scheme-GNUnet is distributed in the hope that it will be useful, but
- ;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- ;; Affero General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU Affero General Public License
- ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
- ;;
- ;; SPDX-License-Identifier: AGPL-3.0-or-later
- (import (tests utils)
- (quickcheck)
- (quickcheck property)
- (quickcheck arbitrary)
- (quickcheck generator)
- (gnu gnunet utils tokeniser)
- (gnu gnunet utils bv-slice)
- (srfi srfi-1)
- (srfi srfi-8)
- (srfi srfi-43)
- (only (ice-9 control) let/ec)
- (ice-9 match)
- (only (system foreign)
- pointer->bytevector bytevector->pointer)
- (only (rnrs base) assert)
- (only (rnrs exceptions) guard)
- (only (rnrs conditions)
- assertion-violation? condition-who)
- (only (rnrs io ports)
- open-bytevector-input-port)
- (rnrs bytevectors)
- (gnu gnunet netstruct syntactic)
- (gnu gnunet util struct))
- (define (fluffed-bytevector %size %off fluff)
- ;; Returned bytevector is a complete message.
- (let* ((size (+ %size (sizeof /:message-header '())))
- (bv (make-bytevector (+ %off size)))
- (s (bv-slice/read-write bv)))
- (bytevector-copy! fluff 0 bv 0
- (min (bytevector-length fluff)
- (bytevector-length bv)))
- (set%! /:message-header '(size)
- (slice-slice s %off (sizeof /:message-header '()))
- size)
- (values bv %off size)))
- (test-begin "tokeniser")
- (define (no-return/overly-small . _)
- (error "unexpected call to return/overly-small"))
- (define (no-return/done . _)
- (error "unexpected call to return/done"))
- (define (no-return/done-eof . _)
- (error "unexpected call to return/done-eof"))
- (define (no-return/premature-eof . _)
- (error "unexpected call to return/premature-eof"))
- (define (no-handle/message . _)
- (error "unexpected call to handle/message"))
- ;; Some bugs this found:
- ;; * in some places, the 'offset' argument was ignored
- ;; and always the first or first two bytes of 'bv'
- ;; in 'continue' in 'add-bytevector!' would be used.
- ;; * some incorrect assertions in the tokeniser code
- ;; * when a message was fragmented (between header and data),
- ;; the data was not copied
- ;; * the type of a message was calculated incorrectly
- ;; whe ‘overly small message errors’ are reported
- ;; * the type of a message could not be calculated
- ;; for some fragmented messages, if the first 'length'
- ;; was 1 and the second 'length' was 3.
- (test-assert "[prop] complete messages are passed through"
- (quickcheck
- (property
- ((%size $natural)
- (%off $natural)
- (fluff $bytevector))
- (receive (bv offset size)
- (fluffed-bytevector %size %off fluff)
- (let ((handled? #f))
- (add-bytevector!
- (make-tokeniser)
- bv offset size
- (lambda (bv2 offset2 length)
- (assert (not handled?))
- (assert (eq? bv bv2))
- (assert (= offset offset2))
- (assert (= length size))
- (set! handled? #t))
- (lambda _ handled?)
- no-return/overly-small))))))
- ;; Test fragmented messages and multiple messages
- ;; are properly handled.
- (define choose-message
- (generator-let*
- ((length (choose-one/weighted
- ;; Very small
- `((1 . ,(choose-integer 4 5))
- (1 . ,(choose-integer 5 6))
- ;; Some length
- (2 . ,(choose-integer 4 9)))))
- ;; Arbitrary 'type' field and data
- (filler (choose-bytevector (- length 2))))
- (let ((bv (make-bytevector length)))
- (bytevector-u16-set! bv 0 length (endianness big))
- (bytevector-copy! filler 0 bv 2 (bytevector-length filler))
- (generator-return bv))))
- ;; Generate a list of message bytevectors
- (define choose-many-messages
- (sized-generator
- (cut choose-list choose-message <>)))
- (define (merge-bytevectors messages)
- (define size (reduce + 0 (map bytevector-length messages)))
- (define bv (make-bytevector size))
- (let loop ((offset 0) (messages messages))
- (if (null? messages)
- bv
- (let* ((head (car messages))
- (tail (cdr messages))
- (message-size (bytevector-length head)))
- (bytevector-copy! head 0 bv offset message-size)
- (loop (+ offset message-size) tail)))))
- ;; Try to occassionally split message in annoying places,
- ;; and avoid splitting at message boundaries.
- (define (choose-split-positions messages)
- (let loop ((offset 0) (messages messages))
- (if (null? messages)
- (generator-return '())
- (let* ((head (car messages))
- (tail (cdr messages))
- (message-size (bytevector-length head))
- (data-splittable? (> message-size 5)))
- (generator-let*
- ((rest-positions
- (loop (+ offset message-size) tail))
- (data-split-positions
- (if data-splittable?
- (generator-lift
- list
- (choose-integer 4 message-size))
- (generator-return '())))
- (end-split-positions
- (choose-one/weighted
- `((2 . ,(generator-return '()))
- (1 . ,(generator-return (list message-size))))))
- (head-split-positions
- (choose-one/weighted
- `((3 . ,(generator-return '())) ; don't split header
- (2 . ,(generator-return '(1))) ; split inside size field
- (2 . ,(generator-return '(2))) ; split between size field and type
- (1 . ,(generator-return '(1 2))))))) ; both of above
- (let* ((all-positions
- (append head-split-positions data-split-positions
- end-split-positions))
- (fixed-positions
- (map (cut + <> offset) all-positions)))
- (generator-return
- (append fixed-positions rest-positions))))))))
- ;; A list of (start . length).
- ;; Starts at the minimal 'start', and ends at 'end' (exclusive)
- (define* (positions->ranges positions end)
- (match positions
- (() `((,end . 0)))
- ((start) `((,start . ,(- end start))))
- ((start next . rest)
- `((,start . ,(- next start))
- ,@(positions->ranges `(,next ,@rest) end)))))
- (define $messages-and-ranges
- (arbitrary
- (gen (generator-let*
- ((messages choose-many-messages)
- (bv (generator-return
- (merge-bytevectors messages)))
- (split-positions
- (choose-split-positions messages))
- (ranges
- (generator-return
- (positions->ranges (cons 0 split-positions)
- (bytevector-length bv)))))
- (generator-return
- `#(,messages ,bv ,ranges))))
- (xform #f))) ; unneeded
- ;; A simplified test failure case of
- ;; "[prop] all fragmented & multiple messages received".
- ;; The issue was that (1 2 3 4) was not copied.
- (test-equal "message fragmented on header/data boundary reassembled"
- #vu8(0 8 50 50 1 2 3 4)
- (let ((tok (make-tokeniser))
- ;; Message size: 8
- (received? #f)
- (bv #vu8(0 8 50 50 1 2 3 4)))
- (add-bytevector! tok bv 0 4
- no-handle/message (const #t) no-return/overly-small)
- (add-bytevector! tok bv 4 4
- (lambda (bv offset length)
- ;; These two assertions are actually an implementation
- ;; detail, and test no overly large allocations are
- ;; made.
- (assert (= 0 offset))
- (assert (= length (bytevector-length bv)))
- (assert (not received?))
- (set! received? (bytevector-copy bv)))
- (const #t) no-return/overly-small)
- received?))
- ;; Found when debugging a test failure of
- ;; "[prop] all fragmented & multiple messages received".
- ;; The bug was a missing set-position! call.
- (test-equal "message fragmented in size field and after message header, some data"
- #vu8(0 6 236 197 216 19)
- (let ((tok (make-tokeniser))
- (received? #f)
- (bv #vu8(0 6 236 197 216 19)))
- ;; copy the zero
- (add-bytevector! tok bv 0 1
- no-handle/message (const #t) no-return/overly-small)
- ;; copy the rest of the message header
- (add-bytevector! tok bv 1 3
- no-handle/message (const #t) no-return/overly-small)
- ;; copy the data
- (add-bytevector! tok bv 4 2
- (lambda (bv offset length)
- ;; see previous test case
- (assert (= 0 offset))
- (assert (= length (bytevector-length bv)))
- (assert (not received?))
- (set! received? (bytevector-copy bv)))
- (const #t)
- no-return/overly-small)
- received?))
- ;; And return/done is called in tail position.
- (test-assert "[prop] all fragmented & multiple messages received"
- (quickcheck
- (property
- ((messages-and-ranges $messages-and-ranges))
- (match messages-and-ranges
- (#(messages bv ranges)
- (assert (= (apply + (map cdr ranges))
- (bytevector-length bv)))
- (guard (e ((assertion-violation? e)
- ;; 2: don't include 'make-stack' or
- ;; this guard
- (display-backtrace (make-stack #t 2) (current-error-port))
- (print-exception (current-error-port) #f '%exception (list e))
- #f))
- (let ((tok (make-tokeniser))
- (remove-message!
- (lambda (bv offset length)
- (define bv/range
- (pointer->bytevector
- (bytevector->pointer bv offset)
- length))
- ;; Sanity check
- (assert (<= 0 offset))
- (assert (<= (+ offset length) (bytevector-length bv)))
- (let/ec ec
- (pair-for-each
- (match-lambda
- (((and message (set! set-message!)) . rest)
- (when (and (bytevector? message)
- (bytevector=? message bv/range))
- (set-message! #f) ; mark it as received
- (ec))))
- messages) ; stop searching
- (assert (and #f
- "message not added but still received"))))))
- (for-each
- (match-lambda
- ((start . length)
- (assert
- (calls-in-tail-position?
- (lambda (return/done)
- (add-bytevector! tok bv start length
- remove-message!
- (lambda () (return/done))
- no-return/overly-small))))))
- ranges)))
- ;; All messages should have been received.
- (not (any identity messages)))))))
- ;; The type was read at an incorrect offset.
- (test-equal "overly small message error (complete header)"
- (map (lambda (n)
- `(#t ; in tail position
- ,(+ (* 256 n) (+ n 1)) ; message type
- ,n)) ; message size
- (iota 4))
- (map (lambda (n)
- (call-with-values
- (lambda ()
- (calls-in-tail-position?
- (lambda (return/overly-small)
- (add-bytevector! (make-tokeniser)
- (u8-list->bytevector
- ;; n (+ n 1): arbitrary message type.
- ;; Two separate values are used for
- ;; the two halves of the u16, to
- ;; detect little / big endianness issues.
- ;;
- ;; GNUnet usually (always?) uses
- ;; big-endian.
- (list 0 n n (+ n 1)))
- 0 4
- no-handle/message
- no-return/done
- return/overly-small))))
- list))
- ;; 4: size of message header
- ;; iota makes a list '(0 1 2 3)
- (iota 4)))
- ;; A bounds check at the call to return/overly-small
- ;; was overly strict, resulting in the message type being missing.
- (test-equal "overly small message error (header split in size field)"
- (map (lambda (n)
- `(#t ; in tail position
- ,(+ (* 256 (+ n 1)) n) ; message type
- ,n))
- (iota 4))
- (map (lambda (n)
- (let ((tok (make-tokeniser))
- (bv (u8-list->bytevector
- ;; see previous test case for why (+ n 1) n
- (list 0 n (+ n 1) n))))
- (add-bytevector! tok bv 0 1
- no-handle/message
- (const #t)
- no-return/overly-small)
- (call-with-values
- (lambda ()
- (calls-in-tail-position?
- (lambda (return/overly-small)
- (add-bytevector! tok bv 1 3
- no-handle/message
- no-return/done
- return/overly-small))))
- list)))
- (iota 4))) ; see previous test case for why (iota 4)
- ;; All the previous tests use 'small' messages. That is,
- ;; the message sizes were always < 256. However, messages
- ;; with size >= 256 definitely exist.
- ;;
- ;; This test detects the mutation
- ;; (bytevector-u8-ref bv offset) --> 0
- ;; in (! size/byte-0 [...]).
- (define huge-bv
- (let ((bv (make-bytevector #xfffe 17)))
- (bytevector-u16-set! bv 0 #xfffe (endianness big))
- bv))
- ;; Tests:
- ;; * the whole message is received
- ;; * return/done is called in tail position
- (test-equal "huge message, split early"
- (map (const #t) (iota 16))
- (map (lambda (split-position)
- (let ((tok (make-tokeniser))
- (received? #f))
- (receive (in-tail-position?)
- (calls-in-tail-position?
- (lambda (return/done)
- (add-bytevector! tok huge-bv 0 split-position
- no-handle/message
- return/done
- no-return/overly-small)))
- (assert in-tail-position?))
- (receive (in-tail-position?)
- (calls-in-tail-position?
- (lambda (return/done)
- (add-bytevector! tok huge-bv split-position
- (- #xfffe split-position)
- (lambda (bv offset length)
- (assert (not received?))
- ;; really an implementation detail,
- ;; but no bytevector-range-copy
- ;; exists.
- (assert (= 0 offset))
- (assert (= length (bytevector-length bv)))
- (set! received?
- (bytevector-copy bv)))
- return/done
- no-return/overly-small)))
- (assert in-tail-position?))
- (equal? huge-bv received?)))
- (iota 16)))
- (define (catch-errors thunk)
- (guard (e ((interrupted-tokeniser-violation? e)
- `(,(condition-who e) . interrupted))
- ((kaput-tokeniser-error? e)
- `(,(condition-who e) . kaput)))
- (thunk)))
- (test-equal "re-entrancy from message handler is detected (complete message)"
- '(add-bytevector! . interrupted)
- (let ((tok (make-tokeniser)))
- (catch-errors
- (lambda ()
- (add-bytevector! tok #vu8(0 4 0 0) 0 4
- (lambda (bv offset length)
- (add-bytevector! tok #vu8(0 4 1 1) 0 4
- no-handle/message
- no-return/done
- no-return/overly-small)
- (assert #f))
- no-return/done
- no-return/overly-small)))))
- (test-equal "tokeniser becomes kaput, split after size field"
- '(add-bytevector! . kaput)
- (let ((tok (make-tokeniser))
- (bv #vu8(0 3)))
- (receive (tail? type size)
- (calls-in-tail-position?
- (lambda (return/overly-small)
- (add-bytevector! tok bv 0 2 no-handle/message
- no-return/done
- return/overly-small)))
- (assert (eq? #f type))
- (assert (= size 3))
- (assert tail?))
- (catch-errors
- (lambda ()
- (add-bytevector! tok #vu8(0) 0 1
- no-handle/message no-return/done no-return/overly-small)
- (error "unreachable")))))
- (test-equal "tokeniser becomes kaput, split inside size field"
- '(add-bytevector! . kaput)
- (let ((tok (make-tokeniser))
- (bv #vu8(0 3 4 5)))
- (receive (tail?)
- (calls-in-tail-position?
- (lambda (return/done)
- (add-bytevector! tok bv 0 1 no-handle/message
- return/done
- no-return/overly-small)))
- (assert tail?))
- (receive (tail? type size)
- (calls-in-tail-position?
- (lambda (return/overly-small)
- (add-bytevector! tok bv 1 2 no-handle/message
- no-return/done
- return/overly-small)))
- (assert tail?)
- (assert (= size 3))
- (assert (eq? type #f)))
- (catch-errors
- (lambda ()
- (add-bytevector! tok bv 2 2
- no-handle/message no-return/done
- no-return/overly-small)
- (error "unreachable")))))
- (test-equal "eof detected"
- '(#t)
- (receive result
- (calls-in-tail-position?
- (lambda (return/done-eof)
- (add-from-port! (make-tokeniser) (%make-void-port "r")
- no-handle/message no-return/overly-small
- return/done-eof no-return/premature-eof)))
- result))
- (test-equal "eof detected (complete data)"
- '(#t)
- (receive result
- (calls-in-tail-position?
- (lambda (return/done-eof)
- (define handled? #f)
- (define (handle/message bv offset length)
- (assert (= length 4))
- ;; Verify the received message is correct
- (assert (= (bytevector-u32-ref bv offset (endianness big))
- (bytevector-u32-ref #vu8(0 4 0 0) 0 (endianness big))))
- (assert (not handled?))
- (set! handled? #t))
- (add-from-port! (make-tokeniser)
- (open-bytevector-input-port #vu8(0 4 0 0))
- handle/message no-return/overly-small return/done-eof
- no-return/done-eof)))
- result))
- (test-equal "premature eof detected"
- '(#t)
- (receive result
- (calls-in-tail-position?
- (lambda (return/premature-eof)
- ;; 4 bytes are expected, but only the stream only has 3.
- (add-from-port! (make-tokeniser) (open-bytevector-input-port #vu8(0 4 0))
- no-handle/message no-return/overly-small no-return/done-eof
- return/premature-eof)))
- result))
- (test-equal "add-from-port! and partial messages (split at header)"
- #vu8(0 8 2 3 4 5 6 7)
- (let ((tok (make-tokeniser))
- (message #f))
- (add-bytevector! tok #vu8(0 8 2 3) 0 4 no-handle/message
- (const #t) no-return/overly-small)
- (add-from-port! tok (open-bytevector-input-port #vu8(4 5 6 7))
- (lambda (bv offset length)
- (assert (not message))
- (let ((bv2 (make-bytevector length)))
- (bytevector-copy! bv offset bv2 0 length)
- (set! message bv2)))
- no-return/overly-small (lambda () message)
- no-return/premature-eof)))
- (test-equal "kaput tokeniser and add-from-port!"
- '(add-from-port! . kaput)
- (let ((tok (make-tokeniser))
- (bv #vu8(0 3 4 5)))
- ;; Make the tokeniser kaput (overly small message size)
- (add-bytevector! tok bv 0 4 no-handle/message no-return/done
- (const #t))
- ;; And feed it some bytes (with add-from-port!) anyway.
- (catch-errors
- (lambda ()
- (add-from-port! tok (open-bytevector-input-port #vu8(1 2 3 4))
- no-handle/message no-return/overly-small
- no-return/done-eof no-return/premature-eof)
- (error "unreachable")))))
- (test-end "tokeniser")
|