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 }