github.com/anacrolix/torrent@v1.61.0/metainfo/magnet.go (about)

     1  package metainfo
     2  
     3  import (
     4  	"encoding/base32"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"net/url"
     9  	"strings"
    10  
    11  	g "github.com/anacrolix/generics"
    12  
    13  	"github.com/anacrolix/torrent/types/infohash"
    14  )
    15  
    16  // Magnet link components.
    17  type Magnet struct {
    18  	InfoHash    Hash       // Expected in this implementation
    19  	Trackers    []string   // "tr" values
    20  	DisplayName string     // "dn" value, if not empty
    21  	Params      url.Values // All other values, such as "x.pe", "as", "xs" etc.
    22  }
    23  
    24  const btihPrefix = "urn:btih:"
    25  
    26  func (m Magnet) String() string {
    27  	// Deep-copy m.Params
    28  	vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
    29  	for k, v := range m.Params {
    30  		vs[k] = append([]string(nil), v...)
    31  	}
    32  
    33  	for _, tr := range m.Trackers {
    34  		vs.Add("tr", tr)
    35  	}
    36  	if m.DisplayName != "" {
    37  		vs.Add("dn", m.DisplayName)
    38  	}
    39  
    40  	// Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
    41  	// start of the magnet link. The InfoHash field is expected to be BitTorrent in this
    42  	// implementation.
    43  	u := url.URL{
    44  		Scheme:   "magnet",
    45  		RawQuery: "xt=" + btihPrefix + m.InfoHash.HexString(),
    46  	}
    47  	if len(vs) != 0 {
    48  		u.RawQuery += "&" + vs.Encode()
    49  	}
    50  	return u.String()
    51  }
    52  
    53  // Deprecated: Use ParseMagnetUri.
    54  var ParseMagnetURI = ParseMagnetUri
    55  
    56  // ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
    57  func ParseMagnetUri(uri string) (m Magnet, err error) {
    58  	u, err := url.Parse(uri)
    59  	if err != nil {
    60  		err = fmt.Errorf("error parsing uri: %w", err)
    61  		return
    62  	}
    63  	if u.Scheme != "magnet" {
    64  		err = fmt.Errorf("unexpected scheme %q", u.Scheme)
    65  		return
    66  	}
    67  	q := u.Query()
    68  	gotInfohash := false
    69  	for _, xt := range q["xt"] {
    70  		if gotInfohash {
    71  			lazyAddParam(&m.Params, "xt", xt)
    72  			continue
    73  		}
    74  		encoded, found := strings.CutPrefix(xt, btihPrefix)
    75  		if !found {
    76  			lazyAddParam(&m.Params, "xt", xt)
    77  			continue
    78  		}
    79  		m.InfoHash, err = parseEncodedV1Infohash(encoded)
    80  		if err != nil {
    81  			err = fmt.Errorf("error parsing v1 infohash %q: %w", xt, err)
    82  			return
    83  		}
    84  		gotInfohash = true
    85  	}
    86  	if !gotInfohash {
    87  		err = errors.New("missing v1 infohash")
    88  		return
    89  	}
    90  	q.Del("xt")
    91  	m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
    92  	m.Trackers = q["tr"]
    93  	q.Del("tr")
    94  	copyParams(&m.Params, q)
    95  	return
    96  }
    97  
    98  func parseEncodedV1Infohash(encoded string) (ih infohash.T, err error) {
    99  	decode := func() func(dst, src []byte) (int, error) {
   100  		switch len(encoded) {
   101  		case 40:
   102  			return hex.Decode
   103  		case 32:
   104  			return base32.StdEncoding.Decode
   105  		}
   106  		return nil
   107  	}()
   108  	if decode == nil {
   109  		err = fmt.Errorf("unhandled xt parameter encoding (encoded length %d)", len(encoded))
   110  		return
   111  	}
   112  	n, err := decode(ih[:], []byte(encoded))
   113  	if err != nil {
   114  		err = fmt.Errorf("error decoding xt: %w", err)
   115  		return
   116  	}
   117  	if n != 20 {
   118  		panic(n)
   119  	}
   120  	return
   121  }
   122  
   123  func popFirstValue(vs url.Values, key string) g.Option[string] {
   124  	sl := vs[key]
   125  	switch len(sl) {
   126  	case 0:
   127  		return g.None[string]()
   128  	case 1:
   129  		vs.Del(key)
   130  		return g.Some(sl[0])
   131  	default:
   132  		vs[key] = sl[1:]
   133  		return g.Some(sl[0])
   134  	}
   135  }