first commit
This commit is contained in:
182
metainfo/metainfo.go
Normal file
182
metainfo/metainfo.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"storrent/bencode"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Info Info
|
||||
InfoHash [20]byte
|
||||
Size int64
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string
|
||||
PieceSize int64
|
||||
Pieces [][20]byte
|
||||
Size int64
|
||||
Files []Entry
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Size int64
|
||||
Offset int64
|
||||
Path []string
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
File int
|
||||
Offset int64
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (i *Info) Segments(off, size int64) []Segment {
|
||||
var segs []Segment
|
||||
for idx, f := range i.Files {
|
||||
end := f.Offset + f.Size
|
||||
if off >= end {
|
||||
continue
|
||||
}
|
||||
n := min(size, end-off)
|
||||
segs = append(segs, Segment{File: idx, Offset: off - f.Offset, Size: n})
|
||||
off += n
|
||||
size -= n
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return segs
|
||||
}
|
||||
|
||||
func Parse(path string) (*File, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseBytes(data)
|
||||
}
|
||||
|
||||
func ParseBytes(data []byte) (*File, error) {
|
||||
v, _, err := bencode.Decode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid torrent dict")
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
raw, err := findInfoBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.InfoHash = sha1.Sum(raw)
|
||||
dict, ok := d["info"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid info dict")
|
||||
}
|
||||
f.Info, err = parseInfo(dict)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.Info.Size > 0 {
|
||||
f.Size = f.Info.Size
|
||||
} else {
|
||||
for _, e := range f.Info.Files {
|
||||
f.Size += e.Size
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func findInfoBytes(data []byte) ([]byte, error) {
|
||||
if len(data) == 0 || data[0] != 'd' {
|
||||
return nil, fmt.Errorf("not a dict")
|
||||
}
|
||||
|
||||
i := 1
|
||||
for i < len(data) && data[i] != 'e' {
|
||||
k, n, err := bencode.DecodeString(data[i:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i += n
|
||||
if k == "info" {
|
||||
_, n, err := bencode.Decode(data[i:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data[i : i+n], nil
|
||||
}
|
||||
_, n, err = bencode.Decode(data[i:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i += n
|
||||
}
|
||||
return nil, fmt.Errorf("info not found")
|
||||
}
|
||||
|
||||
func parseInfo(d map[string]any) (Info, error) {
|
||||
var info Info
|
||||
name, ok := d["name"].(string)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid name")
|
||||
}
|
||||
info.Name = name
|
||||
pl, ok := d["piece length"].(int64)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid piece length")
|
||||
}
|
||||
info.PieceSize = pl
|
||||
ps, ok := d["pieces"].(string)
|
||||
if !ok || len(ps)%20 != 0 {
|
||||
return info, fmt.Errorf("invalid pieces")
|
||||
}
|
||||
npieces := len(ps) / 20
|
||||
info.Pieces = make([][20]byte, npieces)
|
||||
for i := range npieces {
|
||||
copy(info.Pieces[i][:], ps[i*20:(i+1)*20])
|
||||
}
|
||||
if n, ok := d["length"].(int64); ok {
|
||||
info.Size = n
|
||||
return info, nil
|
||||
}
|
||||
|
||||
fs, ok := d["files"].([]any)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid files")
|
||||
}
|
||||
|
||||
off := int64(0)
|
||||
for _, f := range fs {
|
||||
fd, ok := f.(map[string]any)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid file entry")
|
||||
}
|
||||
n, ok := fd["length"].(int64)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid file length")
|
||||
}
|
||||
list, ok := fd["path"].([]any)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid file path")
|
||||
}
|
||||
var path []string
|
||||
for _, p := range list {
|
||||
s, ok := p.(string)
|
||||
if !ok {
|
||||
return info, fmt.Errorf("invalid path element")
|
||||
}
|
||||
path = append(path, s)
|
||||
}
|
||||
info.Files = append(info.Files, Entry{Size: n, Offset: off, Path: path})
|
||||
off += n
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
141
metainfo/metainfo_test.go
Normal file
141
metainfo/metainfo_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"testing"
|
||||
|
||||
"storrent/bencode"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info map[string]any
|
||||
wantName string
|
||||
wantSize int64
|
||||
}{
|
||||
{
|
||||
name: "single file",
|
||||
info: map[string]any{
|
||||
"name": "test.txt",
|
||||
"piece length": int64(16384),
|
||||
"pieces": string(make([]byte, 20)),
|
||||
"length": int64(100),
|
||||
},
|
||||
wantName: "test.txt",
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "multi file",
|
||||
info: map[string]any{
|
||||
"name": "dir",
|
||||
"piece length": int64(16384),
|
||||
"pieces": string(make([]byte, 20)),
|
||||
"files": []any{
|
||||
map[string]any{"length": int64(100), "path": []any{"a.txt"}},
|
||||
map[string]any{"length": int64(200), "path": []any{"b.txt"}},
|
||||
},
|
||||
},
|
||||
wantName: "dir",
|
||||
wantSize: 300,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := bencode.Encode(map[string]any{"info": tt.info})
|
||||
if err != nil {
|
||||
t.Fatalf("Encode: %v", err)
|
||||
}
|
||||
m, err := ParseBytes(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBytes: %v", err)
|
||||
}
|
||||
if m.Info.Name != tt.wantName {
|
||||
t.Errorf("Name: got %s, want %s", m.Info.Name, tt.wantName)
|
||||
}
|
||||
if m.Size != tt.wantSize {
|
||||
t.Errorf("Size: got %d, want %d", m.Size, tt.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
raw: "d6:lengthi1e4:name4:test12:piece lengthi16384e6:pieces20:01234567890123456789e",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
raw := []byte(tt.raw)
|
||||
torrent := append([]byte("d4:info"), raw...)
|
||||
torrent = append(torrent, 'e')
|
||||
m, err := ParseBytes(torrent)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBytes: %v", err)
|
||||
}
|
||||
want := sha1.Sum(raw)
|
||||
if m.InfoHash != want {
|
||||
t.Errorf("InfoHash: got %x, want %x", m.InfoHash, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSegments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files []Entry
|
||||
off int64
|
||||
size int64
|
||||
want []Segment
|
||||
}{
|
||||
{
|
||||
name: "single file",
|
||||
files: []Entry{{Size: 32, Offset: 0}},
|
||||
off: 0,
|
||||
size: 32,
|
||||
want: []Segment{{0, 0, 32}},
|
||||
},
|
||||
{
|
||||
name: "spans two files",
|
||||
files: []Entry{{Size: 16, Offset: 0}, {Size: 16, Offset: 16}},
|
||||
off: 0,
|
||||
size: 32,
|
||||
want: []Segment{{0, 0, 16}, {1, 0, 16}},
|
||||
},
|
||||
{
|
||||
name: "middle of file",
|
||||
files: []Entry{{Size: 32, Offset: 0}},
|
||||
off: 8,
|
||||
size: 16,
|
||||
want: []Segment{{0, 8, 16}},
|
||||
},
|
||||
{
|
||||
name: "skip first file",
|
||||
files: []Entry{{Size: 16, Offset: 0}, {Size: 16, Offset: 16}},
|
||||
off: 16,
|
||||
size: 16,
|
||||
want: []Segment{{1, 0, 16}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
info := Info{Files: tt.files}
|
||||
got := info.Segments(tt.off, tt.size)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("got %d segments, want %d", len(got), len(tt.want))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("segment %d: got %+v, want %+v", i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
68
metainfo/storage.go
Normal file
68
metainfo/storage.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (info *Info) Write(dir string, i int, data []byte) error {
|
||||
off := int64(i) * info.PieceSize
|
||||
if info.Size > 0 {
|
||||
return writeAt(filepath.Join(dir, info.Name), off, data)
|
||||
}
|
||||
for _, seg := range info.Segments(off, int64(len(data))) {
|
||||
f := info.Files[seg.File]
|
||||
path := filepath.Join(dir, info.Name, filepath.Join(f.Path...))
|
||||
if err := writeAt(path, seg.Offset, data[:seg.Size]); err != nil {
|
||||
return err
|
||||
}
|
||||
data = data[seg.Size:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (info *Info) Read(dir string, i int, size int64) []byte {
|
||||
off := int64(i) * info.PieceSize
|
||||
if info.Size > 0 {
|
||||
return readAt(filepath.Join(dir, info.Name), off, size)
|
||||
}
|
||||
data := make([]byte, size)
|
||||
pos := int64(0)
|
||||
for _, seg := range info.Segments(off, size) {
|
||||
f := info.Files[seg.File]
|
||||
path := filepath.Join(dir, info.Name, filepath.Join(f.Path...))
|
||||
chunk := readAt(path, seg.Offset, seg.Size)
|
||||
if chunk == nil {
|
||||
return nil
|
||||
}
|
||||
copy(data[pos:], chunk)
|
||||
pos += seg.Size
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func writeAt(path string, off int64, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteAt(data, off)
|
||||
return err
|
||||
}
|
||||
|
||||
func readAt(path string, off, size int64) []byte {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
data := make([]byte, size)
|
||||
if _, err := f.ReadAt(data, off); err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user