From: Matt Joiner Date: Wed, 6 Mar 2024 11:49:48 +0000 (+1100) Subject: Storages that use piece hashes would panic with pure v2 torrents X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=a9c3dd6084a61132a445a6206e5be0b8b66d52f0;p=btrtrc.git Storages that use piece hashes would panic with pure v2 torrents --- diff --git a/cmd/torrent-verify/main.go b/cmd/torrent-verify/main.go deleted file mode 100644 index 0fbf0242..00000000 --- a/cmd/torrent-verify/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "bytes" - "crypto/sha1" - "fmt" - "io" - "log" - "os" - "path/filepath" - - "github.com/anacrolix/tagflag" - "github.com/edsrzf/mmap-go" - - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/mmap_span" - "github.com/anacrolix/torrent/storage" -) - -func mmapFile(name string) (mm storage.FileMapping, err error) { - f, err := os.Open(name) - if err != nil { - return - } - defer func() { - if err != nil { - f.Close() - } - }() - fi, err := f.Stat() - if err != nil { - return - } - if fi.Size() == 0 { - return - } - reg, err := mmap.MapRegion(f, -1, mmap.RDONLY, mmap.COPY, 0) - if err != nil { - return - } - return storage.WrapFileMapping(reg, f), nil -} - -func verifyTorrent(info *metainfo.Info, root string) error { - span := new(mmap_span.MMapSpan) - for _, file := range info.UpvertedFiles() { - filename := filepath.Join(append([]string{root, info.Name}, file.Path...)...) - mm, err := mmapFile(filename) - if err != nil { - return err - } - if int64(len(mm.Bytes())) != file.Length { - return fmt.Errorf("file %q has wrong length", filename) - } - span.Append(mm) - } - span.InitIndex() - for i, numPieces := 0, info.NumPieces(); i < numPieces; i += 1 { - p := info.Piece(i) - hash := sha1.New() - _, err := io.Copy(hash, io.NewSectionReader(span, p.Offset(), p.Length())) - if err != nil { - return err - } - good := bytes.Equal(hash.Sum(nil), p.Hash().Bytes()) - if !good { - return fmt.Errorf("hash mismatch at piece %d", i) - } - fmt.Printf("%d: %v: %v\n", i, p.Hash(), good) - } - return nil -} - -func main() { - log.SetFlags(log.Flags() | log.Lshortfile) - flags := struct { - DataDir string - tagflag.StartPos - TorrentFile string - }{} - tagflag.Parse(&flags) - metaInfo, err := metainfo.LoadFromFile(flags.TorrentFile) - if err != nil { - log.Fatal(err) - } - info, err := metaInfo.UnmarshalInfo() - if err != nil { - log.Fatalf("error unmarshalling info: %s", err) - } - err = verifyTorrent(&info, flags.DataDir) - if err != nil { - log.Fatalf("torrent failed verification: %s", err) - } -} diff --git a/metainfo/piece.go b/metainfo/piece.go index 4972e529..c1c71221 100644 --- a/metainfo/piece.go +++ b/metainfo/piece.go @@ -1,5 +1,9 @@ package metainfo +import ( + g "github.com/anacrolix/generics" +) + type Piece struct { Info *Info // Can we embed the fields here instead, or is it something to do with saving memory? i pieceIndex @@ -41,8 +45,12 @@ func (p Piece) Offset() int64 { return int64(p.i) * p.Info.PieceLength } -func (p Piece) Hash() (ret Hash) { - copy(ret[:], p.Info.Pieces[p.i*HashSize:(p.i+1)*HashSize]) +func (p Piece) V1Hash() (ret g.Option[Hash]) { + if !p.Info.HasV1() { + return + } + copy(ret.Value[:], p.Info.Pieces[p.i*HashSize:(p.i+1)*HashSize]) + ret.Ok = true return } diff --git a/piece.go b/piece.go index 607e2f88..0aa1e633 100644 --- a/piece.go +++ b/piece.go @@ -56,7 +56,13 @@ func (p *Piece) Info() metainfo.Piece { } func (p *Piece) Storage() storage.Piece { - return p.t.storage.Piece(p.Info()) + var pieceHash g.Option[[]byte] + if p.hash != nil { + pieceHash.Set(p.hash.Bytes()) + } else if p.hashV2.Ok { + pieceHash.Set(p.hashV2.Value.Bytes()) + } + return p.t.storage.PieceWithHash(p.Info(), pieceHash) } func (p *Piece) Flush() { diff --git a/storage/interface.go b/storage/interface.go index 9e8de06c..41b4835e 100644 --- a/storage/interface.go +++ b/storage/interface.go @@ -3,6 +3,8 @@ package storage import ( "io" + g "github.com/anacrolix/generics" + "github.com/anacrolix/torrent/metainfo" ) @@ -20,9 +22,13 @@ type TorrentCapacity *func() (cap int64, capped bool) // Data storage bound to a torrent. type TorrentImpl struct { + // v2 infos might not have the piece hash available even if we have the info. The + // metainfo.Piece.Hash method was removed to enforce this. Piece func(p metainfo.Piece) PieceImpl - Close func() error - Flush func() error + // Preferred over PieceWithHash. Called with the piece hash if it's available. + PieceWithHash func(p metainfo.Piece, pieceHash g.Option[[]byte]) PieceImpl + Close func() error + Flush func() error // Storages that share the same space, will provide equal pointers. The function is called once // to determine the storage for torrents sharing the same function pointer, and mutated in // place. diff --git a/storage/issue95_test.go b/storage/issue95_test.go index 92370797..61acf17e 100644 --- a/storage/issue95_test.go +++ b/storage/issue95_test.go @@ -12,11 +12,12 @@ import ( // Two different torrents opened from the same storage. Closing one should not // break the piece completion on the other. -func testIssue95(t *testing.T, c ClientImpl) { +func testIssue95(t *testing.T, ci ClientImpl) { i1 := &metainfo.Info{ Files: []metainfo.FileInfo{{Path: []string{"a"}}}, Pieces: make([]byte, 20), } + c := NewClient(ci) t1, err := c.OpenTorrent(i1, metainfo.HashBytes([]byte("a"))) require.NoError(t, err) defer t1.Close() diff --git a/storage/issue96_test.go b/storage/issue96_test.go index 726c11c8..1be98aa5 100644 --- a/storage/issue96_test.go +++ b/storage/issue96_test.go @@ -1,6 +1,7 @@ package storage import ( + g "github.com/anacrolix/generics" "testing" "github.com/stretchr/testify/require" @@ -19,7 +20,7 @@ func testMarkedCompleteMissingOnRead(t *testing.T, csf func(string) ClientImplCl } ts, err := cs.OpenTorrent(info, metainfo.Hash{}) require.NoError(t, err) - p := ts.Piece(info.Piece(0)) + p := ts.PieceWithHash(info.Piece(0), g.None[[]byte]()) require.NoError(t, p.MarkComplete()) // require.False(t, p.GetIsComplete()) n, err := p.ReadAt(make([]byte, 1), 0) diff --git a/storage/piece-resource.go b/storage/piece-resource.go index 9e3e63e7..94325027 100644 --- a/storage/piece-resource.go +++ b/storage/piece-resource.go @@ -2,12 +2,14 @@ package storage import ( "bytes" + "encoding/hex" "fmt" "io" "path" "sort" "strconv" + g "github.com/anacrolix/generics" "github.com/anacrolix/missinggo/v2/resource" "github.com/anacrolix/sync" @@ -53,12 +55,13 @@ func (s piecePerResource) OpenTorrent(info *metainfo.Info, infoHash metainfo.Has s, make([]sync.RWMutex, info.NumPieces()), } - return TorrentImpl{Piece: t.Piece, Close: t.Close}, nil + return TorrentImpl{PieceWithHash: t.Piece, Close: t.Close}, nil } -func (s piecePerResourceTorrentImpl) Piece(p metainfo.Piece) PieceImpl { +func (s piecePerResourceTorrentImpl) Piece(p metainfo.Piece, pieceHash g.Option[[]byte]) PieceImpl { return piecePerResourcePiece{ mp: p, + pieceHash: pieceHash, piecePerResource: s.piecePerResource, mu: &s.locks[p.Index()], } @@ -74,6 +77,8 @@ type ConsecutiveChunkReader interface { type piecePerResourcePiece struct { mp metainfo.Piece + // The piece hash if we have it. It could be 20 or 32 bytes depending on the info version. + pieceHash g.Option[[]byte] piecePerResource // This protects operations that move complete/incomplete pieces around, which can trigger read // errors that may cause callers to do more drastic things. @@ -118,7 +123,10 @@ func (s piecePerResourcePiece) mustIsComplete() bool { return completion.Complete } -func (s piecePerResourcePiece) Completion() Completion { +func (s piecePerResourcePiece) Completion() (_ Completion) { + if !s.pieceHash.Ok { + return + } s.mu.RLock() defer s.mu.RUnlock() fi, err := s.completed().Stat() @@ -255,7 +263,7 @@ func (s piecePerResourcePiece) getChunks() (chunks chunks) { } func (s piecePerResourcePiece) completedInstancePath() string { - return path.Join("completed", s.mp.Hash().HexString()) + return path.Join("completed", s.hashHex()) } func (s piecePerResourcePiece) completed() resource.Instance { @@ -267,7 +275,7 @@ func (s piecePerResourcePiece) completed() resource.Instance { } func (s piecePerResourcePiece) incompleteDirPath() string { - return path.Join("incompleted", s.mp.Hash().HexString()) + return path.Join("incompleted", s.hashHex()) } func (s piecePerResourcePiece) incompleteDir() resource.DirInstance { @@ -277,3 +285,7 @@ func (s piecePerResourcePiece) incompleteDir() resource.DirInstance { } return i.(resource.DirInstance) } + +func (me piecePerResourcePiece) hashHex() string { + return hex.EncodeToString(me.pieceHash.Unwrap()) +} diff --git a/storage/sqlite/direct.go b/storage/sqlite/direct.go index 9758ddb8..e4e6981c 100644 --- a/storage/sqlite/direct.go +++ b/storage/sqlite/direct.go @@ -4,10 +4,12 @@ package sqliteStorage import ( + "encoding/hex" "io" "sync" "time" + g "github.com/anacrolix/generics" "github.com/anacrolix/squirrel" "github.com/anacrolix/torrent/metainfo" @@ -63,16 +65,16 @@ func (c *client) capacity() (cap int64, capped bool) { func (c *client) OpenTorrent(*metainfo.Info, metainfo.Hash) (storage.TorrentImpl, error) { t := torrent{c.cache} capFunc := c.capacity - return storage.TorrentImpl{Piece: t.Piece, Close: t.Close, Capacity: &capFunc}, nil + return storage.TorrentImpl{PieceWithHash: t.Piece, Close: t.Close, Capacity: &capFunc}, nil } type torrent struct { c *squirrel.Cache } -func (t torrent) Piece(p metainfo.Piece) storage.PieceImpl { +func (t torrent) Piece(p metainfo.Piece, pieceHash g.Option[[]byte]) storage.PieceImpl { ret := piece{ - sb: t.c.OpenWithLength(p.Hash().HexString(), p.Length()), + sb: t.c.OpenWithLength(hex.EncodeToString(pieceHash.Unwrap()), p.Length()), } ret.ReaderAt = &ret.sb ret.WriterAt = &ret.sb diff --git a/storage/wrappers.go b/storage/wrappers.go index a3907e1d..8dbfdd0a 100644 --- a/storage/wrappers.go +++ b/storage/wrappers.go @@ -4,6 +4,7 @@ import ( "io" "os" + g "github.com/anacrolix/generics" "github.com/anacrolix/missinggo/v2" "github.com/anacrolix/torrent/metainfo" @@ -29,8 +30,19 @@ type Torrent struct { TorrentImpl } +// Deprecated. Use PieceWithHash, as this doesn't work with pure v2 torrents. func (t Torrent) Piece(p metainfo.Piece) Piece { - return Piece{t.TorrentImpl.Piece(p), p} + return t.PieceWithHash(p, g.Some(p.V1Hash().Unwrap().Bytes())) +} + +func (t Torrent) PieceWithHash(p metainfo.Piece, pieceHash g.Option[[]byte]) Piece { + var pieceImpl PieceImpl + if t.TorrentImpl.PieceWithHash != nil { + pieceImpl = t.TorrentImpl.PieceWithHash(p, pieceHash) + } else { + pieceImpl = t.TorrentImpl.Piece(p) + } + return Piece{pieceImpl, p} } type Piece struct {