/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/bencode/v0
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.