Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v0 source pure

Package bencode implements bencoding, the serialization format BitTorrent uses for .torrent files and tracker respons...

Readme View source

gno.land/p/moul/x/daily/bencode/v0

Bencoding, the BitTorrent serialization format: Decode, Value.Encode, NewInt, NewString, NewList, NewDict.

 1import "gno.land/p/moul/x/daily/bencode/v0"
 2
 3v, err := bencode.Decode("d3:cow3:moo4:spam4:eggse")
 4v.Get("cow").Text()   // "moo"
 5v.Keys()              // ["cow", "spam"]
 6v.Encode()            // "d3:cow3:moo4:spam4:eggse", byte for byte
 7
 8d := bencode.NewDict()
 9d.Set("spam", bencode.NewString("eggs"))
10d.Set("cow", bencode.NewString("moo"))
11d.Encode()            // "d3:cow3:moo4:spam4:eggse": insertion order does not matter

Four types and no escape syntax. Integers (i42e), byte strings (5:hello), lists (l...e) and dictionaries (d...e). No float, no boolean, no null. A string carries its length instead of quoting rules, so a value can contain any byte, including the delimiters.

The point is canonicality, and that is why the decoder is strict. One value has exactly one encoding: dictionary keys sorted bytewise, no leading zeros, no negative zero, no trailing bytes. A BitTorrent infohash is the digest of a re-encoded info dictionary, so a decoder that accepts a sloppy encoding and re-emits a tidy one silently changes the identity of what it parsed. This one rejects non-canonical input rather than normalizing it, which makes the round trip a guarantee: for every input Decode accepts, Encode returns the original bytes. TestCanonicalIsAFixedPoint pins that property.

ErrNotCanonical is kept apart from ErrSyntax on purpose: "this parses but no encoder would emit it" is a different message to show a user than "this is not bencode".

Nothing that encodes to nothing can be stored: Set and NewList ignore a nil and the zero Value, because either one would emit a key with no value, or a list whose Len disagrees with its encoding, and Decode would then reject what Encode produced.

Encode is total. It returns "" for anything with no canonical encoding, which means a value nested deeper than MaxDepth and therefore any cycle a caller assembled by hand. Set refuses the direct case (a dictionary holding itself) outright; two dictionaries holding each other cannot be caught at insert time without walking the whole structure, so the depth bound in Encode is what keeps them from aborting the call. "" is not a legal bencode document, so a caller can test for it. Set also never writes through the slices an earlier Set produced, so copying the struct behind the pointer cannot leave two values partially aliased.

Accessors never panic. Int, Text, Len, At and Get return the zero value for the wrong kind or a nil receiver, so walking a document of unknown shape cannot abort a realm call. Input is bounded by MaxLen (8192 bytes) and MaxDepth (16) so gas stays predictable.

Not implemented: streaming, raw []byte values distinct from strings, and integers wider than int64.

Live demo: r/moul/x/daily/bencodedemo · render it at /r/moul/x/daily/bencodedemo/v0.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package bencode implements bencoding, the serialization format BitTorrent uses for .torrent files and tracker responses, as a pure reusable package.

Four types, and that is the whole format: integers (i42e), byte strings (5:hello), lists (l...e) and dictionaries (d...e). There is no float, no boolean and no null, which is most of why the format fits in one file.

The reason to reach for it over JSON is that bencode is CANONICAL: one value has exactly one encoding. Dictionary keys are sorted bytewise, integers carry no leading zeros and no negative zero, and strings carry their length instead of an escape syntax. An infohash is the digest of a re-encoded sub-dictionary, so a decoder that accepts a sloppy encoding and re-emits a tidy one silently changes the identity of the thing it parsed.

This decoder is therefore strict: it REJECTS non-canonical input rather than normalizing it, so for every input it accepts, Decode followed by Encode returns the original bytes unchanged.

A live demo of this package is at r/moul/x/daily/bencodedemo(/r/moul/x/daily/bencodedemo/v0).

Constants 2

const MaxLen, MaxDepth

1const (
2	// MaxLen is the longest input Decode will look at.
3	MaxLen = 8192
4	// MaxDepth is how deeply lists and dictionaries may nest.
5	MaxDepth = 16
6)
source

Limits, so a decode stays bounded on chain.

Variables 1

var ErrTooLong, ErrSyntax, ErrNotCanonical, ErrTrailingData, ErrTooDeep

 1var (
 2	// ErrTooLong is returned when input is longer than [MaxLen].
 3	ErrTooLong = errors.New("bencode: input too long")
 4	// ErrSyntax is returned when input is not bencode at all.
 5	ErrSyntax = errors.New("bencode: malformed input")
 6	// ErrNotCanonical is returned for input that is parseable but could never
 7	// have been produced by an encoder: a leading zero, a negative zero, or
 8	// dictionary keys that are unsorted or duplicated.
 9	ErrNotCanonical = errors.New("bencode: input is not canonical")
10	// ErrTrailingData is returned when bytes follow the top-level value.
11	ErrTrailingData = errors.New("bencode: trailing data after top-level value")
12	// ErrTooDeep is returned when nesting exceeds [MaxDepth].
13	ErrTooDeep = errors.New("bencode: nesting too deep")
14)
source

Errors returned by Decode. They are distinguishable on purpose: a caller feeding the decoder a user-pasted torrent wants to say which of the four strictness rules the paste broke.

Functions 5

func Decode

1func Decode(s string) (*Value, error)
source

Decode parses one bencode value and rejects anything after it. It also rejects input that is parseable but not canonical, which is the whole point: see the package doc.

func NewDict

1func NewDict() *Value
source

NewDict returns an empty dictionary. Fill it with Value.Set, in any order: the encoding sorts.

func NewInt

1func NewInt(n int64) *Value
source

NewInt returns an integer value.

func NewList

1func NewList(items ...*Value) *Value
source

NewList returns a list of the given items, in order. A nil item, and the zero Value, are dropped: both encode to nothing, which would turn a three item list into a two item encoding that still reports a length of three.

func NewString

1func NewString(s string) *Value
source

NewString returns a byte-string value.

Types 2

type Kind

ident
1type Kind int
source

Kind is the type of a Value: one of the four bencode types.

Methods on Kind

func String

method on Kind
1func (k Kind) String() string
source

String names the kind, for rendering and error messages.

type Value

struct
1type Value struct {
2	kind  Kind
3	num   int64
4	text  string
5	elems []*Value
6	keys  []string
7	vals  []*Value
8}
source

Value is one bencode value. Build one with NewInt, NewString, NewList or NewDict, or get one from Decode. Accessors that do not match the value's kind return the zero value rather than panicking, so a chain of lookups through a document of unknown shape never aborts a realm call.

Methods on Value

func At

method on Value
1func (v *Value) At(i int) *Value
source

At returns the i-th list item, or the i-th dictionary value in key order. Out of range, or any other kind, returns nil.

func Encode

method on Value
1func (v *Value) Encode() string
source

Encode returns the canonical bencoding of the value, or "" for a value that has no canonical encoding: a nil, the zero Value, anything nested deeper than MaxDepth, and therefore any cycle a caller assembled by hand. "" is not a legal bencode document, so a caller can test for it.

Encode has no error to return and is called from a realm's Render, so it is total by construction: the depth bound is what keeps a structure the decoder could never have produced from aborting the call that encodes it.

func Get

method on Value
1func (v *Value) Get(key string) *Value
source

Get returns the dictionary entry for key, or nil when it is absent or the value is not a dictionary.

func Int

method on Value
1func (v *Value) Int() int64
source

Int returns the integer, or 0 for any other kind.

func Keys

method on Value
1func (v *Value) Keys() []string
source

Keys returns the dictionary's keys in bytewise order, nil for other kinds.

func Kind

method on Value
1func (v *Value) Kind() Kind
source

Kind reports the value's type. A nil value is KindInvalid.

func Len

method on Value
1func (v *Value) Len() int
source

Len returns the number of list items or dictionary entries, 0 otherwise.

func Set

method on Value
1func (v *Value) Set(key string, val *Value) *Value
source

Set inserts or replaces a dictionary entry and returns the dictionary, so calls chain. Four things are refused rather than stored, each because it would make the encoding disagree with the structure: a receiver that is not a dictionary, a nil value, the zero Value, and the dictionary itself. The first three encode to nothing, which would emit a key with no value and produce a dictionary this package's own Decode rejects; the fourth is a cycle. Removing an entry is deliberately not spelled "set it to nil", because nothing here removes.

An indirect cycle (two dictionaries holding each other) is not detectable here without walking the whole structure on every insert, so it is caught by Value.Encode instead, which is depth-bounded and returns "".

It never writes through the slices a previous Set produced. gno lets a caller copy the struct behind the pointer, and two copies sharing a backing array would see each other's replacements but not each other's insertions.

func Text

method on Value
1func (v *Value) Text() string
source

Text returns the byte string, or "" for any other kind.

Dependencies

Imports 3

/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/bencode/v0

Imported by indexer

Source Files 4