wireform-bencode
wireform-bencode implements Bencode, the encoding used by BitTorrent for
.torrent files, DHT messages, and peer wire protocols. Bencode supports
byte strings, integers, lists, and dictionaries with a deliberately small
grammar. Use this package when you parse or produce BitTorrent metadata,
validate info hashes, or implement peer-facing tooling that must match the
on-wire Bencode layout exactly.
Key features
Section titled “Key features”- Template Haskell deriving via
deriveBencodefromBencode.Derive, withwireform-deriveannotations; Generic defaults (empty instances) work for simple uncustomized records - Sorted dictionary keys enforced on encode and validated on decode, as required by BEP-3 for stable info hashes
- Simple wire grammar of strings, integers, lists, and dictionaries
- Dynamic values via the untyped
ValueADT for.torrentinspection - Direct encoding for buffer-oriented writes
Basic usage
Section titled “Basic usage”Model a metadata record and derive Bencode codecs. Record fields encode as dictionary keys (field names as byte strings):
{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE TemplateHaskell #-}module TorrentInfo where
import Bencode.Class (ToBencode, FromBencode, encodeBencode, decodeBencode)import Bencode.Derive (deriveBencode)import GHC.Generics (Generic)import Data.Text (Text)
data FileInfo = FileInfo { fileLength :: !Int , filePath :: !Text } deriving stock (Show, Eq, Generic)
data Info = Info { infoName :: !Text , infoPieceLen :: !Int , infoFiles :: ![FileInfo] } deriving stock (Show, Eq, Generic)
$(deriveBencode ''FileInfo)$(deriveBencode ''Info)
encodeInfo :: Info -> ByteStringencodeInfo info = encodeBencode info
decodeInfo :: ByteString -> Either String InfodecodeInfo bs = decodeBencode bsFor simple records with no custom wire naming, Generic defaults also work:
declare empty instance ToBencode FileInfo / FromBencode FileInfo (and the
same for Info) after deriving stock (Show, Eq, Generic). Field names go to
the wire verbatim and annotations are not supported.
The encoder sorts dictionary keys by raw byte order before writing. You can pass key/value pairs in any order; the wire output is always canonical for hashing:
import Bencode.Encoding (dictFromList, int, encodingToByteString)import Data.ByteString.Char8 qualified as BS8
canonicalDict :: ByteStringcanonicalDict = encodingToByteString ( dictFromList [ (BS8.pack "zebra", int 1) , (BS8.pack "alpha", int 2) ] )For ad hoc .torrent parsing, use the dynamic ADT and walk the structure:
import Bencode.Value qualified as Bimport Bencode.Decode (decode)import Data.ByteString.Char8 qualified as BS8import Data.Vector qualified as V
infoLength :: B.Value -> Maybe IntegerinfoLength val = case val of B.BDict pairs -> case V.find ((== BS8.pack "length") . fst) (V.toList pairs) of Just (_, B.BInteger n) -> Just n _ -> Nothing _ -> NothingPerformance
Section titled “Performance”Encode/decode
Section titled “Encode/decode”| Operation | encode | decode | ratio |
|---|---|---|---|
| single-file metainfo | 953 ns | 1946 ns | 2.04x |
| 100-file metainfo | 32422 ns | 82927 ns | 2.56x |
Last run 2026-06-27 11:24:28 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5.
Bencode is a simple text-ish format (integers as decimal strings, byte strings length-prefixed). Encode is allocation-lean; decode is dominated by dictionary key sorting.
The chart and table above are regenerated by wireform-stats from wireform-bencode/bench-results/summary/bencode-encode-decode.json — the same source the README chart is built from.
Notable modules
Section titled “Notable modules”| Module | Purpose |
|---|---|
Bencode.Class | ToBencode / FromBencode, encodeBencode, decodeBencode |
Bencode.Encode / Bencode.Decode | Low-level encode and decode with sorted-key enforcement |
Bencode.Encoding | Composable encoding builder for dictionaries and lists |
Bencode.Value | Dynamic untyped Value ADT |
Bencode.Derive | Template Haskell deriver with wireform-derive annotations |