#lang racket/base
(require racket/match)
(require racket/port)
(provide bencode-bytes-limit
bencode-read
bencode-read-to-end
bytes->bencode
bencode-write
bencode->bytes)
(define bencode-bytes-limit (make-parameter 4096))
(define (read-ascii p)
(match (read-byte p)
[(? eof-object?) eof]
[v (integer->char v)]))
(define (digit-value c)
(- (char->integer c) (char->integer #\0)))
(define (read-number terminator p [acc 0])
(let loop ((sign 1) (acc acc))
(match (read-ascii p)
[(? eof-object?) (error 'bencode-read "Unexpected EOF")]
[(== terminator) (* sign acc)]
[#\- (loop (* -1 sign) acc)]
[(? char-numeric? c) (loop sign (+ (* acc 10) (digit-value c)))]
[c (error 'bencode-read "Unexpected character in numeric value: ~v" c)])))
(define (bencode-read* p)
(match (read-ascii p)
[(? eof-object?) eof]
[#\d (read-dictionary p)]
[#\l (read-list p)]
[#\i (read-number #\e p)]
[#\e 'end-marker]
[(? char-numeric? c) (read-string (read-number #\: p (digit-value c)) p)]
[c (error 'bencode-read "Unexpected character: ~v" c)]))
(define (bencode-read p)
(match (bencode-read* p)
['end-marker (error 'bencode-read "Unexpected end-marker")]
[v v]))
(define (bencode-read*/no-eof p)
(match (bencode-read* p)
[(? eof-object?) (error 'bencode-read "Unexpected EOF")]
[v v]))
(define (read-dictionary p)
(let loop ((acc (hash)))
(match (bencode-read*/no-eof p)
['end-marker
acc]
[k
(when (not (bytes? k)) (error 'bencode-read "Dictionary had non-bytes key"))
(let ((v (bencode-read*/no-eof p)))
(loop (hash-set acc k v)))])))
(define (read-list p)
(let loop ((acc '()))
(match (bencode-read*/no-eof p)
['end-marker (reverse acc)]
[v (loop (cons v acc))])))
(define (read-string len p)
(when (negative? len)
(error 'bencode-read "Negative string length not permitted"))
(when (> len (bencode-bytes-limit))
(error 'bencode-read
"String of length ~v exceeds current bencode-bytes-limit ~v"
len
(bencode-bytes-limit)))
(define buf (read-bytes len p))
(when (or (eof-object? buf)
(< (bytes-length buf) len))
(error 'bencode-read "Unexpected EOF in string"))
buf)
(define (bencode-read-to-end p)
(let loop ((acc '()))
(match (bencode-read p)
[(? eof-object?) (reverse acc)]
[v (loop (cons v acc))])))
(define (bytes->bencode bs)
(call-with-input-bytes bs bencode-read-to-end))
(define (bencode-write x p)
(match x
[(? hash?)
(write-char #\d p)
(for ([entry (in-list (sort (hash->list x) bytes<? #:key car))])
(match entry
[(cons (? bytes? k) v)
(bencode-write k p)
(bencode-write v p)]))
(write-char #\e p)]
[(or (? null?) (? pair?))
(write-char #\l p)
(for ([v (in-list x)])
(bencode-write v p))
(write-char #\e p)]
[(? integer?)
(write-char #\i p)
(display (number->string x) p)
(write-char #\e p)]
[(? bytes?)
(display (number->string (bytes-length x)) p)
(write-char #\: p)
(write-bytes x p)]
[v (error 'bencode-write "Cannot encode value as bencode: ~v" v)]))
(define (bencode->bytes x)
(call-with-output-bytes
(lambda (p)
(for ([v (in-list x)])
(bencode-write v p)))))