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.

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}