Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
anacrolix committed Feb 27, 2024
1 parent dde62ee commit 4b1cff1
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 57 deletions.
6 changes: 6 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ The DHT is a bit different: you can't be an active node if you are a badnat, but
- https://www.bittorrent.org/beps/bep_0055.html
- https://github.com/anacrolix/torrent/issues/685
- https://stackoverflow.com/questions/38786438/libutp-%C2%B5tp-and-nat-traversal-udp-hole-punching

### BitTorrent v2

- https://www.bittorrent.org/beps/bep_0052.html

The canonical infohash to use for a torrent will be the v1 infohash, or the short form of the v2 infohash if v1 is not supported. This will apply everywhere that both infohashes are present. If only one 20 byte hash is present, it is always the v1 hash (except in code that interfaces with things that only work with 20 byte hashes, like the DHT).
24 changes: 14 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"expvar"
"fmt"
infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
"github.com/cespare/xxhash"
"io"
"math"
Expand Down Expand Up @@ -1291,8 +1292,9 @@ func (cl *Client) newTorrentOpt(opts AddTorrentOpts) (t *Torrent) {
}

t = &Torrent{
cl: cl,
infoHash: opts.InfoHash,
cl: cl,
infoHash: opts.InfoHash,
infoHashV2: opts.InfoHashV2,
peers: prioritizedPeers{
om: gbtree.New(32),
getPrio: func(p PeerInfo) peerPriority {
Expand Down Expand Up @@ -1396,19 +1398,21 @@ func (cl *Client) AddTorrentOpt(opts AddTorrentOpts) (t *Torrent, new bool) {
}

type AddTorrentOpts struct {
InfoHash infohash.T
Storage storage.ClientImpl
ChunkSize pp.Integer
InfoBytes []byte
InfoHash infohash.T
InfoHashV2 g.Option[infohash_v2.T]
Storage storage.ClientImpl
ChunkSize pp.Integer
InfoBytes []byte
}

// Add or merge a torrent spec. Returns new if the torrent wasn't already in the client. See also
// Torrent.MergeSpec.
func (cl *Client) AddTorrentSpec(spec *TorrentSpec) (t *Torrent, new bool, err error) {
t, new = cl.AddTorrentOpt(AddTorrentOpts{
InfoHash: spec.InfoHash,
Storage: spec.Storage,
ChunkSize: spec.ChunkSize,
InfoHash: spec.InfoHash,
InfoHashV2: spec.InfoHashV2,
Storage: spec.Storage,
ChunkSize: spec.ChunkSize,
})
modSpec := *spec
if new {
Expand Down Expand Up @@ -1459,7 +1463,7 @@ func (t *Torrent) MergeSpec(spec *TorrentSpec) error {
t.maybeNewConns()
t.dataDownloadDisallowed.SetBool(spec.DisallowDataDownload)
t.dataUploadDisallowed = spec.DisallowDataUpload
return nil
return t.AddPieceLayers(spec.PieceLayers)
}

func (cl *Client) dropTorrent(infoHash metainfo.Hash, wg *sync.WaitGroup) (err error) {
Expand Down
9 changes: 7 additions & 2 deletions cmd/torrent/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
return
}

func addTorrents(ctx context.Context, client *torrent.Client, flags downloadFlags, wg *sync.WaitGroup) error {
func addTorrents(
ctx context.Context,
client *torrent.Client,
flags downloadFlags,
wg *sync.WaitGroup,
) error {
testPeers := resolveTestPeers(flags.TestPeer)
for _, arg := range flags.Torrent {
t, err := func() (*torrent.Torrent, error) {
Expand All @@ -112,7 +117,7 @@ func addTorrents(ctx context.Context, client *torrent.Client, flags downloadFlag
}
t, err := client.AddTorrent(metaInfo)
if err != nil {
return nil, fmt.Errorf("adding torrent: %w", err)
return nil, err
}
return t, nil
} else if strings.HasPrefix(arg, "infohash:") {
Expand Down
44 changes: 44 additions & 0 deletions cmd/torrent2/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// This is an alternate to cmd/torrent which has become bloated with awful argument parsing. Since
// this is my most complicated binary, I will try to build something that satisfies only what I need
// here.
package main

import (
"github.com/anacrolix/torrent/metainfo"
"os"
)

type argError struct {
err error
}

func assertOk(err error) {
if err != nil {
panic(err)
}
}

func bail(str string) {
panic(str)
}

func main() {
args := os.Args[1:]
map[string]func(){
"metainfo": func() {
map[string]func(){
"validate-v2": func() {
mi, err := metainfo.LoadFromFile(args[2])
assertOk(err)
info, err := mi.UnmarshalInfo()
assertOk(err)
if !info.HasV2() {
bail("not a v2 torrent")
}
err = metainfo.ValidatePieceLayers(mi.PieceLayers, &info.FileTree, info.PieceLength)
assertOk(err)
},
}[args[1]]()
},
}[args[0]]()
}
15 changes: 13 additions & 2 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package torrent

import (
"crypto/sha256"
"github.com/RoaringBitmap/roaring"
g "github.com/anacrolix/generics"
"github.com/anacrolix/missinggo/v2/bitmap"

"github.com/anacrolix/torrent/metainfo"
Expand All @@ -16,6 +18,11 @@ type File struct {
fi metainfo.FileInfo
displayPath string
prio piecePriority
piecesRoot g.Option[[sha256.Size]byte]
}

func (f *File) String() string {
return f.Path()
}

func (f *File) Torrent() *Torrent {
Expand All @@ -28,12 +35,12 @@ func (f *File) Offset() int64 {
}

// The FileInfo from the metainfo.Info to which this file corresponds.
func (f File) FileInfo() metainfo.FileInfo {
func (f *File) FileInfo() metainfo.FileInfo {
return f.fi
}

// The file's path components joined by '/'.
func (f File) Path() string {
func (f *File) Path() string {
return f.path
}

Expand Down Expand Up @@ -204,3 +211,7 @@ func (f *File) EndPieceIndex() int {
}
return pieceIndex((f.offset + f.length + int64(f.t.usualPieceSize()) - 1) / int64(f.t.usualPieceSize()))
}

func (f *File) numPieces() int {
return f.EndPieceIndex() - f.BeginPieceIndex()
}
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/anacrolix/torrent

go 1.21.4

toolchain go1.21.7
go 1.22

require (
github.com/RoaringBitmap/roaring v1.2.3
Expand Down
42 changes: 42 additions & 0 deletions merkle/merkle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package merkle

import (
"crypto/sha256"
"fmt"
g "github.com/anacrolix/generics"
"math/bits"
)

func Root(hashes [][sha256.Size]byte) [sha256.Size]byte {
if len(hashes) <= 1 {
return hashes[0]
}
numHashes := uint(len(hashes))
if numHashes != RoundUpToPowerOfTwo(uint(len(hashes))) {
panic(fmt.Sprintf("expected power of two number of hashes, got %d", numHashes))
}
var next [][sha256.Size]byte
for i := 0; i < len(hashes); i += 2 {
left := hashes[i]
right := hashes[i+1]
h := sha256.Sum256(append(left[:], right[:]...))
next = append(next, h)
}
return Root(next)
}

func CompactLayerToSliceHashes(compactLayer string) (hashes [][sha256.Size]byte, err error) {
g.MakeSliceWithLength(&hashes, len(compactLayer)/sha256.Size)
for i := range hashes {
n := copy(hashes[i][:], compactLayer[i*sha256.Size:])
if n != sha256.Size {
err = fmt.Errorf("compact layer has incomplete hash at index %d", i)
return
}
}
return
}

func RoundUpToPowerOfTwo(n uint) (ret uint) {
return 1 << bits.Len(n-1)
}
58 changes: 58 additions & 0 deletions metainfo/bep52.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package metainfo

import (
"fmt"
"github.com/anacrolix/torrent/merkle"
)

func ValidatePieceLayers(
pieceLayers map[string]string,
fileTree *FileTree,
pieceLength int64,
) (err error) {
fileTree.Walk(nil, func(path []string, ft *FileTree) {
if err != nil {
return
}
if ft.IsDir() {
return
}
piecesRoot := ft.PiecesRootAsByteArray()
if !piecesRoot.Ok {
return
}
filePieceLayers, ok := pieceLayers[string(piecesRoot.Value[:])]
if !ok {
// BEP 52: "For each file in the file tree that is larger than the piece size it
// contains one string value.". The reference torrent creator in
// https://blog.libtorrent.org/2020/09/bittorrent-v2/ also has this. I'm not sure what
// harm it causes if it's present anyway, possibly it won't be useful to us.
if ft.File.Length > pieceLength {
err = fmt.Errorf("no piece layers for file %q", path)
}
return
}
var layerHashes [][32]byte
layerHashes, err = merkle.CompactLayerToSliceHashes(filePieceLayers)
padHash := HashForPiecePad(pieceLength)
for uint(len(layerHashes)) < merkle.RoundUpToPowerOfTwo(uint(len(layerHashes))) {
layerHashes = append(layerHashes, padHash)
}
var root [32]byte
root = merkle.Root(layerHashes)
if root != piecesRoot.Value {
err = fmt.Errorf("file %q: expected hash %x got %x", path, piecesRoot.Value, root)
return
}
})
return
}

// Returns the padding hash for the hash layer corresponding to a piece. It can't be zero because
// that's the bottom-most layer (the hashes for the smallest blocks).
func HashForPiecePad(pieceLength int64) (hash [32]byte) {
// This should be a power of two, and probably checked elsewhere.
blocksPerPiece := pieceLength / (1 << 14)
blockHashes := make([][32]byte, blocksPerPiece)
return merkle.Root(blockHashes)
}
Loading

0 comments on commit 4b1cff1

Please sign in to comment.