]> Sergey Matveev's repositories - btrtrc.git/commitdiff
Add MagnetV2 and infohash_v2
authorMatt Joiner <anacrolix@gmail.com>
Tue, 27 Feb 2024 00:06:17 +0000 (11:06 +1100)
committerMatt Joiner <anacrolix@gmail.com>
Wed, 28 Feb 2024 05:30:23 +0000 (16:30 +1100)
go.mod
go.sum
metainfo/magnet-v2.go [new file with mode: 0644]
metainfo/magnet-v2_test.go [new file with mode: 0644]
metainfo/magnet.go
metainfo/magnet_test.go
types/infohash-v2/infohash-v2.go [new file with mode: 0644]
types/types.go

diff --git a/go.mod b/go.mod
index ce02313504c57aa822a51c2bc744db4353c763da..075c2bddc58847972c937fa39037cee316de2a14 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -40,6 +40,7 @@ require (
        github.com/google/go-cmp v0.5.9
        github.com/gorilla/websocket v1.5.0
        github.com/jessevdk/go-flags v1.5.0
+       github.com/multiformats/go-multihash v0.2.3
        github.com/pion/datachannel v1.5.2
        github.com/pion/logging v0.2.2
        github.com/pion/webrtc/v3 v3.1.42
@@ -74,11 +75,15 @@ require (
        github.com/google/uuid v1.3.0 // indirect
        github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
        github.com/huandu/xstrings v1.3.2 // indirect
+       github.com/klauspost/cpuid/v2 v2.2.3 // indirect
        github.com/kr/pretty v0.3.1 // indirect
        github.com/kr/text v0.2.0 // indirect
        github.com/mattn/go-isatty v0.0.16 // indirect
        github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+       github.com/minio/sha256-simd v1.0.0 // indirect
+       github.com/mr-tron/base58 v1.2.0 // indirect
        github.com/mschoch/smat v0.2.0 // indirect
+       github.com/multiformats/go-varint v0.0.6 // indirect
        github.com/pion/dtls/v2 v2.2.4 // indirect
        github.com/pion/ice/v2 v2.2.6 // indirect
        github.com/pion/interceptor v0.1.11 // indirect
@@ -102,6 +107,7 @@ require (
        github.com/rogpeppe/go-internal v1.9.0 // indirect
        github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
        github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
+       github.com/spaolacci/murmur3 v1.1.0 // indirect
        go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 // indirect
        go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 // indirect
        go.opentelemetry.io/proto/otlp v0.18.0 // indirect
@@ -114,6 +120,7 @@ require (
        google.golang.org/grpc v1.56.3 // indirect
        google.golang.org/protobuf v1.30.0 // indirect
        gopkg.in/yaml.v3 v3.0.1 // indirect
+       lukechampine.com/blake3 v1.1.6 // indirect
        modernc.org/libc v1.22.3 // indirect
        modernc.org/mathutil v1.5.0 // indirect
        modernc.org/memory v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 75919e87add97c37c95c5e2a67b443cc54296d05..d371109d1d8d1a9176b63252b36751f47f9cdbd4 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -332,6 +332,10 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -348,14 +352,22 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
+github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
+github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
 github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
+github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
+github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
+github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
+github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -477,8 +489,9 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -707,6 +720,7 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -904,6 +918,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
+lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
 modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
 modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
 modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
diff --git a/metainfo/magnet-v2.go b/metainfo/magnet-v2.go
new file mode 100644 (file)
index 0000000..a6c2c8b
--- /dev/null
@@ -0,0 +1,147 @@
+package metainfo
+
+import (
+       "encoding/hex"
+       "errors"
+       "fmt"
+       "net/url"
+       "strings"
+
+       "github.com/multiformats/go-multihash"
+
+       g "github.com/anacrolix/generics"
+       infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
+)
+
+// Magnet link components.
+type MagnetV2 struct {
+       InfoHash    g.Option[Hash] // Expected in this implementation
+       V2InfoHash  g.Option[infohash_v2.T]
+       Trackers    []string   // "tr" values
+       DisplayName string     // "dn" value, if not empty
+       Params      url.Values // All other values, such as "x.pe", "as", "xs" etc.
+}
+
+const (
+       btmhPrefix = "urn:btmh:"
+)
+
+func (m MagnetV2) String() string {
+       // Deep-copy m.Params
+       vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
+       for k, v := range m.Params {
+               vs[k] = append([]string(nil), v...)
+       }
+
+       for _, tr := range m.Trackers {
+               vs.Add("tr", tr)
+       }
+       if m.DisplayName != "" {
+               vs.Add("dn", m.DisplayName)
+       }
+
+       // Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
+       // start of the magnet link. The InfoHash field is expected to be BitTorrent in this
+       // implementation.
+       u := url.URL{
+               Scheme: "magnet",
+       }
+       var queryParts []string
+       if m.InfoHash.Ok {
+               queryParts = append(queryParts, "xt="+btihPrefix+m.InfoHash.Value.HexString())
+       }
+       if m.V2InfoHash.Ok {
+               queryParts = append(
+                       queryParts,
+                       "xt="+btmhPrefix+infohash_v2.ToMultihash(m.V2InfoHash.Value).HexString(),
+               )
+       }
+       if rem := vs.Encode(); rem != "" {
+               queryParts = append(queryParts, rem)
+       }
+       u.RawQuery = strings.Join(queryParts, "&")
+       return u.String()
+}
+
+// ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
+func ParseMagnetV2Uri(uri string) (m MagnetV2, err error) {
+       u, err := url.Parse(uri)
+       if err != nil {
+               err = fmt.Errorf("error parsing uri: %w", err)
+               return
+       }
+       if u.Scheme != "magnet" {
+               err = fmt.Errorf("unexpected scheme %q", u.Scheme)
+               return
+       }
+       q := u.Query()
+       for _, xt := range q["xt"] {
+               if hashStr, found := strings.CutPrefix(xt, btihPrefix); found {
+                       if m.InfoHash.Ok {
+                               err = errors.New("more than one infohash found in magnet link")
+                               return
+                       }
+                       m.InfoHash.Value, err = parseEncodedV1Infohash(hashStr)
+                       if err != nil {
+                               err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
+                               return
+                       }
+                       m.InfoHash.Ok = true
+               } else if hashStr, found := strings.CutPrefix(xt, btmhPrefix); found {
+                       if m.V2InfoHash.Ok {
+                               err = errors.New("more than one infohash found in magnet link")
+                               return
+                       }
+                       m.V2InfoHash.Value, err = parseV2Infohash(hashStr)
+                       if err != nil {
+                               err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
+                               return
+                       }
+                       m.V2InfoHash.Ok = true
+               } else {
+                       lazyAddParam(&m.Params, "xt", xt)
+               }
+       }
+       q.Del("xt")
+       m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
+       m.Trackers = q["tr"]
+       q.Del("tr")
+       // Add everything we haven't consumed.
+       copyParams(&m.Params, q)
+       return
+}
+
+func lazyAddParam(vs *url.Values, k, v string) {
+       if *vs == nil {
+               g.MakeMap(vs)
+       }
+       vs.Add(k, v)
+}
+
+func copyParams(dest *url.Values, src url.Values) {
+       for k, vs := range src {
+               for _, v := range vs {
+                       lazyAddParam(dest, k, v)
+               }
+       }
+}
+
+func parseV2Infohash(encoded string) (ih infohash_v2.T, err error) {
+       b, err := hex.DecodeString(encoded)
+       if err != nil {
+               return
+       }
+       mh, err := multihash.Decode(b)
+       if err != nil {
+               return
+       }
+       if mh.Code != multihash.SHA2_256 || mh.Length != infohash_v2.Size || len(mh.Digest) != infohash_v2.Size {
+               err = errors.New("bad multihash")
+               return
+       }
+       n := copy(ih[:], mh.Digest)
+       if n != infohash_v2.Size {
+               panic(n)
+       }
+       return
+}
diff --git a/metainfo/magnet-v2_test.go b/metainfo/magnet-v2_test.go
new file mode 100644 (file)
index 0000000..620d385
--- /dev/null
@@ -0,0 +1,38 @@
+package metainfo
+
+import (
+       "testing"
+
+       qt "github.com/frankban/quicktest"
+)
+
+func TestParseMagnetV2(t *testing.T) {
+       c := qt.New(t)
+
+       const v2Only = "magnet:?xt=urn:btmh:1220caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e&dn=bittorrent-v2-test"
+
+       m2, err := ParseMagnetV2Uri(v2Only)
+       c.Assert(err, qt.IsNil)
+       c.Check(m2.InfoHash.Ok, qt.IsFalse)
+       c.Check(m2.V2InfoHash.Ok, qt.IsTrue)
+       c.Check(m2.V2InfoHash.Value.HexString(), qt.Equals, "caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e")
+       c.Check(m2.Params, qt.HasLen, 0)
+
+       _, err = ParseMagnetUri(v2Only)
+       c.Check(err, qt.IsNotNil)
+
+       const hybrid = "magnet:?xt=urn:btih:631a31dd0a46257d5078c0dee4e66e26f73e42ac&xt=urn:btmh:1220d8dd32ac93357c368556af3ac1d95c9d76bd0dff6fa9833ecdac3d53134efabb&dn=bittorrent-v1-v2-hybrid-test"
+
+       m2, err = ParseMagnetV2Uri(hybrid)
+       c.Assert(err, qt.IsNil)
+       c.Check(m2.InfoHash.Ok, qt.IsTrue)
+       c.Check(m2.InfoHash.Value.HexString(), qt.Equals, "631a31dd0a46257d5078c0dee4e66e26f73e42ac")
+       c.Check(m2.V2InfoHash.Ok, qt.IsTrue)
+       c.Check(m2.V2InfoHash.Value.HexString(), qt.Equals, "d8dd32ac93357c368556af3ac1d95c9d76bd0dff6fa9833ecdac3d53134efabb")
+       c.Check(m2.Params, qt.HasLen, 0)
+
+       m, err := ParseMagnetUri(hybrid)
+       c.Assert(err, qt.IsNil)
+       c.Check(m.InfoHash.HexString(), qt.Equals, "631a31dd0a46257d5078c0dee4e66e26f73e42ac")
+       c.Check(m.Params["xt"], qt.HasLen, 1)
+}
index 48dc148e8735d889e41461d291a60a91f660d019..7916f9a7fa8b39479b1e8954cba787df513750a9 100644 (file)
@@ -7,6 +7,10 @@ import (
        "fmt"
        "net/url"
        "strings"
+
+       g "github.com/anacrolix/generics"
+
+       "github.com/anacrolix/torrent/types/infohash"
 )
 
 // Magnet link components.
@@ -17,7 +21,7 @@ type Magnet struct {
        Params      url.Values // All other values, such as "x.pe", "as", "xs" etc.
 }
 
-const xtPrefix = "urn:btih:"
+const btihPrefix = "urn:btih:"
 
 func (m Magnet) String() string {
        // Deep-copy m.Params
@@ -38,7 +42,7 @@ func (m Magnet) String() string {
        // implementation.
        u := url.URL{
                Scheme:   "magnet",
-               RawQuery: "xt=" + xtPrefix + m.InfoHash.HexString(),
+               RawQuery: "xt=" + btihPrefix + m.InfoHash.HexString(),
        }
        if len(vs) != 0 {
                u.RawQuery += "&" + vs.Encode()
@@ -61,30 +65,37 @@ func ParseMagnetUri(uri string) (m Magnet, err error) {
                return
        }
        q := u.Query()
-       xt := q.Get("xt")
-       m.InfoHash, err = parseInfohash(q.Get("xt"))
-       if err != nil {
-               err = fmt.Errorf("error parsing infohash %q: %w", xt, err)
+       gotInfohash := false
+       for _, xt := range q["xt"] {
+               if gotInfohash {
+                       lazyAddParam(&m.Params, "xt", xt)
+                       continue
+               }
+               encoded, found := strings.CutPrefix(xt, btihPrefix)
+               if !found {
+                       lazyAddParam(&m.Params, "xt", xt)
+                       continue
+               }
+               m.InfoHash, err = parseEncodedV1Infohash(encoded)
+               if err != nil {
+                       err = fmt.Errorf("error parsing v1 infohash %q: %w", xt, err)
+                       return
+               }
+               gotInfohash = true
+       }
+       if !gotInfohash {
+               err = errors.New("missing v1 infohash")
                return
        }
-       dropFirst(q, "xt")
-       m.DisplayName = q.Get("dn")
-       dropFirst(q, "dn")
+       q.Del("xt")
+       m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
        m.Trackers = q["tr"]
-       delete(q, "tr")
-       if len(q) == 0 {
-               q = nil
-       }
-       m.Params = q
+       q.Del("tr")
+       copyParams(&m.Params, q)
        return
 }
 
-func parseInfohash(xt string) (ih Hash, err error) {
-       if !strings.HasPrefix(xt, xtPrefix) {
-               err = errors.New("bad xt parameter prefix")
-               return
-       }
-       encoded := xt[len(xtPrefix):]
+func parseEncodedV1Infohash(encoded string) (ih infohash.T, err error) {
        decode := func() func(dst, src []byte) (int, error) {
                switch len(encoded) {
                case 40:
@@ -109,12 +120,16 @@ func parseInfohash(xt string) (ih Hash, err error) {
        return
 }
 
-func dropFirst(vs url.Values, key string) {
+func popFirstValue(vs url.Values, key string) g.Option[string] {
        sl := vs[key]
        switch len(sl) {
-       case 0, 1:
+       case 0:
+               return g.None[string]()
+       case 1:
                vs.Del(key)
+               return g.Some(sl[0])
        default:
                vs[key] = sl[1:]
+               return g.Some(sl[0])
        }
 }
index 24ab15b18e7ba88dd4c2fb53d8d3a6dd6443bdcc..ba13ac885700af88f619d5449b86abc2d5c3ac39 100644 (file)
@@ -2,6 +2,8 @@ package metainfo
 
 import (
        "encoding/hex"
+       "github.com/davecgh/go-spew/spew"
+       qt "github.com/frankban/quicktest"
        "testing"
 
        "github.com/stretchr/testify/assert"
@@ -112,3 +114,22 @@ func contains(haystack []string, needle string) bool {
        }
        return false
 }
+
+// Check that we can parse the magnet link generated from a real-world torrent. This was added due
+// to a regression in copyParams.
+func TestParseSintelMagnet(t *testing.T) {
+       c := qt.New(t)
+       mi, err := LoadFromFile("../testdata/sintel.torrent")
+       c.Assert(err, qt.IsNil)
+       m := mi.Magnet(nil, nil)
+       ms := m.String()
+       t.Logf("magnet link: %q", ms)
+       m, err = ParseMagnetUri(ms)
+       c.Check(err, qt.IsNil)
+       spewCfg := spew.NewDefaultConfig()
+       spewCfg.DisableMethods = true
+       spewCfg.Dump(m)
+       m2, err := ParseMagnetV2Uri(ms)
+       spewCfg.Dump(m2)
+       c.Check(err, qt.IsNil)
+}
diff --git a/types/infohash-v2/infohash-v2.go b/types/infohash-v2/infohash-v2.go
new file mode 100644 (file)
index 0000000..02ddd1d
--- /dev/null
@@ -0,0 +1,95 @@
+package infohash_v2
+
+import (
+       "crypto/sha256"
+       "encoding"
+       "encoding/hex"
+       "fmt"
+
+       "github.com/multiformats/go-multihash"
+
+       "github.com/anacrolix/torrent/types/infohash"
+)
+
+const Size = sha256.Size
+
+// 32-byte SHA2-256 hash. See BEP 52.
+type T [Size]byte
+
+var _ fmt.Formatter = (*T)(nil)
+
+func (t *T) Format(f fmt.State, c rune) {
+       // TODO: I can't figure out a nice way to just override the 'x' rune, since it's meaningless
+       // with the "default" 'v', or .String() already returning the hex.
+       f.Write([]byte(t.HexString()))
+}
+
+func (t *T) Bytes() []byte {
+       return t[:]
+}
+
+func (t *T) AsString() string {
+       return string(t[:])
+}
+
+func (t *T) String() string {
+       return t.HexString()
+}
+
+func (t *T) HexString() string {
+       return fmt.Sprintf("%x", t[:])
+}
+
+func (t *T) FromHexString(s string) (err error) {
+       if len(s) != 2*Size {
+               err = fmt.Errorf("hash hex string has bad length: %d", len(s))
+               return
+       }
+       n, err := hex.Decode(t[:], []byte(s))
+       if err != nil {
+               return
+       }
+       if n != Size {
+               panic(n)
+       }
+       return
+}
+
+// Truncates the hash to 20 bytes for use in auxiliary interfaces, like DHT and trackers.
+func (t *T) ToShort() (short infohash.T) {
+       copy(short[:], t[:])
+       return
+}
+
+var (
+       _ encoding.TextUnmarshaler = (*T)(nil)
+       _ encoding.TextMarshaler   = T{}
+)
+
+func (t *T) UnmarshalText(b []byte) error {
+       return t.FromHexString(string(b))
+}
+
+func (t T) MarshalText() (text []byte, err error) {
+       return []byte(t.HexString()), nil
+}
+
+func FromHexString(s string) (h T) {
+       err := h.FromHexString(s)
+       if err != nil {
+               panic(err)
+       }
+       return
+}
+
+func HashBytes(b []byte) (ret T) {
+       hasher := sha256.New()
+       hasher.Write(b)
+       copy(ret[:], hasher.Sum(nil))
+       return
+}
+
+func ToMultihash(t T) multihash.Multihash {
+       b, _ := multihash.Encode(t[:], multihash.SHA2_256)
+       return b
+}
index a06f7e6a8d56559dd07c647cd007d9dcf7b7b2b7..8ec7aedfc68d9b59a491e451876ad834112dc1fc 100644 (file)
@@ -45,8 +45,8 @@ const (
        PiecePriorityNormal                         // Wanted.
        PiecePriorityHigh                           // Wanted a lot.
        PiecePriorityReadahead                      // May be required soon.
-       // Succeeds a piece where a read occurred. Currently the same as Now,
-       // apparently due to issues with caching.
+       // Succeeds a piece where a read occurred. Currently, the same as Now, apparently due to issues
+       // with caching.
        PiecePriorityNext
        PiecePriorityNow // A Reader is reading in this piece. Highest urgency.
 )