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.

bencodedemo.gno

6.84 Kb · 206 lines
  1// Package bencodedemo is a small gnoweb demo of the bencode codec provided by
  2// the [p/moul/x/daily/bencode](/p/moul/x/daily/bencode/v0) library: the four
  3// types, the round trip that makes an infohash reproducible, and the
  4// non-canonical inputs the decoder refuses instead of tidying up.
  5//
  6// It contains no codec logic of its own. Stateless, so Render is
  7// deterministic: every figure below is computed from the fixed tables at the
  8// top of this file, never from chain state.
  9package bencodedemo
 10
 11import (
 12	"strconv"
 13	"strings"
 14
 15	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
 16	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/bencode/v0"
 17)
 18
 19// samples are valid, canonical encodings, one per type plus the composites.
 20var samples = []string{
 21	"i42e",
 22	"i-7e",
 23	"4:spam",
 24	"0:",
 25	"li1ei2ei3ee",
 26	"d3:cow3:moo4:spam4:eggse",
 27}
 28
 29// torrent is the shape the format exists for: a tracker announce beside an
 30// info dictionary whose re-encoding is what gets hashed.
 31const torrent = "d8:announce21:udp://tracker.example4:infod6:lengthi262144e4:name8:disk.img12:piece lengthi16384eee"
 32
 33const backLink = "[back to the demo](/r/moul/x/daily/bencodedemo/v0)\n"
 34
 35// MaxInput bounds what this page will decode. The library accepts up to
 36// bencode.MaxLen bytes, but a Render runs under the chain's query gas meter
 37// and gnoweb does not bound a render argument, so the page stops well short of
 38// the library's own limit. 512 bytes is more than anyone pastes by hand.
 39const MaxInput = 512
 40
 41// rejected are inputs a sloppy decoder accepts and silently rewrites. Each one
 42// would change the digest of whatever contained it.
 43var rejected = []struct {
 44	in  string
 45	why string
 46}{
 47	{"i007e", "leading zeros in an integer"},
 48	{"i-0e", "negative zero"},
 49	{"03:abc", "leading zeros in a string length"},
 50	{"d4:spam4:eggs3:cow3:mooe", "dictionary keys out of order"},
 51	{"d3:cow3:moo3:cow3:mooe", "duplicate dictionary key"},
 52	{"i1ei2e", "a second value after the first"},
 53}
 54
 55// Render renders the demo for gnoweb.
 56//
 57//	Render("") / Render("/")        -> the types, the round trip, the refusals
 58//	Render("/<bencoded value>")     -> decode that input and show the tree
 59func Render(path string) string {
 60	var b strings.Builder
 61
 62	b.WriteString("# Bencode\n\n")
 63	b.WriteString("The BitTorrent serialization format, demoing the ")
 64	b.WriteString("[`p/moul/x/daily/bencode`](/p/moul/x/daily/bencode/v0) library.\n\n")
 65
 66	if in := parseArg(path); in != "" {
 67		b.WriteString("## Your input\n\n")
 68		if len(in) > MaxInput {
 69			b.WriteString(strconv.Itoa(len(in)) + " bytes. This page decodes at most ")
 70			b.WriteString(strconv.Itoa(MaxInput) + ".\n\n")
 71			b.WriteString(backLink)
 72			return b.String()
 73		}
 74		b.WriteString(ui.Inline(in) + "\n\n")
 75		v, err := bencode.Decode(in)
 76		if err != nil {
 77			b.WriteString("Rejected: " + ui.Inline(err.Error()) + "\n\n")
 78			b.WriteString(backLink)
 79			return b.String()
 80		}
 81		tree(&b, v, 0)
 82		b.WriteString("\nRe-encodes to " + ui.Inline(v.Encode()))
 83		if v.Encode() == in {
 84			b.WriteString(", byte for byte.\n\n")
 85		} else {
 86			b.WriteString(", which differs from the input.\n\n")
 87		}
 88		b.WriteString(backLink)
 89		return b.String()
 90	}
 91
 92	b.WriteString("## The four types\n\n")
 93	tt := ui.NewTable("encoding", "kind", "reads as")
 94	for _, s := range samples {
 95		v, err := bencode.Decode(s)
 96		if err != nil {
 97			tt.Row("`"+s+"`", "error", err.Error())
 98			continue
 99		}
100		tt.Row("`"+s+"`", v.Kind().String(), summarize(v))
101	}
102	b.WriteString(tt.String() + "\n")
103
104	b.WriteString("## A torrent, decoded\n\n")
105	t, err := bencode.Decode(torrent)
106	if err != nil {
107		b.WriteString("the sample failed to decode: " + ui.Inline(err.Error()) + "\n")
108		return b.String()
109	}
110	tree(&b, t, 0)
111	b.WriteString("\nThe `info` sub-dictionary re-encodes to ")
112	info := t.Get("info")
113	b.WriteString("`" + info.Encode() + "`, ")
114	b.WriteString(strconv.Itoa(len(info.Encode())) + " bytes. ")
115	b.WriteString("Hash those bytes and you have the infohash, which is why the ")
116	b.WriteString("decoder is not allowed to tidy anything up on the way through.\n\n")
117
118	b.WriteString("## What it refuses\n\n")
119	rt := ui.NewTable("input", "why", "error")
120	for _, r := range rejected {
121		_, err := bencode.Decode(r.in)
122		msg := "accepted"
123		if err != nil {
124			msg = err.Error()
125		}
126		rt.Row("`"+r.in+"`", r.why, msg)
127	}
128	b.WriteString(rt.String() + "\n")
129
130	b.WriteString("> Every one of those parses. A decoder that accepted them ")
131	b.WriteString("and re-emitted the canonical form would change the identity ")
132	b.WriteString("of the document it was handed.\n\n")
133	b.WriteString("Decode your own: ")
134	b.WriteString("[`d3:cow3:mooe`](/r/moul/x/daily/bencodedemo/v0:d3:cow3:mooe)\n")
135	return b.String()
136}
137
138// summarize describes a value in one cell, without recursing.
139func summarize(v *bencode.Value) string {
140	switch v.Kind() {
141	case bencode.KindInt:
142		return strconv.FormatInt(v.Int(), 10)
143	case bencode.KindString:
144		if v.Text() == "" {
145			return "_(empty string)_"
146		}
147		return ui.Cell(v.Text())
148	case bencode.KindList:
149		return strconv.Itoa(v.Len()) + " item(s)"
150	case bencode.KindDict:
151		return strconv.Itoa(v.Len()) + " key(s): " + ui.Cell(strings.Join(v.Keys(), ", "))
152	default:
153		return "_(nothing)_"
154	}
155}
156
157// tree writes a value as a nested markdown list. It takes the builder rather
158// than returning a string: a dictionary of a few hundred entries is reachable
159// inside MaxInput, and += over that many lines copies the page once per line.
160func tree(b *strings.Builder, v *bencode.Value, depth int) {
161	pad := strings.Repeat("  ", depth)
162	switch v.Kind() {
163	case bencode.KindList:
164		b.WriteString(pad + "- list, " + strconv.Itoa(v.Len()) + " item(s)\n")
165		for i := 0; i < v.Len(); i++ {
166			tree(b, v.At(i), depth+1)
167		}
168	case bencode.KindDict:
169		b.WriteString(pad + "- dict, " + strconv.Itoa(v.Len()) + " key(s)\n")
170		for _, k := range v.Keys() {
171			child := v.Get(k)
172			if child.Kind() == bencode.KindList || child.Kind() == bencode.KindDict {
173				b.WriteString(pad + "  - **" + ui.Inline(k) + "**:\n")
174				tree(b, child, depth+2)
175				continue
176			}
177			b.WriteString(pad + "  - **" + ui.Inline(k) + "**: " + leaf(child) + "\n")
178		}
179	default:
180		b.WriteString(pad + "- " + leaf(v) + "\n")
181	}
182}
183
184// leaf renders an int or a string as one line.
185func leaf(v *bencode.Value) string {
186	switch v.Kind() {
187	case bencode.KindInt:
188		return "int `" + strconv.FormatInt(v.Int(), 10) + "`"
189	case bencode.KindString:
190		if v.Text() == "" {
191			return "string, empty"
192		}
193		return "string " + ui.Inline(v.Text())
194	default:
195		return "nothing"
196	}
197}
198
199// parseArg takes the whole render path as the value to decode: a bencoded
200// string contains colons and digits, and splitting on them would mangle it.
201// Only the routing slash comes off. Trimming whitespace would eat a byte the
202// caller declared the length of, so "6:hello " would arrive as a five byte
203// string claiming six and be rejected for the trim rather than for itself.
204func parseArg(path string) string {
205	return strings.TrimPrefix(path, "/")
206}