From ac62a81efc5fdc0de7a99e66bc9a88eb73eadb50 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Tue, 27 Feb 2024 11:06:17 +1100 Subject: [PATCH] Add MagnetV2 and infohash_v2 --- go.mod | 7 ++ go.sum | 18 +++- metainfo/magnet-v2.go | 147 +++++++++++++++++++++++++++++++ metainfo/magnet-v2_test.go | 38 ++++++++ metainfo/magnet.go | 59 ++++++++----- metainfo/magnet_test.go | 21 +++++ types/infohash-v2/infohash-v2.go | 95 ++++++++++++++++++++ types/types.go | 4 +- 8 files changed, 364 insertions(+), 25 deletions(-) create mode 100644 metainfo/magnet-v2.go create mode 100644 metainfo/magnet-v2_test.go create mode 100644 types/infohash-v2/infohash-v2.go diff --git a/go.mod b/go.mod index ce023135..075c2bdd 100644 --- 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 75919e87..d371109d 100644 --- 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 index 00000000..a6c2c8b4 --- /dev/null +++ b/metainfo/magnet-v2.go @@ -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 index 00000000..620d385c --- /dev/null +++ b/metainfo/magnet-v2_test.go @@ -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) +} diff --git a/metainfo/magnet.go b/metainfo/magnet.go index 48dc148e..7916f9a7 100644 --- a/metainfo/magnet.go +++ b/metainfo/magnet.go @@ -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]) } } diff --git a/metainfo/magnet_test.go b/metainfo/magnet_test.go index 24ab15b1..ba13ac88 100644 --- a/metainfo/magnet_test.go +++ b/metainfo/magnet_test.go @@ -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 index 00000000..02ddd1d8 --- /dev/null +++ b/types/infohash-v2/infohash-v2.go @@ -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 +} diff --git a/types/types.go b/types/types.go index a06f7e6a..8ec7aedf 100644 --- a/types/types.go +++ b/types/types.go @@ -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. ) -- 2.44.0