]> Sergey Matveev's repositories - btrtrc.git/blob - metainfo/magnet-v2.go
cmd/btrtrc client
[btrtrc.git] / metainfo / magnet-v2.go
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 }