Sitemap
ACHINTYA SHARMA

ACHINTYA SHARMA

Learner

Building a BitTorrent Client from Scratch (part 2): Bencoding implementation in C

9 min readJun 23, 2026

--

Press enter or click to view image in full size

“We can only see a short distance ahead, but we can see plenty there that needs to be done.” — Alan Turing

First and foremost, let’s answer the basic question,

What is Bencoding and Why is Bencoding?

Bencoding is a way to specify and organize data in a terse format. It supports the following types: byte strings, integers, lists, and dictionaries, no booleans, no special structs or any complex datatypes, just four major datatypes and we’re done.

Bencoding somewhat looks like this :

TypeFormatExampleIntegeri<number>ei42e → 42String<length>:<data>4:spam → "spam"Listl<items>el4:spami42eeDictionaryd<key-value pairs>ed3:cow3:moo4:spam4:eggse

Now the main reason why bencoding was developed by Bram Cohen (creator of BitTorrent protocols) is because, bittorrent identifies files using hashes. The info section inside a torrent file is hashed using SHA-1 to produce the info-hash.

‘Every peer in the swarm must calculate the exact same hash’

Why not JSON or other serializations?

JSON has many representations for the same data, for example :

{"name":"file.txt","length":12345}
{
"name": "file.txt",
"length": 12345
}
{
"length":12345,
"name":"file.txt"
}

These all are the same, yet their SHA-1 hashes will yield completely different. Thus bencoding ensures that :

  • For each possible (complex) value, there is only a single valid bencoding; i.e. there is a bijection between values and their encodings. This has the advantage that applications may compare bencoded values by comparing their encoded forms, eliminating the need to decode the values.
  • Bencoding serves similar purposes as data languages like JSON and YAML, allowing complex yet loosely structured data to be stored in a platform-independent way. Thus allowing a linear memory storage for complex data.

For the readers, you may also look into these blogs that provide better detail into specifications of the bittorrent protocol, here are some that helped my (and are helping still) throughout my journey :

  1. BitTorrentSpecification — TheoryOr : Pretty decent, convers almost all the topics, you can use the specifications here to translate into code logic.
  2. Bencode — Wikipedia — Specific for bencoding.
  3. bep_0003.rst_post — Some torrent blogs, there are multiple blogs here proposing improvements over the standard torrent protocol, thus do give a read if going deeper.

For all the folks waiting for the code implementation, let us begin.

( All the source files can be found on my GitHub : github.com/Achintya47/tnerroT, we’ll be dealing with the files btypes.c and bencoder.c )

The major problem, implementing dictionaries and lists in C

Well, BitTorrent supports nested and heterogeneous lists, for dictionaries, dictionary keys are restricted to strings, but values can be anything form another dictionary, to lists to integer or strings, thus the standard approach was to encode these datatypes using C-Structs.

typedef enum {
BINT,
BSTRING,
BDICT,
BLIST
} BType;

/* Standardised container for all four type of objects, no confusion */
struct BValue {
BType type;
union {
BInt integer;
BString string;
BList list;
BDict dict;
} value;
};

Further BInt, BString, BList and BDict are encoded as follows :

typedef struct {
long long value;
} BInt;

typedef struct {
char* data;
int length;
} BString;

You may think what was the need for encoding BInt inside a struct, well the reason was to ensure a consistent format, makes debugging easier and the function headers also become similar. Further BList :

typedef struct {
BValue **items;
int count;
int capacity;
} BList;

Here’s the magic, BValue** items points to any of the four datatypes, thus supporting heterogenous data.

typedef struct {
BString key;
BValue* value;
} BDictEntry;


typedef struct {
BDictEntry* entries;
int count;
int capacity;
} BDict;

As mentioned, keys are restricted to strings, and values can again, be any of the four datatypes mentioned in the BitTorrent protocol.

Get ACHINTYA SHARMA’s stories in your inbox

Join Medium for free to get updates from this writer.

Now personally it took a while for me to reach to this implementation, but once you get the idea behind using BValue, it gets simple. The only thing we now need to remember and get used to is how to reference to the data stored inside these containers. Example: here’s code for creating a BValue string :

BValue* v = malloc(sizeof(BValue));
if (v == NULL)
return NULL;
v->type = BSTRING;
v->value.string.data = malloc(length); // This reference needs to be kept in mind every time using BValue objects (structs)
memcpy(v->value.string.data, data, length);
v->value.string.length = length;
return v;

We’re still not touching the bencoding part, since we have these four datatypes, we also need to develop certain functions that help us work with these datatypes. For example, accessing dictionary values using keys, adding elements in a list, etc., Since these aren’t much related to Bencoding, rather these are simple logic, I’ll show code snippets of a few functions and then we move towards bencoding.

/**
* @brief : Append a BValue item to a BValue list
* @param : BValue* list and BValue* item to be appended
* @return : void
*/
void list_append(BValue* list, BValue* item) {
if (list == NULL || list->type != BLIST)
return;

BList* l = &list->value.list;

if (l->count == l->capacity) {
l->capacity *= 2;
/*
BUG FIX : reallocating BValue instead of BValue*, as items
is a BValue** pointer
*/
l->items = realloc(
l->items,
sizeof(BValue*) * l->capacity
);
}
l->items[l->count++] = item;
}
/**
* @brief : Invert a string key : BValue value pair in a BValue dict
* @param : BValue* dict, char* key, int key_length, BValue* value -> key's pair
* @return : void
*/
void dict_insert(BValue* dict, const char* key, int key_length, BValue* value) {

if (dict == NULL || dict->type != BDICT)
return;

BDict* d = &dict->value.dict;
if (d->count == d->capacity) {
d->capacity *= 2;
d->entries = realloc(
d->entries,
sizeof(BDictEntry) * d->capacity
);
}
BDictEntry* entry = &d->entries[d->count++];
entry->key.data = malloc(key_length);
memcpy(entry->key.data, key, key_length);
entry->key.length = key_length;
entry->value = value;
}

Now comes bencoding logic, let’s first look at integers :

So integers are encoded as, 32 -> i32e and for negative integers, -32 -> i-32e. Now there are a couple of ways of doing this, my initial code was like this:

I hadn’t thought of BTypes and BValues back then as I was only working on bencoding integers, I am getting the length of the integer + starting identifier ‘i’ + ending identifier ‘e’, you can get that using snprintf. Not going into much detail, but executing snprintf(NULL, 0, ….) will not print anything, rather it will simply return the number of bytes or characters that would be written to the buffer, meaning the total number of characters that should have been printed.

But still, I am overcomplicating stuff here, and also, when implementing the ‘encoder’ I will be taking input as raw numbers, and encoding them to a FILE* , not hold the encoded values in some variable or pointer. As encoding will be used mainly to create .torrent files, thus the improved and final version of bencoding integers is:

void encode_int(FILE* out, const BValue* value) {
fprintf(out, "i%llde", value->value.integer.value);
}

Wait, that’s it? Yes, this does the job (for now). fprintf : prints content to a file instead of a stdout buffer, “i%llde” here %lld represents the format specifier for a long long integer, that’s the integer’s value.

Also, remember I was talking about how using BTypes and BValues results in similar function headers, here, take a look at it yourself:

void encode_value(FILE* out, const BValue* value);
void encode_int(FILE* out, const BValue* value);
void encode_string(FILE* out, const BValue* value);
void encode_list(FILE* out, const BValue* value);
void encode_dict(FILE* out, const BValue* value);

Looking at bencoding strings now :

void encode_string(FILE* out, const BValue* value) {
const BString* str = &value->value.string;
fprintf(out, "%d:", str->length);
fwrite(str->data, 1, str->length, out);
}

Again, quite simple. First we’ll be printing the length of the string to the file buffer and then we’ll write string data using fwrite(). Keep in mind, strings do not have an ending identifier, thus after printing the raw string, our job’s done.

(I also want to explain each parameter of these standard functions, fwrite(), snprintf(), but I honestly can’t do it all. If you want deeper clarity, look into the official documentation of these function, it’ll help you get a better understanding)

For lists and dictionaries, we need to call the respective value’s encoder as well. Thus, a helper function was used, which takes in the BValue, and based on its BType, calls the respective encoder :

void encode_value(FILE* out, const BValue* value) {
switch (value->type)
{
case BINT:
encode_int(out, value);
break;
case BSTRING:
encode_string(out, value);
break;
case BLIST:
encode_list(out, value);
break;
case BDICT:
encode_dict(out, value);
break;
}

Encoding Lists (take a look, we’re passing the starting identifier ‘l’ encoding the value of the list and further at the end, passing the ending identifier ‘e’):

void encode_list(FILE* out, const BValue* value) {
const BList* list = &value->value.list;
fputc('l', out);
for (int i = 0; i < list->count; i++)
{
encode_value(out, list->items[i]);
}
fputc('e', out);
}

Encoding Dictionaries:

void encode_dict(FILE* out, const BValue* value) {
const BDict* dict = &value->value.dict;
fputc('d', out);
for (int i = 0; i < dict->count; i++)
{
BDictEntry* entry = &dict->entries[i];
fprintf(out, "%d:", entry->key.length);
fwrite(entry->key.data, 1, entry->key.length, out);
encode_value(out, entry->value);
}
fputc('e', out);
}

Well, we’re done with implementing Bencoding Encoders; to be honest, you won’t be using these functions anytime soon, rather the decoders will be the ones used the most initially. But if you’ve followed through so far, writing decoders will be a cakewalk for you all, detect the starting identifiers, if it is an ‘i’ : decode_int, if it is a ‘l’ : decode_list, if it is a ‘d’ : decode_dict, else if it is a number : decode_string.

Here’s the distributer/main caller function :

BValue* decode_value(FILE* in) {
int c = fgetc(in);
if (c == EOF)
return NULL;
if (c == 'i')
return decode_int(in);
if (c == 'l')
return decode_list(in);
if (c == 'd')
return decode_dict(in);
if (c >= '0' && c <= '9')
return decode_string(in, c);
return NULL;
}

Here are the respective decoders,

BValue* decode_int(FILE* in) {
char buffer[32];
int pos = 0;
int c;
while ((c = fgetc(in)) != 'e')
{
if (c == EOF)
return NULL;
/*
BUG FIX : Buffer Overflow risk, thus added buffer index validation
*/
if (pos >= (int)sizeof(buffer)-1)
return NULL;
buffer[pos++] = (char)c;
}
buffer[pos] = '\0';
/* atoll converts a string to a long long */
long long value = atoll(buffer);
return create_int(value);
}
BValue* decode_string(FILE* in, int first_digit) {
int length = first_digit - '0';
int c;
while ((c = fgetc(in)) != ':')
{
if (c == EOF)
return NULL;

/*
BUG FIX : Protecting against malformed input
Example : 4a:spam, here without validation, 'a' will be evaluated
and 'a' - '0' will yield and accumulate in the length
*/
if (c < '0' || c > '9')
return NULL;

/* Potential BUG here, a carefully crafted length input can lead to ovflow
thus causing a segmentation failure (some cyber sec stuff)
*/
length = length * 10 + (c - '0');
}
char* buffer = malloc(length);
/*
BUG FIX : File reading issue
*/
if (fread(buffer, 1, length, in) != (size_t)length) {
free(buffer);
return NULL;
}
BValue* value =
create_string(buffer, length);
free(buffer);
return value;
}
BValue* decode_list(FILE* in) {
BValue* list = create_list();
while (1)
{
int c = fgetc(in);
if (c == EOF)
return NULL;
if (c == 'e')
break;
ungetc(c, in);
BValue* item =
decode_value(in);
list_append(list, item);
}
return list;
}
BValue* decode_dict(FILE* in) {
BValue* dict = create_dict();
while (1)
{
int c = fgetc(in);
if (c == EOF)
return NULL;
if (c == 'e')
break;
if (!(c >= '0' && c <= '9'))
return NULL;
BValue* key =
decode_string(in, c);
BValue* value =
decode_value(in);
dict_insert(
dict,
key->value.string.data,
key->value.string.length,
value);
destroy_value(key);
}
return dict;
}

Now you can easily guess the function logic of functions create_string() and create_dict(), mainly it revolves around memory allocation and assigning values, however I am still providing the code of destroy_value() as memory de-allocation is also supposed to be done sequentially.

/* Well, we're all aware of de-allocating our allocated memories, thus for me
this is the most important function yet. */
void destroy_value(BValue* value) {
if (value == NULL)
return;
switch (value->type)
{
case BINT:
break;
case BSTRING:
free(value->value.string.data);
break;
case BLIST:
{
BList* list = &value->value.list;
for (int i = 0; i < list->count; i++)
destroy_value(list->items[i]);
free(list->items);
break;
}
case BDICT:
{
BDict* dict = &value->value.dict;
for (int i = 0; i < dict->count; i++)
{
free(dict->entries[i].key.data);
destroy_value(dict->entries[i].value);
}
free(dict->entries);
break;
}
}
free(value);
}

Well, we’ve successfully implemented Encoding and Decoding functionality for Bencoding protocol. We won’t be using these decoders yet, in the next part when parsing .torrent files, we’ll be using these decoders and finally we’ll be working with an actual .torrent sample file. Till then, Aloha! (Locke and Key reference)

--

--