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}