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()