Bencoder - A luau module for encoding / decoding bencoded data

Bencode (pronounced like B encode) is the encoding used by the peer-to-peer file sharing system BitTorrent for storing and transmitting loosely structured data.

Introduction:

I was taking a look at how peer-to-peer file sharing systems work and I came across a very interesting encoding system that BitTorrent uses. It’s called Bencoding and it’s a fairly easy encoding algorithm.

So I decided to port it to Luau! Well, have fun If you’re gonna use this module for anything! :happy3:

It supports four different types of values:

  • strings
  • numbers
  • tables (array’s and dictionary)
  • boolean (custom implementation made by me)

Example:

  • 32 gets encoded to: i32e
  • false gets encoded to: b0e
  • "Hello World" gets encoded to: 11:Hello World
  • {"foo", "bar"} gets encoded to: l3:foo3:bare
  • { foo = "bar" } gets encoded to: d3:foo3:bare

Source Code:

Bencoder.lua
local bencoder = {
	boolType = {
		["0"] = false,
		["1"] = true,
		[true] = "1",
		[false] = "0"
	}
}

type validTable = { [string | number | boolean] : string | number | boolean }
type validTypes = string | number | boolean | validTable

function getTableType(t : validTable)
	local index = 1

	for i, _ in t do
		if i ~= index then
			return "d"
		end

		index += 1
	end

	return "l"
end

function bencoder:encode(unencodedValue : validTypes): string
	local valueType = typeof(unencodedValue)

	if valueType == "string" then
		local stringLength = tostring(string.len(unencodedValue :: string)) 
		return stringLength..":"..unencodedValue :: string
	elseif valueType == "number" then
		return "i"..tostring(unencodedValue).."e"
	elseif valueType == "boolean" then
		return "b"..self.boolType[unencodedValue].."e"
	elseif valueType == "table" then
		local encodedString = getTableType(unencodedValue :: validTable)

		for key, value in unencodedValue :: validTable do
			local encodedKey = ""
			local encodedValue = self:encode(value)

			if string.sub(encodedString, 1, 1) == "d" then
				encodedKey = self:encode(key)
			end

			encodedString = encodedString..encodedKey..encodedValue
		end

		return encodedString.."e"
	else
		error("Argument #1 must be either string / number / boolean / validTable, got ", valueType)
	end
end

function bencoder:decode(encodedString : string, returnRemainingString : boolean?): validTypes
	assert(typeof(encodedString) == "string", "Argument #1 must be a string, got "..typeof(encodedString))

	local startCharacter = string.sub(encodedString, 1, 1)
	local decodedString, remainingString

	returnRemainingString = returnRemainingString or false

	if tonumber(startCharacter) then
		local firstColonIndex = string.find(encodedString, ":")

		assert(firstColonIndex, "The following string was incorrectly encoded: "..encodedString)

		local stringLength = tonumber(string.sub(encodedString, 1, firstColonIndex - 1))
		local stringStart = firstColonIndex + 1
		local stringEnd = firstColonIndex + stringLength

		decodedString, remainingString = string.sub(encodedString, stringStart, stringEnd), string.sub(encodedString, stringEnd + 1)
	elseif startCharacter == "i" then
		local stringEnd = string.find(encodedString, "e")
		local number = tonumber(string.sub(encodedString, 2, stringEnd - 1))

		assert(number, "The following number was incorrectly encoded: "..encodedString)

		decodedString, remainingString = number, string.sub(encodedString, stringEnd + 1)
	elseif startCharacter == "b" then
		local stringEnd = string.find(encodedString, "e")
		local bool = self.boolType[string.sub(encodedString, 2, stringEnd - 1)]

		assert(bool ~= nil, "The following boolean was incorrectly encoded: "..encodedString)

		decodedString, remainingString = bool, string.sub(encodedString, stringEnd + 1)
	elseif startCharacter == "l" then
		local array = {}

		remainingString = string.sub(encodedString, 2)

		while string.sub(remainingString, 1, 1) ~= "e" do
			local decodedValue, newRemainingString = table.unpack(self:decode(remainingString, true))

			table.insert(array, decodedValue)
			remainingString = newRemainingString or ""
		end

		decodedString, remainingString = array, string.sub(remainingString, 2)
	elseif startCharacter == "d" then
		local dictionary = {}

		remainingString = string.sub(encodedString, 2)

		while string.sub(remainingString, 1, 1) ~= "e" do
			local decodedKey, newRemainingString1 = table.unpack(self:decode(remainingString, true))
			local decodedValue, newRemainingString2 = table.unpack(self:decode(newRemainingString1, true))

			dictionary[decodedKey] = decodedValue
			remainingString = newRemainingString2 or ""
		end

		decodedString, remainingString = dictionary, string.sub(remainingString, 2)
	else
		error("Expected valid data type (string / number / boolean / table), got "..encodedString)
	end

	if returnRemainingString then
		return {
			decodedString,
			remainingString
		}
	else
		return decodedString
	end
end

return bencoder

API

Types:

type validTable = { [string | number | boolean] : string | number | boolean }
type validTypes = string | number | boolean | validTable

Methods:

bencoder:encode(unencodedValue : validTypes): string

  • Takes any of the valid types as an argument, returns an encoded string.

bencoder:decode(encodedString : string, returnRemainingString : boolean?): validTypes

  • Takes an encoded string as an argument, returns any of the valid types. Optional argument is the returnRemainingString which returns any value that was not decoded.
3 Likes

What is a bencode? I’ve never even heard of that

2 Likes

There is a link in the introduction.

1 Like

ok. clicked it and it got the right information

2 Likes