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 }