Bencoding values for the BitTorrent protocol is explained in a Wikipedia article on Bencoding or in the more general BitTorrent protocol page.

Briefly, there are four types :

  • byte strings : [string length encoded in base ten ASCII]:[string data] ;
  • integers : i[integer encoded in base ten ASCII]e ;
  • lists : l[bencoded values]e ;
  • and dictionaries : d[bencoded string][bencoded element]e.

Byte strings can be strings represented as byte strings (such as the description of the exchanged data), as well as an array of bytes (such as a hash). Representing them with the .Net strings can thus lead to errors. Hence we shall keep the byte array representation.

Integers can be longs, hence we represent them with int64.

Since the bencoded values can be recursive (with lists and dictionaries), we use the following representation :

type BValue =
  | BString of byte[]
  | BInt of int64
  | BList of System.Collections.Generic.List<BValue>
  | BDict of System.Collections.Generic.SortedDictionary<string, BValue>
  with
    override ToString : unit -> string

We then use helper functions to extract data from this representation.

Signature

namespace BitTorrent

///BValue is the base type of values that can be encoded in a .torrent file.
type BValue =
  | BString of byte[]
  | BInt of int64
  | BList of System.Collections.Generic.List<BValue>
  | BDict of System.Collections.Generic.SortedDictionary<string, BValue>
  with
    override ToString : unit -> string

//---------------------------------------------------------------
// BVALUE
//---------------------------------------------------------------

exception BValueException of string

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module BValue =
  type t = BValue

  ///[getString bvalue] returns an ASCII-encoded string if [bvalue] matches [String _].
  ///Otherwise, it raises an exception.
  val getString : bvalue:t -> string

  ///[getBytes bvalue] returns a byte array if [bvalue] matches [BString _].
  ///Otherwise, it raises an exception.
  val getBytes : bvalue:t -> byte[]

  ///[getBytes bvalue] returns a byte array if [bvalue] matches [BInt _].
  ///Otherwise, it raises an exception.
  val getInt64 : bvalue:t -> int64

  ///[getList bvalue] returns a System.Collections.Generic.List if [bvalue] matches [BList _].
  ///Otherwise, it raises an exception.
  val getList : bvalue:t -> System.Collections.Generic.List<t>

  ///[getDict bvalue] returns a System.Collections.Generic.SortedDictionary if
  ///[bvalue] matches [BDict _]. Otherwise, it raises an exception.
  val getDict : bvalue:t -> System.Collections.Generic.SortedDictionary<string, t>

  ///[tryGetValue key dictBValue] returns [true, value] where [value] is the BValue found
  ///matching the [key] in the [dictBValue], [false, null] otherwise.
  val tryGetValue : key:string -> bdict:t -> option<t>

   ///[toBytes bvalue] converts [bvalue] into a byte[].
  val toBytes : bvalue:t -> byte[]

  ///[fromStream stream] reads [stream] and attempts to convert it into a BValue.
  val fromStream : stream:System.IO.Stream -> t

  val fromBytes : data:byte[] -> t

  ///[fromFile filepath] reads [filepath] and attempts to convert it into a BValue.
  val fromFile : filePath:string -> t

  val toStream : stream:System.IO.Stream -> bvalue:t -> unit

  val toFile : filePath:string -> bvalue:t -> unit

Implementation

namespace BitTorrent

//---------------------------------------------------------------
// LIBRARIES
//---------------------------------------------------------------

open System
open System.Collections.Generic
open System.IO
open System.Net
open System.Text

open Common
open Common.IO

//---------------------------------------------------------------
// BEncoding
//---------------------------------------------------------------

exception BValueException of string

type BValue =
  | BString of byte[]
  | BInt of int64
  | BList of ResizeArray<BValue>
  | BDict of SortedDictionary<string, BValue>
  with
    override bvalue.ToString() =
      match bvalue with
      | BString s ->
          if s.Length > 100 then
            sprintf "'%s...'" <| String.fromBytes s.[0..50]
          else
            sprintf "'%s'" <| String.fromBytes s
      | BInt i -> string i
      | BList xs ->
          let buf = Buffer.create 1000
          Printf.bprintf buf "[ "
          for x in xs do
            Printf.bprintf buf "%O; " x
          Printf.bprintf buf "]"
          buf.ToString()
      | BDict xs ->
          let buf = Buffer.create 1000
          Printf.bprintf buf "{ "
          for x in xs do
            Printf.bprintf buf "%O : %O ;" x.Key x.Value
          Printf.bprintf buf "}"
          buf.ToString()   

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module BValue =
  type t = BValue

  let getString = function
    | BString x -> String.fromBytes x
    | _ -> raise <| BValueException "The value was not a BValue string"

  let getBytes = function
    | BString x -> x
    | _ -> raise <| BValueException "The value was not a BValue string"

  let getInt64 = function
    | BInt x -> x
    | _ -> raise <| BValueException "The value was not a BValue integer"

  let getList = function
    | BList xs -> xs
    | _ -> raise <| BValueException "The value was not a BValue list"

  let getDict = function
    | BDict xs -> xs
    | _ -> raise <| BValueException "The value was not a BValue dictionary" 

  let tryGetValue key dict =
    let mutable res = Unchecked.defaultof<BValue>
    if (getDict dict).TryGetValue(key, &res) then Some res else None      

  let rec toBytes = function
    | BString s ->
        let sLength = string s.Length
        [|for c in sLength do
            yield byte c
          yield ':'B
          yield! s
        |]
    | BInt x ->
        let s = string x
        [|yield 'i'B
          for c in s do
            yield byte c
          yield 'e'B
        |]
    | BList xs ->
        [|yield 'l'B
          for x in xs do
            yield! toBytes x
          yield 'e'B
        |]
    | BDict xs ->
        [|yield 'd'B
          for KeyValue(k, v) in xs do
            yield! k |> String.toBytes |> BString |> toBytes
            yield! toBytes v
          yield 'e'B
        |]

  let rec indexOf x arr idx =
    if idx < 0 || idx >= Array.length arr then -1
    elif arr.[idx] = x then idx
    else indexOf x arr (idx+1)

  let rec parseMap = function
    | 'i'B -> parseBInt
    | 'd'B -> parseBDict
    | 'l'B -> parseBList
    | c when c >= '0'B && c <= '9'B -> parseBString
    | _ -> raise <| BValueException "Unknown BValue format"

  and parseBInt s idx =
    let idx = idx + 1
    let endIdx = indexOf 'e'B s idx
    if endIdx >= 0 then
      let res = s.[idx..endIdx-1]
      if res.Length >= 2 && (res.[0] = '0'B || (res.[0] = '-'B && res.[1] = '0'B)) then
        raise <| BValueException "Non-zero numbers can't start with '0'"
      BInt (res |> String.fromBytes |> int64), endIdx
    else
      raise <| BValueException "Invalid integer format : ending 'e' was not found"

  and parseBString s idx =
    let endLengthIdx = indexOf ':'B s idx
    if endLengthIdx >= 0 then
      let len = s.[idx..endLengthIdx-1]
      if len.Length = 0 then
        raise <| BValueException "BString length cannot be zero"
      let stringEnd = endLengthIdx + (len |> String.fromBytes |> Int32.Parse)
      BString s.[endLengthIdx+1..stringEnd], stringEnd
    else
      raise <| BValueException "Invalid string format : separating ':' was not found"

  and parseBList s idx =
    let res = new List<_>()
    let movingIdx = ref <| idx + 1
    while s.[!movingIdx] <> 'e'B do
      let toAdd, endIdx = (parseMap s.[!movingIdx]) s !movingIdx
      res.Add toAdd
      movingIdx := endIdx + 1
    BList res, !movingIdx

  and parseBDict s idx =
    //Some clients do not order keys alphabetically, hence raising an exception
    //when keys aren't sorted may be inappropriate.
    //Therefore, while keys ordering is enforced on BValue creation, it is not
    //when interpreting them.
    let res = new SortedDictionary<_, _>()
    let movingIdx = ref <| idx + 1
    while s.[!movingIdx] <> 'e'B do
      let key, endIdx = parseBString s !movingIdx
      movingIdx := endIdx + 1
      let value, endIdx = (parseMap s.[!movingIdx]) s !movingIdx
      res.Add (getString key, value)
      movingIdx := endIdx + 1
    BDict res, !movingIdx

  and parse (s:byte[]) =
    let decoded, len = (parseMap s.[0]) s 0
    if len + 1 <> s.Length then
      raise <| BValueException "String could not be decoded completely"
    decoded

  let fromStream (s:Stream) =
    use dataStream = new MemoryStream()
    let buffer = Array.zeroCreate 2048
    let ok = ref true
    while !ok do
      let nRead = s.Read(buffer, 0, 2048)
      if nRead > 0 then
        dataStream.Write(buffer, 0, nRead)
      else
        ok:= false
    dataStream.ToArray() |> parse

  let fromBytes data =
    use ms = new MemoryStream(data, 0, data.Length)
    ms.Seek(0L, SeekOrigin.Begin) |> ignore
    fromStream ms

  let fromFile path =
    if not <| File.Exists path then
      invalidArg "path" <| sprintf "File [%s] does not exist" path
    use stream = File.OpenRead(path)
    fromStream stream

  let toStreamAux (s:Stream) buffer =
    s.AsyncWrite(buffer, 0, buffer.Length) |> Async.Start

  let toStream s bvalue =
    toBytes bvalue |> toStreamAux s

  let toFile path bvalue =
    let buffer = toBytes bvalue
    if not <| File.Exists path then
      File.create path (int64 buffer.Length) true
    use s = File.OpenRead(path)
    toStreamAux s buffer

Helpers

This requires String.fromBytes and File.create from the Common namespace. Here is an excerpt from the string module, and the io one.

namespace Common

module String =
  let fromBytes (b:byte[]) = System.Text.Encoding.UTF8.GetString(b)

  let toBytes (s:string) = System.Text.Encoding.UTF8.GetBytes(s)
namespace Common

module IO =

  open System.IO
  open System.Threading

  open Common

  module Directory =
    let create dir =
      if not <| Directory.Exists(dir) then
        let dirInfo = Directory.CreateDirectory(dir)
        if not dirInfo.Exists then
          invalidArg "dir" "Directory [%s] could not be created" dir  

  module File =
    let rec create fullPath len overwrite =
      if not <| File.Exists(fullPath) then
        let dir = Path.GetDirectoryName fullPath
        Directory.create dir
        let drive = Application.drive fullPath
        let freeSpace = Application.diskFreeSpace drive
        if freeSpace >= len then
          if len > 0L then
            use fs = File.Create fullPath
            fs.Seek(len - 1L, SeekOrigin.Begin) |> ignore
            fs.WriteByte 0uy
        else
          failwith <| sprintf "createFile %A : Not enough space available (%d)" fullPath freeSpace
      elif overwrite then
        File.Delete(fullPath)
        if File.Exists(fullPath) then
          failwith <| sprintf "createFile %A file could not be deleted to be overwritten" fullPath
        create fullPath len overwrite

    let length file =
      let info = new FileInfo(file)
      info.Length

    let copy fileFrom fileTo overwrite =
      if (overwrite || not <| File.Exists(fileTo)) then
        File.Copy(fileFrom, fileTo, overwrite)

    type LockStore = Map<string, Semaphore>

    let addLock (store:LockStore ref) path =
      if not <| Map.containsKey path !store then
        store := Map.add path (new Semaphore(1, 1)) !store

    let asyncAcquireLock (store:LockStore ref) path =
      async {
        addLock store path
        return! Async.AwaitWaitHandle ((!store).[path])
      }

    let releaseLock (store:LockStore) path =
      if Map.containsKey path store then
        ignore <| store.[path].Release()

Comments are closed.