first commit

This commit is contained in:
2026-01-19 21:13:01 +09:00
commit c70d24be5c
28 changed files with 3674 additions and 0 deletions

173
bencode/bencode.go Normal file
View 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
View 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)
}
})
}
}