first commit
This commit is contained in:
173
bencode/bencode.go
Normal file
173
bencode/bencode.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package bencode
|
||||
|
||||
// bencode types:
|
||||
// string <len>:<data> "4:spam"
|
||||
// int i<n>e "i42e"
|
||||
// list l<items>e "l4:spam4:eggse"
|
||||
// dict d<kv pairs>e "d3:cow3:mooe"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Decode(data []byte) (any, int, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, 0, fmt.Errorf("empty data")
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case 'i':
|
||||
return decodeInt(data)
|
||||
case 'l':
|
||||
return decodeList(data)
|
||||
case 'd':
|
||||
return decodeDict(data)
|
||||
default:
|
||||
if data[0] >= '0' && data[0] <= '9' {
|
||||
return DecodeString(data)
|
||||
}
|
||||
return nil, 0, fmt.Errorf("invalid bencode: %q", data[0])
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeString(data []byte) (string, int, error) {
|
||||
i := 0
|
||||
for i < len(data) && data[i] != ':' {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return "", 0, fmt.Errorf("bencode: missing ':' in string at %d", i)
|
||||
}
|
||||
n, err := strconv.Atoi(string(data[:i]))
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
i++
|
||||
if i+n > len(data) {
|
||||
return "", 0, fmt.Errorf("bencode: truncated string at %d", i)
|
||||
}
|
||||
return string(data[i : i+n]), i + n, nil
|
||||
}
|
||||
|
||||
func decodeInt(data []byte) (int64, int, error) {
|
||||
if len(data) < 3 {
|
||||
return 0, 0, fmt.Errorf("bencode: int too short at 0")
|
||||
}
|
||||
i := 1
|
||||
for i < len(data) && data[i] != 'e' {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return 0, 0, fmt.Errorf("bencode: missing 'e' in int at %d", i)
|
||||
}
|
||||
val, err := strconv.ParseInt(string(data[1:i]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return val, i + 1, nil
|
||||
}
|
||||
|
||||
func decodeList(data []byte) ([]any, int, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, 0, fmt.Errorf("bencode: empty list at 0")
|
||||
}
|
||||
|
||||
var list []any
|
||||
i := 1
|
||||
for i < len(data) && data[i] != 'e' {
|
||||
v, n, err := Decode(data[i:])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
list = append(list, v)
|
||||
i += n
|
||||
}
|
||||
if i >= len(data) {
|
||||
return nil, 0, fmt.Errorf("bencode: truncated list at %d", i)
|
||||
}
|
||||
return list, i + 1, nil
|
||||
}
|
||||
|
||||
func decodeDict(data []byte) (map[string]any, int, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, 0, fmt.Errorf("bencode: empty dict at 0")
|
||||
}
|
||||
|
||||
d := make(map[string]any)
|
||||
i := 1
|
||||
for i < len(data) && data[i] != 'e' {
|
||||
k, n, err := DecodeString(data[i:])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
i += n
|
||||
v, n, err := Decode(data[i:])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
d[k] = v
|
||||
i += n
|
||||
}
|
||||
if i >= len(data) {
|
||||
return nil, 0, fmt.Errorf("bencode: truncated dict at %d", i)
|
||||
}
|
||||
return d, i + 1, nil
|
||||
}
|
||||
|
||||
func Encode(v any) ([]byte, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return encodeString(v), nil
|
||||
case []byte:
|
||||
return encodeString(string(v)), nil
|
||||
case int:
|
||||
return encodeInt(int64(v)), nil
|
||||
case int64:
|
||||
return encodeInt(v), nil
|
||||
case []any:
|
||||
return encodeList(v)
|
||||
case map[string]any:
|
||||
return encodeDict(v)
|
||||
}
|
||||
return nil, fmt.Errorf("cannot encode %T", v)
|
||||
}
|
||||
|
||||
func encodeString(s string) []byte {
|
||||
return fmt.Appendf(nil, "%d:%s", len(s), s)
|
||||
}
|
||||
|
||||
func encodeInt(n int64) []byte {
|
||||
return fmt.Appendf(nil, "i%de", n)
|
||||
}
|
||||
|
||||
func encodeList(list []any) ([]byte, error) {
|
||||
buf := []byte{'l'}
|
||||
for _, v := range list {
|
||||
enc, err := Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, enc...)
|
||||
}
|
||||
return append(buf, 'e'), nil
|
||||
}
|
||||
|
||||
func encodeDict(d map[string]any) ([]byte, error) {
|
||||
keys := make([]string, 0, len(d))
|
||||
for k := range d {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
buf := []byte{'d'}
|
||||
for _, k := range keys {
|
||||
buf = append(buf, encodeString(k)...)
|
||||
enc, err := Encode(d[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, enc...)
|
||||
}
|
||||
return append(buf, 'e'), nil
|
||||
}
|
||||
113
bencode/bencode_test.go
Normal file
113
bencode/bencode_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package bencode
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want any
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
in: "4:spam",
|
||||
want: "spam",
|
||||
},
|
||||
{
|
||||
in: "0:",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: "i42e",
|
||||
want: int64(42),
|
||||
},
|
||||
{
|
||||
in: "i-42e",
|
||||
want: int64(-42),
|
||||
},
|
||||
{
|
||||
in: "i0e",
|
||||
want: int64(0),
|
||||
},
|
||||
{
|
||||
in: "",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
got, _, err := Decode([]byte(tt.in))
|
||||
if tt.err {
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "string",
|
||||
in: "spam",
|
||||
want: "4:spam",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
in: "",
|
||||
want: "0:",
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
in: int64(42),
|
||||
want: "i42e",
|
||||
},
|
||||
{
|
||||
name: "negative int",
|
||||
in: int64(-42),
|
||||
want: "i-42e",
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
in: []any{"a", "b"},
|
||||
want: "l1:a1:be",
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
in: []any{},
|
||||
want: "le",
|
||||
},
|
||||
{
|
||||
name: "dict",
|
||||
in: map[string]any{"b": int64(2), "a": int64(1)},
|
||||
want: "d1:ai1e1:bi2ee",
|
||||
},
|
||||
{
|
||||
name: "empty dict",
|
||||
in: map[string]any{},
|
||||
want: "de",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Encode(tt.in)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("got %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user