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

     1  package metainfo
     2  
     3  import (
     4  	"encoding/hex"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"strings"
     9  
    10  	g "github.com/anacrolix/generics"
    11  	"github.com/multiformats/go-multihash"
    12  
    13  	infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
    14  )
    15  
    16  // Magnet link components.
    17  type MagnetV2 struct {
    18  	InfoHash    g.Option[Hash] // Expected in this implementation
    19  	V2InfoHash  g.Option[infohash_v2.T]
    20  	Trackers    []string   // "tr" values
    21  	DisplayName string     // "dn" value, if not empty
    22  	Params      url.Values // All other values, such as "x.pe", "as", "xs" etc.
    23  }
    24  
    25  const (
    26  	btmhPrefix = "urn:btmh:"
    27  )
    28  
    29  func (m MagnetV2) String() string {
    30  	// Deep-copy m.Params
    31  	vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
    32  	for k, v := range m.Params {
    33  		vs[k] = append([]string(nil), v...)
    34  	}
    35  
    36  	for _, tr := range m.Trackers {
    37  		vs.Add("tr", tr)
    38  	}
    39  	if m.DisplayName != "" {
    40  		vs.Add("dn", m.DisplayName)
    41  	}
    42  
    43  	// Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
    44  	// start of the magnet link. The InfoHash field is expected to be BitTorrent in this
    45  	// implementation.
    46  	u := url.URL{
    47  		Scheme: "magnet",
    48  	}
    49  	var queryParts []string
    50  	if m.InfoHash.Ok {
    51  		queryParts = append(queryParts, "xt="+btihPrefix+m.InfoHash.Value.HexString())
    52  	}
    53  	if m.V2InfoHash.Ok {
    54  		queryParts = append(
    55  			queryParts,
    56  			"xt="+btmhPrefix+infohash_v2.ToMultihash(m.V2InfoHash.Value).HexString(),
    57  		)
    58  	}
    59  	if rem := vs.Encode(); rem != "" {
    60  		queryParts = append(queryParts, rem)
    61  	}
    62  	u.RawQuery = strings.Join(queryParts, "&")
    63  	return u.String()
    64  }
    65  
    66  // ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
    67  func ParseMagnetV2Uri(uri string) (m MagnetV2, err error) {
    68  	u, err := url.Parse(uri)
    69  	if err != nil {
    70  		err = fmt.Errorf("error parsing uri: %w", err)
    71  		return
    72  	}
    73  	if u.Scheme != "magnet" {
    74  		err = fmt.Errorf("unexpected scheme %q", u.Scheme)
    75  		return
    76  	}
    77  	q := u.Query()
    78  	for _, xt := range q["xt"] {
    79  		if hashStr, found := strings.CutPrefix(xt, btihPrefix); found {
    80  			if m.InfoHash.Ok {
    81  				err = errors.New("more than one infohash found in magnet link")
    82  				return
    83  			}
    84  			m.InfoHash.Value, err = parseEncodedV1Infohash(hashStr)
    85  			if err != nil {
    86  				err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
    87  				return
    88  			}
    89  			m.InfoHash.Ok = true
    90  		} else if hashStr, found := strings.CutPrefix(xt, btmhPrefix); found {
    91  			if m.V2InfoHash.Ok {
    92  				err = errors.New("more than one infohash found in magnet link")
    93  				return
    94  			}
    95  			m.V2InfoHash.Value, err = parseV2Infohash(hashStr)
    96  			if err != nil {
    97  				err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
    98  				return
    99  			}
   100  			m.V2InfoHash.Ok = true
   101  		} else {
   102  			lazyAddParam(&m.Params, "xt", xt)
   103  		}
   104  	}
   105  	q.Del("xt")
   106  	m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
   107  	m.Trackers = q["tr"]
   108  	q.Del("tr")
   109  	// Add everything we haven't consumed.
   110  	copyParams(&m.Params, q)
   111  	return
   112  }
   113  
   114  func lazyAddParam(vs *url.Values, k, v string) {
   115  	if *vs == nil {
   116  		g.MakeMap(vs)
   117  	}
   118  	vs.Add(k, v)
   119  }
   120  
   121  func copyParams(dest *url.Values, src url.Values) {
   122  	for k, vs := range src {
   123  		for _, v := range vs {
   124  			lazyAddParam(dest, k, v)
   125  		}
   126  	}
   127  }
   128  
   129  func parseV2Infohash(encoded string) (ih infohash_v2.T, err error) {
   130  	b, err := hex.DecodeString(encoded)
   131  	if err != nil {
   132  		return
   133  	}
   134  	mh, err := multihash.Decode(b)
   135  	if err != nil {
   136  		return
   137  	}
   138  	if mh.Code != multihash.SHA2_256 || mh.Length != infohash_v2.Size || len(mh.Digest) != infohash_v2.Size {
   139  		err = errors.New("bad multihash")
   140  		return
   141  	}
   142  	n := copy(ih[:], mh.Digest)
   143  	if n != infohash_v2.Size {
   144  		panic(n)
   145  	}
   146  	return
   147  }