To exchange files, a BitTorrent client needs some information about the exchanged file(s). This information is contained within a file named the “metainfo” file.

More details are explaind in the Metainfo File Structure of the BitTorrent specification web page.

In short, the structure is a UTF-8 bencoded dictionary containing some required and optional key/value pairs.

Example

{ "Announce":"http://.../announce"
  "Comment":"..."
  "created by":"..."
  "creation date":1210177305
  "encoding":"UTF-8"
  "info":{  "files":  [ { "length":3649577
                          "path":[ "foo.pdf" ]
                        }
                        { "length":47
                          "path":[ "bar.txt" ]
                        }
                      ]
            "name":"foobar"
            "piece Length":65536
            "Pieces":"º©g£îèÊÉ\016…\015..."
        }
}

Signature

namespace BitTorrent

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

open Common

//---------------------------------------------------------------
// META INFO
//---------------------------------------------------------------

type MetainfoFileEntry =
  { Path : string
    Length : int64
    MD5 : option<byte[]>
  } 

type Node = //used for Distributed Hash Table (DHT) protocol
  { IP : string
    Port : uint16
  }

type Metainfo =
  { mutable Tracker : string //if single tracker
    mutable Trackers : string[][] //if multiple trackers

    //-----------------------------------------------------------
    // Optional
    //-----------------------------------------------------------
    mutable Comment : string option
    mutable CreationDate : int64 option
    mutable CreatedBy : string option 

    //-----------------------------------------------------------
    // Base info dictionary
    //-----------------------------------------------------------
    mutable PieceLength : int64
    mutable SHA1s : byte[][]
    mutable IsPrivate : bool 

    //-----------------------------------------------------------
    // File info : single and multiple files
    //-----------------------------------------------------------
    mutable Length : int64 //total length of the download
    mutable BaseDirectory : string
    mutable Files : MetainfoFileEntry[] //file entries if multiple file

    //-----------------------------------------------------------
    // Misc
    //-----------------------------------------------------------
    mutable Infohash : byte[] 

    //-----------------------------------------------------------
    // DHT
    //-----------------------------------------------------------
    mutable Nodes : list<Node>
  }

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

  ///[zeroCreate ()] returns a new Metainfo instance.
  val zeroCreate : unit -> t

  ///[fromBValue bvalue] attempts to read [bvalue] into a [Metainfo] structure.
  val fromBValue : bvalue:BValue.t -> t

  ///[fromFile filepath] attempts to read [filepath] into a [Metainfo] structure.
  val fromFile : path:string -> t

  val ( |SingleTracker|MultipleTrackers| ) : metainfo:t -> Choice<string, (string[][] * string[][])>

  ///[pieceLength nth metainfo] returns the length of the [nth] piece of the [metainfo].
  val pieceLength : piece:int -> metainfo:t -> int64

  ///[numBlocks nth metainfo] returns the number of blocks in the [nth] piece of the [metainfo].
  ///The default length of a block is assumed to be [BlockLength].
  val numBlocks : piece:int -> metainfo:t -> int

  ///[blockLength p_th b_th metainfo] returns the length of the [b_th] block of
  ///the [p_th] piece of the [metainfo].
  val blockLength : piece:int -> block:int -> metainfo:t -> int

  val toBValue : metainfo:t -> BValue

Implementation

The required helpers are described at the end and those described when we defined bencoding functions.

namespace BitTorrent

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

open System
open System.Collections.Generic
open System.IO
open System.Threading

open Common

open BitTorrent
open BitTorrent.BValue

//---------------------------------------------------------------
// METAINFO
//---------------------------------------------------------------

exception MetainfoException of string

type MetainfoFileEntry =
  { Path : string
    Length : int64
    MD5 : option<byte[]>
  } 

type Node =
  { IP : string
    Port : uint16
  }

type Metainfo =
  { mutable Tracker : string
    mutable Trackers : string[][]

    //-----------------------------------------------------------
    // Optional
    //-----------------------------------------------------------
    mutable Comment : string option
    mutable CreationDate : int64 option
    mutable CreatedBy : string option 

    //-----------------------------------------------------------
    // Base info dictionary
    //-----------------------------------------------------------
    mutable PieceLength : int64
    mutable SHA1s : byte[][]
    mutable IsPrivate : bool 

    //-----------------------------------------------------------
    // File info : single and multiple files
    //-----------------------------------------------------------
    mutable Length : int64 //total length of the download
    mutable BaseDirectory : string
    mutable Files : MetainfoFileEntry[] //file entries if multiple file

    //-----------------------------------------------------------
    // Misc
    //-----------------------------------------------------------
    mutable Infohash : byte[] 

    //-----------------------------------------------------------
    // DHT
    //-----------------------------------------------------------
    mutable Nodes : list<Node>
  }

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

  let fail s = raise <| MetainfoException s

  let zeroCreate() =
    { Tracker = ""
      Trackers = [||]

      //-----------------------------------------------------------
      // Optional
      //-----------------------------------------------------------
      Comment = None
      CreationDate = None
      CreatedBy = None

      //-----------------------------------------------------------
      // Base info dictionary
      //-----------------------------------------------------------
      PieceLength = 0L
      SHA1s = [||]
      IsPrivate = false

      //-----------------------------------------------------------
      // File info : single and multiple files
      //-----------------------------------------------------------
      Length = 0L
      BaseDirectory = ""
      Files = [||]

      //-----------------------------------------------------------
      // Misc
      //-----------------------------------------------------------
      Infohash = [||]

      //-----------------------------------------------------------
      // DHT
      //-----------------------------------------------------------
      Nodes = []
    }          

  let fromBValue bvalue =
    let metainfo = zeroCreate()    

    //
    // Compulsory fields
    //
    match tryGetValue "announce" bvalue with
    | Some announce ->
        let announceUrl = getString announce
        if announceUrl.StartsWith "http" then metainfo.Tracker <- announceUrl
    | None -> fail "Missing [announce] key"   

    //we need to keep track of the info dictionary
    let mutable info = Unchecked.defaultof<BValue>

    //temporary extract to make sure the bvalue contains an info dictionary
    match tryGetValue "info" bvalue with
    | Some res ->
        info <- res
        metainfo.Infohash <- info |> BValue.toBytes |> Digest.SHA1.hexdigest
    | None -> fail "Missing [info] key"      

    //
    // Info dictionary parsing
    //  

    // Common fields

    match tryGetValue "piece length" info with
    | Some pieceLength -> metainfo.PieceLength <- getInt64 pieceLength
    | None -> fail "Missing [piece length] key in [info] dictionary"

    match tryGetValue "pieces" info with
    | Some pieces ->
        let pieces = getBytes pieces
        let piecesCount = Array.length pieces
        if piecesCount % 20 <> 0 then
          fail "Invalid [pieces] value in info dictionary"
        metainfo.SHA1s <- [|for i in 0 .. 20 .. piecesCount - 1 -> pieces.[i .. i + 19] |]
    | None -> fail "Missing [pieces] key in [info] dictionary"

    let mutable nameOrDirectory = ""
    match tryGetValue "name" info with
    | Some name -> nameOrDirectory <- getString name
    | None -> fail "Missing [name] key in [info] dictionary"

    //optional field used for single file download

    match tryGetValue "length" info with
    //single file
    | Some length ->
        metainfo.Length <- getInt64 length   

        let md5 =
          match tryGetValue "md5sum" info with
          | Some md5 -> Some <| getBytes md5
          | None -> None

        let entry =
          { Path = nameOrDirectory
            Length = metainfo.Length
            MD5 = md5
          }
        metainfo.Files <- [|entry|]

    //multiple files
    | None ->
        match tryGetValue "files" info with
        | Some files ->
            //with multiple files, the "name" key refers to the base directory
            metainfo.BaseDirectory <- nameOrDirectory

            let entries =
              [|let length = ref 0L
                let fullpath = ref ""
                let md5sum : option<byte[]> ref = ref None

                for file in getList files do
                  match tryGetValue "length" file with
                  | Some len -> length := getInt64 len
                  | None -> fail "Missing [length] key in [files] value of [info] dictionary"
                  //the full path is given as a list of folders
                  fullpath := ""
                  match tryGetValue "path" file with
                  | Some pathComponents ->
                      for subdir in getList pathComponents do
                        fullpath := Filename.concat !fullpath (getString subdir)
                  | None -> fail "Missing [path] key in [files] value of [info] dictionary"
                  //md5 is optional and not used
                  md5sum := None
                  tryGetValue "md5sum" file |> Option.iter (fun md5 ->
                    md5sum := Some <| getBytes md5
                  )
                  //aggregate the files lengths to compute the transfer length
                  metainfo.Length <- metainfo.Length + !length

                  let metainfoFile =
                    { Path = !fullpath
                      Length = !length
                      MD5 = !md5sum
                    }

                  yield metainfoFile
              |]

            metainfo.Files <- entries   

        | None -> fail "Missing [files] or [length] key in [info] dictionary"

    //
    // Optional fields from the info dictionary
    //

    //used for Distributed Hash Tables (DHT)
    tryGetValue "private" info |> Option.iter (fun isPrivate ->
      metainfo.IsPrivate <- getInt64 isPrivate = 1L
    )

    //used for Distributed Hash Tables (DHT)
    tryGetValue "nodes" info |> Option.iter (fun nodes ->
      let nodes =
        [ for node in getList nodes do
            let nodeData = getList node
            if nodeData.Count <> 2 then
              fail "Invalid [nodes] value in the [info] BDict"
            let node =
              { IP = getString nodeData.[0]
                Port = nodeData.[1] |> getInt64 |> uint16
              }
            yield node
        ]
      metainfo.Nodes <- nodes
    )

    //
    // Optional fields from the main dictionary
    //

    //Note : when announce-list is present, the "announce" value is overriden
    tryGetValue "announce-list" bvalue |> Option.iter (fun announceList ->
      let tiers = getList announceList
      metainfo.Trackers <- Array.zeroCreate tiers.Count
      let tierIdx = ref 0
      let randomizedTrackers =
        Array.init tiers.Count (fun i ->
          [|for url in getList tiers.[i] do
              let surl = getString url
              yield surl
          |]
          |> Array.shuffled
        ) |> Array.filter (fun tier -> tier.Length > 0)
      metainfo.Trackers <- randomizedTrackers
    ) 

    tryGetValue "comment" bvalue |> Option.iter (fun comment ->
      metainfo.Comment <- comment |> getString |> Some
    )

    tryGetValue "creation date" bvalue |> Option.iter (fun creationDate ->
      metainfo.CreationDate <- creationDate |> getInt64 |> Some
    )

    tryGetValue "created by" bvalue |> Option.iter (fun createdBy ->
      metainfo.CreatedBy <- createdBy |> getString |> Some
    )

    metainfo

  let fromFile path =
    path |> BValue.fromFile |> fromBValue 

  let ( |SingleTracker|MultipleTrackers| ) metainfo =
    if metainfo.Trackers.Length = 0 then
      SingleTracker metainfo.Tracker
    else
      let http =
        [|for tier in metainfo.Trackers do
            let xs = tier |> Array.filter (fun url -> url.StartsWith "http://")
            if xs.Length > 0 then yield xs
        |]
      let udp =
        [|for tier in metainfo.Trackers do
            let xs = tier |> Array.filter (fun url -> url.StartsWith "udp://")
            if xs.Length > 0 then yield xs
        |]
      MultipleTrackers (http, udp)

  let isLastPiece piece metainfo =
    piece = metainfo.SHA1s.Length - 1

  let pieceLength piece metainfo =
    if piece < 0 || piece >= metainfo.SHA1s.Length then
      let reason = sprintf "piece %d is out of bounds" piece
      invalidArg "piece" reason
    if isLastPiece piece metainfo then
      metainfo.Length % metainfo.PieceLength
    else
      metainfo.PieceLength

  let numBlocks piece metainfo =
    let pieceLength = pieceLength piece metainfo
    let blockLength = int64 App.BlockLength
    let nBlocks = pieceLength / blockLength |>int
    if pieceLength % blockLength = 0L then nBlocks else nBlocks + 1

  let isLastBlock piece block metainfo =
    let nBlocks = numBlocks piece metainfo
    isLastPiece piece metainfo && block = nBlocks - 1

  let blockLength piece block metainfo =
    let nBlocks = numBlocks piece metainfo
    if block >= nBlocks then
      let reason = sprintf "block %d is out of bounds" block
      invalidArg "block" reason
    if isLastBlock piece block metainfo then
      metainfo.Length % int64 App.BlockLength |> int
    else
      App.BlockLength

  //-------------------------------------------------------------
  // Metainfo creation
  //-------------------------------------------------------------    

  let announceListToBValue announceList =
    let tiers = List<_>()
    for tier in announceList do
      let btier = List<_>()
      for tracker in tier do
        btier.Add(BString (String.toBytes tracker))
      tiers.Add(BList btier)
    BList tiers

  let sha1sToBValue (sha1s:byte[][]) =
    BString [|for sha1 in sha1s do yield! sha1|]

  let pathToBValue (path:string) =
    let elements = List<_>()
    path.Split([|Path.DirectorySeparatorChar|], StringSplitOptions.RemoveEmptyEntries)
    |> Array.iter (fun elt -> String.toBytes elt |> BString |> elements.Add)
    BList elements

  let nodesToBValue (nodes:list<Node>) =
    let xs = List<_>()
    nodes |> List.iter (fun node ->
      let bnode = List<_>()
      bnode.Add (BString (String.toBytes node.IP))
      bnode.Add (BInt (int64 node.Port))
      xs.Add(BList bnode)
    )
    BList xs

  let toBValue metainfo =
    let dict = new SortedDictionary<_, _>()
    dict.Add("announce", BString (String.toBytes metainfo.Tracker))
    dict.Add("announce-list", (announceListToBValue metainfo.Trackers))
    metainfo.Comment |> Option.iter (fun comment ->
      dict.Add("comment", BString (String.toBytes comment))
    )
    metainfo.CreationDate |> Option.iter (fun date ->
      dict.Add("creation date", BInt date)
    )
    metainfo.CreatedBy |> Option.iter (fun createdBy ->
      dict.Add("created by", BString (String.toBytes createdBy))
    )

    let info = new SortedDictionary<_, _>()
    info.Add("piece length", BInt metainfo.PieceLength)
    info.Add("pieces", sha1sToBValue metainfo.SHA1s)
    info.Add("private", BInt (if metainfo.IsPrivate then 1L else 0L))
    info.Add("nodes", nodesToBValue metainfo.Nodes)
    let isSingleFile = metainfo.Files.Length = 1
    if isSingleFile then
      info.Add("name", BString (String.toBytes metainfo.Files.[0].Path))
      info.Add("length", BInt metainfo.Files.[0].Length)
    else
      info.Add("name", BString (String.toBytes metainfo.BaseDirectory))
      let files = List<_>()
      metainfo.Files |> Array.iter (fun file ->
        let bfile = new SortedDictionary<_, _>()
        bfile.Add("length", BInt file.Length)
        bfile.Add("path", pathToBValue file.Path)
        files.Add(BDict bfile)
      )
      info.Add("files", BList files)

    dict.Add("info", BDict info)
    BDict dict

Helpers

namespace Common

open System

module Array =

  let random = new Random(DateTime.Now.Ticks >>> 32 |> int)

  let swap n n' data =
    let max = Array.length data
    if n' >= max || n' < 0 then
      invalidArg "n'" <| sprintf "swap n:%d n':%d data:%A : index was out of array bounds" n n' data
    if n >= max || n < 0 then
      invalidArg "n'" <| sprintf "swap n:%d n':%d data:%A : index was out of array bounds" n n' data
    let tmp = data.[n]
    data.[n] <- data.[n']
    data.[n'] <- tmp

  let shuffle data =
    data |> Array.iteri (fun i _ -> data |> swap i (random.Next(data.Length)))

  let shuffled data =
    shuffle data
    data

//=========================================================================

open System.IO
open System.Security.Cryptography

module Digest =

  module SHA1 =
    let hexdigest (bytes : byte[]) =
      let csp = new SHA1CryptoServiceProvider()
      csp.ComputeHash(bytes)

    let digestFile path =
      if File.Exists(path) then
        let csp = new SHA1CryptoServiceProvider()
        use s = File.OpenRead(path)
        csp.ComputeHash(s :> Stream)
      else
        invalidArg "file" "file not found"      

Comments are closed.