const MaxLen, MaxDepth
Limits, so a decode stays bounded on chain.
Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.
Package bencode implements bencoding, the serialization format BitTorrent uses for .torrent files and tracker respons...
gno.land/p/moul/x/daily/bencode/v0Bencoding, 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.
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).
Limits, so a decode stays bounded on chain.
The four bencode types, plus the zero value.
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)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.
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.
NewDict returns an empty dictionary. Fill it with Value.Set, in any order: the encoding sorts.
NewInt returns an integer value.
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.
NewString returns a byte-string value.
Kind is the type of a Value: one of the four bencode types.
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.
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.
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.
Get returns the dictionary entry for key, or nil when it is absent or the value is not a dictionary.
Int returns the integer, or 0 for any other kind.
Keys returns the dictionary's keys in bytewise order, nil for other kinds.
Kind reports the value's type. A nil value is KindInvalid.
Len returns the number of list items or dictionary entries, 0 otherwise.
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.
Text returns the byte string, or "" for any other kind.