Skip to content

Commit

Permalink
bencode: Support unmarshalling into maps with non-string key types
Browse files Browse the repository at this point in the history
Fixes #952
  • Loading branch information
anacrolix committed Jun 26, 2024
1 parent cf7e9a9 commit 2f61e30
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 13 deletions.
47 changes: 35 additions & 12 deletions bencode/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ type dictField struct {
}

// Returns specifics for parsing a dict field value.
func getDictField(dict reflect.Type, key string) (_ dictField, err error) {
func getDictField(dict reflect.Type, key reflect.Value) (_ dictField, err error) {
// get valuev as a map value or as a struct field
switch k := dict.Kind(); k {
case reflect.Map:
Expand All @@ -293,13 +293,18 @@ func getDictField(dict reflect.Type, key string) (_ dictField, err error) {
mapValue.Set(reflect.MakeMap(dict))
}
// Assigns the value into the map.
// log.Printf("map type: %v", mapValue.Type())
mapValue.SetMapIndex(reflect.ValueOf(key).Convert(dict.Key()), value)
mapValue.SetMapIndex(key, value)
}
},
}, nil
case reflect.Struct:
return getStructFieldForKey(dict, key), nil
if key.Kind() != reflect.String {
// This doesn't make sense for structs. They have to use strings. If they didn't they
// should at least have things that convert to strings trivially and somehow much the
// bencode tag.
panic(key)
}
return getStructFieldForKey(dict, key.String()), nil
// if sf.r.PkgPath != "" {
// panic(&UnmarshalFieldError{
// Key: key,
Expand Down Expand Up @@ -382,11 +387,29 @@ func getStructFieldForKey(struct_ reflect.Type, key string) (f dictField) {
return
}

var structKeyType = reflect.TypeFor[string]()

func keyType(v reflect.Value) reflect.Type {
switch v.Kind() {
case reflect.Map:
return v.Type().Key()
case reflect.Struct:
return structKeyType
default:
return nil
}
}

func (d *Decoder) parseDict(v reflect.Value) error {
// At this point 'd' byte was consumed, now read key/value pairs
// At this point 'd' byte was consumed, now read key/value pairs.

// The key type does not need to be a string for maps.
keyType := keyType(v)
if keyType == nil {
return fmt.Errorf("cannot parse dicts into %v", v.Type())
}
for {
var keyStr string
keyValue := reflect.ValueOf(&keyStr).Elem()
keyValue := reflect.New(keyType).Elem()
ok, err := d.parseValue(keyValue)
if err != nil {
return fmt.Errorf("error parsing dict key: %w", err)
Expand All @@ -395,7 +418,7 @@ func (d *Decoder) parseDict(v reflect.Value) error {
return nil
}

df, err := getDictField(v.Type(), keyStr)
df, err := getDictField(v.Type(), keyValue)
if err != nil {
return fmt.Errorf("parsing bencode dict into %v: %w", v.Type(), err)
}
Expand All @@ -406,10 +429,10 @@ func (d *Decoder) parseDict(v reflect.Value) error {
var if_ interface{}
if_, ok = d.parseValueInterface()
if if_ == nil {
return fmt.Errorf("error parsing value for key %q", keyStr)
return fmt.Errorf("error parsing value for key %q", keyValue)
}
if !ok {
return fmt.Errorf("missing value for key %q", keyStr)
return fmt.Errorf("missing value for key %q", keyValue)
}
continue
}
Expand All @@ -419,11 +442,11 @@ func (d *Decoder) parseDict(v reflect.Value) error {
if err != nil {
var target *UnmarshalTypeError
if !(errors.As(err, &target) && df.Tags.IgnoreUnmarshalTypeError()) {
return fmt.Errorf("parsing value for key %q: %w", keyStr, err)
return fmt.Errorf("parsing value for key %q: %w", keyValue, err)
}
}
if !ok {
return fmt.Errorf("missing value for key %q", keyStr)
return fmt.Errorf("missing value for key %q", keyValue)
}
df.Get(v)(setValue)
}
Expand Down
2 changes: 1 addition & 1 deletion bencode/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func TestDecodeDictIntoUnsupported(t *testing.T) {
c := qt.New(t)
err := Unmarshal([]byte("d1:a1:be"), &i)
t.Log(err)
c.Check(err, qt.Not(qt.IsNil))
c.Check(err, qt.IsNotNil)
}

func TestUnmarshalDictKeyNotString(t *testing.T) {
Expand Down
38 changes: 38 additions & 0 deletions tests/issue-952/issue-952_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package issue_952

import (
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/types/infohash"
qt "github.com/frankban/quicktest"
"testing"
)

type scrapeResponse struct {
Files map[metainfo.Hash]scrapeResponseFile `bencode:"files"`
}

type scrapeResponseFile struct {
Complete int `bencode:"complete"`
Downloaded int `bencode:"downloaded"`
Incomplete int `bencode:"incomplete"`
}

// This tests unmarshalling to a map with a non-string dict key.
func TestUnmarshalStringToByteArray(t *testing.T) {
var s scrapeResponse
const hashStr = "\x05a~F\xfd{c\xd1`\xb8\xd9\x89\xceM\xb9t\x1d\\\x0b\xded"
err := bencode.Unmarshal([]byte("d5:filesd20:\x05a~F\xfd{c\xd1`\xb8\xd9\x89\xceM\xb9t\x1d\\\x0b\xded9:completedi1e10:downloadedi1eeee"), &s)
c := qt.New(t)
c.Assert(err, qt.IsNil)
c.Check(s.Files, qt.HasLen, 1)
file, ok := s.Files[(infohash.T)([]byte(hashStr))]
c.Assert(ok, qt.IsTrue)
c.Check(file, qt.Equals, scrapeResponseFile{
// Note that complete is misspelled in the example. I don't know why.
Complete: 0,
Downloaded: 1,
Incomplete: 0,
})

}

0 comments on commit 2f61e30

Please sign in to comment.