storrent/metainfo/metainfo.go
2026-01-19 21:13:01 +09:00

183 lines
3.3 KiB
Go

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
}