]> Sergey Matveev's repositories - btrtrc.git/commitdiff
Merge branch 'master' into squirrel
authorMatt Joiner <anacrolix@gmail.com>
Thu, 2 Sep 2021 04:19:29 +0000 (14:19 +1000)
committerMatt Joiner <anacrolix@gmail.com>
Thu, 2 Sep 2021 04:19:29 +0000 (14:19 +1000)
17 files changed:
.github/workflows/go.yml
client_test.go
cmd/torrent-create/main.go
cmd/torrent/main.go
go.mod
go.sum
metainfo/info.go
storage/file-deprecated.go [new file with mode: 0644]
storage/file-paths.go [new file with mode: 0644]
storage/file.go
storage/map-piece-completion.go
storage/mmap.go
storage/safe-path_test.go
storage/sqlite-piece-completion.go
t.go
torrent.go
torrent_test.go

index ac4ff74e7b76ce982a51663a7a6e204a31172181..072eda75528b091d2425d077419fd5b18b82b3a0 100644 (file)
@@ -17,7 +17,7 @@ jobs:
     - name: Set up Go
       uses: actions/setup-go@v2
       with:
-        go-version: 1.16
+        go-version: 1.17
 
     - name: Download Go modules
       run: go mod download
index df0aeaaa07d9b197b5bcbf1ef8d3637511ae92a6..a8192eb1dd5d89df05f2925ab792bc71359aa8a9 100644 (file)
@@ -86,7 +86,7 @@ func TestTorrentInitialState(t *testing.T) {
        )
        tor.setChunkSize(2)
        tor.cl.lock()
-       err := tor.setInfoBytes(mi.InfoBytes)
+       err := tor.setInfoBytesLocked(mi.InfoBytes)
        tor.cl.unlock()
        require.NoError(t, err)
        require.Len(t, tor.pieces, 3)
index d36dd0b5a9699cf884139f3567cf8e3f1f40caae..973d4e4fa269596e5430d7b867245dd62e641185 100644 (file)
@@ -12,9 +12,9 @@ import (
 
 var (
        builtinAnnounceList = [][]string{
-               {"udp://tracker.openbittorrent.com:80"},
-               {"udp://tracker.publicbt.com:80"},
-               {"udp://tracker.istole.it:6969"},
+               {"http://p4p.arenabg.com:1337/announce"},
+               {"udp://tracker.opentrackr.org:1337/announce"},
+               {"udp://tracker.openbittorrent.com:6969/announce"},
        }
 )
 
index 61fd2ab2791cafc5ec80d2f5249181b02fe82f5e..a11373f7bb5326a041686b2a223d6bd439a9d0e3 100644 (file)
@@ -355,8 +355,12 @@ func downloadErr() error {
                return xerrors.New("y u no complete torrents?!")
        }
        if flags.Seed {
-               outputStats(client)
-               <-stop.C()
+               if len(client.Torrents()) == 0 {
+                       log.Print("no torrents to seed")
+               } else {
+                       outputStats(client)
+                       <-stop.C()
+               }
        }
        return nil
 }
diff --git a/go.mod b/go.mod
index d60d2e817bdd9f2bc7fd95a1711559ad7c7e657d..952d25a391b1ded7322897f6adc8ae9e8059c73e 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -26,14 +26,14 @@ require (
        github.com/edsrzf/mmap-go v1.0.0
        github.com/elliotchance/orderedmap v1.4.0
        github.com/frankban/quicktest v1.13.1
-       github.com/fsnotify/fsnotify v1.4.9
+       github.com/fsnotify/fsnotify v1.5.1
        github.com/google/btree v1.0.1
        github.com/gorilla/websocket v1.4.2
-       github.com/jessevdk/go-flags v1.4.0
+       github.com/jessevdk/go-flags v1.5.0
        github.com/pion/datachannel v1.4.21
        github.com/pion/ice/v2 v2.1.12 // indirect
        github.com/pion/interceptor v0.0.15 // indirect
-       github.com/pion/rtp v1.7.1 // indirect
+       github.com/pion/rtp v1.7.2 // indirect
        github.com/pion/srtp/v2 v2.0.5 // indirect
        github.com/pion/webrtc/v3 v3.0.32
        github.com/pkg/errors v0.9.1
@@ -47,9 +47,9 @@ require (
 )
 
 require (
-       github.com/alexflint/go-scalar v1.0.0 // indirect
+       github.com/alexflint/go-scalar v1.1.0 // indirect
        github.com/anacrolix/mmsg v1.0.0 // indirect
-       github.com/anacrolix/stm v0.3.0-alpha // indirect
+       github.com/anacrolix/stm v0.3.0 // indirect
        github.com/benbjohnson/immutable v0.3.0 // indirect
        github.com/bits-and-blooms/bitset v1.2.0 // indirect
        github.com/google/go-cmp v0.5.6 // indirect
@@ -70,7 +70,7 @@ require (
        github.com/pion/turn/v2 v2.0.5 // indirect
        github.com/pion/udp v0.1.1 // indirect
        github.com/pmezard/go-difflib v1.0.0 // indirect
-       github.com/rogpeppe/go-internal v1.6.1 // indirect
+       github.com/rogpeppe/go-internal v1.8.0 // indirect
        github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
        github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
        github.com/spaolacci/murmur3 v1.1.0 // indirect
@@ -82,6 +82,9 @@ require (
 
 go 1.17
 
-exclude bazil.org/fuse v0.0.0-20200419173433-3ba628eaf417
+exclude (
+       bazil.org/fuse v0.0.0-20200419173433-3ba628eaf417
+       bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05
+)
 
 exclude github.com/willf/bitset v1.2.0
diff --git a/go.sum b/go.sum
index 334e78995064828741245b068215a95b16278008..39172aa75ad58e3c0f5aab2dfc2b6b466c9febbe 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -41,8 +41,9 @@ github.com/alexflint/go-arg v1.2.0/go.mod h1:3Rj4baqzWaGGmZA2+bVTV8zQOZEjBQAPBnL
 github.com/alexflint/go-arg v1.3.0/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
 github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0=
 github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
-github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
 github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
+github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
+github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
 github.com/anacrolix/chansync v0.0.0-20210524073341-a336ebc2de92/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
 github.com/anacrolix/chansync v0.1.0 h1:4cIfJmEV8sYkSEMW2AXnPjX6iQT4plqELJ65pLna6OA=
 github.com/anacrolix/chansync v0.1.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
@@ -123,8 +124,9 @@ github.com/anacrolix/stm v0.1.1-0.20191106051447-e749ba3531cf/go.mod h1:zoVQRvSi
 github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
 github.com/anacrolix/stm v0.2.1-0.20201002073511-c35a2c748c6a/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
 github.com/anacrolix/stm v0.2.1-0.20210310231625-45c211559de6/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
-github.com/anacrolix/stm v0.3.0-alpha h1:yhOHk1NPkpGKqCAOB4XkgFXwB5Eh2KU/WVMksNnxr2Y=
 github.com/anacrolix/stm v0.3.0-alpha/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
+github.com/anacrolix/stm v0.3.0 h1:peQncJSNJtk1YBrFbW0DLKYqll+sa0kOk8EvXRcO+wA=
+github.com/anacrolix/stm v0.3.0/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
 github.com/anacrolix/sync v0.0.0-20171108081538-eee974e4f8c1/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w=
 github.com/anacrolix/sync v0.0.0-20180611022320-3c4cb11f5a01/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w=
 github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
@@ -234,8 +236,9 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM
 github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8=
 github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
+github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -362,8 +365,9 @@ github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmK
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
-github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -518,8 +522,8 @@ github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko
 github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
 github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
 github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.1 h1:hCaxfVgPGt13eF/Tu9RhVn04c+dAcRZmhdDWqUE13oY=
-github.com/pion/rtp v1.7.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/rtp v1.7.2 h1:HCDKDCixh7PVjkQTsqHAbk1lg+bx059EHxcnyl42dYs=
+github.com/pion/rtp v1.7.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
 github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
 github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
 github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
@@ -555,6 +559,7 @@ github.com/pion/webrtc/v3 v3.0.11/go.mod h1:WEvXneGTeqNmiR59v5jTsxMc4yXQyOQcRsrd
 github.com/pion/webrtc/v3 v3.0.27/go.mod h1:QpLDmsU5a/a05n230gRtxZRvfHhFzn9ukGUL2x4G5ic=
 github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4=
 github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -596,8 +601,9 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rs/dnscache v0.0.0-20190621150935-06bb5526f76b/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
 github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 h1:IJ6Df0uxPDtNoByV0KkzVKNseWvZFCNM/S9UoyOMCSI=
 github.com/rs/dnscache v0.0.0-20210201191234-295bba877686/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
@@ -845,11 +851,13 @@ golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
index 1c2d0b85b9abef37b32b662fc189c504108b957d..c907f492df311f3dace7589b0a83a93d4d9af513 100644 (file)
@@ -23,10 +23,27 @@ type Info struct {
        Files  []FileInfo `bencode:"files,omitempty"` // BEP3, mutually exclusive with Length
 }
 
-// This is a helper that sets Files and Pieces from a root path and its
-// children.
+// The Info.Name field is "advisory". For multi-file torrents it's usually a suggested directory
+// name. There are situations where we don't want a directory (like using the contents of a torrent
+// as the immediate contents of a directory), or the name is invalid. Transmission will inject the
+// name of the torrent file if it doesn't like the name, resulting in a different infohash
+// (https://github.com/transmission/transmission/issues/1775). To work around these situations, we
+// will use a sentinel name for compatibility with Transmission and to signal to our own client that
+// we intended to have no directory name. By exposing it in the API we can check for references to
+// this behaviour within this implementation.
+const NoName = "-"
+
+// This is a helper that sets Files and Pieces from a root path and its children.
 func (info *Info) BuildFromFilePath(root string) (err error) {
-       info.Name = filepath.Base(root)
+       info.Name = func() string {
+               b := filepath.Base(root)
+               switch b {
+               case ".", "..", string(filepath.Separator):
+                       return NoName
+               default:
+                       return b
+               }
+       }()
        info.Files = nil
        err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
                if err != nil {
diff --git a/storage/file-deprecated.go b/storage/file-deprecated.go
new file mode 100644 (file)
index 0000000..4560b9d
--- /dev/null
@@ -0,0 +1,34 @@
+package storage
+
+import (
+       "github.com/anacrolix/torrent/metainfo"
+)
+
+func NewFileWithCompletion(baseDir string, completion PieceCompletion) ClientImplCloser {
+       return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
+}
+
+// File storage with data partitioned by infohash.
+func NewFileByInfoHash(baseDir string) ClientImplCloser {
+       return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
+}
+
+// Deprecated: Allows passing a function to determine the path for storing torrent data. The
+// function is responsible for sanitizing the info if it uses some part of it (for example
+// sanitizing info.Name).
+func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
+       return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
+}
+
+// Deprecated: Allows passing custom PieceCompletion
+func NewFileWithCustomPathMakerAndCompletion(
+       baseDir string,
+       pathMaker TorrentDirFilePathMaker,
+       completion PieceCompletion,
+) ClientImplCloser {
+       return NewFileOpts(NewFileClientOpts{
+               ClientBaseDir:   baseDir,
+               TorrentDirMaker: pathMaker,
+               PieceCompletion: completion,
+       })
+}
diff --git a/storage/file-paths.go b/storage/file-paths.go
new file mode 100644 (file)
index 0000000..8d338f8
--- /dev/null
@@ -0,0 +1,38 @@
+package storage
+
+import (
+       "os"
+       "path/filepath"
+       "strings"
+
+       "github.com/anacrolix/torrent/metainfo"
+)
+
+// Determines the filepath to be used for each file in a torrent.
+type FilePathMaker func(opts FilePathMakerOpts) string
+
+// Determines the directory for a given torrent within a storage client.
+type TorrentDirFilePathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
+
+// Info passed to a FilePathMaker.
+type FilePathMakerOpts struct {
+       Info *metainfo.Info
+       File *metainfo.FileInfo
+}
+
+// defaultPathMaker just returns the storage client's base directory.
+func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
+       return baseDir
+}
+
+func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
+       return filepath.Join(baseDir, infoHash.HexString())
+}
+
+func isSubFilepath(base, sub string) bool {
+       rel, err := filepath.Rel(base, sub)
+       if err != nil {
+               return false
+       }
+       return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
+}
index 22c1d8499c06bd418290436db1a8a4a847001940..bbfe6d66122a4eb2e8e42e88ccb15d222ff60dbd 100644 (file)
@@ -13,73 +13,63 @@ import (
        "github.com/anacrolix/torrent/metainfo"
 )
 
-// File-based storage for torrents, that isn't yet bound to a particular
-// torrent.
+// File-based storage for torrents, that isn't yet bound to a particular torrent.
 type fileClientImpl struct {
-       baseDir   string
-       pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
-       pc        PieceCompletion
+       opts NewFileClientOpts
 }
 
-// The Default path maker just returns the current path
-func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return baseDir
-}
-
-func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return filepath.Join(baseDir, infoHash.HexString())
-}
-
-// All Torrent data stored in this baseDir
+// All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
 func NewFile(baseDir string) ClientImplCloser {
        return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
 }
 
-func NewFileWithCompletion(baseDir string, completion PieceCompletion) *fileClientImpl {
-       return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
-}
-
-// File storage with data partitioned by infohash.
-func NewFileByInfoHash(baseDir string) ClientImplCloser {
-       return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
+type NewFileClientOpts struct {
+       // The base directory for all downloads.
+       ClientBaseDir   string
+       FilePathMaker   FilePathMaker
+       TorrentDirMaker TorrentDirFilePathMaker
+       PieceCompletion PieceCompletion
 }
 
-// Allows passing a function to determine the path for storing torrent data. The function is
-// responsible for sanitizing the info if it uses some part of it (for example sanitizing
-// info.Name).
-func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
-       return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
-}
-
-// Allows passing custom PieceCompletion
-func NewFileWithCustomPathMakerAndCompletion(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string, completion PieceCompletion) *fileClientImpl {
-       if pathMaker == nil {
-               pathMaker = defaultPathMaker
+// NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
+func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
+       if opts.TorrentDirMaker == nil {
+               opts.TorrentDirMaker = defaultPathMaker
+       }
+       if opts.FilePathMaker == nil {
+               opts.FilePathMaker = func(opts FilePathMakerOpts) string {
+                       var parts []string
+                       if opts.Info.Name != metainfo.NoName {
+                               parts = append(parts, opts.Info.Name)
+                       }
+                       return filepath.Join(append(parts, opts.File.Path...)...)
+               }
        }
-       return &fileClientImpl{
-               baseDir:   baseDir,
-               pathMaker: pathMaker,
-               pc:        completion,
+       if opts.PieceCompletion == nil {
+               opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
        }
+       return fileClientImpl{opts}
 }
 
-func (me *fileClientImpl) Close() error {
-       return me.pc.Close()
+func (me fileClientImpl) Close() error {
+       return me.opts.PieceCompletion.Close()
 }
 
-func (fs *fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
-       dir := fs.pathMaker(fs.baseDir, info, infoHash)
+func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
+       dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
        upvertedFiles := info.UpvertedFiles()
        files := make([]file, 0, len(upvertedFiles))
        for i, fileInfo := range upvertedFiles {
-               var s string
-               s, err = ToSafeFilePath(append([]string{info.Name}, fileInfo.Path...)...)
-               if err != nil {
-                       err = fmt.Errorf("file %v has unsafe path %q: %w", i, fileInfo.Path, err)
+               filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
+                       Info: info,
+                       File: &fileInfo,
+               }))
+               if !isSubFilepath(dir, filePath) {
+                       err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
                        return
                }
                f := file{
-                       path:   filepath.Join(dir, s),
+                       path:   filePath,
                        length: fileInfo.Length,
                }
                if f.length == 0 {
@@ -95,7 +85,7 @@ func (fs *fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Has
                files,
                segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
                infoHash,
-               fs.pc,
+               fs.opts.PieceCompletion,
        }
        return TorrentImpl{
                Piece: t.Piece,
index 5f7f87eff31dc49d965baac40c5e022786bd58e5..afb1e97b64d6a1310f0235a5e19cb516f0fbfadd 100644 (file)
@@ -7,6 +7,7 @@ import (
 )
 
 type mapPieceCompletion struct {
+       // TODO: Generics
        m sync.Map
 }
 
index 9c0a27edeb9fb07b1239fac5175504d6ed7b6288..9c9e84710137f73a662c2def304316badaea0470 100644 (file)
@@ -22,6 +22,7 @@ type mmapClientImpl struct {
        pc      PieceCompletion
 }
 
+// TODO: Support all the same native filepath configuration that NewFileOpts provides.
 func NewMMap(baseDir string) ClientImplCloser {
        return NewMMapWithCompletion(baseDir, pieceCompletionForDir(baseDir))
 }
index bf7e7ba0f1448f3dc4fc630abb37d0f7ad183750..e12d3333cdb5d626d89934d50aa01b88eb09a888 100644 (file)
@@ -1,28 +1,38 @@
 package storage
 
 import (
+       "fmt"
        "log"
        "path/filepath"
        "testing"
+
+       "github.com/anacrolix/torrent/metainfo"
+       qt "github.com/frankban/quicktest"
 )
 
 func init() {
        log.SetFlags(log.Flags() | log.Lshortfile)
 }
 
-func TestSafePath(t *testing.T) {
-       for _, _case := range []struct {
-               input     []string
-               expected  string
-               expectErr bool
-       }{
-               {input: []string{"a", filepath.FromSlash(`b/../../..`)}, expectErr: true},
-               {input: []string{"a", filepath.FromSlash(`b/../.././..`)}, expectErr: true},
-               {input: []string{
-                       filepath.FromSlash(`NewSuperHeroMovie-2019-English-720p.avi /../../../../../Roaming/Microsoft/Windows/Start Menu/Programs/Startup/test3.exe`)},
-                       expectErr: true,
-               },
-       } {
+// I think these are mainly tests for bad metainfos that try to escape the client base directory.
+var safeFilePathTests = []struct {
+       input     []string
+       expectErr bool
+}{
+       // We might want a test for invalid chars inside components, or file maker opt funcs returning
+       // absolute paths (and thus presumably clobbering earlier "makers").
+       {input: []string{"a", filepath.FromSlash(`b/..`)}, expectErr: false},
+       {input: []string{"a", filepath.FromSlash(`b/../../..`)}, expectErr: true},
+       {input: []string{"a", filepath.FromSlash(`b/../.././..`)}, expectErr: true},
+       {input: []string{
+               filepath.FromSlash(`NewSuperHeroMovie-2019-English-720p.avi /../../../../../Roaming/Microsoft/Windows/Start Menu/Programs/Startup/test3.exe`)},
+               expectErr: true,
+       },
+}
+
+// Tests the ToSafeFilePath func.
+func TestToSafeFilePath(t *testing.T) {
+       for _, _case := range safeFilePathTests {
                actual, err := ToSafeFilePath(_case.input...)
                if _case.expectErr {
                        if err != nil {
@@ -32,3 +42,27 @@ func TestSafePath(t *testing.T) {
                }
        }
 }
+
+// Check that safe file path handling still exists for the newer file-opt-maker variants.
+func TestFileOptsSafeFilePathHandling(t *testing.T) {
+       c := qt.New(t)
+       for i, _case := range safeFilePathTests {
+               c.Run(fmt.Sprintf("Case%v", i), func(c *qt.C) {
+                       info := metainfo.Info{
+                               Files: []metainfo.FileInfo{
+                                       {Path: _case.input},
+                               },
+                       }
+                       client := NewFileOpts(NewFileClientOpts{
+                               ClientBaseDir: "somedir",
+                       })
+                       defer func() { c.Check(client.Close(), qt.IsNil) }()
+                       torImpl, err := client.OpenTorrent(&info, metainfo.Hash{})
+                       if _case.expectErr {
+                               c.Check(err, qt.Not(qt.IsNil))
+                       } else {
+                               c.Check(torImpl.Close(), qt.IsNil)
+                       }
+               })
+       }
+}
index 196f1e5007e6c3697250540515916c754296ee3f..857b97a2b9243db2acb993fecb0ba6d8c9a74cfc 100644 (file)
@@ -64,6 +64,7 @@ func (me *sqlitePieceCompletion) Close() (err error) {
        defer me.mu.Unlock()
        if me.db != nil {
                err = me.db.Close()
+               me.db = nil
        }
        return
 }
diff --git a/t.go b/t.go
index a593a6bbf9a0c669945e111514860e9d89f4f857..69cef55410a391ab9e1bbfb546a859ee90fe785e 100644 (file)
--- a/t.go
+++ b/t.go
@@ -44,20 +44,26 @@ func (t *Torrent) NewReader() Reader {
 
 type PieceStateRuns []PieceStateRun
 
-func (me PieceStateRuns) String() string {
-       ss := make([]string, 0, len(me))
-       for _, psr := range me {
-               ss = append(ss, psr.String())
+func (me PieceStateRuns) String() (s string) {
+       if len(me) > 0 {
+               var sb strings.Builder
+               sb.WriteString(me[0].String())
+               for i := 1; i < len(me); i += 1 {
+                       sb.WriteByte(' ')
+                       sb.WriteString(me[i].String())
+               }
+               return sb.String()
        }
-       return strings.Join(ss, " ")
+       return
 }
 
 // Returns the state of pieces of the torrent. They are grouped into runs of same state. The sum of
 // the state run-lengths is the number of pieces in the torrent.
-func (t *Torrent) PieceStateRuns() PieceStateRuns {
+func (t *Torrent) PieceStateRuns() (runs PieceStateRuns) {
        t.cl.rLock()
-       defer t.cl.rUnlock()
-       return t.pieceStateRuns()
+       runs = t.pieceStateRuns()
+       t.cl.rUnlock()
+       return
 }
 
 func (t *Torrent) PieceState(piece pieceIndex) PieceState {
index 7fbcd2781f7589b5d437e5d1a85cdccae436bdd7..8e2b39174a7be8e2980c9ce3c6412ec12e7b5c97 100644 (file)
@@ -449,7 +449,7 @@ func (t *Torrent) onSetInfo() {
 }
 
 // Called when metadata for a torrent becomes available.
-func (t *Torrent) setInfoBytes(b []byte) error {
+func (t *Torrent) setInfoBytesLocked(b []byte) error {
        if metainfo.HashBytes(b) != t.infoHash {
                return errors.New("info bytes have wrong hash")
        }
@@ -1280,7 +1280,7 @@ func (t *Torrent) maybeCompleteMetadata() error {
                // Don't have enough metadata pieces.
                return nil
        }
-       err := t.setInfoBytes(t.metadataBytes)
+       err := t.setInfoBytesLocked(t.metadataBytes)
        if err != nil {
                t.invalidateMetadata()
                return fmt.Errorf("error setting info bytes: %s", err)
@@ -1355,7 +1355,7 @@ func (t *Torrent) bytesCompleted() int64 {
 func (t *Torrent) SetInfoBytes(b []byte) (err error) {
        t.cl.lock()
        defer t.cl.unlock()
-       return t.setInfoBytes(b)
+       return t.setInfoBytesLocked(b)
 }
 
 // Returns true if connection is removed from torrent.Conns.
@@ -1658,7 +1658,7 @@ func (t *Torrent) AnnounceToDht(s DhtServer) (done <-chan struct{}, stop func(),
        return
 }
 
-func (t *Torrent) announceToDht(s DhtServer) error {
+func (t *Torrent) timeboxedAnnounceToDht(s DhtServer) error {
        _, stop, err := t.AnnounceToDht(s)
        if err != nil {
                return err
@@ -1695,7 +1695,7 @@ func (t *Torrent) dhtAnnouncer(s DhtServer) {
                        t.numDHTAnnounces++
                        cl.unlock()
                        defer cl.lock()
-                       err := t.announceToDht(s)
+                       err := t.timeboxedAnnounceToDht(s)
                        if err != nil {
                                t.logger.WithDefaultLevel(log.Warning).Printf("error announcing %q to DHT: %s", t, err)
                        }
index b413a39e8c928926cb6c0dfa60a6872540c9d25c..053ed5d1a444b8b8d9c9461e865ed5ca03343b73 100644 (file)
@@ -147,7 +147,7 @@ func TestPieceHashFailed(t *testing.T) {
        cl.initLogger()
        tt := cl.newTorrent(mi.HashInfoBytes(), badStorage{})
        tt.setChunkSize(2)
-       require.NoError(t, tt.setInfoBytes(mi.InfoBytes))
+       require.NoError(t, tt.setInfoBytesLocked(mi.InfoBytes))
        tt.cl.lock()
        tt.pieces[1]._dirtyChunks.AddRange(0, 3)
        require.True(t, tt.pieceAllDirty(1))