+1
@@ -0,0 +1 @@
|
||||
A library for manipulating ".torrent" files.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package metainfo
|
||||
|
||||
type AnnounceList [][]string
|
||||
|
||||
func (al AnnounceList) Clone() (ret AnnounceList) {
|
||||
for _, tier := range al {
|
||||
ret = append(ret, append([]string(nil), tier...))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Whether the AnnounceList should be preferred over a single URL announce.
|
||||
func (al AnnounceList) OverridesAnnounce(announce string) bool {
|
||||
for _, tier := range al {
|
||||
for _, url := range tier {
|
||||
if url != "" || announce == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (al AnnounceList) DistinctValues() (ret []string) {
|
||||
seen := make(map[string]struct{})
|
||||
for _, tier := range al {
|
||||
for _, v := range tier {
|
||||
if _, ok := seen[v]; !ok {
|
||||
seen[v] = struct{}{}
|
||||
ret = append(ret, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package metainfo
|
||||
|
||||
import "strings"
|
||||
|
||||
// Information specific to a single file inside the MetaInfo structure.
|
||||
type FileInfo struct {
|
||||
Length int64 `bencode:"length"` // BEP3
|
||||
Path []string `bencode:"path"` // BEP3
|
||||
PathUTF8 []string `bencode:"path.utf-8,omitempty"`
|
||||
}
|
||||
|
||||
func (fi *FileInfo) DisplayPath(info *Info) string {
|
||||
if info.IsDir() {
|
||||
return strings.Join(fi.Path, "/")
|
||||
} else {
|
||||
return info.Name
|
||||
}
|
||||
}
|
||||
|
||||
func (me FileInfo) Offset(info *Info) (ret int64) {
|
||||
for _, fi := range info.UpvertedFiles() {
|
||||
if me.DisplayPath(info) == fi.DisplayPath(info) {
|
||||
return
|
||||
}
|
||||
ret += fi.Length
|
||||
}
|
||||
panic("not found")
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const HashSize = 20
|
||||
|
||||
// 20-byte SHA1 hash used for info and pieces.
|
||||
type Hash [HashSize]byte
|
||||
|
||||
var _ fmt.Formatter = (*Hash)(nil)
|
||||
|
||||
func (h Hash) Format(f fmt.State, c rune) {
|
||||
// TODO: I can't figure out a nice way to just override the 'x' rune, since it's meaningless
|
||||
// with the "default" 'v', or .String() already returning the hex.
|
||||
f.Write([]byte(h.HexString()))
|
||||
}
|
||||
|
||||
func (h Hash) Bytes() []byte {
|
||||
return h[:]
|
||||
}
|
||||
|
||||
func (h Hash) AsString() string {
|
||||
return string(h[:])
|
||||
}
|
||||
|
||||
func (h Hash) String() string {
|
||||
return h.HexString()
|
||||
}
|
||||
|
||||
func (h Hash) HexString() string {
|
||||
return fmt.Sprintf("%x", h[:])
|
||||
}
|
||||
|
||||
func (h *Hash) FromHexString(s string) (err error) {
|
||||
if len(s) != 2*HashSize {
|
||||
err = fmt.Errorf("hash hex string has bad length: %d", len(s))
|
||||
return
|
||||
}
|
||||
n, err := hex.Decode(h[:], []byte(s))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n != HashSize {
|
||||
panic(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
_ encoding.TextUnmarshaler = (*Hash)(nil)
|
||||
_ encoding.TextMarshaler = Hash{}
|
||||
)
|
||||
|
||||
func (h *Hash) UnmarshalText(b []byte) error {
|
||||
return h.FromHexString(string(b))
|
||||
}
|
||||
|
||||
func (h Hash) MarshalText() (text []byte, err error) {
|
||||
return []byte(h.HexString()), nil
|
||||
}
|
||||
|
||||
func NewHashFromHex(s string) (h Hash) {
|
||||
err := h.FromHexString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HashBytes(b []byte) (ret Hash) {
|
||||
hasher := sha1.New()
|
||||
hasher.Write(b)
|
||||
copy(ret[:], hasher.Sum(nil))
|
||||
return
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anacrolix/missinggo/slices"
|
||||
)
|
||||
|
||||
// The info dictionary.
|
||||
type Info struct {
|
||||
PieceLength int64 `bencode:"piece length"` // BEP3
|
||||
Pieces []byte `bencode:"pieces"` // BEP3
|
||||
Name string `bencode:"name"` // BEP3
|
||||
Length int64 `bencode:"length,omitempty"` // BEP3, mutually exclusive with Files
|
||||
Private *bool `bencode:"private,omitempty"` // BEP27
|
||||
// TODO: Document this field.
|
||||
Source string `bencode:"source,omitempty"`
|
||||
Files []FileInfo `bencode:"files,omitempty"` // BEP3, mutually exclusive with Length
|
||||
}
|
||||
|
||||
// The Info.Name field is "advisory". For multi-file torrents it's usually a suggested directory
|
||||
// name. There are situations where we don't want a directory (like using the contents of a torrent
|
||||
// as the immediate contents of a directory), or the name is invalid. Transmission will inject the
|
||||
// name of the torrent file if it doesn't like the name, resulting in a different infohash
|
||||
// (https://github.com/transmission/transmission/issues/1775). To work around these situations, we
|
||||
// will use a sentinel name for compatibility with Transmission and to signal to our own client that
|
||||
// we intended to have no directory name. By exposing it in the API we can check for references to
|
||||
// this behaviour within this implementation.
|
||||
const NoName = "-"
|
||||
|
||||
// This is a helper that sets Files and Pieces from a root path and its children.
|
||||
func (info *Info) BuildFromFilePath(root string) (err error) {
|
||||
info.Name = func() string {
|
||||
b := filepath.Base(root)
|
||||
switch b {
|
||||
case ".", "..", string(filepath.Separator):
|
||||
return NoName
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}()
|
||||
info.Files = nil
|
||||
err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
// Directories are implicit in torrent files.
|
||||
return nil
|
||||
} else if path == root {
|
||||
// The root is a file.
|
||||
info.Length = fi.Size()
|
||||
return nil
|
||||
}
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting relative path: %s", err)
|
||||
}
|
||||
info.Files = append(info.Files, FileInfo{
|
||||
Path: strings.Split(relPath, string(filepath.Separator)),
|
||||
Length: fi.Size(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
slices.Sort(info.Files, func(l, r FileInfo) bool {
|
||||
return strings.Join(l.Path, "/") < strings.Join(r.Path, "/")
|
||||
})
|
||||
err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
|
||||
return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator))))
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error generating pieces: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Concatenates all the files in the torrent into w. open is a function that
|
||||
// gets at the contents of the given file.
|
||||
func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error {
|
||||
for _, fi := range info.UpvertedFiles() {
|
||||
r, err := open(fi)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening %v: %s", fi, err)
|
||||
}
|
||||
wn, err := io.CopyN(w, r, fi.Length)
|
||||
r.Close()
|
||||
if wn != fi.Length {
|
||||
return fmt.Errorf("error copying %v: %s", fi, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets Pieces (the block of piece hashes in the Info) by using the passed
|
||||
// function to get at the torrent data.
|
||||
func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) (err error) {
|
||||
if info.PieceLength == 0 {
|
||||
return errors.New("piece length must be non-zero")
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := info.writeFiles(pw, open)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
defer pr.Close()
|
||||
info.Pieces, err = GeneratePieces(pr, info.PieceLength, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (info *Info) TotalLength() (ret int64) {
|
||||
if info.IsDir() {
|
||||
for _, fi := range info.Files {
|
||||
ret += fi.Length
|
||||
}
|
||||
} else {
|
||||
ret = info.Length
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (info *Info) NumPieces() int {
|
||||
return len(info.Pieces) / 20
|
||||
}
|
||||
|
||||
func (info *Info) IsDir() bool {
|
||||
return len(info.Files) != 0
|
||||
}
|
||||
|
||||
// The files field, converted up from the old single-file in the parent info
|
||||
// dict if necessary. This is a helper to avoid having to conditionally handle
|
||||
// single and multi-file torrent infos.
|
||||
func (info *Info) UpvertedFiles() []FileInfo {
|
||||
if len(info.Files) == 0 {
|
||||
return []FileInfo{{
|
||||
Length: info.Length,
|
||||
// Callers should determine that Info.Name is the basename, and
|
||||
// thus a regular file.
|
||||
Path: nil,
|
||||
}}
|
||||
}
|
||||
return info.Files
|
||||
}
|
||||
|
||||
func (info *Info) Piece(index int) Piece {
|
||||
return Piece{info, pieceIndex(index)}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Magnet link components.
|
||||
type Magnet struct {
|
||||
InfoHash Hash // Expected in this implementation
|
||||
Trackers []string // "tr" values
|
||||
DisplayName string // "dn" value, if not empty
|
||||
Params url.Values // All other values, such as "x.pe", "as", "xs" etc.
|
||||
}
|
||||
|
||||
const xtPrefix = "urn:btih:"
|
||||
|
||||
func (m Magnet) String() string {
|
||||
// Deep-copy m.Params
|
||||
vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
|
||||
for k, v := range m.Params {
|
||||
vs[k] = append([]string(nil), v...)
|
||||
}
|
||||
|
||||
for _, tr := range m.Trackers {
|
||||
vs.Add("tr", tr)
|
||||
}
|
||||
if m.DisplayName != "" {
|
||||
vs.Add("dn", m.DisplayName)
|
||||
}
|
||||
|
||||
// Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
|
||||
// start of the magnet link. The InfoHash field is expected to be BitTorrent in this
|
||||
// implementation.
|
||||
u := url.URL{
|
||||
Scheme: "magnet",
|
||||
RawQuery: "xt=" + xtPrefix + m.InfoHash.HexString(),
|
||||
}
|
||||
if len(vs) != 0 {
|
||||
u.RawQuery += "&" + vs.Encode()
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Deprecated: Use ParseMagnetUri.
|
||||
var ParseMagnetURI = ParseMagnetUri
|
||||
|
||||
// ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
|
||||
func ParseMagnetUri(uri string) (m Magnet, err error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing uri: %w", err)
|
||||
return
|
||||
}
|
||||
if u.Scheme != "magnet" {
|
||||
err = fmt.Errorf("unexpected scheme %q", u.Scheme)
|
||||
return
|
||||
}
|
||||
q := u.Query()
|
||||
xt := q.Get("xt")
|
||||
m.InfoHash, err = parseInfohash(q.Get("xt"))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing infohash %q: %w", xt, err)
|
||||
return
|
||||
}
|
||||
dropFirst(q, "xt")
|
||||
m.DisplayName = q.Get("dn")
|
||||
dropFirst(q, "dn")
|
||||
m.Trackers = q["tr"]
|
||||
delete(q, "tr")
|
||||
if len(q) == 0 {
|
||||
q = nil
|
||||
}
|
||||
m.Params = q
|
||||
return
|
||||
}
|
||||
|
||||
func parseInfohash(xt string) (ih Hash, err error) {
|
||||
if !strings.HasPrefix(xt, xtPrefix) {
|
||||
err = errors.New("bad xt parameter prefix")
|
||||
return
|
||||
}
|
||||
encoded := xt[len(xtPrefix):]
|
||||
decode := func() func(dst, src []byte) (int, error) {
|
||||
switch len(encoded) {
|
||||
case 40:
|
||||
return hex.Decode
|
||||
case 32:
|
||||
return base32.StdEncoding.Decode
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if decode == nil {
|
||||
err = fmt.Errorf("unhandled xt parameter encoding (encoded length %d)", len(encoded))
|
||||
return
|
||||
}
|
||||
n, err := decode(ih[:], []byte(encoded))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error decoding xt: %w", err)
|
||||
return
|
||||
}
|
||||
if n != 20 {
|
||||
panic(n)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dropFirst(vs url.Values, key string) {
|
||||
sl := vs[key]
|
||||
switch len(sl) {
|
||||
case 0, 1:
|
||||
vs.Del(key)
|
||||
default:
|
||||
vs[key] = sl[1:]
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent/bencode"
|
||||
)
|
||||
|
||||
type MetaInfo struct {
|
||||
InfoBytes bencode.Bytes `bencode:"info,omitempty"` // BEP 3
|
||||
Announce string `bencode:"announce,omitempty"` // BEP 3
|
||||
AnnounceList AnnounceList `bencode:"announce-list,omitempty"` // BEP 12
|
||||
Nodes []Node `bencode:"nodes,omitempty"` // BEP 5
|
||||
// Where's this specified? Mentioned at
|
||||
// https://wiki.theory.org/index.php/BitTorrentSpecification: (optional) the creation time of
|
||||
// the torrent, in standard UNIX epoch format (integer, seconds since 1-Jan-1970 00:00:00 UTC)
|
||||
CreationDate int64 `bencode:"creation date,omitempty,ignore_unmarshal_type_error"`
|
||||
Comment string `bencode:"comment,omitempty"`
|
||||
CreatedBy string `bencode:"created by,omitempty"`
|
||||
Encoding string `bencode:"encoding,omitempty"`
|
||||
UrlList UrlList `bencode:"url-list,omitempty"` // BEP 19 WebSeeds
|
||||
}
|
||||
|
||||
// Load a MetaInfo from an io.Reader. Returns a non-nil error in case of
|
||||
// failure.
|
||||
func Load(r io.Reader) (*MetaInfo, error) {
|
||||
var mi MetaInfo
|
||||
d := bencode.NewDecoder(r)
|
||||
err := d.Decode(&mi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mi, nil
|
||||
}
|
||||
|
||||
// Convenience function for loading a MetaInfo from a file.
|
||||
func LoadFromFile(filename string) (*MetaInfo, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return Load(f)
|
||||
}
|
||||
|
||||
func (mi MetaInfo) UnmarshalInfo() (info Info, err error) {
|
||||
err = bencode.Unmarshal(mi.InfoBytes, &info)
|
||||
return
|
||||
}
|
||||
|
||||
func (mi MetaInfo) HashInfoBytes() (infoHash Hash) {
|
||||
return HashBytes(mi.InfoBytes)
|
||||
}
|
||||
|
||||
// Encode to bencoded form.
|
||||
func (mi MetaInfo) Write(w io.Writer) error {
|
||||
return bencode.NewEncoder(w).Encode(mi)
|
||||
}
|
||||
|
||||
// Set good default values in preparation for creating a new MetaInfo file.
|
||||
func (mi *MetaInfo) SetDefaults() {
|
||||
mi.Comment = ""
|
||||
mi.CreatedBy = "github.com/anacrolix/torrent"
|
||||
mi.CreationDate = time.Now().Unix()
|
||||
// mi.Info.PieceLength = 256 * 1024
|
||||
}
|
||||
|
||||
// Creates a Magnet from a MetaInfo. Optional infohash and parsed info can be provided.
|
||||
func (mi *MetaInfo) Magnet(infoHash *Hash, info *Info) (m Magnet) {
|
||||
m.Trackers = append(m.Trackers, mi.UpvertedAnnounceList().DistinctValues()...)
|
||||
if info != nil {
|
||||
m.DisplayName = info.Name
|
||||
}
|
||||
if infoHash != nil {
|
||||
m.InfoHash = *infoHash
|
||||
} else {
|
||||
m.InfoHash = mi.HashInfoBytes()
|
||||
}
|
||||
m.Params = make(url.Values)
|
||||
m.Params["ws"] = mi.UrlList
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the announce list converted from the old single announce field if
|
||||
// necessary.
|
||||
func (mi *MetaInfo) UpvertedAnnounceList() AnnounceList {
|
||||
if mi.AnnounceList.OverridesAnnounce(mi.Announce) {
|
||||
return mi.AnnounceList
|
||||
}
|
||||
if mi.Announce != "" {
|
||||
return [][]string{{mi.Announce}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/anacrolix/torrent/bencode"
|
||||
)
|
||||
|
||||
type Node string
|
||||
|
||||
var _ bencode.Unmarshaler = (*Node)(nil)
|
||||
|
||||
func (n *Node) UnmarshalBencode(b []byte) (err error) {
|
||||
var iface interface{}
|
||||
err = bencode.Unmarshal(b, &iface)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch v := iface.(type) {
|
||||
case string:
|
||||
*n = Node(v)
|
||||
case []interface{}:
|
||||
func() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
*n = Node(net.JoinHostPort(v[0].(string), strconv.FormatInt(v[1].(int64), 10)))
|
||||
}()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported type: %T", iface)
|
||||
}
|
||||
return
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package metainfo
|
||||
|
||||
type Piece struct {
|
||||
Info *Info // Can we embed the fields here instead, or is it something to do with saving memory?
|
||||
i pieceIndex
|
||||
}
|
||||
|
||||
type pieceIndex = int
|
||||
|
||||
func (p Piece) Length() int64 {
|
||||
if int(p.i) == p.Info.NumPieces()-1 {
|
||||
return p.Info.TotalLength() - int64(p.i)*p.Info.PieceLength
|
||||
}
|
||||
return p.Info.PieceLength
|
||||
}
|
||||
|
||||
func (p Piece) Offset() int64 {
|
||||
return int64(p.i) * p.Info.PieceLength
|
||||
}
|
||||
|
||||
func (p Piece) Hash() (ret Hash) {
|
||||
copy(ret[:], p.Info.Pieces[p.i*HashSize:(p.i+1)*HashSize])
|
||||
return
|
||||
}
|
||||
|
||||
func (p Piece) Index() pieceIndex {
|
||||
return p.i
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package metainfo
|
||||
|
||||
// Uniquely identifies a piece.
|
||||
type PieceKey struct {
|
||||
InfoHash Hash
|
||||
Index pieceIndex
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"io"
|
||||
)
|
||||
|
||||
func GeneratePieces(r io.Reader, pieceLength int64, b []byte) ([]byte, error) {
|
||||
for {
|
||||
h := sha1.New()
|
||||
written, err := io.CopyN(h, r, pieceLength)
|
||||
if written > 0 {
|
||||
b = h.Sum(b)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"github.com/anacrolix/torrent/bencode"
|
||||
)
|
||||
|
||||
type UrlList []string
|
||||
|
||||
var _ bencode.Unmarshaler = (*UrlList)(nil)
|
||||
|
||||
func (me *UrlList) UnmarshalBencode(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
if b[0] == 'l' {
|
||||
var l []string
|
||||
err := bencode.Unmarshal(b, &l)
|
||||
*me = l
|
||||
return err
|
||||
}
|
||||
var s string
|
||||
err := bencode.Unmarshal(b, &s)
|
||||
*me = []string{s}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user