10 g "github.com/anacrolix/generics"
11 "github.com/multiformats/go-multihash"
13 infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
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.
26 btmhPrefix = "urn:btmh:"
29 func (m MagnetV2) String() string {
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...)
36 for _, tr := range m.Trackers {
39 if m.DisplayName != "" {
40 vs.Add("dn", m.DisplayName)
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
49 var queryParts []string
51 queryParts = append(queryParts, "xt="+btihPrefix+m.InfoHash.Value.HexString())
56 "xt="+btmhPrefix+infohash_v2.ToMultihash(m.V2InfoHash.Value).HexString(),
59 if rem := vs.Encode(); rem != "" {
60 queryParts = append(queryParts, rem)
62 u.RawQuery = strings.Join(queryParts, "&")
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)
70 err = fmt.Errorf("error parsing uri: %w", err)
73 if u.Scheme != "magnet" {
74 err = fmt.Errorf("unexpected scheme %q", u.Scheme)
78 for _, xt := range q["xt"] {
79 if hashStr, found := strings.CutPrefix(xt, btihPrefix); found {
81 err = errors.New("more than one infohash found in magnet link")
84 m.InfoHash.Value, err = parseEncodedV1Infohash(hashStr)
86 err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
90 } else if hashStr, found := strings.CutPrefix(xt, btmhPrefix); found {
92 err = errors.New("more than one infohash found in magnet link")
95 m.V2InfoHash.Value, err = parseV2Infohash(hashStr)
97 err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
100 m.V2InfoHash.Ok = true
102 lazyAddParam(&m.Params, "xt", xt)
106 m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
109 // Add everything we haven't consumed.
110 copyParams(&m.Params, q)
114 func lazyAddParam(vs *url.Values, k, v string) {
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)
129 func parseV2Infohash(encoded string) (ih infohash_v2.T, err error) {
130 b, err := hex.DecodeString(encoded)
134 mh, err := multihash.Decode(b)
138 if mh.Code != multihash.SHA2_256 || mh.Length != infohash_v2.Size || len(mh.Digest) != infohash_v2.Size {
139 err = errors.New("bad multihash")
142 n := copy(ih[:], mh.Digest)
143 if n != infohash_v2.Size {