magnet.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package metainfo
  2. import (
  3. "encoding/base32"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "net/url"
  8. "strings"
  9. g "github.com/anacrolix/generics"
  10. "github.com/anacrolix/torrent/types/infohash"
  11. )
  12. // Magnet link components.
  13. type Magnet struct {
  14. InfoHash Hash // Expected in this implementation
  15. Trackers []string // "tr" values
  16. DisplayName string // "dn" value, if not empty
  17. Params url.Values // All other values, such as "x.pe", "as", "xs" etc.
  18. }
  19. const btihPrefix = "urn:btih:"
  20. func (m Magnet) String() string {
  21. // Deep-copy m.Params
  22. vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
  23. for k, v := range m.Params {
  24. vs[k] = append([]string(nil), v...)
  25. }
  26. for _, tr := range m.Trackers {
  27. vs.Add("tr", tr)
  28. }
  29. if m.DisplayName != "" {
  30. vs.Add("dn", m.DisplayName)
  31. }
  32. // Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
  33. // start of the magnet link. The InfoHash field is expected to be BitTorrent in this
  34. // implementation.
  35. u := url.URL{
  36. Scheme: "magnet",
  37. RawQuery: "xt=" + btihPrefix + m.InfoHash.HexString(),
  38. }
  39. if len(vs) != 0 {
  40. u.RawQuery += "&" + vs.Encode()
  41. }
  42. return u.String()
  43. }
  44. // Deprecated: Use ParseMagnetUri.
  45. var ParseMagnetURI = ParseMagnetUri
  46. // ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
  47. func ParseMagnetUri(uri string) (m Magnet, err error) {
  48. u, err := url.Parse(uri)
  49. if err != nil {
  50. err = fmt.Errorf("error parsing uri: %w", err)
  51. return
  52. }
  53. if u.Scheme != "magnet" {
  54. err = fmt.Errorf("unexpected scheme %q", u.Scheme)
  55. return
  56. }
  57. q := u.Query()
  58. gotInfohash := false
  59. for _, xt := range q["xt"] {
  60. if gotInfohash {
  61. lazyAddParam(&m.Params, "xt", xt)
  62. continue
  63. }
  64. encoded, found := strings.CutPrefix(xt, btihPrefix)
  65. if !found {
  66. lazyAddParam(&m.Params, "xt", xt)
  67. continue
  68. }
  69. m.InfoHash, err = parseEncodedV1Infohash(encoded)
  70. if err != nil {
  71. err = fmt.Errorf("error parsing v1 infohash %q: %w", xt, err)
  72. return
  73. }
  74. gotInfohash = true
  75. }
  76. if !gotInfohash {
  77. err = errors.New("missing v1 infohash")
  78. return
  79. }
  80. q.Del("xt")
  81. m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
  82. m.Trackers = q["tr"]
  83. q.Del("tr")
  84. copyParams(&m.Params, q)
  85. return
  86. }
  87. func parseEncodedV1Infohash(encoded string) (ih infohash.T, err error) {
  88. decode := func() func(dst, src []byte) (int, error) {
  89. switch len(encoded) {
  90. case 40:
  91. return hex.Decode
  92. case 32:
  93. return base32.StdEncoding.Decode
  94. }
  95. return nil
  96. }()
  97. if decode == nil {
  98. err = fmt.Errorf("unhandled xt parameter encoding (encoded length %d)", len(encoded))
  99. return
  100. }
  101. n, err := decode(ih[:], []byte(encoded))
  102. if err != nil {
  103. err = fmt.Errorf("error decoding xt: %w", err)
  104. return
  105. }
  106. if n != 20 {
  107. panic(n)
  108. }
  109. return
  110. }
  111. func popFirstValue(vs url.Values, key string) g.Option[string] {
  112. sl := vs[key]
  113. switch len(sl) {
  114. case 0:
  115. return g.None[string]()
  116. case 1:
  117. vs.Del(key)
  118. return g.Some(sl[0])
  119. default:
  120. vs[key] = sl[1:]
  121. return g.Some(sl[0])
  122. }
  123. }