F#ParsingActive PatternsBitTorrentFunctional Programming

Using Active Patterns to decode and encode BEEncoding in F#

Implementing a BEEncoding (BitTorrent) decoder and encoder in F# using composable Active Patterns operating on lists of bytes.


In the excellent post Building a BitTorrent client from scratch in C#, Seán O’Flynn implements decoding and encoding of the BEEncoding format — used in torrent files and defined by The BitTorrent Protocol Specification.

Seán’s implementation works on an IEnumerable<byte> in C#. Here I look at another approach: decoding and encoding BEEncoding in F# using Active Patterns operating on byte list. Active Patterns have been available in F# since version 1.9.1 and are a natural fit for parsing work.

Basic building blocks

We start with a basic function that consumes one element from a list if a predicate holds:

let basic e = function
    | c :: cs when e c -> Some(c, cs)
    | _ -> None

let (|Any1|_|)    e = basic ((=) e)
let (|Any1But|_|) e = basic ((<>) e)

Any1 consumes the first element if it equals e; Any1But consumes it if it differs from e.

Because Active Patterns are just functions, we can parameterise one over another to build combinators:

let (|ZeroOrMore|) q inp =
    let rec queryAcc rvs e =
        match q e with
        | Some(v, body) -> queryAcc (v :: rvs) body
        | None          -> (List.rev rvs, e)
    queryAcc [] inp

let (|OneOrMore|) q inp =
    match q inp with
    | Some (v, body) ->
        let xs, t = (|ZeroOrMore|) q body
        v :: xs, t
    | None -> [], inp

ZeroOrMore and OneOrMore take an Active Pattern as parameter and repeatedly apply it, accumulating results. They work exactly like * and + in regular expressions.

The BEEncoding data structure

The spec defines four types:

  • String — length-prefixed: 4:spam"spam"
  • Integeri3e3, i-3e-3
  • Listl4:spam4:eggse["spam"; "eggs"]
  • Dictionaryd3:cow3:mooe{"cow" → "moo"}, keys must be sorted
open System
open System.Text

type BEBytelist = byte list

type BEEncoding =
    | BENumber of Int64
    | BEString of BEBytelist
    | BEDict   of Map<BEBytelist, BEEncoding>
    | BEList   of BEEncoding list

And some helpers:

let dictStart, dictEnd   = byte 'd', byte 'e'
let listStart, listEnd   = byte 'l', byte 'e'
let numStart,  numEnd    = byte 'i', byte 'e'
let byteArrDivider       = byte ':'

let strToByteList (str : string)    = Encoding.UTF8.GetBytes str |> List.ofArray
let byteListToStr (xs : BEBytelist) = Encoding.UTF8.GetString (xs |> Array.ofList)

let byteListToTyp f byteList =
    let valid, num = Encoding.UTF8.GetString(Array.ofList byteList) |> f
    if not valid then failwithf "failed to parse %s" (byteListToStr byteList)
    num

let byteListToInt64 = byteListToTyp Int64.TryParse
let byteListToInt   = byteListToTyp Int32.TryParse

Parsing integers

BEEncoded integers look like i42e. We consume the i, then all bytes that are not e, then the e:

let (|INT|_|) = function
    | Any1 numStart (_, OneOrMore ((|Any1But|_|) numEnd) (xs, Any1 numEnd (_, t))) ->
        Some(byteListToInt64 xs, t)
    | _ -> None
let (INT i) = [byte 'i'; byte '-'; byte '3'; byte 'e']
// i = -3L

Parsing strings

BEEncoded strings are length-prefixed: 4:spam. We read bytes until :, parse the length, then take that many bytes:

let (|STRING|_|) = function
    | OneOrMore ((|Any1But|_|) byteArrDivider) (cs, Any1 byteArrDivider (_, t)) ->
        Some(List.splitAt (byteListToInt cs) t)
    | _ -> None
let (STRING(s, _)) = [byte '4'; byte ':'; byte 's'; byte 'p'; byte 'a'; byte 'm']
byteListToStr s  // "spam"

Parsing lists and dictionaries

Lists and dicts are recursive — they contain other BEEncoded values. We express this with three mutually recursive Active Patterns:

let rec (|BEENCODING|_|) = function
    | INT(i, t)    -> Some(BENumber i, t)
    | DICT(d, t)   -> Some(BEDict d,   t)
    | LIST(l, t)   -> Some(BEList l,   t)
    | STRING(s, t) -> Some(BEString s, t)
    | _            -> None

and (|LIST|_|) = function
    | Any1 listStart (_, ZeroOrMore (|BEENCODING|_|) (xs, Any1 listEnd (_, t))) ->
        Some(xs, t)
    | _ -> None

and (|DICT|_|) xs =
    let (|PAIRS|_|) = function
        | STRING(key, BEENCODING(value, t)) -> Some((key, value), t)
        | _ -> None
    let validateDict xs =
        let keys, _ = List.unzip xs
        if keys <> List.sort keys then failwith "dictionary keys not sorted"
    match xs with
    | Any1 dictStart (_, ZeroOrMore (|PAIRS|_|) (xs, Any1 dictEnd (_, t))) ->
        validateDict xs
        Some(Map.ofList xs, t)
    | _ -> None

The key insight: ZeroOrMore accepts (|BEENCODING|_|) as its pattern argument, so parsing a list body is just ZeroOrMore (|BEENCODING|_|). The grammar almost writes itself.

Decode and encode

let decode = function
    | BEENCODING(x, []) -> x
    | t -> failwithf "failed to decode: %s" (byteListToStr t)

let encode value =
    let s = StringBuilder()
    let append (str : string) = s.Append str |> ignore
    let rec encode' = function
        | BEString xs  -> append (sprintf "%i:%s" xs.Length (byteListToStr xs))
        | BENumber n   -> append (sprintf "i%ie" n)
        | BEDict m     -> append "d"; encodeMap m; append "e"
        | BEList l     -> append "l"; encodeList l; append "e"
    and encodeMap m =
        Map.toList m
        |> List.sortBy fst
        |> List.iter (fun (k, v) ->
            append (sprintf "%i:%s" k.Length (byteListToStr k))
            encode' v)
    and encodeList l = List.iter encode' l
    encode' value
    s.ToString() |> strToByteList

Round-trip test

let testData =
    "d8:announce33:http://192.168.1.74:6969/announce7:comment17:Comment goes here10:created by25:" +
    "Transmission/2.92 (14714)13:creation datei1460444420e8:encoding5:UTF-84:infod6:lengthi59616e" +
    "4:name9:lorem.txt12:piece lengthi32768e6:pieces40:..." +
    "7:privatei0eee"
    |> strToByteList

testData = (decode testData |> encode)  // true

Decode then re-encode produces the identical byte sequence — our implementation is at least internally consistent.

Takeaways

The ZeroOrMore/OneOrMore combinator pattern works just as well on byte list as on char list. The approach scales naturally: each Active Pattern is independently testable, the recursive structure mirrors the grammar directly, and there are zero external dependencies.

If you want to extend this — say, to validate integer constraints (i-0e is invalid per the spec) — you just add a guard inside the relevant Active Pattern without touching anything else.