bencode_test.gno
7.86 Kb · 290 lines
1package bencode
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8)
9
10func TestDecodeRoundTrip(t *testing.T) {
11 cases := []struct {
12 name string
13 in string
14 }{
15 {"zero", "i0e"},
16 {"positive", "i42e"},
17 {"negative", "i-42e"},
18 {"empty string", "0:"},
19 {"string", "5:hello"},
20 {"string with colon", "9:a:b:c:d:e"},
21 {"empty list", "le"},
22 {"list of ints", "li1ei2ei3ee"},
23 {"nested list", "ll1:aee"},
24 {"empty dict", "de"},
25 {"dict", "d3:cow3:moo4:spam4:eggse"},
26 {"dict of list", "d4:spaml1:a1:bee"},
27 {"torrent-ish", "d8:announce9:udp://x:y4:infod6:lengthi1024e4:name4:fileee"},
28 }
29 for _, c := range cases {
30 t.Run(c.name, func(t *testing.T) {
31 v, err := Decode(c.in)
32 uassert.NoError(t, err)
33 uassert.Equal(t, c.in, v.Encode())
34 })
35 }
36}
37
38func TestDecodeRejects(t *testing.T) {
39 cases := []struct {
40 name string
41 in string
42 }{
43 {"empty input", ""},
44 {"bare letter", "x"},
45 {"unterminated int", "i42"},
46 {"empty int", "ie"},
47 {"int with leading zero", "i03e"},
48 {"negative zero", "i-0e"},
49 {"explicit plus", "i+1e"},
50 {"lone minus", "i-e"},
51 {"non digit int", "i4a2e"},
52 {"string too short", "5:abc"},
53 {"string length leading zero", "03:abc"},
54 {"string without colon", "5abcde"},
55 {"unterminated list", "li1e"},
56 {"unterminated dict", "d3:cow3:mooe4:spam"},
57 {"dict with odd entries", "d3:cowe"},
58 {"dict with int key", "di1e3:mooe"},
59 {"dict keys out of order", "d4:spam4:eggs3:cow3:mooe"},
60 {"dict duplicate keys", "d3:cow3:moo3:cow3:mooe"},
61 {"trailing data", "i1ei2e"},
62 {"trailing junk", "lexx"},
63 {"over MaxLen", "0:" + strings.Repeat("x", MaxLen)},
64 }
65 for _, c := range cases {
66 t.Run(c.name, func(t *testing.T) {
67 v, err := Decode(c.in)
68 uassert.Error(t, err)
69 if v != nil {
70 t.Errorf("want nil value on error, got %s", v.Encode())
71 }
72 })
73 }
74}
75
76func TestDecodeTooDeep(t *testing.T) {
77 ok := strings.Repeat("l", MaxDepth) + strings.Repeat("e", MaxDepth)
78 _, err := Decode(ok)
79 uassert.NoError(t, err)
80
81 deep := strings.Repeat("l", MaxDepth+1) + strings.Repeat("e", MaxDepth+1)
82 _, err = Decode(deep)
83 uassert.ErrorIs(t, err, ErrTooDeep)
84}
85
86func TestDecodeErrorKinds(t *testing.T) {
87 cases := []struct {
88 in string
89 want error
90 }{
91 {"i007e", ErrNotCanonical},
92 {"d4:spam4:eggs3:cow3:mooe", ErrNotCanonical},
93 {"i1ei2e", ErrTrailingData},
94 {"lx", ErrSyntax},
95 {"0:" + strings.Repeat("x", MaxLen), ErrTooLong},
96 }
97 for _, c := range cases {
98 t.Run(c.in, func(t *testing.T) {
99 _, err := Decode(c.in)
100 uassert.ErrorIs(t, err, c.want)
101 })
102 }
103}
104
105func TestAccessors(t *testing.T) {
106 v, err := Decode("d3:cowl1:ai7ee4:spam4:eggse")
107 uassert.NoError(t, err)
108 uassert.Equal(t, KindDict.String(), v.Kind().String())
109 uassert.Equal(t, 2, v.Len())
110 uassert.Equal(t, "cow,spam", strings.Join(v.Keys(), ","))
111
112 cow := v.Get("cow")
113 uassert.Equal(t, KindList.String(), cow.Kind().String())
114 uassert.Equal(t, 2, cow.Len())
115 uassert.Equal(t, "a", cow.At(0).Text())
116 uassert.Equal(t, int64(7), cow.At(1).Int())
117
118 uassert.Equal(t, "eggs", v.Get("spam").Text())
119 uassert.True(t, v.Get("absent") == nil)
120 uassert.True(t, cow.At(9) == nil)
121 uassert.True(t, cow.At(-1) == nil)
122}
123
124func TestWrongKindAccessorsAreZero(t *testing.T) {
125 s := NewString("hi")
126 uassert.Equal(t, int64(0), s.Int())
127 uassert.Equal(t, 0, s.Len())
128 uassert.True(t, s.At(0) == nil)
129 uassert.True(t, s.Get("k") == nil)
130 uassert.Equal(t, 0, len(s.Keys()))
131
132 n := NewInt(3)
133 uassert.Equal(t, "", n.Text())
134
135 var nilv *Value
136 uassert.Equal(t, KindInvalid.String(), nilv.Kind().String())
137 uassert.Equal(t, "", nilv.Encode())
138}
139
140func TestDictEncodesSortedWhateverTheInsertionOrder(t *testing.T) {
141 d := NewDict()
142 d.Set("spam", NewString("eggs"))
143 d.Set("cow", NewString("moo"))
144 d.Set("announce", NewInt(1))
145 uassert.Equal(t, "announce,cow,spam", strings.Join(d.Keys(), ","))
146 uassert.Equal(t, "d8:announcei1e3:cow3:moo4:spam4:eggse", d.Encode())
147}
148
149func TestSetReplacesInPlace(t *testing.T) {
150 d := NewDict()
151 d.Set("k", NewInt(1))
152 d.Set("k", NewInt(2))
153 uassert.Equal(t, 1, d.Len())
154 uassert.Equal(t, int64(2), d.Get("k").Int())
155}
156
157func TestEncodeBuiltValues(t *testing.T) {
158 l := NewList(NewInt(-3), NewString("x"), NewList())
159 uassert.Equal(t, "li-3e1:xlee", l.Encode())
160 uassert.Equal(t, "i-3e", NewInt(-3).Encode())
161 uassert.Equal(t, "4:spam", NewString("spam").Encode())
162}
163
164func TestKindString(t *testing.T) {
165 cases := []struct {
166 k Kind
167 want string
168 }{
169 {KindInvalid, "invalid"},
170 {KindInt, "int"},
171 {KindString, "string"},
172 {KindList, "list"},
173 {KindDict, "dict"},
174 {Kind(99), "invalid"},
175 }
176 for _, c := range cases {
177 uassert.Equal(t, c.want, c.k.String())
178 }
179}
180
181func TestCanonicalIsAFixedPoint(t *testing.T) {
182 // Every value this package builds encodes to something it accepts, and
183 // decoding that again changes nothing. That is what makes a bencode
184 // digest reproducible.
185 d := NewDict()
186 d.Set("b", NewList(NewInt(0), NewString("")))
187 d.Set("a", NewDict())
188 first := d.Encode()
189
190 v, err := Decode(first)
191 uassert.NoError(t, err)
192 uassert.Equal(t, first, v.Encode())
193}
194
195func TestNilValuesNeverReachTheEncoding(t *testing.T) {
196 // A nil child would encode to nothing, which silently produces a
197 // dictionary or a list that this package's own Decode rejects.
198 d := NewDict()
199 d.Set("k", nil)
200 uassert.Equal(t, 0, d.Len())
201 uassert.Equal(t, "de", d.Encode())
202
203 d.Set("k", NewInt(1))
204 d.Set("k", nil)
205 uassert.Equal(t, int64(1), d.Get("k").Int(), "a nil never removes a live entry")
206
207 l := NewList(NewInt(1), nil, NewString("x"))
208 uassert.Equal(t, 2, l.Len())
209 uassert.Equal(t, "li1e1:xe", l.Encode())
210
211 // The property that matters: anything Encode emits, Decode accepts.
212 for _, enc := range []string{d.Encode(), l.Encode()} {
213 _, err := Decode(enc)
214 uassert.NoError(t, err)
215 }
216}
217
218func TestStringLengthNeverOverflows(t *testing.T) {
219 // The length literal is bounded by nothing but MaxLen, so a 19-digit
220 // length fits in the input and in an int64. start+n must not be the
221 // thing that decides whether the slice is in range.
222 for _, in := range []string{
223 "9223372036854775807:x",
224 "9223372036854775806:x",
225 "8000000000000000000:x",
226 } {
227 v, err := Decode(in)
228 uassert.Error(t, err)
229 uassert.True(t, v == nil)
230 }
231}
232
233func TestEncodeIsTotal(t *testing.T) {
234 // A caller can build what no input can produce: a cycle, or nesting past
235 // MaxDepth. Encode has no error to return, so it must not be the thing
236 // that aborts a realm call.
237 d := NewDict()
238 d.Set("self", d)
239 uassert.Equal(t, 0, d.Len(), "a dictionary cannot hold itself")
240 uassert.Equal(t, "de", d.Encode())
241
242 a, b := NewDict(), NewDict()
243 a.Set("b", b)
244 b.Set("a", a)
245 uassert.Equal(t, "", a.Encode(), "an indirect cycle encodes to nothing")
246 uassert.Equal(t, "", b.Encode())
247
248 deep := NewList()
249 for i := 0; i < MaxDepth; i++ {
250 deep = NewList(deep)
251 }
252 uassert.Equal(t, "", deep.Encode(), "past MaxDepth there is no canonical encoding")
253
254 shallow := NewList()
255 for i := 0; i < MaxDepth-1; i++ {
256 shallow = NewList(shallow)
257 }
258 _, err := Decode(shallow.Encode())
259 uassert.NoError(t, err, "and everything under it still round-trips")
260}
261
262func TestZeroValuesNeverReachTheEncoding(t *testing.T) {
263 // Value is zero-constructible from outside the package, and that zero is
264 // KindInvalid, which writes nothing. Retaining one would make Len lie.
265 l := NewList(NewInt(1), &Value{}, NewString("x"))
266 uassert.Equal(t, 2, l.Len())
267 uassert.Equal(t, "li1e1:xe", l.Encode())
268
269 d := NewDict()
270 d.Set("k", &Value{})
271 uassert.Equal(t, 0, d.Len())
272 uassert.Equal(t, "de", d.Encode())
273}
274
275func TestSetNeverWritesThroughAStructCopy(t *testing.T) {
276 // *Value is the intended handle, but the struct is copyable, and slice
277 // headers in a copied struct share their backing array.
278 d := NewDict()
279 d.Set("a", NewInt(1))
280 d.Set("b", NewInt(2))
281
282 c := *d
283 c.Set("a", NewInt(99))
284 uassert.Equal(t, int64(1), d.Get("a").Int(), "the original is untouched")
285 uassert.Equal(t, int64(99), c.Get("a").Int())
286
287 c.Set("c", NewInt(3))
288 uassert.Equal(t, 2, d.Len(), "and an insertion does not leak either")
289 uassert.Equal(t, 3, c.Len())
290}