Skip to content

Instantly share code, notes, and snippets.

@bemasher
Last active August 29, 2015 14:01
Show Gist options
  • Save bemasher/85051f0d65ccbaa8c5e1 to your computer and use it in GitHub Desktop.
Save bemasher/85051f0d65ccbaa8c5e1 to your computer and use it in GitHub Desktop.
Dirty Hack #37: Reflection is hard. Have a decoder you've written in Go and don't want to write all the logic for reflection to store data in native types? Does your decoder output map[string]interface{} and []interface{} flavored data? Problem solved: just marshal and unmarshal using the JSON package.
package main
import (
"bufio"
"log"
"strconv"
)
// Bencoding Specification
// Strings are length-prefixed base ten followed by a colon and the string.
// For example 4:spam corresponds to 'spam'.
// Integers are represented by an 'i' followed by the number in base 10
// followed by an 'e'. For example i3e corresponds to 3 and i-3e corresponds
// to -3. Integers have no size limitation. i-0e is invalid. All encodings
// with a leading zero, such as i03e, are invalid, other than i0e, which of
// course corresponds to 0.
// Lists are encoded as an 'l' followed by their elements (also bencoded)
// followed by an 'e'. For example l4:spam4:eggse corresponds to ['spam',
// 'eggs'].
// Dictionaries are encoded as a 'd' followed by a list of alternating keys
// and their corresponding values followed by an 'e'. For example,
// d3:cow3:moo4:spam4:eggse corresponds to {'cow': 'moo', 'spam': 'eggs'} and
// d4:spaml1:a1:bee corresponds to {'spam': ['a', 'b']}. Keys must be strings
// and appear in sorted order (sorted as raw strings, not alphanumerics).
type Decoder struct {
*bufio.Reader
}
func NewDecoder(buf *bufio.Reader) Decoder {
return Decoder{buf}
}
func (d Decoder) Decode() interface{} {
return d.decode()
}
func (d Decoder) decode() interface{} {
data, err := d.Peek(1)
if err != nil {
log.Fatal("Error reading byte:", err)
}
switch data[0] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return d.decodeString()
case 'i':
return d.decodeInt()
case 'd':
return d.decodeMap()
case 'l':
return d.decodeList()
case 'e':
return d.decodeList()
}
log.Println("This should never happen.")
return nil
}
func (d Decoder) decodeMap() (m map[string]interface{}) {
c, err := d.ReadByte()
if err != nil || c != 'd' {
log.Fatal("Invalid map prefix:", err)
}
m = make(map[string]interface{})
for {
c, err := d.Peek(1)
if err != nil {
log.Fatal("Error decoding map:", err)
}
if c[0] == 'e' {
_, err := d.ReadByte()
if err != nil {
log.Fatal("Error reading termination:", err)
}
return
}
key := d.decodeString()
val := d.decode()
m[key] = val
}
return
}
func (d Decoder) decodeList() (l []interface{}) {
c, err := d.ReadByte()
if err != nil || c != 'l' {
log.Fatal("Invalid list prefix:", err)
}
for {
c, err := d.Peek(1)
if err != nil {
log.Fatal("Error decoding list:", err)
}
if c[0] == 'e' {
_, err := d.ReadByte()
if err != nil {
log.Fatal("Error reading termination:", err)
}
return
}
l = append(l, d.decode())
}
return
}
func (d Decoder) decodeString() (s string) {
lenString, err := d.ReadString(':')
if err != nil {
log.Fatal("Error reading string length:", err)
}
lenString = lenString[:len(lenString)-1]
length, err := strconv.ParseInt(lenString, 10, 64)
if err != nil {
log.Fatal("Error parsing string length:", err)
}
stringBytes := make([]byte, length)
_, err = d.Read(stringBytes)
if err != nil {
log.Fatal("Error reading string:", err)
}
return string(stringBytes)
}
func (d Decoder) decodeInt() (i int) {
data, err := d.ReadString('e')
if err != nil {
log.Fatal("Error finding int sentinel:", err)
}
data = data[1 : len(data)-1]
val, err := strconv.ParseInt(data, 10, 64)
if err != nil {
log.Fatal("Error parsing int:", err)
}
return int(val)
}
d8:announce44:udp://tracker.openbittorrent.com:80/announce13:announce-listll44:udp://tracker.openbittorrent.com:80/announceel38:udp://tracker.publicbt.com:80/announceel32:udp://tracker.ccc.de:80/announceee10:created by16:BitTorrent/7.9.113:creation datei1401190995e8:encoding5:UTF-84:infod5:filesld6:lengthi4003e4:pathl12:interface.goeed6:lengthi4003e4:pathl10:reflect.goeed6:lengthi379e4:pathl12:file.torrenteee4:name7:bencode12:piece lengthi16384e6:pieces20:w&��'�~=W4���i̓ee
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"os"
"github.com/kr/pretty"
)
type Torrent struct {
Announce string `json:"announce"`
AnnounceList [][]string `json:"announce-list,omitempty"`
CreatedBy string `json:"created by,omitempty"`
CreationDate int `json:"creation date,omitempty"`
Encoding string `json:"encoding,omitempty"`
Info struct {
Name string `json:"name"`
File
Files []File `json:"files,omitempty"`
PieceLength int `json:"piece length"`
Pieces string `json:"pieces"`
Private int `json:"private,omitempty"`
} `json:"info"`
}
type File struct {
Path []string `json:"path"`
Length int `json:"length"`
MD5Sum string `json:"md5sum,omitempty"`
}
func init() {
log.SetFlags(log.Lshortfile)
}
func main() {
filename := "directory.torrent"
torrentFile, err := os.Open(filename)
if err != nil {
log.Fatal("Error opening torrent:", err)
}
defer torrentFile.Close()
torrentBuf := bufio.NewReader(torrentFile)
decoder := NewDecoder(torrentBuf)
data := decoder.Decode()
r, w := io.Pipe()
jsonEncoder := json.NewEncoder(w)
jsonDecoder := json.NewDecoder(r)
var torrent Torrent
go jsonEncoder.Encode(data)
jsonDecoder.Decode(&torrent)
fmt.Println("Before:")
pretty.Println(data)
fmt.Println("After:")
pretty.Println(torrent)
}
Before:
map[string]interface {}{
"announce": "udp://tracker.openbittorrent.com:80/announce",
"announce-list": []interface {}{
[]interface {}{
"udp://tracker.openbittorrent.com:80/announce",
},
[]interface {}{
"udp://tracker.publicbt.com:80/announce",
},
[]interface {}{
"udp://tracker.ccc.de:80/announce",
},
},
"created by": "BitTorrent/7.9.1",
"creation date": int(1401190995),
"encoding": "UTF-8",
"info": map[string]interface {}{
"files": []interface {}{
map[string]interface {}{
"length": int(4003),
"path": []interface {}{
"interface.go",
},
},
map[string]interface {}{
"length": int(4003),
"path": []interface {}{
"reflect.go",
},
},
map[string]interface {}{
"length": int(379),
"path": []interface {}{
"file.torrent",
},
},
},
"name": "bencode",
"piece length": int(16384),
"pieces": "w&\xef\xf4\x03'\xc7~=W4\a\x86\x04\x19\xfd\xb4i̓",
},
}
After:
main.Torrent{
Announce: "udp://tracker.openbittorrent.com:80/announce",
AnnounceList: {
{"udp://tracker.openbittorrent.com:80/announce"},
{"udp://tracker.publicbt.com:80/announce"},
{"udp://tracker.ccc.de:80/announce"},
},
CreatedBy: "BitTorrent/7.9.1",
CreationDate: 1401190995,
Encoding: "UTF-8",
Info: {
Name: "bencode",
File: {},
Files: {
{
Path: {"interface.go"},
Length: 4003,
MD5Sum: "",
},
{
Path: {"reflect.go"},
Length: 4003,
MD5Sum: "",
},
{
Path: {"file.torrent"},
Length: 379,
MD5Sum: "",
},
},
PieceLength: 16384,
Pieces: "w&��\x03'�~=W4\a�\x04\x19��i̓",
Private: 0,
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment