librats 1.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/README.md +145 -331
  2. package/binding.gyp +16 -3
  3. package/lib/index.d.ts +288 -696
  4. package/lib/index.js +407 -44
  5. package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
  6. package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
  7. package/native-src/CMakeLists.txt +404 -179
  8. package/native-src/LICENSE +1 -1
  9. package/native-src/src/librats/bindings/rats.cpp +762 -0
  10. package/native-src/src/librats/bindings/rats.h +380 -0
  11. package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
  12. package/native-src/src/librats/bittorrent/bencode.h +176 -0
  13. package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
  14. package/native-src/src/librats/bittorrent/bitfield.h +76 -0
  15. package/native-src/src/librats/bittorrent/byte_io.h +58 -0
  16. package/native-src/src/librats/bittorrent/choker.cpp +25 -0
  17. package/native-src/src/librats/bittorrent/choker.h +46 -0
  18. package/native-src/src/librats/bittorrent/client.cpp +413 -0
  19. package/native-src/src/librats/bittorrent/client.h +227 -0
  20. package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
  21. package/native-src/src/librats/bittorrent/disk_io.h +150 -0
  22. package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
  23. package/native-src/src/librats/bittorrent/extensions.h +94 -0
  24. package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
  25. package/native-src/src/librats/bittorrent/file_storage.h +79 -0
  26. package/native-src/src/librats/bittorrent/log.h +42 -0
  27. package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
  28. package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
  29. package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
  30. package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
  31. package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
  32. package/native-src/src/librats/bittorrent/peer_list.h +75 -0
  33. package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
  34. package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
  35. package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
  36. package/native-src/src/librats/bittorrent/reactor.h +89 -0
  37. package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
  38. package/native-src/src/librats/bittorrent/resume_data.h +41 -0
  39. package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
  40. package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
  41. package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
  42. package/native-src/src/librats/bittorrent/torrent.h +260 -0
  43. package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
  44. package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
  45. package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
  46. package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
  47. package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
  48. package/native-src/src/librats/bittorrent/tracker.h +108 -0
  49. package/native-src/src/librats/bittorrent/types.cpp +206 -0
  50. package/native-src/src/librats/bittorrent/types.h +86 -0
  51. package/native-src/src/librats/core/address.cpp +35 -0
  52. package/native-src/src/librats/core/address.h +78 -0
  53. package/native-src/src/librats/core/bytes.h +69 -0
  54. package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
  55. package/native-src/src/librats/core/chained_send_buffer.h +183 -0
  56. package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
  57. package/native-src/src/librats/core/endpoint_parse.h +31 -0
  58. package/native-src/src/librats/core/event_bus.h +70 -0
  59. package/native-src/src/librats/core/host_endpoint.h +56 -0
  60. package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
  61. package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
  62. package/native-src/src/librats/core/ip_address.cpp +120 -0
  63. package/native-src/src/librats/core/ip_address.h +109 -0
  64. package/native-src/src/librats/core/mpsc_queue.h +47 -0
  65. package/native-src/src/librats/core/notifier.h +74 -0
  66. package/native-src/src/librats/core/receive_buffer.cpp +219 -0
  67. package/native-src/src/librats/core/receive_buffer.h +171 -0
  68. package/native-src/src/librats/core/service_registry.h +58 -0
  69. package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
  70. package/native-src/src/librats/core/socket.h +496 -0
  71. package/native-src/src/librats/core/timer_queue.h +105 -0
  72. package/native-src/src/librats/core/types.cpp +43 -0
  73. package/native-src/src/librats/core/types.h +103 -0
  74. package/native-src/src/librats/core/wakeup_pipe.h +83 -0
  75. package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
  76. package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
  77. package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
  78. package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
  79. package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
  80. package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
  81. package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
  82. package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
  83. package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
  84. package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
  85. package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
  86. package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
  87. package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
  88. package/native-src/src/librats/crypto/hkdf.c +266 -0
  89. package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
  90. package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
  91. package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
  92. package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
  93. package/native-src/src/librats/crypto/poly1305.h +37 -0
  94. package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
  95. package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
  96. package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
  97. package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
  98. package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
  99. package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
  100. package/native-src/src/librats/dht/announce.cpp +37 -0
  101. package/native-src/src/librats/dht/announce.h +41 -0
  102. package/native-src/src/librats/dht/bep42.cpp +109 -0
  103. package/native-src/src/librats/dht/bep42.h +48 -0
  104. package/native-src/src/librats/dht/dht.cpp +501 -0
  105. package/native-src/src/librats/dht/dht.h +119 -0
  106. package/native-src/src/librats/dht/dht_runner.cpp +103 -0
  107. package/native-src/src/librats/dht/dht_runner.h +71 -0
  108. package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
  109. package/native-src/src/librats/dht/dos_blocker.h +47 -0
  110. package/native-src/src/librats/dht/find_peers.cpp +52 -0
  111. package/native-src/src/librats/dht/find_peers.h +73 -0
  112. package/native-src/src/librats/dht/id.h +167 -0
  113. package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
  114. package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
  115. package/native-src/src/librats/dht/log.h +38 -0
  116. package/native-src/src/librats/dht/node.cpp +473 -0
  117. package/native-src/src/librats/dht/node.h +164 -0
  118. package/native-src/src/librats/dht/node_entry.h +81 -0
  119. package/native-src/src/librats/dht/observer.h +72 -0
  120. package/native-src/src/librats/dht/persistence.cpp +90 -0
  121. package/native-src/src/librats/dht/persistence.h +32 -0
  122. package/native-src/src/librats/dht/routing_table.cpp +559 -0
  123. package/native-src/src/librats/dht/routing_table.h +185 -0
  124. package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
  125. package/native-src/src/librats/dht/rpc_manager.h +77 -0
  126. package/native-src/src/librats/dht/storage.cpp +92 -0
  127. package/native-src/src/librats/dht/storage.h +74 -0
  128. package/native-src/src/librats/dht/transport.h +27 -0
  129. package/native-src/src/librats/dht/traversal.cpp +326 -0
  130. package/native-src/src/librats/dht/traversal.h +120 -0
  131. package/native-src/src/librats/dht/udp_transport.cpp +49 -0
  132. package/native-src/src/librats/dht/udp_transport.h +51 -0
  133. package/native-src/src/librats/mdns/log.h +22 -0
  134. package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
  135. package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
  136. package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
  137. package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
  138. package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
  139. package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
  140. package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
  141. package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
  142. package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
  143. package/native-src/src/librats/node/circuit_service.h +84 -0
  144. package/native-src/src/librats/node/config.h +110 -0
  145. package/native-src/src/librats/node/dial_service.h +54 -0
  146. package/native-src/src/librats/node/dialer.cpp +264 -0
  147. package/native-src/src/librats/node/dialer.h +188 -0
  148. package/native-src/src/librats/node/host_events.h +26 -0
  149. package/native-src/src/librats/node/identify.cpp +130 -0
  150. package/native-src/src/librats/node/identify.h +71 -0
  151. package/native-src/src/librats/node/nat_status.cpp +103 -0
  152. package/native-src/src/librats/node/nat_status.h +118 -0
  153. package/native-src/src/librats/node/node.cpp +865 -0
  154. package/native-src/src/librats/node/node.h +344 -0
  155. package/native-src/src/librats/node/node_context.h +33 -0
  156. package/native-src/src/librats/node/peer_network.h +91 -0
  157. package/native-src/src/librats/peer/peer.h +49 -0
  158. package/native-src/src/librats/peer/peer_book.cpp +181 -0
  159. package/native-src/src/librats/peer/peer_book.h +88 -0
  160. package/native-src/src/librats/peer/peer_id.cpp +72 -0
  161. package/native-src/src/librats/peer/peer_id.h +62 -0
  162. package/native-src/src/librats/peer/peer_info.h +37 -0
  163. package/native-src/src/librats/peer/peer_table.cpp +170 -0
  164. package/native-src/src/librats/peer/peer_table.h +148 -0
  165. package/native-src/src/librats/security/handshaker.h +66 -0
  166. package/native-src/src/librats/security/identity.h +43 -0
  167. package/native-src/src/librats/security/noise_security.cpp +122 -0
  168. package/native-src/src/librats/security/noise_security.h +37 -0
  169. package/native-src/src/librats/security/plaintext_security.h +106 -0
  170. package/native-src/src/librats/security/session.h +37 -0
  171. package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
  172. package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
  173. package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
  174. package/native-src/src/librats/subsystems/bittorrent.h +136 -0
  175. package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
  176. package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
  177. package/native-src/src/librats/subsystems/dht_service.h +36 -0
  178. package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
  179. package/native-src/src/librats/subsystems/file_transfer.h +367 -0
  180. package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
  181. package/native-src/src/librats/subsystems/hole_punch.h +290 -0
  182. package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
  183. package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
  184. package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
  185. package/native-src/src/librats/subsystems/message_json.cpp +112 -0
  186. package/native-src/src/librats/subsystems/message_json.h +88 -0
  187. package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
  188. package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
  189. package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
  190. package/native-src/src/librats/subsystems/ping_service.h +66 -0
  191. package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
  192. package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
  193. package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
  194. package/native-src/src/librats/subsystems/pubsub.h +175 -0
  195. package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
  196. package/native-src/src/librats/subsystems/reconnection.h +126 -0
  197. package/native-src/src/librats/subsystems/relay.cpp +1142 -0
  198. package/native-src/src/librats/subsystems/relay.h +211 -0
  199. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  200. package/native-src/src/librats/transport/connection.cpp +343 -0
  201. package/native-src/src/librats/transport/connection.h +283 -0
  202. package/native-src/src/librats/transport/link.h +96 -0
  203. package/native-src/src/librats/transport/reactor.cpp +588 -0
  204. package/native-src/src/librats/transport/reactor.h +262 -0
  205. package/native-src/src/librats/transport/reactor_pool.h +81 -0
  206. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  207. package/native-src/src/librats/transport/relay_link.h +303 -0
  208. package/native-src/src/librats/transport/tcp_link.cpp +49 -0
  209. package/native-src/src/librats/transport/tcp_link.h +43 -0
  210. package/native-src/src/librats/transport/udp_mux.cpp +617 -0
  211. package/native-src/src/librats/transport/udp_mux.h +363 -0
  212. package/native-src/src/librats/transport/udp_packet.cpp +121 -0
  213. package/native-src/src/librats/transport/udp_packet.h +190 -0
  214. package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
  215. package/native-src/src/librats/transport/udp_stream.h +614 -0
  216. package/native-src/src/librats/util/features.h.in +51 -0
  217. package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
  218. package/native-src/src/librats/util/fs.h +136 -0
  219. package/native-src/src/librats/util/json.cpp +1002 -0
  220. package/native-src/src/librats/util/json.h +444 -0
  221. package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
  222. package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
  223. package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
  224. package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
  225. package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
  226. package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
  227. package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
  228. package/native-src/src/librats/util/rats_export.h +69 -0
  229. package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
  230. package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
  231. package/native-src/src/librats/wire/frame.cpp +76 -0
  232. package/native-src/src/librats/wire/frame.h +111 -0
  233. package/native-src/src/librats/wire/message_router.cpp +45 -0
  234. package/native-src/src/librats/wire/message_router.h +47 -0
  235. package/package.json +5 -4
  236. package/scripts/build-librats.js +1 -0
  237. package/scripts/postinstall.js +3 -3
  238. package/scripts/prepare-package.js +4 -4
  239. package/scripts/verify-installation.js +63 -105
  240. package/src/librats_node.cpp +1067 -1323
  241. package/native-src/src/bencode.cpp +0 -485
  242. package/native-src/src/bencode.h +0 -145
  243. package/native-src/src/bittorrent.cpp +0 -14
  244. package/native-src/src/bittorrent.h +0 -74
  245. package/native-src/src/bt_bitfield.cpp +0 -372
  246. package/native-src/src/bt_bitfield.h +0 -316
  247. package/native-src/src/bt_choker.cpp +0 -228
  248. package/native-src/src/bt_choker.h +0 -147
  249. package/native-src/src/bt_client.cpp +0 -1047
  250. package/native-src/src/bt_client.h +0 -445
  251. package/native-src/src/bt_create_torrent.cpp +0 -677
  252. package/native-src/src/bt_create_torrent.h +0 -473
  253. package/native-src/src/bt_extension.cpp +0 -469
  254. package/native-src/src/bt_extension.h +0 -309
  255. package/native-src/src/bt_file_storage.cpp +0 -261
  256. package/native-src/src/bt_file_storage.h +0 -298
  257. package/native-src/src/bt_handshake.cpp +0 -134
  258. package/native-src/src/bt_handshake.h +0 -157
  259. package/native-src/src/bt_messages.cpp +0 -364
  260. package/native-src/src/bt_messages.h +0 -324
  261. package/native-src/src/bt_network.cpp +0 -1007
  262. package/native-src/src/bt_network.h +0 -417
  263. package/native-src/src/bt_peer_connection.cpp +0 -742
  264. package/native-src/src/bt_peer_connection.h +0 -592
  265. package/native-src/src/bt_piece_picker.cpp +0 -786
  266. package/native-src/src/bt_piece_picker.h +0 -473
  267. package/native-src/src/bt_resume_data.cpp +0 -410
  268. package/native-src/src/bt_resume_data.h +0 -249
  269. package/native-src/src/bt_torrent.cpp +0 -2120
  270. package/native-src/src/bt_torrent.h +0 -641
  271. package/native-src/src/bt_torrent_info.cpp +0 -659
  272. package/native-src/src/bt_torrent_info.h +0 -418
  273. package/native-src/src/bt_types.h +0 -621
  274. package/native-src/src/chained_send_buffer.cpp +0 -75
  275. package/native-src/src/chained_send_buffer.h +0 -137
  276. package/native-src/src/crypto/hkdf.c +0 -266
  277. package/native-src/src/crypto/poly1305.h +0 -36
  278. package/native-src/src/dht.cpp +0 -3311
  279. package/native-src/src/dht.h +0 -717
  280. package/native-src/src/disk_io.cpp +0 -632
  281. package/native-src/src/disk_io.h +0 -315
  282. package/native-src/src/file_transfer.cpp +0 -1415
  283. package/native-src/src/file_transfer.h +0 -286
  284. package/native-src/src/fs.h +0 -108
  285. package/native-src/src/gossipsub.cpp +0 -1139
  286. package/native-src/src/gossipsub.h +0 -403
  287. package/native-src/src/ice.cpp +0 -893
  288. package/native-src/src/ice.h +0 -559
  289. package/native-src/src/json.hpp +0 -25526
  290. package/native-src/src/librats.cpp +0 -2378
  291. package/native-src/src/librats.h +0 -2324
  292. package/native-src/src/librats_bittorrent.cpp +0 -601
  293. package/native-src/src/librats_c.cpp +0 -1557
  294. package/native-src/src/librats_c.h +0 -323
  295. package/native-src/src/librats_discovery.cpp +0 -402
  296. package/native-src/src/librats_encryption.cpp +0 -275
  297. package/native-src/src/librats_file_transfer.cpp +0 -144
  298. package/native-src/src/librats_gossipsub.cpp +0 -289
  299. package/native-src/src/librats_ice.cpp +0 -213
  300. package/native-src/src/librats_log_macros.h +0 -36
  301. package/native-src/src/librats_logging.cpp +0 -173
  302. package/native-src/src/librats_mdns.cpp +0 -166
  303. package/native-src/src/librats_persistence.cpp +0 -796
  304. package/native-src/src/librats_portmap.cpp +0 -419
  305. package/native-src/src/librats_reconnection.cpp +0 -218
  306. package/native-src/src/librats_statistic.cpp +0 -105
  307. package/native-src/src/librats_storage.cpp +0 -189
  308. package/native-src/src/rats_export.h +0 -17
  309. package/native-src/src/receive_buffer.cpp +0 -82
  310. package/native-src/src/receive_buffer.h +0 -127
  311. package/native-src/src/socket.h +0 -228
  312. package/native-src/src/threadmanager.cpp +0 -105
  313. package/native-src/src/threadmanager.h +0 -53
  314. package/native-src/src/tracker.cpp +0 -1264
  315. package/native-src/src/tracker.h +0 -319
  316. package/native-src/src/turn.cpp +0 -762
  317. package/native-src/src/turn.h +0 -460
  318. package/native-src/src/wakeup_pipe.h +0 -60
  319. /package/native-src/src/{os.h → librats/util/os.h} +0 -0
@@ -0,0 +1,870 @@
1
+ #include "librats/bittorrent/torrent.h"
2
+
3
+ #include "librats/bittorrent/byte_io.h"
4
+ #include "librats/bittorrent/log.h"
5
+ #include "librats/util/fs.h"
6
+
7
+ #include <algorithm>
8
+ #include <chrono>
9
+ #include <cstdlib>
10
+ #include <unordered_map>
11
+
12
+ namespace librats::bittorrent {
13
+
14
+ Torrent::Torrent(Reactor& reactor, TorrentHost& host, TorrentInfo info, std::string save_path)
15
+ : reactor_(reactor)
16
+ , host_(host)
17
+ , info_(std::move(info))
18
+ , save_path_(std::move(save_path))
19
+ , choker_(4) {}
20
+
21
+ Torrent::~Torrent() {
22
+ // Invalidate the liveness token before tearing down: stop() joins the disk /
23
+ // tracker workers, and a completion a worker posts during that join lands in
24
+ // the reactor queue after `this` is gone — the guard in post() drops it.
25
+ alive_->store(false, std::memory_order_release);
26
+ stop();
27
+ }
28
+
29
+ void Torrent::post(std::function<void()> fn) {
30
+ reactor_.post([alive = alive_, fn = std::move(fn)]() mutable {
31
+ if (alive->load(std::memory_order_acquire)) fn();
32
+ });
33
+ }
34
+
35
+ void Torrent::start() {
36
+ if (running_) return;
37
+ running_ = true;
38
+ has_metadata_ = info_.has_metadata();
39
+
40
+ if (has_metadata_) {
41
+ picker_ = std::make_unique<PiecePicker>(info_.num_pieces(), info_.piece_length(), info_.total_size());
42
+ disk_ = std::make_unique<ThreadedDiskIo>(
43
+ info_, save_path_,
44
+ [this](std::function<void()> fn) { post(std::move(fn)); });
45
+ state_ = State::Checking;
46
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " \"" << info_.name() << "\" → Checking ("
47
+ << info_.num_pieces() << " pieces, " << info_.total_size() << " bytes)");
48
+ // Pieces in resume_have_ are trusted (skip the hash); the rest are verified.
49
+ disk_->async_check_files(resume_have_, nullptr,
50
+ [this](Bitfield have) { on_check_complete(std::move(have)); });
51
+ } else {
52
+ // Magnet: no metadata yet — fetch the info dict from peers (BEP 9) first.
53
+ state_ = State::Metadata;
54
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " → Metadata (magnet, awaiting info dict)");
55
+ }
56
+
57
+ // Trackers come from the .torrent or the magnet link; announce to find peers
58
+ // (which also seeds the metadata fetch for magnets).
59
+ if (!info_.all_trackers().empty()) {
60
+ trackers_ = std::make_unique<TrackerAnnouncer>(
61
+ info_.all_trackers(), [this](std::function<void()> fn) { post(std::move(fn)); });
62
+ announce_trackers(TrackerEvent::Started);
63
+ }
64
+
65
+ schedule_tick();
66
+ try_connect();
67
+ }
68
+
69
+ void Torrent::stop() {
70
+ if (!running_) return;
71
+ running_ = false;
72
+ paused_ = false;
73
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " stopped (" << peers_.size() << " peers)");
74
+ if (tick_timer_ != kInvalidTimerId) { reactor_.cancel(tick_timer_); tick_timer_ = kInvalidTimerId; }
75
+ if (trackers_) {
76
+ announce_trackers(TrackerEvent::Stopped); // tell trackers we're leaving (H12)
77
+ trackers_->stop(); // drains the in-flight Stopped announce
78
+ trackers_.reset();
79
+ }
80
+ for (PeerConnection* pc : peers_) pc->close("torrent stopped");
81
+ peers_.clear();
82
+ outstanding_.clear();
83
+ recent_down_.clear();
84
+ seed_peers_.clear();
85
+ if (disk_) disk_->stop();
86
+ disk_.reset();
87
+ picker_.reset();
88
+ state_ = State::Stopped;
89
+ }
90
+
91
+ void Torrent::on_check_complete(Bitfield have) {
92
+ if (!running_) return;
93
+ picker_->set_have_bitfield(have);
94
+ // Account the bytes already on disk toward `left` via verified_bytes_ — NOT
95
+ // bytes_downloaded_, which is the cumulative download counter restored verbatim
96
+ // from resume. Folding on-disk pieces into it too would double-count a resumed
97
+ // torrent (and drive `left` to 0, falsely announcing us as a seed).
98
+ for (std::uint32_t p = 0; p < info_.num_pieces(); ++p)
99
+ if (picker_->have_piece(p)) verified_bytes_ += info_.piece_size(p);
100
+
101
+ if (picker_->is_finished()) { state_ = State::Seeding; completed_announced_ = true; }
102
+ else { state_ = State::Downloading; }
103
+
104
+ // A peer that connected while the check was still running was handed an EMPTY
105
+ // bitfield: it is sent from on_peer_connected, and the protocol pins it to the
106
+ // first message after the handshake, so it cannot be re-sent now that we know
107
+ // what is on disk (our own receiver rejects it too — see piece_state_begun_).
108
+ // Announce the verified pieces with HAVE instead — otherwise such a peer never
109
+ // sees us as interesting, never asks for a block, and the transfer sits at 0%
110
+ // forever. A tracker/DHT answer landing inside the check window hits this every
111
+ // time, which is what BtTracker.DiscoversSeederViaTrackerAndDownloads tripped on.
112
+ //
113
+ // Cost: send_have() flushes per message, so this is one syscall per piece per
114
+ // peer — ~3 us each, i.e. ~50 ms per peer on a 20k-piece torrent (whose check
115
+ // runs for minutes, so peers really do pile up). It is a cold path — once per
116
+ // torrent, never per piece or per block — so the burst is accepted rather than
117
+ // batched; revisit with a batched send_haves() if the stall ever shows up.
118
+ //
119
+ // Iterate a SNAPSHOT: a failing send closes the connection inline (flush ->
120
+ // close -> on_closed -> remove_peer), which erases from peers_ mid-loop. With
121
+ // num_pieces sends per peer, hitting a dead socket in here is likely enough to
122
+ // matter, and iterating the live vector would be UB.
123
+ for (PeerConnection* pc : std::vector<PeerConnection*>(peers_)) {
124
+ if (!alive(pc)) continue; // dropped by an earlier peer's teardown
125
+ for (std::uint32_t p = 0; p < info_.num_pieces(); ++p)
126
+ if (picker_->have_piece(p)) pc->send_have(p);
127
+ if (!alive(pc)) continue; // ...or by its own
128
+ update_interest(*pc); // our own interest is stale too: we may now have it all
129
+ }
130
+
131
+ if (state_ == State::Seeding)
132
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " check complete: all "
133
+ << info_.num_pieces() << " pieces present → Seeding");
134
+ else
135
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " check complete: " << picker_->num_have()
136
+ << '/' << info_.num_pieces() << " pieces (" << int(progress() * 100)
137
+ << "%) → Downloading");
138
+
139
+ try_connect();
140
+ }
141
+
142
+ void Torrent::pause() {
143
+ if (!running_ || paused_) return;
144
+ paused_ = true;
145
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " paused (" << peers_.size() << " peers dropped)");
146
+ // Drop all peers and their per-peer request state, but keep picker_/disk_ so a
147
+ // later resume() does not have to re-hash what is already on disk.
148
+ for (PeerConnection* pc : peers_) pc->close("torrent paused");
149
+ peers_.clear();
150
+ outstanding_.clear();
151
+ request_time_.clear();
152
+ recent_down_.clear();
153
+ recent_up_.clear();
154
+ seed_peers_.clear();
155
+ peer_ext_.clear();
156
+ optimistic_ = nullptr;
157
+ // Leave the swarm: tell trackers we're gone so they stop handing our address to
158
+ // others (mirrors stop(), minus tearing down picker_/disk_). tick() is gated on
159
+ // paused_ below, so DHT/tracker re-announces halt too — no more inbound dials.
160
+ announce_trackers(TrackerEvent::Stopped);
161
+ // The 1 s tick keeps running (cheap) but returns early while paused.
162
+ }
163
+
164
+ void Torrent::resume() {
165
+ if (!running_ || !paused_) return;
166
+ paused_ = false;
167
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " resumed");
168
+ // Rejoin the swarm: a fresh Started announce repopulates the peer list (the DHT
169
+ // get_peers / periodic announces resume with the tick).
170
+ announce_trackers(TrackerEvent::Started);
171
+ try_connect();
172
+ }
173
+
174
+ void Torrent::add_peer(const std::string& ip, std::uint16_t port) {
175
+ if (peer_list_.add(ip, port, PeerSource::Tracker) && running_ && !paused_)
176
+ post([this] { try_connect(); });
177
+ }
178
+
179
+ void Torrent::try_connect() {
180
+ if (!running_ || paused_ || peers_.size() >= kMaxPeers) return;
181
+ auto candidates = peer_list_.connect_candidates(kMaxPeers - peers_.size());
182
+ for (const auto& c : candidates) host_.connect_peer(*this, c.ip, c.port);
183
+ }
184
+
185
+ void Torrent::on_connect_failed(const std::string& ip, std::uint16_t port) {
186
+ peer_list_.on_connect_failed(ip, port);
187
+ }
188
+
189
+ // ---- scheduling ----
190
+
191
+ void Torrent::schedule_tick() {
192
+ if (!running_) return;
193
+ tick_timer_ = reactor_.schedule(std::chrono::seconds(1), [this] { tick(); });
194
+ }
195
+
196
+ void Torrent::tick() {
197
+ if (!running_) return;
198
+ ++tick_count_;
199
+
200
+ // Paused: no swarm activity — skip DHT/tracker announces, dialing, choking and
201
+ // PEX. Keep the tick alive so resume() picks straight back up. (peers_ is empty
202
+ // while paused, so there is nothing to time out or choke anyway.)
203
+ if (paused_) { schedule_tick(); return; }
204
+
205
+ check_request_timeouts(); // free blocks stuck on stalled peers before anything else
206
+
207
+ // Ask the DHT for fresh peers periodically (every ~30 s).
208
+ if (tick_count_ % 30 == 1) {
209
+ LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT get_peers");
210
+ host_.find_peers_via_dht(info_hash(), [this](const std::string& ip, std::uint16_t port) {
211
+ if (peer_list_.add(ip, port, PeerSource::Dht)) try_connect();
212
+ });
213
+ }
214
+ // Announce ourselves to the DHT so others can find us — promptly on startup,
215
+ // then every ~15 min per BEP 5 (H15). Was previously never done → undiscoverable.
216
+ if (tick_count_ % 900 == 5) {
217
+ LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT announce_peer port "
218
+ << host_.listen_port());
219
+ host_.announce_to_dht(info_hash(), host_.listen_port());
220
+ }
221
+ // Re-announce when the tracker's requested interval has elapsed (H13).
222
+ if (tick_count_ >= next_announce_tick_) {
223
+ next_announce_tick_ = tick_count_ + 300; // fallback until the response updates it
224
+ announce_trackers(TrackerEvent::None);
225
+ }
226
+
227
+ try_connect();
228
+ send_pex();
229
+
230
+ // Choking runs on a coarse cadence, not every tick: recompute the unchoke set
231
+ // every ~10 s and rotate the optimistic slot every ~30 s. Recomputing every
232
+ // second (with a 1 s scoring window) made peers near the slot boundary flip
233
+ // choke/unchoke constantly (H4). The tit-for-tat window therefore accumulates
234
+ // over the whole 10 s and is only reset when we recompute.
235
+ if (tick_count_ % 30 == 0) rotate_optimistic();
236
+ if (tick_count_ % 10 == 0) {
237
+ recompute_choker();
238
+ for (auto& [pc, score] : recent_down_) score = 0;
239
+ for (auto& [pc, score] : recent_up_) score = 0;
240
+ }
241
+ schedule_tick();
242
+ }
243
+
244
+ // ---- peer event handling ----
245
+
246
+ void Torrent::on_handshake(PeerConnection& pc, const InfoHash&, const PeerId&) {
247
+ // Reject inbound peers while paused — pause() means "no swarm activity".
248
+ if (paused_) { pc.close("torrent paused"); return; }
249
+ // Cap peers per torrent. Outbound dials are already gated by try_connect(), but
250
+ // inbound peers reach us straight through the handshake, so enforce it here too
251
+ // (H3) — otherwise a flood of incoming handshakes grows peers_ without bound.
252
+ if (peers_.size() >= kMaxPeers) { pc.close("too many peers"); return; }
253
+ peers_.push_back(&pc);
254
+ outstanding_[&pc] = 0;
255
+ recent_down_[&pc] = 0;
256
+ peer_list_.set_connected(pc.remote_ip(), pc.remote_port(), true);
257
+ // Milestone: the first peer on a torrent is worth an INFO line; the rest are
258
+ // routine (each peer's handshake is already logged at DEBUG in bt.peer).
259
+ if (peers_.size() == 1)
260
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " first peer connected "
261
+ << pc.remote_ip() << ':' << pc.remote_port());
262
+
263
+ if (pc.peer_supports_extensions()) send_extended_handshake(pc);
264
+ // Only announce a bitfield once we know the piece count (i.e. have metadata).
265
+ if (has_metadata_ && picker_) pc.send_bitfield(picker_->have_bitfield());
266
+ }
267
+
268
+ void Torrent::on_bitfield(PeerConnection& pc, const Bitfield& bf) {
269
+ if (!picker_) return;
270
+ // A full bitfield is a seed: count it as one O(1) seed instead of bumping every
271
+ // piece's availability. Remember which peers we counted this way so remove_peer
272
+ // undoes it symmetrically. The insert() guard also makes a (spurious) repeat
273
+ // full bitfield idempotent for the seed count.
274
+ const std::uint32_t n = info_.num_pieces();
275
+ if (n > 0 && bf.size() == n && bf.count() == n) {
276
+ if (seed_peers_.insert(&pc).second) picker_->inc_availability_all();
277
+ } else {
278
+ picker_->inc_availability(bf);
279
+ }
280
+ update_interest(pc);
281
+ }
282
+
283
+ void Torrent::on_have(PeerConnection& pc, std::uint32_t piece) {
284
+ if (!picker_ || piece >= info_.num_pieces()) return;
285
+ picker_->peer_has_piece(piece);
286
+ // A single new piece can only *gain* us interest, never lose it, and the piece
287
+ // the peer just announced is enough to decide — so this stays O(1) instead of
288
+ // rescanning the peer's whole bitfield via is_interesting() on every HAVE.
289
+ if (!pc.am_interested() && picker_->piece_interesting(piece)) pc.send_interested();
290
+ if (pc.am_interested() && !pc.peer_choking()) refill(pc);
291
+ }
292
+
293
+ void Torrent::on_choke(PeerConnection& pc, bool peer_choking) {
294
+ if (!picker_) return;
295
+ if (peer_choking) {
296
+ picker_->cancel_peer(&pc); // they dropped our outstanding requests
297
+ outstanding_[&pc] = 0;
298
+ request_time_.erase(&pc);
299
+ } else {
300
+ refill(pc);
301
+ }
302
+ }
303
+
304
+ void Torrent::on_interest(PeerConnection&, bool) {
305
+ recompute_choker(); // a peer's interest in us changed → re-evaluate upload slots
306
+ }
307
+
308
+ void Torrent::update_interest(PeerConnection& pc) {
309
+ const bool want = picker_->is_interesting(pc.peer_bitfield());
310
+ if (want && !pc.am_interested()) pc.send_interested();
311
+ else if (!want && pc.am_interested()) pc.send_not_interested();
312
+ if (want && !pc.peer_choking()) refill(pc);
313
+ }
314
+
315
+ void Torrent::refill(PeerConnection& pc) {
316
+ if (!running_ || !picker_ || pc.peer_choking() || !pc.am_interested()) return;
317
+ // Disk backpressure: while too many writes are still pending, stop requesting
318
+ // new blocks so the write queue can drain (D-2). on_block_written() resumes
319
+ // requesting once it falls back under the watermark.
320
+ if (disk_ && disk_->queued_write_bytes() > kDiskWriteHighWater) { write_stalled_ = true; return; }
321
+ int budget = kPipelineDepth - outstanding_[&pc];
322
+ if (budget <= 0) return;
323
+
324
+ const int before = outstanding_[&pc];
325
+ auto blocks = picker_->pick_blocks(pc.peer_bitfield(), budget, &pc);
326
+ for (const PieceBlock& b : blocks) {
327
+ picker_->mark_requested(b, &pc);
328
+ pc.send_request(b.piece, b.block * kBlockSize, picker_->block_size(b.piece, b.block));
329
+ ++outstanding_[&pc];
330
+ }
331
+ if (!blocks.empty())
332
+ LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → request " << blocks.size()
333
+ << " block(s) from " << pc.remote_ip() << ':' << pc.remote_port()
334
+ << " (" << outstanding_[&pc] << " in flight)");
335
+ // Start the stall clock when a previously-idle peer gets a fresh batch. If it
336
+ // already had requests in flight, keep the older timestamp so a peer that
337
+ // stops delivering can't hide behind newly-added requests.
338
+ if (before == 0 && outstanding_[&pc] > 0)
339
+ request_time_[&pc] = std::chrono::steady_clock::now();
340
+ }
341
+
342
+ void Torrent::on_piece(PeerConnection& pc, std::uint32_t piece, std::uint32_t offset, ByteView data) {
343
+ if (!picker_ || piece >= info_.num_pieces()) return;
344
+
345
+ // Reject a malformed or unsolicited block before it can index the picker's
346
+ // block vector out of bounds: the offset must be block-aligned and inside the
347
+ // piece, and the payload must be exactly that block's size. A hostile peer
348
+ // could otherwise drive an out-of-bounds access via mark_writing()/mark_finished().
349
+ const std::uint32_t piece_bytes = info_.piece_size(piece);
350
+ if (offset % kBlockSize != 0 || offset >= piece_bytes) {
351
+ LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ bad block from " << pc.remote_ip()
352
+ << ':' << pc.remote_port() << " piece " << piece << " off " << offset
353
+ << " (misaligned/out of range), dropped");
354
+ return;
355
+ }
356
+ if (data.size() != picker_->block_size(piece, offset / kBlockSize)) {
357
+ LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ bad block from " << pc.remote_ip()
358
+ << ':' << pc.remote_port() << " piece " << piece
359
+ << " wrong size " << data.size() << ", dropped");
360
+ return;
361
+ }
362
+
363
+ if (outstanding_[&pc] > 0) --outstanding_[&pc];
364
+ // Progress: this peer just delivered, so reset its stall clock — any remaining
365
+ // outstanding blocks get a fresh timeout window.
366
+ request_time_[&pc] = std::chrono::steady_clock::now();
367
+
368
+ // We already completed this piece — an end-game duplicate that crossed our
369
+ // CANCEL, or a block arriving after the piece verified. Discard it: writing it
370
+ // again would be wasted I/O and would resurrect a stale picker entry. The
371
+ // request slot is already freed, so just keep this peer's pipeline full.
372
+ if (picker_->have_piece(piece)) { refill(pc); return; }
373
+
374
+ const PieceBlock block{piece, offset / kBlockSize};
375
+ bytes_downloaded_ += data.size();
376
+ recent_down_[&pc] += data.size();
377
+
378
+ // End-game: this block may have been requested from several peers at once. Now
379
+ // that it has arrived, CANCEL the duplicate requests still outstanding on the
380
+ // *other* peers so we don't download the same block again from each of them.
381
+ const std::vector<const void*> others = picker_->mark_writing(block, &pc);
382
+ for (const void* o : others) {
383
+ auto* opc = static_cast<PeerConnection*>(const_cast<void*>(o));
384
+ if (!alive(opc)) continue;
385
+ auto it = outstanding_.find(opc);
386
+ if (it != outstanding_.end() && it->second > 0) --it->second; // freed a slot
387
+ opc->send_cancel(piece, block.block * kBlockSize, picker_->block_size(piece, block.block));
388
+ }
389
+
390
+ disk_->async_write(piece, offset, data.to_bytes(),
391
+ [this, block](bool ok) { on_block_written(block, ok); });
392
+
393
+ refill(pc); // keep the pipeline full while the write is in flight
394
+ }
395
+
396
+ void Torrent::check_request_timeouts() {
397
+ if (!picker_) return;
398
+ const auto now = std::chrono::steady_clock::now();
399
+
400
+ // Find peers that hold outstanding requests but have made no progress within
401
+ // the timeout. A peer sending only keep-alives (which refresh the 120 s idle
402
+ // deadline) would otherwise sit on its blocks forever, stalling those pieces.
403
+ std::vector<PeerConnection*> stalled;
404
+ for (const auto& [pc, n] : outstanding_) {
405
+ if (n <= 0) continue;
406
+ auto it = request_time_.find(pc);
407
+ if (it != request_time_.end() && now - it->second > request_timeout_)
408
+ stalled.push_back(pc);
409
+ }
410
+ if (stalled.empty()) return;
411
+
412
+ // Release each stalled peer's blocks back to the picker so another peer can
413
+ // pick them, and leave the peer idle (no re-request this round) so we don't
414
+ // immediately hand the same blocks back to the peer that just stalled.
415
+ for (PeerConnection* pc : stalled) {
416
+ picker_->cancel_peer(pc);
417
+ outstanding_[pc] = 0;
418
+ request_time_.erase(pc);
419
+ }
420
+ LOG_WARN("bt.torrent", short_hash(info_hash()) << " snubbed " << stalled.size()
421
+ << " stalled peer(s), freed their blocks for re-request");
422
+
423
+ // Re-request the freed blocks from peers that are actually delivering.
424
+ for (PeerConnection* pc : peers_) {
425
+ if (std::find(stalled.begin(), stalled.end(), pc) != stalled.end()) continue;
426
+ if (pc->am_interested() && !pc->peer_choking()) refill(*pc);
427
+ }
428
+ }
429
+
430
+ void Torrent::on_block_written(PieceBlock block, bool ok) {
431
+ if (!running_ || !picker_) return;
432
+ if (!ok) {
433
+ LOG_ERROR("bt.torrent", short_hash(info_hash()) << " ✗ disk write failed for piece "
434
+ << block.piece << ", will refetch");
435
+ picker_->restore_piece(block.piece);
436
+ return;
437
+ }
438
+ if (picker_->mark_finished(block)) verify_piece(block.piece);
439
+
440
+ // A write just drained: if backpressure had paused requesting and the queue is
441
+ // back under the watermark, resume filling peers' pipelines (D-2).
442
+ if (write_stalled_ && (!disk_ || disk_->queued_write_bytes() <= kDiskWriteHighWater)) {
443
+ write_stalled_ = false;
444
+ refill_all();
445
+ }
446
+ }
447
+
448
+ void Torrent::refill_all() {
449
+ for (PeerConnection* pc : peers_)
450
+ if (pc->am_interested() && !pc->peer_choking()) refill(*pc);
451
+ }
452
+
453
+ void Torrent::verify_piece(std::uint32_t piece) {
454
+ disk_->async_hash(piece, [this, piece](bool ok, std::array<std::uint8_t, 20> hash) {
455
+ on_piece_hashed(piece, ok, hash);
456
+ });
457
+ }
458
+
459
+ void Torrent::on_piece_hashed(std::uint32_t piece, bool ok, std::array<std::uint8_t, 20> hash) {
460
+ if (!running_ || !picker_) return;
461
+ if (!ok || hash != info_.piece_hash(piece)) {
462
+ LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ piece " << piece
463
+ << " hash mismatch, refetch");
464
+ picker_->restore_piece(piece); // corrupt — fetch it again
465
+ return;
466
+ }
467
+
468
+ picker_->we_have(piece);
469
+ verified_bytes_ += info_.piece_size(piece); // one more piece is now on disk → shrinks `left`
470
+ for (PeerConnection* pc : peers_) {
471
+ pc->send_have(piece);
472
+ update_interest(*pc); // we may no longer need some peers
473
+ }
474
+
475
+ LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " ✓ piece " << piece << " verified ("
476
+ << picker_->num_have() << '/' << info_.num_pieces() << ')');
477
+ // Progress milestone: log once per 10% crossed, not once per piece (H: keep INFO
478
+ // readable as a clean per-torrent story instead of thousands of lines).
479
+ const int pct = int(progress() * 100);
480
+ if (pct / 10 > progress_logged_ && pct < 100) {
481
+ progress_logged_ = pct / 10;
482
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " progress " << (progress_logged_ * 10) << '%');
483
+ }
484
+
485
+ if (picker_->is_finished() && !completed_announced_) {
486
+ completed_announced_ = true;
487
+ state_ = State::Seeding;
488
+ LOG_INFO("bt.torrent", short_hash(info_hash()) << " ✓ download complete → Seeding");
489
+ announce_trackers(TrackerEvent::Completed); // tell trackers we're now a seed (H12)
490
+ if (on_complete_) on_complete_();
491
+ }
492
+ }
493
+
494
+ void Torrent::on_request(PeerConnection& pc, std::uint32_t piece, std::uint32_t offset, std::uint32_t length) {
495
+ if (!picker_ || pc.am_choking()) return; // we are not serving this peer
496
+ if (piece >= info_.num_pieces() || !picker_->have_piece(piece)) return;
497
+ if (length == 0 || length > kMaxBlockSize) return;
498
+ // The requested range must lie wholly within the piece, else the disk read
499
+ // would spill into an adjacent piece's file region and serve unrelated bytes.
500
+ const std::uint32_t piece_bytes = info_.piece_size(piece);
501
+ if (offset >= piece_bytes || length > piece_bytes - offset) return;
502
+
503
+ PeerConnection* peer = &pc;
504
+ disk_->async_read(piece, offset, length, [this, peer, piece, offset](bool ok, Bytes data) {
505
+ if (ok && alive(peer)) {
506
+ const std::size_t sent = data.size(); // data is about to be moved away
507
+ peer->send_piece(piece, offset, std::move(data));
508
+ bytes_uploaded_ += sent;
509
+ recent_up_[peer] += sent; // seed-choking score for this window
510
+ }
511
+ });
512
+ }
513
+
514
+ void Torrent::on_closed(PeerConnection& pc, const std::string&) {
515
+ peer_list_.set_connected(pc.remote_ip(), pc.remote_port(), false);
516
+ pex_sent_.erase(&pc);
517
+ remove_peer(&pc);
518
+ }
519
+
520
+ // ---- choking ----
521
+
522
+ void Torrent::recompute_choker() {
523
+ if (!running_) return;
524
+ // Score peers by what they've done for us this window: while downloading that
525
+ // is bytes received (tit-for-tat); while seeding we receive nothing, so score
526
+ // by bytes we served them instead — otherwise every peer scores 0 and a seed
527
+ // would serve only whichever peers happened to connect first, forever (H2).
528
+ const bool seeding = picker_ && picker_->is_finished();
529
+ std::vector<Choker::Candidate> candidates;
530
+ for (PeerConnection* pc : peers_)
531
+ if (pc->peer_interested())
532
+ candidates.push_back(Choker::Candidate{pc, seeding ? recent_up_[pc] : recent_down_[pc]});
533
+
534
+ auto unchoke = choker_.select(std::move(candidates), optimistic_);
535
+ for (PeerConnection* pc : peers_) {
536
+ const bool should_unchoke =
537
+ std::find(unchoke.begin(), unchoke.end(), pc) != unchoke.end();
538
+ if (should_unchoke && pc->am_choking()) pc->send_unchoke();
539
+ else if (!should_unchoke && !pc->am_choking()) pc->send_choke();
540
+ }
541
+ }
542
+
543
+ void Torrent::rotate_optimistic() {
544
+ // Round-robin the optimistic slot through the interested-but-choked peers so a
545
+ // newcomer that hasn't earned a tit-for-tat slot still gets a chance to prove
546
+ // itself (H1). Advance from just after the current optimistic in peer order.
547
+ std::vector<PeerConnection*> candidates;
548
+ for (PeerConnection* pc : peers_)
549
+ if (pc->peer_interested()) candidates.push_back(pc);
550
+ if (candidates.empty()) { optimistic_ = nullptr; return; }
551
+
552
+ auto it = std::find(candidates.begin(), candidates.end(), optimistic_);
553
+ const std::size_t next = (it == candidates.end()) ? 0
554
+ : (std::size_t(it - candidates.begin()) + 1) % candidates.size();
555
+ optimistic_ = candidates[next];
556
+ recompute_choker(); // apply the new optimistic slot immediately
557
+ }
558
+
559
+ // ---- helpers ----
560
+
561
+ bool Torrent::alive(PeerConnection* pc) const {
562
+ return std::find(peers_.begin(), peers_.end(), pc) != peers_.end();
563
+ }
564
+
565
+ void Torrent::remove_peer(PeerConnection* pc) {
566
+ auto it = std::find(peers_.begin(), peers_.end(), pc);
567
+ if (it == peers_.end()) return;
568
+ if (picker_) {
569
+ // Undo the availability accounting the same way it was applied: a seed via
570
+ // the seed counter, anyone else per-piece from their bitfield.
571
+ if (seed_peers_.erase(pc)) picker_->dec_availability_all();
572
+ else picker_->dec_availability(pc->peer_bitfield());
573
+ picker_->cancel_peer(pc);
574
+ }
575
+ if (optimistic_ == pc) optimistic_ = nullptr;
576
+ peers_.erase(it);
577
+ outstanding_.erase(pc);
578
+ request_time_.erase(pc);
579
+ recent_down_.erase(pc);
580
+ recent_up_.erase(pc);
581
+ peer_ext_.erase(pc);
582
+ pex_last_tick_.erase(pc);
583
+ }
584
+
585
+ std::size_t Torrent::num_outstanding_requests() const noexcept {
586
+ std::size_t n = 0;
587
+ for (const auto& [pc, count] : outstanding_) if (count > 0) n += std::size_t(count);
588
+ return n;
589
+ }
590
+
591
+ double Torrent::progress() const {
592
+ if (!picker_ || info_.num_pieces() == 0) return 0.0;
593
+ return double(picker_->num_have()) / double(info_.num_pieces());
594
+ }
595
+
596
+ // ---- extension protocol / metadata (BEP 10 / BEP 9) ----
597
+
598
+ void Torrent::on_extended(PeerConnection& pc, std::uint8_t ext_id, ByteView payload) {
599
+ if (ext_id == 0) handle_ext_handshake(pc, payload);
600
+ else if (ext_id == ext::kUtMetadataLocalId) handle_ut_metadata(pc, payload);
601
+ else if (ext_id == ext::kUtPexLocalId) handle_pex(pc, payload);
602
+ }
603
+
604
+ void Torrent::send_extended_handshake(PeerConnection& pc) {
605
+ const std::uint32_t ms = has_metadata_ ? std::uint32_t(info_.info_dict_bytes().size()) : 0;
606
+ const Bytes hs = ext::encode_handshake(ms, host_.listen_port());
607
+ pc.send_extended(0, ByteView(hs));
608
+ }
609
+
610
+ void Torrent::handle_ext_handshake(PeerConnection& pc, ByteView payload) {
611
+ auto pe = ext::decode_handshake(payload);
612
+ if (!pe) return;
613
+ peer_ext_[&pc] = *pe;
614
+ if (!has_metadata_ && pe->metadata_size > 0) {
615
+ ensure_metadata_buffer(pe->metadata_size);
616
+ request_metadata(pc);
617
+ }
618
+ }
619
+
620
+ void Torrent::ensure_metadata_buffer(std::uint32_t total_size) {
621
+ if (metadata_size_ != 0 || total_size == 0) return; // already sized, or unknown
622
+ // The size is self-reported by a peer (extended handshake or ut_metadata data
623
+ // message), so reject an implausible value before allocating — otherwise a
624
+ // hostile peer advertising ~4 GiB drives an out-of-memory allocation (C3).
625
+ if (total_size > kMaxMetadataSize) {
626
+ LOG_WARN("bt.meta", short_hash(info_hash()) << " ✗ rejected oversize metadata "
627
+ << total_size << " bytes (> " << kMaxMetadataSize << ')');
628
+ return;
629
+ }
630
+ metadata_size_ = total_size;
631
+ metadata_pieces_ = (total_size + kMetadataPieceSize - 1) / kMetadataPieceSize;
632
+ metadata_buf_.assign(total_size, 0);
633
+ metadata_have_.assign(metadata_pieces_, false);
634
+ metadata_received_ = 0;
635
+ }
636
+
637
+ void Torrent::request_metadata(PeerConnection& pc) {
638
+ if (has_metadata_ || metadata_pieces_ == 0) return;
639
+ auto it = peer_ext_.find(&pc);
640
+ if (it == peer_ext_.end() || it->second.ut_metadata_id == 0) return;
641
+ for (std::uint32_t p = 0; p < metadata_pieces_; ++p)
642
+ if (!metadata_have_[p])
643
+ pc.send_extended(it->second.ut_metadata_id, ByteView(ext::encode_metadata_request(p)));
644
+ }
645
+
646
+ void Torrent::handle_ut_metadata(PeerConnection& pc, ByteView payload) {
647
+ auto msg = ext::decode_metadata(payload);
648
+ if (!msg) return;
649
+
650
+ if (msg->type == ext::MetadataType::Request) {
651
+ auto it = peer_ext_.find(&pc);
652
+ const std::uint8_t id = (it != peer_ext_.end()) ? it->second.ut_metadata_id : 0;
653
+ if (id == 0) return;
654
+ if (has_metadata_) {
655
+ const Bytes& info = info_.info_dict_bytes();
656
+ const std::uint32_t total = std::uint32_t(info.size());
657
+ const std::uint32_t pieces = (total + kMetadataPieceSize - 1) / kMetadataPieceSize;
658
+ if (msg->piece < pieces) {
659
+ const std::uint32_t off = msg->piece * kMetadataPieceSize;
660
+ const std::uint32_t len = (std::min)(kMetadataPieceSize, total - off);
661
+ pc.send_extended(id, ByteView(ext::encode_metadata_data(
662
+ msg->piece, total, ByteView(info.data() + off, len))));
663
+ return;
664
+ }
665
+ }
666
+ pc.send_extended(id, ByteView(ext::encode_metadata_reject(msg->piece)));
667
+ } else if (msg->type == ext::MetadataType::Data) {
668
+ on_metadata_piece(msg->piece, msg->total_size, ByteView(msg->block));
669
+ }
670
+ // Reject: leave the piece unmarked so it can be re-requested from another peer.
671
+ }
672
+
673
+ void Torrent::on_metadata_piece(std::uint32_t piece, std::uint32_t total_size, ByteView block) {
674
+ if (has_metadata_) return;
675
+ ensure_metadata_buffer(total_size);
676
+ if (metadata_size_ == 0 || piece >= metadata_pieces_ || metadata_have_[piece]) return;
677
+
678
+ const std::uint32_t off = piece * kMetadataPieceSize;
679
+ const std::uint32_t expect = (std::min)(kMetadataPieceSize, metadata_size_ - off);
680
+ if (block.size() != expect) return; // malformed slice — ignore
681
+
682
+ std::copy(block.begin(), block.end(), metadata_buf_.begin() + std::ptrdiff_t(off));
683
+ metadata_have_[piece] = true;
684
+ if (++metadata_received_ == metadata_pieces_) try_complete_metadata();
685
+ }
686
+
687
+ void Torrent::try_complete_metadata() {
688
+ if (info_.set_metadata(metadata_buf_)) { // verifies SHA-1 == info-hash
689
+ has_metadata_ = true;
690
+ LOG_INFO("bt.meta", short_hash(info_hash()) << " ✓ metadata resolved: \"" << info_.name()
691
+ << "\" " << info_.num_pieces() << " pieces, " << info_.total_size() << " bytes");
692
+ if (on_metadata_) on_metadata_(info_);
693
+ promote_to_downloading();
694
+ } else {
695
+ // Verification failed — discard and re-request from every peer.
696
+ LOG_WARN("bt.meta", short_hash(info_hash()) << " ✗ metadata verification failed, re-requesting");
697
+ std::fill(metadata_have_.begin(), metadata_have_.end(), false);
698
+ metadata_received_ = 0;
699
+ for (PeerConnection* pc : peers_) request_metadata(*pc);
700
+ }
701
+ }
702
+
703
+ void Torrent::promote_to_downloading() {
704
+ picker_ = std::make_unique<PiecePicker>(info_.num_pieces(), info_.piece_length(), info_.total_size());
705
+ disk_ = std::make_unique<ThreadedDiskIo>(
706
+ info_, save_path_, [this](std::function<void()> fn) { post(std::move(fn)); });
707
+ state_ = State::Downloading;
708
+
709
+ // Adopt the bitfields we received while metadata-less and (re)evaluate interest.
710
+ for (PeerConnection* pc : peers_) {
711
+ picker_->inc_availability(pc->peer_bitfield());
712
+ update_interest(*pc);
713
+ }
714
+ disk_->async_check_files(Bitfield{}, nullptr,
715
+ [this](Bitfield have) { on_check_complete(std::move(have)); });
716
+ }
717
+
718
+ // ---- peer exchange (BEP 11) ----
719
+
720
+ void Torrent::handle_pex(PeerConnection& pc, ByteView payload) {
721
+ auto msg = ext::decode_pex(payload);
722
+ if (!msg) return;
723
+ bool added_any = false;
724
+ for (const auto& p : msg->added)
725
+ if (peer_list_.add(p.ip, p.port, PeerSource::Pex)) added_any = true;
726
+ if (added_any) try_connect();
727
+ }
728
+
729
+ std::optional<ext::PexPeer> Torrent::dialable(PeerConnection& pc) const {
730
+ if (pc.remote_ip().empty()) return std::nullopt;
731
+ if (pc.outgoing()) // we dialed its listen port directly
732
+ return ext::PexPeer{pc.remote_ip(), pc.remote_port()};
733
+ // Incoming: the source port is ephemeral; use the listen port it advertised.
734
+ auto it = peer_ext_.find(const_cast<PeerConnection*>(&pc));
735
+ if (it == peer_ext_.end() || it->second.listen_port == 0) return std::nullopt;
736
+ return ext::PexPeer{pc.remote_ip(), it->second.listen_port};
737
+ }
738
+
739
+ void Torrent::send_pex() {
740
+ for (PeerConnection* pc : peers_) {
741
+ auto ext_it = peer_ext_.find(pc);
742
+ if (ext_it == peer_ext_.end() || ext_it->second.ut_pex_id == 0) continue;
743
+
744
+ // BEP 11: the first PEX message to a peer may go out promptly, but
745
+ // subsequent ones must be at least 60 s apart or the peer treats it as
746
+ // abuse and disconnects (too_frequent_pex) (H14). Gate per peer.
747
+ auto tick_it = pex_last_tick_.find(pc);
748
+ const bool first = (tick_it == pex_last_tick_.end());
749
+ if (!first && tick_count_ - tick_it->second < 60) continue;
750
+
751
+ // Diff the set of dialable peers we've told this one about against the
752
+ // current set: newly-seen peers go in `added`, departed ones in `dropped`.
753
+ std::unordered_map<std::string, ext::PexPeer> current;
754
+ for (PeerConnection* other : peers_) {
755
+ if (other == pc) continue;
756
+ if (auto ep = dialable(*other))
757
+ current.emplace(ep->ip + ":" + std::to_string(ep->port), *ep);
758
+ }
759
+
760
+ auto& sent = pex_sent_[pc];
761
+ std::vector<ext::PexPeer> added, dropped;
762
+ for (const auto& [key, ep] : current)
763
+ if (!sent.count(key)) added.push_back(ep);
764
+ for (const std::string& key : sent) {
765
+ if (current.count(key)) continue;
766
+ const std::size_t c = key.rfind(':'); // reconstruct the departed peer's endpoint
767
+ if (c != std::string::npos)
768
+ dropped.push_back(ext::PexPeer{key.substr(0, c),
769
+ std::uint16_t(std::atoi(key.c_str() + c + 1))});
770
+ }
771
+
772
+ if (!added.empty() || !dropped.empty()) {
773
+ pc->send_extended(ext_it->second.ut_pex_id, ByteView(ext::encode_pex(added, dropped)));
774
+ pex_last_tick_[pc] = tick_count_;
775
+ }
776
+
777
+ sent.clear();
778
+ for (const auto& [key, ep] : current) sent.insert(key);
779
+ }
780
+ }
781
+
782
+ // ---- trackers ----
783
+
784
+ TrackerRequest Torrent::make_tracker_request(TrackerEvent event) const {
785
+ TrackerRequest r;
786
+ r.info_hash = info_hash();
787
+ r.peer_id = host_.peer_id();
788
+ r.port = host_.listen_port();
789
+ r.uploaded = bytes_uploaded_;
790
+ r.downloaded = bytes_downloaded_;
791
+ // `left` is what remains to fetch = total size minus what is verified on disk.
792
+ // Must use verified_bytes_, not the cumulative bytes_downloaded_ (which can
793
+ // exceed on-disk bytes after re-downloads and would understate `left`).
794
+ r.left = (has_metadata_ && info_.total_size() > std::int64_t(verified_bytes_))
795
+ ? std::uint64_t(info_.total_size()) - verified_bytes_
796
+ : 0;
797
+ r.event = event;
798
+ r.numwant = 50;
799
+ return r;
800
+ }
801
+
802
+ void Torrent::announce_trackers(TrackerEvent event) {
803
+ if (!trackers_) return;
804
+ trackers_->announce(make_tracker_request(event),
805
+ [this](const std::vector<Address>& peers, std::uint32_t interval) {
806
+ // Schedule the next periodic announce off the tracker's requested interval
807
+ // rather than a fixed cadence (H13). tick_count_ advances ~1/s.
808
+ next_announce_tick_ = tick_count_ + int(interval);
809
+ int added = 0;
810
+ for (const Address& a : peers)
811
+ if (peer_list_.add(a.ip.to_string(), a.port, PeerSource::Tracker)) ++added;
812
+ LOG_INFO("bt.tracker", short_hash(info_hash()) << " ← " << peers.size() << " peers (+"
813
+ << added << " new), next announce in " << interval << "s");
814
+ if (added) try_connect();
815
+ });
816
+ }
817
+
818
+ // ---- fast resume ----
819
+
820
+ void Torrent::load_resume_data(const ResumeData& rd) {
821
+ if (running_) return; // must be applied before start()
822
+ // Recover metadata for a magnet torrent from the embedded info section.
823
+ if (!info_.has_metadata() && !rd.info_dict.empty()) info_.set_metadata(rd.info_dict);
824
+ if (!is_all_zero(rd.info_hash) && rd.info_hash != info_hash()) return; // not ours
825
+ resume_have_ = rd.have;
826
+ bytes_uploaded_ = rd.total_uploaded;
827
+ bytes_downloaded_ = rd.total_downloaded;
828
+ }
829
+
830
+ bool Torrent::try_load_resume_data() {
831
+ if (running_) return false; // must be applied before start()
832
+ std::size_t size = 0;
833
+ void* raw = read_file_binary(default_resume_path().c_str(), &size);
834
+ if (!raw) return false;
835
+ Bytes data(static_cast<std::uint8_t*>(raw), static_cast<std::uint8_t*>(raw) + size);
836
+ std::free(raw);
837
+ auto rd = ResumeData::decode(data);
838
+ if (!rd) return false;
839
+ load_resume_data(*rd);
840
+ return true;
841
+ }
842
+
843
+ ResumeData Torrent::generate_resume_data() const {
844
+ ResumeData rd;
845
+ rd.info_hash = info_hash();
846
+ rd.name = info_.name();
847
+ rd.save_path = save_path_;
848
+ rd.have = picker_ ? picker_->have_bitfield() : Bitfield{};
849
+ rd.total_uploaded = bytes_uploaded_;
850
+ rd.total_downloaded = bytes_downloaded_;
851
+ if (info_.has_metadata()) rd.info_dict = info_.info_dict_bytes();
852
+ return rd;
853
+ }
854
+
855
+ std::string Torrent::default_resume_path() const {
856
+ return combine_paths(combine_paths(save_path_, ".resume"), info_.info_hash_hex() + ".resume");
857
+ }
858
+
859
+ bool Torrent::save_resume_data() const {
860
+ return save_resume_data(default_resume_path());
861
+ }
862
+
863
+ bool Torrent::save_resume_data(const std::string& path) const {
864
+ const std::string parent = get_parent_directory(path.c_str());
865
+ if (!parent.empty() && !directory_exists(parent.c_str())) create_directories(parent.c_str());
866
+ const Bytes data = generate_resume_data().encode();
867
+ return create_file_binary(path.c_str(), data.data(), data.size());
868
+ }
869
+
870
+ } // namespace librats::bittorrent