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
@@ -1,2120 +0,0 @@
1
- #include "bt_torrent.h"
2
- #include "bt_extension.h"
3
- #include "bt_resume_data.h"
4
- #include "bencode.h"
5
- #include "disk_io.h"
6
- #include "logger.h"
7
- #include "fs.h"
8
- #include "sha1.h"
9
- #include <algorithm>
10
- #include <cctype>
11
- #include <cstring>
12
- #include <thread>
13
- #include <ctime>
14
-
15
- namespace librats {
16
-
17
- //=============================================================================
18
- // Helpers
19
- //=============================================================================
20
-
21
- const char* torrent_state_to_string(TorrentState state) {
22
- switch (state) {
23
- case TorrentState::Stopped: return "Stopped";
24
- case TorrentState::CheckingFiles: return "CheckingFiles";
25
- case TorrentState::DownloadingMetadata: return "DownloadingMetadata";
26
- case TorrentState::Downloading: return "Downloading";
27
- case TorrentState::Seeding: return "Seeding";
28
- case TorrentState::Paused: return "Paused";
29
- case TorrentState::Error: return "Error";
30
- default: return "Unknown";
31
- }
32
- }
33
-
34
- // Format bytes to human-readable string (KB, MB, GB)
35
- static std::string format_size(uint64_t bytes) {
36
- const char* units[] = {"B", "KB", "MB", "GB", "TB"};
37
- int unit_idx = 0;
38
- double size = static_cast<double>(bytes);
39
-
40
- while (size >= 1024.0 && unit_idx < 4) {
41
- size /= 1024.0;
42
- unit_idx++;
43
- }
44
-
45
- char buf[32];
46
- if (unit_idx == 0) {
47
- snprintf(buf, sizeof(buf), "%.0f %s", size, units[unit_idx]);
48
- } else {
49
- snprintf(buf, sizeof(buf), "%.2f %s", size, units[unit_idx]);
50
- }
51
- return buf;
52
- }
53
-
54
- // Format speed to human-readable string (KB/s, MB/s)
55
- static std::string format_speed(uint64_t bytes_per_sec) {
56
- return format_size(bytes_per_sec) + "/s";
57
- }
58
-
59
- // Format ETA to human-readable string
60
- static std::string format_eta(std::chrono::seconds eta) {
61
- if (eta.count() <= 0) return "∞";
62
-
63
- auto total_secs = eta.count();
64
- int hours = static_cast<int>(total_secs / 3600);
65
- int mins = static_cast<int>((total_secs % 3600) / 60);
66
- int secs = static_cast<int>(total_secs % 60);
67
-
68
- char buf[32];
69
- if (hours > 0) {
70
- snprintf(buf, sizeof(buf), "%dh %dm %ds", hours, mins, secs);
71
- } else if (mins > 0) {
72
- snprintf(buf, sizeof(buf), "%dm %ds", mins, secs);
73
- } else {
74
- snprintf(buf, sizeof(buf), "%ds", secs);
75
- }
76
- return buf;
77
- }
78
-
79
- //=============================================================================
80
- // Constructor / Destructor
81
- //=============================================================================
82
-
83
- Torrent::Torrent(const TorrentInfo& info,
84
- const TorrentConfig& config,
85
- const PeerID& our_peer_id)
86
- : info_hash_(info.info_hash())
87
- , name_(info.name())
88
- , our_peer_id_(our_peer_id)
89
- , info_(std::make_unique<TorrentInfo>(info))
90
- , config_(config)
91
- , state_(TorrentState::Stopped)
92
- , have_pieces_(info.num_pieces()) {
93
-
94
- LOG_INFO("Torrent", "Creating torrent '" + name_ + "' with " +
95
- std::to_string(info.num_pieces()) + " pieces, total size=" +
96
- std::to_string(info.total_size()) + " bytes");
97
-
98
- // Initialize piece picker
99
- uint32_t last_piece_len = info.piece_size(info.num_pieces() - 1);
100
- picker_ = std::make_unique<PiecePicker>(
101
- info.num_pieces(),
102
- info.piece_length(),
103
- last_piece_len
104
- );
105
-
106
- if (config_.sequential_download) {
107
- picker_->set_mode(PickerMode::Sequential);
108
- LOG_DEBUG("Torrent", "Sequential download mode enabled");
109
- }
110
-
111
- // Configure choker
112
- ChokerConfig choker_config;
113
- choker_config.max_uploads = config_.max_uploads;
114
- choker_.set_config(choker_config);
115
-
116
- stats_.total_size = info.total_size();
117
- stats_.pieces_total = info.num_pieces();
118
-
119
- // Initialize time tracking
120
- added_time_ = std::time(nullptr);
121
- last_activity_check_ = std::chrono::steady_clock::now();
122
-
123
- LOG_DEBUG("Torrent", "Torrent '" + name_ + "' initialized successfully");
124
- }
125
-
126
- Torrent::Torrent(const BtInfoHash& info_hash,
127
- const std::string& name,
128
- const TorrentConfig& config,
129
- const PeerID& our_peer_id)
130
- : info_hash_(info_hash)
131
- , name_(name)
132
- , our_peer_id_(our_peer_id)
133
- , config_(config)
134
- , state_(TorrentState::Stopped)
135
- , have_pieces_(0) {
136
-
137
- LOG_INFO("Torrent", "Creating torrent '" + name + "' from magnet link (no metadata yet), info_hash=" +
138
- info_hash_to_hex(info_hash));
139
-
140
- // No picker until we have metadata
141
-
142
- ChokerConfig choker_config;
143
- choker_config.max_uploads = config_.max_uploads;
144
- choker_.set_config(choker_config);
145
-
146
- // Initialize time tracking
147
- added_time_ = std::time(nullptr);
148
- last_activity_check_ = std::chrono::steady_clock::now();
149
-
150
- LOG_DEBUG("Torrent", "Torrent '" + name + "' initialized (awaiting metadata)");
151
- }
152
-
153
- Torrent::~Torrent() {
154
- LOG_DEBUG("Torrent", "Destroying torrent '" + name_ + "'");
155
- stop();
156
- }
157
-
158
- //=============================================================================
159
- // Lifecycle
160
- //=============================================================================
161
-
162
- void Torrent::start() {
163
- std::lock_guard<std::mutex> lock(mutex_);
164
-
165
- LOG_INFO("Torrent", "Starting torrent '" + name_ + "'");
166
-
167
- if (state_ == TorrentState::Downloading ||
168
- state_ == TorrentState::Seeding) {
169
- LOG_DEBUG("Torrent", "Torrent '" + name_ + "' already active, ignoring start()");
170
- return;
171
- }
172
-
173
- stats_.started_at = std::chrono::steady_clock::now();
174
- last_stats_update_ = std::chrono::steady_clock::now();
175
- last_choker_run_ = std::chrono::steady_clock::now();
176
-
177
- if (!info_ || !info_->has_metadata()) {
178
- // No metadata yet - need to download it from peers
179
- LOG_INFO("Torrent", "No metadata available, starting metadata download for '" + name_ + "'");
180
- metadata_download_started_ = std::chrono::steady_clock::now();
181
- set_state(TorrentState::DownloadingMetadata);
182
- } else if (config_.seed_mode) {
183
- // Seed mode: assume all files are complete, mark all pieces as "have"
184
- LOG_INFO("Torrent", "Torrent '" + name_ + "' in seed mode, marking all pieces as have");
185
-
186
- // Mark all pieces as have in the picker
187
- if (picker_) {
188
- picker_->mark_have_all();
189
- }
190
-
191
- // Also set our have_pieces bitfield
192
- have_pieces_ = Bitfield(info_->num_pieces());
193
- have_pieces_.set_all();
194
-
195
- // Update stats
196
- stats_.pieces_done = info_->num_pieces();
197
- stats_.bytes_done = info_->total_size();
198
- stats_.progress = 1.0f;
199
-
200
- set_state(TorrentState::Seeding);
201
- choker_.set_seed_mode(true);
202
-
203
- LOG_INFO("Torrent", "Torrent '" + name_ + "' seeding with " +
204
- std::to_string(info_->num_pieces()) + " pieces");
205
- } else if (is_complete_unlocked()) {
206
- LOG_INFO("Torrent", "Torrent '" + name_ + "' is complete, entering seeding mode");
207
- set_state(TorrentState::Seeding);
208
- choker_.set_seed_mode(true);
209
- } else {
210
- LOG_INFO("Torrent", "Starting download for '" + name_ + "'");
211
- set_state(TorrentState::Downloading);
212
- }
213
- }
214
-
215
- void Torrent::stop() {
216
- std::lock_guard<std::mutex> lock(mutex_);
217
-
218
- LOG_INFO("Torrent", "Stopping torrent '" + name_ + "'");
219
-
220
- // Disconnect all peers
221
- size_t num_connections = connections_.size();
222
- for (auto& conn : connections_) {
223
- conn->close();
224
- }
225
- connections_.clear();
226
- LOG_DEBUG("Torrent", "Disconnected " + std::to_string(num_connections) + " peers");
227
-
228
- // Clear pending peers
229
- size_t num_pending = pending_peers_.size();
230
- pending_peers_.clear();
231
- LOG_DEBUG("Torrent", "Cleared " + std::to_string(num_pending) + " pending peers");
232
-
233
- // Clear metadata exchange state
234
- metadata_buffer_.clear();
235
- metadata_pieces_received_.clear();
236
- metadata_pieces_requested_.clear();
237
- peer_metadata_size_.clear();
238
- peer_ut_metadata_id_.clear();
239
-
240
- set_state(TorrentState::Stopped);
241
-
242
- // Clear callbacks to prevent dangling pointer issues after stop
243
- // Note: Keeping on_state_change_ until after set_state() call above
244
- on_state_change_ = nullptr;
245
- on_piece_complete_ = nullptr;
246
- on_complete_ = nullptr;
247
- on_error_ = nullptr;
248
- on_metadata_received_ = nullptr;
249
-
250
- // Extended callbacks (rats-search compatibility)
251
- on_progress_ = nullptr;
252
- on_torrent_complete_ = nullptr;
253
- on_metadata_complete_ = nullptr;
254
- on_peer_connected_ext_ = nullptr;
255
- on_peer_disconnected_ext_ = nullptr;
256
-
257
- LOG_INFO("Torrent", "Torrent '" + name_ + "' stopped");
258
- }
259
-
260
- void Torrent::pause() {
261
- std::lock_guard<std::mutex> lock(mutex_);
262
-
263
- if (state_ == TorrentState::Downloading ||
264
- state_ == TorrentState::Seeding) {
265
- LOG_INFO("Torrent", "Pausing torrent '" + name_ + "' (was " +
266
- torrent_state_to_string(state_) + ")");
267
- set_state(TorrentState::Paused);
268
- } else {
269
- LOG_DEBUG("Torrent", "Cannot pause torrent '" + name_ + "' in state " +
270
- torrent_state_to_string(state_));
271
- }
272
- }
273
-
274
- void Torrent::resume() {
275
- std::lock_guard<std::mutex> lock(mutex_);
276
-
277
- if (state_ == TorrentState::Paused) {
278
- if (is_complete_unlocked()) {
279
- LOG_INFO("Torrent", "Resuming torrent '" + name_ + "' in seeding mode");
280
- set_state(TorrentState::Seeding);
281
- } else {
282
- LOG_INFO("Torrent", "Resuming torrent '" + name_ + "' in downloading mode");
283
- set_state(TorrentState::Downloading);
284
- }
285
- } else {
286
- LOG_DEBUG("Torrent", "Cannot resume torrent '" + name_ + "' in state " +
287
- torrent_state_to_string(state_));
288
- }
289
- }
290
-
291
- void Torrent::recheck() {
292
- std::lock_guard<std::mutex> lock(mutex_);
293
-
294
- LOG_INFO("Torrent", "Starting recheck for torrent '" + name_ + "'");
295
-
296
- // Would trigger file checking
297
- set_state(TorrentState::CheckingFiles);
298
-
299
- // Reset have bitfield
300
- if (info_) {
301
- have_pieces_ = Bitfield(info_->num_pieces());
302
- picker_->set_have_bitfield(have_pieces_);
303
- LOG_DEBUG("Torrent", "Reset bitfield for " + std::to_string(info_->num_pieces()) + " pieces");
304
- }
305
-
306
- // TODO: Actually verify files
307
- LOG_WARN("Torrent", "File verification not yet implemented");
308
- }
309
-
310
- //=============================================================================
311
- // State
312
- //=============================================================================
313
-
314
- bool Torrent::is_active() const {
315
- TorrentState s = state_;
316
- return s == TorrentState::Downloading ||
317
- s == TorrentState::Seeding ||
318
- s == TorrentState::DownloadingMetadata;
319
- }
320
-
321
- bool Torrent::is_complete() const {
322
- std::lock_guard<std::mutex> lock(mutex_);
323
- return is_complete_unlocked();
324
- }
325
-
326
- bool Torrent::is_complete_unlocked() const {
327
- return picker_ && picker_->is_complete();
328
- }
329
-
330
- bool Torrent::has_metadata() const {
331
- std::lock_guard<std::mutex> lock(mutex_);
332
- return has_metadata_unlocked();
333
- }
334
-
335
- bool Torrent::has_metadata_unlocked() const {
336
- return info_ != nullptr && info_->has_metadata();
337
- }
338
-
339
- //=============================================================================
340
- // Accessor Methods (rats-search compatibility)
341
- //=============================================================================
342
-
343
- // Static empty TorrentInfo for when metadata is not available
344
- static TorrentInfo s_empty_torrent_info;
345
-
346
- const TorrentInfo& Torrent::get_torrent_info() const {
347
- std::lock_guard<std::mutex> lock(mutex_);
348
- if (info_) {
349
- return *info_;
350
- }
351
- return s_empty_torrent_info;
352
- }
353
-
354
- uint64_t Torrent::downloaded_bytes() const {
355
- std::lock_guard<std::mutex> lock(mutex_);
356
- return stats_.bytes_done;
357
- }
358
-
359
- double Torrent::progress_percentage() const {
360
- std::lock_guard<std::mutex> lock(mutex_);
361
- return stats_.progress * 100.0;
362
- }
363
-
364
- double Torrent::download_speed() const {
365
- std::lock_guard<std::mutex> lock(mutex_);
366
- return static_cast<double>(stats_.download_rate);
367
- }
368
-
369
- void Torrent::set_state(TorrentState new_state) {
370
- TorrentState old_state = state_.exchange(new_state);
371
- if (old_state != new_state) {
372
- LOG_DEBUG("Torrent", "State change: " + std::string(torrent_state_to_string(old_state)) +
373
- " -> " + std::string(torrent_state_to_string(new_state)) +
374
- " for '" + name_ + "'");
375
- if (on_state_change_) {
376
- on_state_change_(this, new_state);
377
- }
378
- }
379
- }
380
-
381
- //=============================================================================
382
- // Statistics
383
- //=============================================================================
384
-
385
- TorrentStats Torrent::stats() const {
386
- std::lock_guard<std::mutex> lock(mutex_);
387
- return stats_;
388
- }
389
-
390
- Bitfield Torrent::get_have_bitfield() const {
391
- std::lock_guard<std::mutex> lock(mutex_);
392
- if (picker_) {
393
- return picker_->get_have_bitfield();
394
- }
395
- return have_pieces_;
396
- }
397
-
398
- //=============================================================================
399
- // Peer Management
400
- //=============================================================================
401
-
402
- void Torrent::add_peer(const std::string& ip, uint16_t port) {
403
- std::lock_guard<std::mutex> lock(mutex_);
404
-
405
- // Check if already connected
406
- for (const auto& conn : connections_) {
407
- if (conn->ip() == ip && conn->port() == port) {
408
- LOG_DEBUG("Torrent", "Peer " + ip + ":" + std::to_string(port) + " already connected");
409
- return;
410
- }
411
- }
412
-
413
- // Check if already pending
414
- for (const auto& peer : pending_peers_) {
415
- if (peer.first == ip && peer.second == port) {
416
- LOG_DEBUG("Torrent", "Peer " + ip + ":" + std::to_string(port) + " already pending");
417
- return;
418
- }
419
- }
420
-
421
- pending_peers_.emplace_back(ip, port);
422
- LOG_DEBUG("Torrent", "Added pending peer " + ip + ":" + std::to_string(port) +
423
- " (total pending=" + std::to_string(pending_peers_.size()) + ")");
424
- }
425
-
426
- void Torrent::add_peers(const std::vector<std::pair<std::string, uint16_t>>& peers) {
427
- for (const auto& peer : peers) {
428
- add_peer(peer.first, peer.second);
429
- }
430
- }
431
-
432
- size_t Torrent::num_peers() const {
433
- std::lock_guard<std::mutex> lock(mutex_);
434
- return connections_.size();
435
- }
436
-
437
- std::vector<BtPeerConnection*> Torrent::peers() const {
438
- std::lock_guard<std::mutex> lock(mutex_);
439
- std::vector<BtPeerConnection*> result;
440
- result.reserve(connections_.size());
441
- for (const auto& conn : connections_) {
442
- result.push_back(conn.get());
443
- }
444
- return result;
445
- }
446
-
447
- std::vector<std::pair<std::string, uint16_t>> Torrent::get_pending_peers() const {
448
- std::lock_guard<std::mutex> lock(mutex_);
449
- return pending_peers_;
450
- }
451
-
452
- void Torrent::clear_pending_peers() {
453
- std::lock_guard<std::mutex> lock(mutex_);
454
- pending_peers_.clear();
455
- }
456
-
457
- void Torrent::add_connection(std::shared_ptr<BtPeerConnection> connection) {
458
- if (!connection) return;
459
- {
460
- std::lock_guard<std::mutex> lock(mutex_);
461
-
462
- LOG_DEBUG("Torrent", "Adding connection from " + connection->ip() + ":" +
463
- std::to_string(connection->port()));
464
-
465
- // Remove from pending if present
466
- auto it = std::remove_if(pending_peers_.begin(), pending_peers_.end(),
467
- [&](const auto& p) {
468
- return p.first == connection->ip() && p.second == connection->port();
469
- });
470
- pending_peers_.erase(it, pending_peers_.end());
471
-
472
- connection->set_message_callback(
473
- [this](BtPeerConnection* peer, const BtMessage& msg) {
474
- on_peer_message(peer, msg);
475
- }
476
- );
477
-
478
- connection->set_state_callback(
479
- [this](BtPeerConnection* peer, PeerConnectionState state) {
480
- if (state == PeerConnectionState::Disconnected) {
481
- on_peer_disconnected(peer);
482
- } else if (state == PeerConnectionState::Connected) {
483
- on_peer_connected(peer);
484
- }
485
- }
486
- );
487
-
488
- // Add to picker for availability tracking
489
- if (picker_ && connection->is_connected()) {
490
- picker_->add_peer(connection.get(), connection->peer_pieces());
491
- }
492
-
493
- connections_.push_back(connection); // shared_ptr - just copy
494
-
495
- // Notify peer we're connected and setup extension handshake
496
- on_peer_connected(connection.get());
497
- }
498
-
499
- // Extended callback (rats-search compatibility)
500
- if (on_peer_connected_ext_) {
501
- Peer peer_address(connection->ip(), connection->port());
502
- on_peer_connected_ext_(peer_address);
503
- }
504
- }
505
-
506
- void Torrent::remove_connection(BtPeerConnection* connection) {
507
- std::lock_guard<std::mutex> lock(mutex_);
508
-
509
- auto it = std::find_if(connections_.begin(), connections_.end(),
510
- [connection](const auto& conn) {
511
- return conn.get() == connection;
512
- });
513
-
514
- if (it != connections_.end()) {
515
- LOG_DEBUG("Torrent", "Removing connection from " + connection->ip() + ":" +
516
- std::to_string(connection->port()));
517
-
518
- // Cleanup peer from picker (cancel requests, remove from availability)
519
- // Note: on_peer_disconnected may have already been called via state_callback,
520
- // but these operations are idempotent
521
- if (picker_) {
522
- picker_->cancel_peer_requests(connection);
523
- picker_->remove_peer(connection);
524
- }
525
-
526
- // Clear metadata tracking for this peer
527
- peer_metadata_size_.erase(connection);
528
- peer_ut_metadata_id_.erase(connection);
529
-
530
- connections_.erase(it);
531
- LOG_DEBUG("Torrent", "Active connections: " + std::to_string(connections_.size()));
532
- } else {
533
- LOG_DEBUG("Torrent", "Connection not found for removal: " + connection->ip());
534
- }
535
- }
536
-
537
- //=============================================================================
538
- // Configuration
539
- //=============================================================================
540
-
541
- void Torrent::set_sequential(bool sequential) {
542
- std::lock_guard<std::mutex> lock(mutex_);
543
- config_.sequential_download = sequential;
544
- LOG_INFO("Torrent", "Sequential download " + std::string(sequential ? "enabled" : "disabled") +
545
- " for '" + name_ + "'");
546
- if (picker_) {
547
- picker_->set_mode(sequential ? PickerMode::Sequential : PickerMode::RarestFirst);
548
- }
549
- }
550
-
551
- void Torrent::set_download_limit(uint64_t bytes_per_sec) {
552
- std::lock_guard<std::mutex> lock(mutex_);
553
- config_.download_limit = bytes_per_sec;
554
- LOG_INFO("Torrent", "Download limit set to " + std::to_string(bytes_per_sec) +
555
- " bytes/sec for '" + name_ + "'");
556
- }
557
-
558
- void Torrent::set_upload_limit(uint64_t bytes_per_sec) {
559
- std::lock_guard<std::mutex> lock(mutex_);
560
- config_.upload_limit = bytes_per_sec;
561
- LOG_INFO("Torrent", "Upload limit set to " + std::to_string(bytes_per_sec) +
562
- " bytes/sec for '" + name_ + "'");
563
- }
564
-
565
- //=============================================================================
566
- // Metadata
567
- //=============================================================================
568
-
569
- bool Torrent::set_metadata(const std::vector<uint8_t>& metadata) {
570
- std::lock_guard<std::mutex> lock(mutex_);
571
-
572
- LOG_INFO("Torrent", "Setting metadata (" + std::to_string(metadata.size()) + " bytes)");
573
-
574
- auto new_info = TorrentInfo::from_info_dict(metadata, info_hash_);
575
- if (!new_info) {
576
- LOG_ERROR("Torrent", "Failed to parse metadata - info_hash mismatch or invalid data");
577
- return false;
578
- }
579
-
580
- info_ = std::make_unique<TorrentInfo>(*new_info);
581
- name_ = info_->name();
582
- LOG_INFO("Torrent", "Metadata parsed successfully: name='" + name_ + "'");
583
-
584
- // Save any previously loaded resume data (from try_load_resume_data before metadata)
585
- Bitfield saved_have_pieces = have_pieces_;
586
- bool had_resume_data = (saved_have_pieces.size() > 0 && saved_have_pieces.count() > 0);
587
-
588
- // Initialize pieces
589
- have_pieces_ = Bitfield(info_->num_pieces());
590
-
591
- uint32_t last_piece_len = info_->piece_size(info_->num_pieces() - 1);
592
- picker_ = std::make_unique<PiecePicker>(
593
- info_->num_pieces(),
594
- info_->piece_length(),
595
- last_piece_len
596
- );
597
-
598
- if (config_.sequential_download) {
599
- picker_->set_mode(PickerMode::Sequential);
600
- }
601
-
602
- stats_.total_size = info_->total_size();
603
- stats_.pieces_total = info_->num_pieces();
604
-
605
- // Restore resume data if we had it loaded before metadata arrived
606
- if (had_resume_data && saved_have_pieces.size() == info_->num_pieces()) {
607
- LOG_INFO("Torrent", "Restoring " + std::to_string(saved_have_pieces.count()) +
608
- " pieces from resume data");
609
- have_pieces_ = saved_have_pieces;
610
- for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
611
- if (have_pieces_.get_bit(i)) {
612
- picker_->mark_have(i);
613
- }
614
- }
615
- // Calculate bytes_done from pieces
616
- stats_.pieces_done = picker_->num_have();
617
- stats_.bytes_done = 0;
618
- for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
619
- if (have_pieces_.get_bit(i)) {
620
- stats_.bytes_done += info_->piece_size(i);
621
- }
622
- }
623
- stats_.progress = static_cast<float>(stats_.pieces_done) /
624
- static_cast<float>(stats_.pieces_total);
625
- LOG_INFO("Torrent", "Resume data restored: " + std::to_string(stats_.pieces_done) +
626
- "/" + std::to_string(stats_.pieces_total) + " pieces (" +
627
- std::to_string(stats_.bytes_done) + " bytes, " +
628
- std::to_string(stats_.progress * 100.0f) + "%)");
629
- }
630
-
631
- LOG_DEBUG("Torrent", "Torrent stats: " + std::to_string(info_->num_pieces()) + " pieces, " +
632
- std::to_string(info_->total_size()) + " bytes total");
633
-
634
- // CRITICAL: Initialize existing peer connections with the new metadata
635
- // This is necessary because peers connected during DownloadingMetadata state
636
- // were not added to the picker (it didn't exist yet).
637
-
638
- // In metadata-only mode (empty save_path), we skip sending INTERESTED and bitfield
639
- // because we're not going to download anything - just needed the metadata
640
- const bool metadata_only = config_.save_path.empty();
641
-
642
- LOG_DEBUG("Torrent", "Initializing " + std::to_string(connections_.size()) +
643
- " existing connections with metadata" +
644
- (metadata_only ? " (metadata-only mode)" : ""));
645
-
646
- for (auto& conn : connections_) {
647
- LOG_DEBUG("Torrent", "Processing connection " + conn->ip() +
648
- " is_connected=" + (conn->is_connected() ? "yes" : "no") +
649
- " peer_has_all=" + (conn->peer_has_all() ? "yes" : "no") +
650
- " peer_pieces_count=" + std::to_string(conn->peer_pieces().count()));
651
-
652
- if (!conn->is_connected()) {
653
- LOG_DEBUG("Torrent", "Skipping " + conn->ip() + " - not connected");
654
- continue;
655
- }
656
-
657
- // Update peer's bitfield size to match the torrent
658
- // This is needed because peer may have sent Bitfield/HaveAll before we had metadata
659
- conn->set_torrent_info(info_hash_, info_->num_pieces());
660
-
661
- // Skip download-related initialization in metadata-only mode
662
- if (metadata_only) {
663
- continue;
664
- }
665
-
666
- // Get peer's pieces (now correctly sized)
667
- const auto& peer_pieces = conn->peer_pieces();
668
-
669
- // Add peer to picker for availability tracking
670
- picker_->add_peer(conn.get(), peer_pieces);
671
-
672
- // Check if this peer has pieces we need (either via bitfield or peer_has_all flag)
673
- bool is_interesting = false;
674
- if (conn->peer_has_all()) {
675
- // Peer has everything, we have nothing - definitely interesting!
676
- is_interesting = true;
677
- LOG_DEBUG("Torrent", "Peer " + conn->ip() + " has all pieces (HaveAll)");
678
- } else if (picker_->is_interesting(peer_pieces)) {
679
- is_interesting = true;
680
- }
681
-
682
- if (is_interesting) {
683
- // Send INTERESTED to peer so they will (hopefully) unchoke us
684
- if (!conn->am_interested()) {
685
- LOG_DEBUG("Torrent", "Peer " + conn->ip() + " has pieces we need, sending interested");
686
- conn->send_interested();
687
- }
688
- }
689
-
690
- // Send our bitfield to the peer (we have no pieces yet, but protocol requires it)
691
- conn->send_bitfield(have_pieces_);
692
- }
693
-
694
- set_state(TorrentState::Downloading);
695
-
696
- return true;
697
- }
698
-
699
- //=============================================================================
700
- // Tick
701
- //=============================================================================
702
-
703
- void Torrent::tick() {
704
- std::lock_guard<std::mutex> lock(mutex_);
705
-
706
- if (!is_active()) {
707
- return;
708
- }
709
-
710
- auto now = std::chrono::steady_clock::now();
711
-
712
- // Check metadata download timeout
713
- if (state_ == TorrentState::DownloadingMetadata) {
714
- auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
715
- now - metadata_download_started_).count();
716
-
717
- if (elapsed >= METADATA_TIMEOUT_SECONDS) {
718
- LOG_WARN("Torrent", "Metadata download timeout for " +
719
- info_hash_to_hex(info_hash_).substr(0, 8) + "...");
720
-
721
- // Notify callback of failure
722
- if (on_metadata_received_) {
723
- on_metadata_received_(this, false);
724
- }
725
-
726
- // Clear metadata state
727
- metadata_buffer_.clear();
728
- metadata_pieces_received_.clear();
729
- metadata_pieces_requested_.clear();
730
- peer_metadata_size_.clear();
731
- peer_ut_metadata_id_.clear();
732
-
733
- // Set error state
734
- error_message_ = "Metadata download timeout";
735
- set_state(TorrentState::Error);
736
-
737
- return;
738
- }
739
- }
740
-
741
- // Run choker periodically
742
- if (choker_.should_rechoke()) {
743
- run_choker();
744
- last_choker_run_ = now;
745
- }
746
-
747
- // Request pieces from peers
748
- if (state_ == TorrentState::Downloading && picker_) {
749
- // Log peer status periodically (every 5 seconds)
750
- static auto last_peer_log = std::chrono::steady_clock::time_point{};
751
- if ((now - last_peer_log) >= std::chrono::seconds(5)) {
752
- LOG_DEBUG("Torrent", "Peer status for '" + name_ + "' (" +
753
- std::to_string(connections_.size()) + " peers):");
754
- for (const auto& conn : connections_) {
755
- LOG_DEBUG("Torrent", " " + conn->ip() +
756
- " connected=" + (conn->is_connected() ? "yes" : "no") +
757
- " peer_choking=" + (conn->peer_choking() ? "yes" : "no") +
758
- " am_interested=" + (conn->am_interested() ? "yes" : "no") +
759
- " pieces=" + std::to_string(conn->peer_pieces().count()));
760
- }
761
- last_peer_log = now;
762
- }
763
- }
764
- request_pieces();
765
-
766
- // Update statistics
767
- auto stats_interval = std::chrono::seconds(1);
768
- if ((now - last_stats_update_) >= stats_interval) {
769
- update_stats();
770
- last_stats_update_ = now;
771
- }
772
-
773
- // TODO: Connect to pending peers
774
- // TODO: Announce to trackers
775
- // TODO: DHT announces
776
- }
777
-
778
- //=============================================================================
779
- // Internal Methods
780
- //=============================================================================
781
-
782
- void Torrent::on_peer_connected(BtPeerConnection* peer) {
783
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected" +
784
- (peer->peer_client_name().empty() ? "" : " [" + peer->peer_client_name() + "]") +
785
- ", has_metadata=" + (has_metadata_unlocked() ? "true" : "false"));
786
-
787
- // Send extension handshake if peer supports it
788
- if (peer->peer_extensions().extension_protocol) {
789
- send_extension_handshake(peer);
790
- }
791
-
792
- // Send bitfield if we have pieces
793
- if (picker_ && picker_->num_have() > 0) {
794
- peer->send_bitfield(picker_->get_have_bitfield());
795
- }
796
-
797
- // Check if peer already received extension handshake before we set up callbacks
798
- // This happens because network data arrives before Torrent takes ownership
799
- if (state_ == TorrentState::DownloadingMetadata &&
800
- peer->extension_handshake_received() &&
801
- peer->peer_metadata_size() > 0 &&
802
- peer->peer_ut_metadata_id() != 0) {
803
-
804
- LOG_DEBUG("Torrent", "Peer " + peer->ip() +
805
- " already has extension handshake data: metadata_size=" +
806
- std::to_string(peer->peer_metadata_size()) +
807
- " ut_metadata_id=" + std::to_string(peer->peer_ut_metadata_id()));
808
-
809
- // Store peer's metadata info and request metadata
810
- peer_metadata_size_[peer] = peer->peer_metadata_size();
811
- peer_ut_metadata_id_[peer] = peer->peer_ut_metadata_id();
812
- request_metadata(peer);
813
- }
814
- }
815
-
816
- void Torrent::on_peer_disconnected(BtPeerConnection* peer) {
817
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " disconnected");
818
-
819
- // Extended callback (rats-search compatibility)
820
- if (on_peer_disconnected_ext_) {
821
- Peer p(peer->ip(), peer->port());
822
- on_peer_disconnected_ext_(p);
823
- }
824
-
825
- // Cancel pending requests
826
- if (picker_) {
827
- picker_->cancel_peer_requests(peer);
828
- LOG_DEBUG("Torrent", "Cancelled pending requests for peer " + peer->ip());
829
- }
830
-
831
- // Remove from availability
832
- if (picker_) {
833
- picker_->remove_peer(peer);
834
- }
835
-
836
- // Clear metadata tracking for this peer
837
- peer_metadata_size_.erase(peer);
838
- peer_ut_metadata_id_.erase(peer);
839
- }
840
-
841
- void Torrent::on_peer_message(BtPeerConnection* peer, const BtMessage& msg) {
842
- // Log all message types for debugging
843
- switch (msg.type) {
844
- case BtMessageType::Bitfield:
845
- if (msg.bitfield) {
846
- LOG_DEBUG("Torrent", "on_peer_message: Bitfield from " + peer->ip() +
847
- ", bits_set=" + std::to_string(msg.bitfield->count()));
848
- }
849
- if (msg.bitfield && picker_) {
850
- picker_->add_peer(peer, *msg.bitfield);
851
-
852
- // Check if interesting
853
- if (picker_->is_interesting(*msg.bitfield)) {
854
- peer->send_interested();
855
- }
856
- }
857
- break;
858
-
859
- case BtMessageType::Have:
860
- LOG_DEBUG("Torrent", "on_peer_message: Have piece=" + std::to_string(msg.have_piece) +
861
- " from " + peer->ip());
862
- if (picker_) {
863
- picker_->peer_has_piece(peer, msg.have_piece);
864
- // Check if we should become interested
865
- if (!peer->am_interested() && !have_pieces_.get_bit(msg.have_piece)) {
866
- peer->send_interested();
867
- }
868
- }
869
- break;
870
-
871
- case BtMessageType::HaveAll:
872
- // Fast extension: peer has all pieces (seeder)
873
- LOG_DEBUG("Torrent", "on_peer_message: HaveAll from " + peer->ip());
874
- if (picker_) {
875
- // Add peer to picker with all pieces
876
- Bitfield all_pieces(info_ ? info_->num_pieces() : 0);
877
- all_pieces.set_all();
878
- picker_->add_peer(peer, all_pieces);
879
-
880
- // We're definitely interested in a seeder
881
- if (!peer->am_interested()) {
882
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " has all pieces, sending interested");
883
- peer->send_interested();
884
- }
885
- }
886
- break;
887
-
888
- case BtMessageType::HaveNone:
889
- // Fast extension: peer has no pieces
890
- LOG_DEBUG("Torrent", "on_peer_message: HaveNone from " + peer->ip());
891
- break;
892
-
893
- case BtMessageType::Piece:
894
- if (msg.piece) {
895
- LOG_DEBUG("Torrent", "on_peer_message: Piece idx=" + std::to_string(msg.piece->piece_index) +
896
- " begin=" + std::to_string(msg.piece->begin) +
897
- " len=" + std::to_string(msg.piece->data.size()) + " from " + peer->ip());
898
- on_piece_received(msg.piece->piece_index, msg.piece->begin, msg.piece->data);
899
- }
900
- break;
901
-
902
- case BtMessageType::Request:
903
- // Peer is requesting a block from us
904
- if (msg.request) {
905
- LOG_DEBUG("Torrent", "on_peer_message: Request piece=" + std::to_string(msg.request->piece_index) +
906
- " begin=" + std::to_string(msg.request->begin) +
907
- " len=" + std::to_string(msg.request->length) + " from " + peer->ip());
908
- }
909
- if (msg.request && !peer->am_choking() && have_pieces_.get_bit(msg.request->piece_index)) {
910
- // Track this request so we can handle Cancel
911
- peer->add_incoming_request(*msg.request);
912
- // Read from disk and send piece data to peer
913
- read_piece_from_disk(msg.request->piece_index, peer,
914
- msg.request->begin, msg.request->length);
915
- }
916
- break;
917
-
918
- case BtMessageType::Cancel:
919
- // Peer is cancelling a previous request (don't send the piece)
920
- if (msg.request) {
921
- LOG_DEBUG("Torrent", "on_peer_message: Cancel piece=" + std::to_string(msg.request->piece_index) +
922
- " begin=" + std::to_string(msg.request->begin) +
923
- " len=" + std::to_string(msg.request->length) + " from " + peer->ip());
924
- peer->remove_incoming_request(*msg.request);
925
- }
926
- break;
927
-
928
- case BtMessageType::RejectRequest:
929
- // Peer rejected our request (Fast Extension) - need to request from another peer
930
- if (msg.request) {
931
- LOG_DEBUG("Torrent", "on_peer_message: RejectRequest piece=" + std::to_string(msg.request->piece_index) +
932
- " begin=" + std::to_string(msg.request->begin) + " from " + peer->ip());
933
- // Remove from pending and mark as available for re-request
934
- if (picker_) {
935
- BlockInfo block(msg.request->piece_index, msg.request->begin, msg.request->length);
936
- picker_->abort_download(block, peer);
937
- }
938
- }
939
- break;
940
-
941
- case BtMessageType::Unchoke:
942
- // Peer unchoked us - can start requesting
943
- LOG_DEBUG("Torrent", "on_peer_message: Unchoke from " + peer->ip());
944
- // Immediately request blocks from this peer
945
- if (picker_ && peer->am_interested()) {
946
- request_blocks_from_peer(peer);
947
- }
948
- break;
949
-
950
- case BtMessageType::Choke:
951
- // Peer choked us - stop requesting
952
- LOG_DEBUG("Torrent", "on_peer_message: Choke from " + peer->ip());
953
- if (picker_) {
954
- picker_->cancel_peer_requests(peer);
955
- }
956
- break;
957
-
958
- case BtMessageType::Interested:
959
- LOG_DEBUG("Torrent", "on_peer_message: Interested from " + peer->ip());
960
- break;
961
-
962
- case BtMessageType::NotInterested:
963
- LOG_DEBUG("Torrent", "on_peer_message: NotInterested from " + peer->ip());
964
- break;
965
-
966
- case BtMessageType::Extended:
967
- LOG_DEBUG("Torrent", "on_peer_message: Extended ext_id=" + std::to_string(msg.extension_id) +
968
- " payload=" + std::to_string(msg.extension_payload.size()) + " bytes from " + peer->ip());
969
- // Handle extension protocol messages
970
- on_extension_message(peer, msg.extension_id, msg.extension_payload);
971
- break;
972
-
973
- default:
974
- LOG_DEBUG("Torrent", "on_peer_message: Unknown type from " + peer->ip());
975
- break;
976
- }
977
- }
978
-
979
- void Torrent::on_piece_received(uint32_t piece, uint32_t begin,
980
- const std::vector<uint8_t>& data) {
981
- // Note: This is called from network thread, need to acquire mutex
982
- std::lock_guard<std::mutex> lock(mutex_);
983
-
984
- if (!picker_ || !info_) {
985
- LOG_WARN("Torrent", "Received piece data but picker/info not initialized");
986
- return;
987
- }
988
-
989
- LOG_DEBUG("Torrent", "Block received: piece=" + std::to_string(piece) +
990
- " offset=" + std::to_string(begin) + " len=" + std::to_string(data.size()));
991
-
992
- BlockInfo block(piece, begin, static_cast<uint32_t>(data.size()));
993
-
994
- // Mark block as writing (will be marked finished after disk write completes)
995
- picker_->mark_writing(block);
996
-
997
- // Write block directly to disk (no in-memory buffering)
998
- // This prevents memory bloat when downloading from many peers
999
- write_block_to_disk(block, data);
1000
- }
1001
-
1002
- void Torrent::on_piece_verified(uint32_t piece, bool valid) {
1003
- if (!picker_) return;
1004
-
1005
- if (valid) {
1006
- LOG_INFO("Torrent", "Piece " + std::to_string(piece) + " verified successfully (" +
1007
- std::to_string(picker_->num_have() + 1) + "/" +
1008
- std::to_string(stats_.pieces_total) + ")");
1009
-
1010
- picker_->mark_have(piece);
1011
- have_pieces_.set_bit(piece);
1012
-
1013
- // Blocks were written directly to disk - no buffer to clear
1014
-
1015
- // Notify
1016
- if (on_piece_complete_) {
1017
- on_piece_complete_(this, piece);
1018
- }
1019
-
1020
- // Broadcast HAVE to peers
1021
- for (auto& conn : connections_) {
1022
- conn->send_have(piece);
1023
- }
1024
-
1025
- // Check if complete
1026
- if (picker_->is_complete()) {
1027
- LOG_INFO("Torrent", "Download complete for '" + name_ + "'!");
1028
- set_state(TorrentState::Seeding);
1029
- choker_.set_seed_mode(true);
1030
-
1031
- if (on_complete_) {
1032
- on_complete_(this);
1033
- }
1034
-
1035
- // Extended callback (rats-search compatibility)
1036
- if (on_torrent_complete_) {
1037
- on_torrent_complete_(name_);
1038
- }
1039
- }
1040
-
1041
- ++stats_.pieces_done;
1042
- } else {
1043
- LOG_WARN("Torrent", "Piece " + std::to_string(piece) + " hash verification FAILED, will re-download");
1044
- // Hash check failed - need to re-download the piece
1045
- // Reset the piece state in picker so it can be re-requested
1046
- if (picker_) {
1047
- // The piece was marked as 'have' prematurely - we need to remove it
1048
- // from downloading state and allow it to be picked again
1049
- // Note: mark_have was not called yet since verification failed
1050
- }
1051
- }
1052
- }
1053
-
1054
- void Torrent::request_pieces() {
1055
- if (!picker_) return;
1056
-
1057
- for (auto& conn : connections_) {
1058
- request_blocks_from_peer(conn.get());
1059
- }
1060
- }
1061
-
1062
- void Torrent::request_blocks_from_peer(BtPeerConnection* peer) {
1063
- if (!picker_ || !peer) return;
1064
-
1065
- if (!peer->is_connected()) {
1066
- LOG_DEBUG("Torrent", "request_blocks_from_peer: Peer " + peer->ip() + " is not connected, skipping request");
1067
- return;
1068
- }
1069
- if (peer->peer_choking()) {
1070
- LOG_DEBUG("Torrent", "request_blocks_from_peer: Peer " + peer->ip() + " is choking, skipping request");
1071
- return;
1072
- }
1073
-
1074
- // Check disk I/O backpressure - don't request more blocks if disk can't keep up
1075
- if (!DiskIO::instance().can_accept_write()) {
1076
- LOG_DEBUG("Torrent", "request_blocks_from_peer: Disk I/O under backpressure, deferring requests");
1077
- return;
1078
- }
1079
-
1080
- int requested = 0;
1081
- while (peer->can_request()) {
1082
- auto blocks = picker_->pick_pieces(peer->peer_pieces(), 1, peer);
1083
- if (blocks.empty()) {
1084
- break;
1085
- }
1086
-
1087
- const auto& block = blocks[0].block;
1088
- LOG_DEBUG("Torrent", "request_blocks_from_peer: Requesting piece=" + std::to_string(block.piece_index) +
1089
- " offset=" + std::to_string(block.offset) +
1090
- " len=" + std::to_string(block.length) + " from " + peer->ip());
1091
- peer->send_request(block.piece_index, block.offset, block.length);
1092
- ++requested;
1093
- }
1094
-
1095
- if (requested > 0) {
1096
- LOG_DEBUG("Torrent", "Requested " + std::to_string(requested) + " blocks from " + peer->ip());
1097
- }
1098
- }
1099
-
1100
- void Torrent::run_choker() {
1101
- std::vector<ChokePeerInfo> peer_infos;
1102
- peer_infos.reserve(connections_.size());
1103
-
1104
- for (auto& conn : connections_) {
1105
- if (!conn->is_connected()) continue;
1106
-
1107
- ChokePeerInfo info;
1108
- info.connection = conn.get();
1109
- info.download_rate = conn->stats().download_rate();
1110
- info.upload_rate = conn->stats().upload_rate();
1111
- info.am_choking = conn->am_choking();
1112
- info.am_interested = conn->am_interested();
1113
- info.peer_interested = conn->peer_interested();
1114
- info.connected_at = conn->stats().connected_at;
1115
-
1116
- peer_infos.push_back(info);
1117
- }
1118
-
1119
- auto result = choker_.run(peer_infos);
1120
-
1121
- if (!result.to_choke.empty() || !result.to_unchoke.empty()) {
1122
- LOG_DEBUG("Torrent", "Choker: choking " + std::to_string(result.to_choke.size()) +
1123
- " peers, unchoking " + std::to_string(result.to_unchoke.size()) + " peers");
1124
- }
1125
-
1126
- for (auto* peer : result.to_choke) {
1127
- LOG_DEBUG("Torrent", "Choking peer " + peer->ip());
1128
- peer->send_choke();
1129
- }
1130
-
1131
- for (auto* peer : result.to_unchoke) {
1132
- LOG_DEBUG("Torrent", "Unchoking peer " + peer->ip());
1133
- peer->send_unchoke();
1134
- }
1135
- }
1136
-
1137
- void Torrent::update_stats() {
1138
- if (picker_) {
1139
- stats_.pieces_done = picker_->num_have();
1140
- stats_.progress = static_cast<float>(stats_.pieces_done) /
1141
- static_cast<float>(stats_.pieces_total);
1142
-
1143
- // Calculate bytes done
1144
- if (info_) {
1145
- uint64_t bytes = 0;
1146
- for (uint32_t i = 0; i < stats_.pieces_done; ++i) {
1147
- bytes += info_->piece_size(i);
1148
- }
1149
- stats_.bytes_done = bytes;
1150
- }
1151
- }
1152
-
1153
- stats_.peers_connected = static_cast<uint32_t>(connections_.size());
1154
- stats_.peers_total = static_cast<uint32_t>(
1155
- connections_.size() + pending_peers_.size());
1156
-
1157
- // Count seeders/leechers
1158
- stats_.seeders = 0;
1159
- stats_.leechers = 0;
1160
- for (const auto& conn : connections_) {
1161
- if (conn->peer_pieces().all_set()) {
1162
- ++stats_.seeders;
1163
- } else {
1164
- ++stats_.leechers;
1165
- }
1166
- }
1167
-
1168
- // Calculate rates
1169
- uint64_t total_dl = 0, total_ul = 0;
1170
- for (const auto& conn : connections_) {
1171
- total_dl += static_cast<uint64_t>(conn->stats().download_rate());
1172
- total_ul += static_cast<uint64_t>(conn->stats().upload_rate());
1173
- }
1174
- stats_.download_rate = total_dl;
1175
- stats_.upload_rate = total_ul;
1176
-
1177
- // Calculate ETA
1178
- if (stats_.download_rate > 0 && stats_.bytes_done < stats_.total_size) {
1179
- uint64_t remaining = stats_.total_size - stats_.bytes_done;
1180
- stats_.eta = std::chrono::seconds(remaining / stats_.download_rate);
1181
- }
1182
-
1183
- // Log progress (only when downloading)
1184
- if (state_ == TorrentState::Downloading && stats_.total_size > 0) {
1185
- LOG_INFO("Torrent", "[" + name_ + "] " +
1186
- std::to_string(static_cast<int>(stats_.progress * 100)) + "% | " +
1187
- format_size(stats_.bytes_done) + " / " + format_size(stats_.total_size) + " | " +
1188
- "DL: " + format_speed(stats_.download_rate) + " | " +
1189
- "UL: " + format_speed(stats_.upload_rate) + " | " +
1190
- "Peers: " + std::to_string(stats_.peers_connected) +
1191
- " (" + std::to_string(stats_.seeders) + "S/" +
1192
- std::to_string(stats_.leechers) + "L) | " +
1193
- "Pieces: " + std::to_string(stats_.pieces_done) + "/" +
1194
- std::to_string(stats_.pieces_total) + " | " +
1195
- "ETA: " + format_eta(stats_.eta));
1196
-
1197
- // Log individual peer stats
1198
- for (const auto& conn : connections_) {
1199
- if (!conn->is_connected()) continue;
1200
-
1201
- const auto& ps = conn->stats();
1202
- uint32_t peer_pieces = static_cast<uint32_t>(conn->peer_pieces().count());
1203
-
1204
- // Build state flags string
1205
- std::string flags;
1206
- flags += conn->am_interested() ? "I" : "-"; // we are interested
1207
- flags += conn->peer_interested() ? "i" : "-"; // peer interested in us
1208
- flags += conn->am_choking() ? "C" : "-"; // we are choking
1209
- flags += conn->peer_choking() ? "c" : "-"; // peer choking us
1210
-
1211
- bool is_seeder = conn->peer_pieces().all_set() || conn->peer_has_all();
1212
-
1213
- LOG_INFO("Torrent", " Peer " + conn->ip() + ":" + std::to_string(conn->port()) +
1214
- " | " + flags +
1215
- " | DL: " + format_speed(static_cast<uint64_t>(ps.download_rate())) +
1216
- " | UL: " + format_speed(static_cast<uint64_t>(ps.upload_rate())) +
1217
- " | Req: " + std::to_string(conn->pending_requests()) +
1218
- " | Got: " + format_size(ps.bytes_downloaded) +
1219
- " | Pieces: " + std::to_string(peer_pieces) + "/" + std::to_string(stats_.pieces_total) +
1220
- (is_seeder ? " [Seeder]" : ""));
1221
- }
1222
- }
1223
-
1224
- // Extended progress callback (rats-search compatibility)
1225
- if (on_progress_ && stats_.total_size > 0) {
1226
- double percentage = stats_.progress * 100.0;
1227
- on_progress_(stats_.bytes_done, stats_.total_size, percentage);
1228
- }
1229
- }
1230
-
1231
- //=============================================================================
1232
- // Disk I/O Helpers
1233
- //=============================================================================
1234
-
1235
- std::vector<FileMappingInfo> Torrent::get_file_mappings() const {
1236
- std::vector<FileMappingInfo> mappings;
1237
-
1238
- if (!info_) return mappings;
1239
-
1240
- const auto& files = info_->files().files();
1241
- mappings.reserve(files.size());
1242
-
1243
- for (const auto& file : files) {
1244
- FileMappingInfo mapping;
1245
- mapping.path = file.path;
1246
- mapping.length = static_cast<uint64_t>(file.size);
1247
- mapping.torrent_offset = static_cast<uint64_t>(file.offset);
1248
- mappings.push_back(mapping);
1249
- }
1250
-
1251
- return mappings;
1252
- }
1253
-
1254
- void Torrent::write_block_to_disk(const BlockInfo& block, const std::vector<uint8_t>& data) {
1255
- // Note: Called with mutex_ already held from on_piece_received
1256
- if (!info_) return;
1257
-
1258
- LOG_DEBUG("Torrent", "Writing block to disk: piece=" + std::to_string(block.piece_index) +
1259
- " offset=" + std::to_string(block.offset) + " len=" + std::to_string(data.size()));
1260
-
1261
- auto weak_self = weak_from_this();
1262
-
1263
- DiskIO::instance().async_write_block(
1264
- config_.save_path,
1265
- get_file_mappings(),
1266
- block.piece_index,
1267
- info_->piece_length(),
1268
- block.offset,
1269
- data,
1270
- [weak_self, block](bool success) {
1271
- auto self = weak_self.lock();
1272
- if (!self) return;
1273
-
1274
- std::lock_guard<std::mutex> lock(self->mutex_);
1275
-
1276
- if (success) {
1277
- LOG_DEBUG("Torrent", "Block written to disk: piece=" + std::to_string(block.piece_index) +
1278
- " offset=" + std::to_string(block.offset));
1279
-
1280
- // Write successful - mark block as finished
1281
- if (self->picker_) {
1282
- bool piece_complete = self->picker_->mark_finished(block);
1283
-
1284
- if (piece_complete) {
1285
- LOG_INFO("Torrent", "Piece " + std::to_string(block.piece_index) +
1286
- " complete (all blocks written), verifying hash");
1287
- self->verify_piece_hash(block.piece_index);
1288
- }
1289
- }
1290
- } else {
1291
- // Handle write error - abort the block so it can be re-requested
1292
- LOG_ERROR("Torrent", "Failed to write block to disk: piece=" +
1293
- std::to_string(block.piece_index) + " offset=" + std::to_string(block.offset));
1294
- if (self->picker_) {
1295
- self->picker_->abort_download(block, nullptr);
1296
- }
1297
- if (self->on_error_) {
1298
- self->on_error_(self.get(), "Failed to write block to disk");
1299
- }
1300
- }
1301
- }
1302
- );
1303
- }
1304
-
1305
- void Torrent::read_piece_from_disk(uint32_t piece, BtPeerConnection* peer,
1306
- uint32_t begin, uint32_t length) {
1307
- if (!info_) return;
1308
-
1309
- LOG_DEBUG("Torrent", "Reading piece " + std::to_string(piece) + " from disk for peer " +
1310
- peer->ip() + " (offset=" + std::to_string(begin) + ", len=" + std::to_string(length) + ")");
1311
-
1312
- auto mappings = get_file_mappings();
1313
- auto weak_self = weak_from_this();
1314
- uint32_t actual_length = info_->piece_size(piece);
1315
-
1316
- DiskIO::instance().async_read_piece(
1317
- config_.save_path,
1318
- mappings,
1319
- piece,
1320
- info_->piece_length(),
1321
- actual_length,
1322
- [weak_self, peer, piece, begin, length](bool success, const std::vector<uint8_t>& data) {
1323
- auto self = weak_self.lock();
1324
- if (!self) return;
1325
-
1326
- if (success && data.size() >= begin + length) {
1327
- // Send the requested block to peer
1328
- peer->send_piece(piece, begin, data.data() + begin, length);
1329
- }
1330
- }
1331
- );
1332
- }
1333
-
1334
- void Torrent::verify_piece_hash(uint32_t piece) {
1335
- // Note: This method may be called with or without mutex held.
1336
- // The async callback will acquire its own lock.
1337
- if (!info_) return;
1338
-
1339
- LOG_DEBUG("Torrent", "Verifying hash for piece " + std::to_string(piece));
1340
-
1341
- auto mappings = get_file_mappings();
1342
- auto weak_self = weak_from_this();
1343
- uint32_t actual_length = info_->piece_size(piece);
1344
-
1345
- DiskIO::instance().async_hash_piece(
1346
- config_.save_path,
1347
- mappings,
1348
- piece,
1349
- info_->piece_length(),
1350
- actual_length,
1351
- [weak_self, piece](bool success, const std::string& calculated_hash) {
1352
- auto self = weak_self.lock();
1353
- if (!self) return;
1354
-
1355
- // Acquire lock for on_piece_verified which modifies shared state
1356
- std::lock_guard<std::mutex> lock(self->mutex_);
1357
-
1358
- if (!success) {
1359
- self->on_piece_verified(piece, false);
1360
- return;
1361
- }
1362
-
1363
- // Compare with expected hash
1364
- auto expected_hash = self->info_->piece_hash(piece);
1365
- std::string expected_hex;
1366
- for (uint8_t b : expected_hash) {
1367
- char hex[3];
1368
- snprintf(hex, sizeof(hex), "%02x", b);
1369
- expected_hex += hex;
1370
- }
1371
-
1372
- bool valid = (calculated_hash == expected_hex);
1373
- self->on_piece_verified(piece, valid);
1374
- }
1375
- );
1376
- }
1377
-
1378
- //=============================================================================
1379
- // Extension Protocol Methods
1380
- //=============================================================================
1381
-
1382
- void Torrent::send_extension_handshake(BtPeerConnection* peer) {
1383
- // Create extension handshake
1384
- BencodeValue handshake = BencodeValue::create_dict();
1385
-
1386
- // Build 'm' dictionary with extension name -> message ID
1387
- BencodeValue m = BencodeValue::create_dict();
1388
- m["ut_metadata"] = BencodeValue(static_cast<int64_t>(BT_EXT_UT_METADATA_ID));
1389
- m["ut_pex"] = BencodeValue(static_cast<int64_t>(BT_EXT_UT_PEX_ID));
1390
- handshake["m"] = m;
1391
-
1392
- // Add metadata_size if we have metadata
1393
- if (info_ && info_->has_metadata()) {
1394
- // We'd need the raw info dict size here
1395
- // For now, skip this - we're mainly interested in receiving metadata
1396
- }
1397
-
1398
- // Add client ID
1399
- handshake["v"] = BencodeValue("librats/1.0");
1400
-
1401
- // Encode and send as extended message ID 0 (handshake)
1402
- auto payload = handshake.encode();
1403
- peer->send_extended(0, payload);
1404
-
1405
- LOG_DEBUG("Torrent", "Sent extension handshake to " + peer->ip());
1406
- }
1407
-
1408
- void Torrent::on_extension_message(BtPeerConnection* peer, uint8_t ext_id,
1409
- const std::vector<uint8_t>& payload) {
1410
- LOG_DEBUG("Torrent", "on_extension_message: ext_id=" + std::to_string(ext_id) +
1411
- " payload=" + std::to_string(payload.size()) + " bytes from " + peer->ip() +
1412
- " (state=" + std::string(torrent_state_to_string(state_)) + ")");
1413
-
1414
- if (ext_id == 0) {
1415
- // Extension handshake
1416
- LOG_DEBUG("Torrent", "Processing extension handshake from " + peer->ip());
1417
- on_extension_handshake(peer, payload);
1418
- } else if (ext_id == BT_EXT_UT_METADATA_ID) {
1419
- // ut_metadata message (our local ID = 1)
1420
- if (state_ == TorrentState::DownloadingMetadata) {
1421
- LOG_DEBUG("Torrent", "Processing ut_metadata message from " + peer->ip());
1422
- on_metadata_message(peer, payload);
1423
- } else {
1424
- LOG_DEBUG("Torrent", "Ignoring ut_metadata message (already have metadata)");
1425
- }
1426
- } else if (ext_id == BT_EXT_UT_PEX_ID) {
1427
- // ut_pex message (our local ID = 2)
1428
- LOG_DEBUG("Torrent", "Received ut_pex message from " + peer->ip() + " (not implemented)");
1429
- // TODO: Handle PEX for peer discovery
1430
- } else {
1431
- LOG_DEBUG("Torrent", "Unknown extension message ext_id=" + std::to_string(ext_id) +
1432
- " from " + peer->ip());
1433
- }
1434
- }
1435
-
1436
- void Torrent::on_extension_handshake(BtPeerConnection* peer,
1437
- const std::vector<uint8_t>& payload) {
1438
- BencodeValue decoded;
1439
- try {
1440
- decoded = BencodeDecoder::decode(payload);
1441
- } catch (...) {
1442
- LOG_WARN("Torrent", "Failed to decode extension handshake from " + peer->ip());
1443
- return;
1444
- }
1445
-
1446
- if (!decoded.is_dict()) {
1447
- return;
1448
- }
1449
-
1450
- const auto& dict = decoded.as_dict();
1451
-
1452
- // Extract metadata_size if present (for magnet links)
1453
- auto metadata_size_it = dict.find("metadata_size");
1454
- if (metadata_size_it != dict.end() && metadata_size_it->second.is_integer()) {
1455
- size_t metadata_size = static_cast<size_t>(metadata_size_it->second.as_integer());
1456
- LOG_INFO("Torrent", "Peer " + peer->ip() + " has metadata, size=" +
1457
- std::to_string(metadata_size));
1458
-
1459
- // If we're downloading metadata, store this info and start requesting
1460
- if (state_ == TorrentState::DownloadingMetadata && metadata_size > 0) {
1461
- // Store metadata size for this peer
1462
- peer_metadata_size_[peer] = metadata_size;
1463
-
1464
- // Parse the peer's ut_metadata ID from 'm' dict
1465
- auto m_it = dict.find("m");
1466
- if (m_it != dict.end() && m_it->second.is_dict()) {
1467
- const auto& m = m_it->second.as_dict();
1468
- auto ut_meta_it = m.find("ut_metadata");
1469
- if (ut_meta_it != m.end() && ut_meta_it->second.is_integer()) {
1470
- uint8_t peer_ut_metadata_id = static_cast<uint8_t>(ut_meta_it->second.as_integer());
1471
- peer_ut_metadata_id_[peer] = peer_ut_metadata_id;
1472
-
1473
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " ut_metadata ID = " +
1474
- std::to_string(peer_ut_metadata_id));
1475
-
1476
- // Start requesting metadata pieces
1477
- request_metadata(peer);
1478
- }
1479
- }
1480
- }
1481
- }
1482
-
1483
- LOG_DEBUG("Torrent", "Received extension handshake from " + peer->ip());
1484
- }
1485
-
1486
- void Torrent::request_metadata(BtPeerConnection* peer) {
1487
- if (has_metadata_unlocked()) {
1488
- LOG_DEBUG("Torrent", "request_metadata: already have metadata, skipping");
1489
- return; // Already have metadata
1490
- }
1491
-
1492
- auto size_it = peer_metadata_size_.find(peer);
1493
- auto id_it = peer_ut_metadata_id_.find(peer);
1494
-
1495
- if (size_it == peer_metadata_size_.end() || id_it == peer_ut_metadata_id_.end()) {
1496
- LOG_DEBUG("Torrent", "request_metadata: no metadata info for peer " + peer->ip() +
1497
- " (size_found=" + (size_it != peer_metadata_size_.end() ? "yes" : "no") +
1498
- ", id_found=" + (id_it != peer_ut_metadata_id_.end() ? "yes" : "no") + ")");
1499
- return; // Don't have peer's metadata info
1500
- }
1501
-
1502
- size_t metadata_size = size_it->second;
1503
- uint8_t peer_ut_id = id_it->second;
1504
-
1505
- // Calculate number of pieces (16KB each)
1506
- uint32_t num_pieces = static_cast<uint32_t>((metadata_size + BT_METADATA_PIECE_SIZE - 1) / BT_METADATA_PIECE_SIZE);
1507
-
1508
- // Initialize metadata buffer if needed
1509
- if (metadata_buffer_.empty()) {
1510
- metadata_buffer_.resize(metadata_size);
1511
- metadata_pieces_received_.resize(num_pieces, false);
1512
- metadata_pieces_requested_.resize(num_pieces, false);
1513
- }
1514
-
1515
- // Request pieces we don't have yet (pipeline up to 4 requests)
1516
- int requests_sent = 0;
1517
- const int max_pending_requests = 4;
1518
-
1519
- for (uint32_t piece = 0; piece < num_pieces; ++piece) {
1520
- if (!metadata_pieces_received_[piece] && !metadata_pieces_requested_[piece]) {
1521
- // Create request message
1522
- BencodeValue req = BencodeValue::create_dict();
1523
- req["msg_type"] = BencodeValue(static_cast<int64_t>(0)); // Request
1524
- req["piece"] = BencodeValue(static_cast<int64_t>(piece));
1525
-
1526
- auto req_payload = req.encode();
1527
- peer->send_extended(peer_ut_id, req_payload);
1528
- metadata_pieces_requested_[piece] = true;
1529
-
1530
- LOG_DEBUG("Torrent", "Requesting metadata piece " + std::to_string(piece) +
1531
- " from " + peer->ip());
1532
- if (++requests_sent >= max_pending_requests) break;
1533
- }
1534
- }
1535
- }
1536
-
1537
- void Torrent::on_metadata_message(BtPeerConnection* peer,
1538
- const std::vector<uint8_t>& payload) {
1539
- LOG_DEBUG("Torrent", "on_metadata_message: " + std::to_string(payload.size()) +
1540
- " bytes from " + peer->ip());
1541
-
1542
- // Parse the bencoded message
1543
- BencodeValue decoded;
1544
- try {
1545
- decoded = BencodeDecoder::decode(payload);
1546
- } catch (...) {
1547
- LOG_WARN("Torrent", "Failed to decode metadata message from " + peer->ip());
1548
- return;
1549
- }
1550
-
1551
- if (!decoded.is_dict()) {
1552
- LOG_WARN("Torrent", "Metadata message is not a dict from " + peer->ip());
1553
- return;
1554
- }
1555
-
1556
- const auto& dict = decoded.as_dict();
1557
-
1558
- auto msg_type_it = dict.find("msg_type");
1559
- if (msg_type_it == dict.end() || !msg_type_it->second.is_integer()) {
1560
- LOG_WARN("Torrent", "Metadata message missing msg_type from " + peer->ip());
1561
- return;
1562
- }
1563
-
1564
- int msg_type = static_cast<int>(msg_type_it->second.as_integer());
1565
- LOG_DEBUG("Torrent", "on_metadata_message: msg_type=" + std::to_string(msg_type) +
1566
- " (0=request, 1=data, 2=reject) from " + peer->ip());
1567
-
1568
- if (msg_type == 1) { // Data
1569
- auto piece_it = dict.find("piece");
1570
- if (piece_it == dict.end() || !piece_it->second.is_integer()) {
1571
- return;
1572
- }
1573
-
1574
- uint32_t piece = static_cast<uint32_t>(piece_it->second.as_integer());
1575
-
1576
- // Find where the data starts (after the bencoded dict)
1577
- size_t dict_end = find_bencode_end(payload);
1578
- if (dict_end >= payload.size()) {
1579
- return;
1580
- }
1581
-
1582
- // Copy data to buffer
1583
- size_t offset = static_cast<size_t>(piece) * BT_METADATA_PIECE_SIZE;
1584
- size_t data_size = payload.size() - dict_end;
1585
-
1586
- if (offset + data_size <= metadata_buffer_.size()) {
1587
- std::memcpy(metadata_buffer_.data() + offset, payload.data() + dict_end, data_size);
1588
-
1589
- if (piece < metadata_pieces_received_.size()) {
1590
- metadata_pieces_received_[piece] = true;
1591
- metadata_pieces_requested_[piece] = false;
1592
- }
1593
-
1594
- LOG_DEBUG("Torrent", "Received metadata piece " + std::to_string(piece) +
1595
- " (" + std::to_string(data_size) + " bytes)");
1596
-
1597
- // Check if we have all pieces
1598
- bool complete = true;
1599
- for (bool received : metadata_pieces_received_) {
1600
- if (!received) {
1601
- complete = false;
1602
- break;
1603
- }
1604
- }
1605
-
1606
- if (complete) {
1607
- on_metadata_complete();
1608
- } else {
1609
- // Request next piece
1610
- request_metadata(peer);
1611
- }
1612
- }
1613
- } else if (msg_type == 2) { // Reject
1614
- LOG_DEBUG("Torrent", "Peer " + peer->ip() + " rejected metadata request");
1615
- }
1616
- }
1617
-
1618
- size_t Torrent::find_bencode_end(const std::vector<uint8_t>& data) {
1619
- // Find the end of a bencoded dictionary
1620
- int depth = 0;
1621
- for (size_t i = 0; i < data.size(); ++i) {
1622
- if (data[i] == 'd' || data[i] == 'l') {
1623
- ++depth;
1624
- } else if (data[i] == 'e') {
1625
- --depth;
1626
- if (depth == 0) {
1627
- return i + 1;
1628
- }
1629
- } else if (std::isdigit(data[i])) {
1630
- // Skip string
1631
- size_t len = 0;
1632
- while (i < data.size() && std::isdigit(data[i])) {
1633
- len = len * 10 + (data[i] - '0');
1634
- ++i;
1635
- }
1636
- if (i < data.size() && data[i] == ':') {
1637
- i += len; // Skip the string content
1638
- }
1639
- } else if (data[i] == 'i') {
1640
- // Skip integer
1641
- while (i < data.size() && data[i] != 'e') {
1642
- ++i;
1643
- }
1644
- }
1645
- }
1646
- return data.size();
1647
- }
1648
-
1649
- void Torrent::on_metadata_complete() {
1650
- LOG_INFO("Torrent", "Metadata download complete!");
1651
-
1652
- // Validate and set metadata
1653
- if (set_metadata(metadata_buffer_)) {
1654
- LOG_INFO("Torrent", "Metadata validated, starting download of " + name_);
1655
-
1656
- // Clear metadata tracking data
1657
- metadata_buffer_.clear();
1658
- metadata_pieces_received_.clear();
1659
- metadata_pieces_requested_.clear();
1660
- peer_metadata_size_.clear();
1661
- peer_ut_metadata_id_.clear();
1662
-
1663
- // State transition happens in set_metadata()
1664
-
1665
- // Notify callback that metadata was received successfully
1666
- if (on_metadata_received_) {
1667
- on_metadata_received_(this, true);
1668
- }
1669
-
1670
- // Extended callback with TorrentInfo (rats-search compatibility)
1671
- if (on_metadata_complete_ && info_) {
1672
- on_metadata_complete_(*info_);
1673
- }
1674
- } else {
1675
- LOG_ERROR("Torrent", "Metadata validation failed");
1676
-
1677
- // Notify callback of failure
1678
- if (on_metadata_received_) {
1679
- on_metadata_received_(this, false);
1680
- }
1681
-
1682
- // Could retry from other peers
1683
- metadata_buffer_.clear();
1684
- metadata_pieces_received_.clear();
1685
- metadata_pieces_requested_.clear();
1686
- }
1687
- }
1688
-
1689
- //=============================================================================
1690
- // Resume Data Methods
1691
- //=============================================================================
1692
-
1693
- TorrentResumeData Torrent::generate_resume_data() const {
1694
- std::lock_guard<std::mutex> lock(mutex_);
1695
-
1696
- TorrentResumeData data;
1697
-
1698
- // Identification
1699
- data.info_hash = info_hash_;
1700
- data.name = name_;
1701
- data.save_path = config_.save_path;
1702
-
1703
- // Have pieces
1704
- if (picker_) {
1705
- data.have_pieces = picker_->get_have_bitfield();
1706
- } else {
1707
- data.have_pieces = have_pieces_;
1708
- }
1709
-
1710
- // Unfinished pieces (partial downloads)
1711
- if (picker_) {
1712
- auto downloading = picker_->downloading_pieces();
1713
- for (const auto& dp : downloading) {
1714
- if (dp.blocks_finished > 0 && dp.blocks_finished < dp.blocks_total) {
1715
- // This piece has some but not all blocks - save state
1716
- Bitfield blocks(dp.blocks_total);
1717
-
1718
- // Get block states from piece picker
1719
- uint32_t piece_len = info_ ? info_->piece_length() : 16384;
1720
- uint32_t block_size = BT_BLOCK_SIZE;
1721
-
1722
- for (uint32_t i = 0; i < dp.blocks_total; ++i) {
1723
- BlockInfo block(dp.piece_index, i * block_size,
1724
- (std::min)(block_size, piece_len - i * block_size));
1725
- BlockState state = picker_->block_state(block);
1726
- if (state == BlockState::Finished) {
1727
- blocks.set_bit(i);
1728
- }
1729
- }
1730
-
1731
- data.unfinished_pieces[dp.piece_index] = blocks;
1732
- }
1733
- }
1734
- }
1735
-
1736
- // Statistics
1737
- data.total_uploaded = stats_.total_uploaded;
1738
- data.total_downloaded = stats_.total_downloaded;
1739
- data.active_time = active_time_;
1740
- data.seeding_time = seeding_time_;
1741
- data.added_time = added_time_;
1742
- data.completed_time = completed_time_;
1743
-
1744
- // Configuration
1745
- data.sequential_download = config_.sequential_download;
1746
- data.max_connections = static_cast<int>(config_.max_connections);
1747
- data.max_uploads = static_cast<int>(config_.max_uploads);
1748
- data.download_limit = static_cast<int64_t>(config_.download_limit);
1749
- data.upload_limit = static_cast<int64_t>(config_.upload_limit);
1750
-
1751
- // Peer cache (save connected peers for quick reconnection)
1752
- for (const auto& conn : connections_) {
1753
- if (conn->is_connected()) {
1754
- data.peers.emplace_back(conn->ip(), conn->port());
1755
- }
1756
- }
1757
-
1758
- // Save info_dict (metadata) so we don't need to re-download it on restore
1759
- // This is critical for seeding torrents - without it, the torrent would
1760
- // enter DownloadingMetadata state and show 0% until metadata is re-fetched
1761
- if (info_ && info_->has_metadata()) {
1762
- const auto& info_bytes = info_->info_dict_bytes();
1763
- if (!info_bytes.empty()) {
1764
- data.info_dict = info_bytes;
1765
- LOG_DEBUG("Torrent", "Saving info_dict in resume data: " +
1766
- std::to_string(info_bytes.size()) + " bytes");
1767
- }
1768
- }
1769
-
1770
- LOG_DEBUG("Torrent", "Generated resume data: " + std::to_string(data.have_pieces.count()) +
1771
- "/" + std::to_string(data.have_pieces.size()) + " pieces, " +
1772
- std::to_string(data.unfinished_pieces.size()) + " partial");
1773
-
1774
- return data;
1775
- }
1776
-
1777
- bool Torrent::save_resume_data() {
1778
- std::string path;
1779
- {
1780
- std::lock_guard<std::mutex> lock(mutex_);
1781
- path = get_resume_file_path(config_.get_resume_path(), info_hash_);
1782
- }
1783
- return save_resume_data(path);
1784
- }
1785
-
1786
- bool Torrent::save_resume_data(const std::string& path) {
1787
- auto data = generate_resume_data();
1788
-
1789
- if (!write_resume_data_file(data, path)) {
1790
- LOG_ERROR("Torrent", "Failed to save resume data to " + path);
1791
- return false;
1792
- }
1793
-
1794
- LOG_INFO("Torrent", "Saved resume data for '" + name_ + "' to " + path);
1795
- return true;
1796
- }
1797
-
1798
- bool Torrent::load_resume_data(const TorrentResumeData& resume_data) {
1799
- std::lock_guard<std::mutex> lock(mutex_);
1800
-
1801
- // Verify info hash matches
1802
- if (resume_data.info_hash != info_hash_) {
1803
- LOG_ERROR("Torrent", "Resume data info_hash mismatch");
1804
- return false;
1805
- }
1806
-
1807
- // If we don't have metadata but resume data has info_dict, restore it first
1808
- // This is critical for seeding torrents - without metadata, torrent shows 0%
1809
- if (!has_metadata_unlocked() && !resume_data.info_dict.empty()) {
1810
- LOG_INFO("Torrent", "Restoring metadata from resume data (" +
1811
- std::to_string(resume_data.info_dict.size()) + " bytes)");
1812
-
1813
- auto new_info = TorrentInfo::from_info_dict(resume_data.info_dict, info_hash_);
1814
- if (new_info) {
1815
- info_ = std::make_unique<TorrentInfo>(*new_info);
1816
- name_ = info_->name();
1817
-
1818
- // Initialize picker now that we have metadata
1819
- have_pieces_ = Bitfield(info_->num_pieces());
1820
- uint32_t last_piece_len = info_->piece_size(info_->num_pieces() - 1);
1821
- picker_ = std::make_unique<PiecePicker>(
1822
- info_->num_pieces(),
1823
- info_->piece_length(),
1824
- last_piece_len
1825
- );
1826
-
1827
- if (config_.sequential_download) {
1828
- picker_->set_mode(PickerMode::Sequential);
1829
- }
1830
-
1831
- stats_.total_size = info_->total_size();
1832
- stats_.pieces_total = info_->num_pieces();
1833
-
1834
- LOG_INFO("Torrent", "Metadata restored from resume data: '" + name_ +
1835
- "', " + std::to_string(info_->num_pieces()) + " pieces");
1836
- } else {
1837
- LOG_WARN("Torrent", "Failed to parse info_dict from resume data");
1838
- }
1839
- }
1840
-
1841
- // Apply have pieces
1842
- if (resume_data.have_pieces.size() > 0) {
1843
- if (picker_) {
1844
- // Update picker with have pieces
1845
- for (uint32_t i = 0; i < resume_data.have_pieces.size(); ++i) {
1846
- if (resume_data.have_pieces.get_bit(i)) {
1847
- picker_->mark_have(i);
1848
- }
1849
- }
1850
- have_pieces_ = picker_->get_have_bitfield();
1851
- } else {
1852
- // No picker yet (metadata not received), store for later
1853
- // When metadata arrives, set_metadata() will use this
1854
- have_pieces_ = resume_data.have_pieces;
1855
- LOG_DEBUG("Torrent", "Stored " + std::to_string(resume_data.have_pieces.count()) +
1856
- " pieces for when metadata arrives");
1857
- }
1858
-
1859
- LOG_INFO("Torrent", "Loaded resume data: " +
1860
- std::to_string(resume_data.have_pieces.count()) + " pieces");
1861
- }
1862
-
1863
- // Apply unfinished pieces
1864
- // NOTE: For now, we don't restore partial pieces - they will be re-downloaded
1865
- // This is safer than trying to restore partial block state
1866
- // A future optimization could verify the existing blocks on disk
1867
- if (!resume_data.unfinished_pieces.empty()) {
1868
- LOG_DEBUG("Torrent", "Resume data has " +
1869
- std::to_string(resume_data.unfinished_pieces.size()) +
1870
- " partial pieces (will be re-downloaded)");
1871
- }
1872
-
1873
- // Restore statistics
1874
- stats_.total_uploaded = resume_data.total_uploaded;
1875
- stats_.total_downloaded = resume_data.total_downloaded;
1876
- active_time_ = resume_data.active_time;
1877
- seeding_time_ = resume_data.seeding_time;
1878
- added_time_ = resume_data.added_time;
1879
- completed_time_ = resume_data.completed_time;
1880
-
1881
- // Restore configuration
1882
- if (resume_data.max_connections > 0) {
1883
- config_.max_connections = static_cast<size_t>(resume_data.max_connections);
1884
- }
1885
- if (resume_data.max_uploads > 0) {
1886
- config_.max_uploads = static_cast<size_t>(resume_data.max_uploads);
1887
- }
1888
- if (resume_data.download_limit > 0) {
1889
- config_.download_limit = static_cast<uint64_t>(resume_data.download_limit);
1890
- }
1891
- if (resume_data.upload_limit > 0) {
1892
- config_.upload_limit = static_cast<uint64_t>(resume_data.upload_limit);
1893
- }
1894
- config_.sequential_download = resume_data.sequential_download;
1895
- if (picker_ && config_.sequential_download) {
1896
- picker_->set_mode(PickerMode::Sequential);
1897
- }
1898
-
1899
- // Add cached peers
1900
- for (const auto& [ip, port] : resume_data.peers) {
1901
- pending_peers_.emplace_back(ip, port);
1902
- }
1903
-
1904
- // Update stats
1905
- if (picker_) {
1906
- stats_.pieces_done = picker_->num_have();
1907
- stats_.bytes_done = 0;
1908
- if (info_) {
1909
- // Loop through ALL pieces and check if we have each one
1910
- for (uint32_t i = 0; i < have_pieces_.size(); ++i) {
1911
- if (have_pieces_.get_bit(i)) {
1912
- stats_.bytes_done += info_->piece_size(i);
1913
- }
1914
- }
1915
- }
1916
- stats_.progress = static_cast<float>(stats_.pieces_done) /
1917
- static_cast<float>(stats_.pieces_total);
1918
- LOG_INFO("Torrent", "Resume data applied: " + std::to_string(stats_.pieces_done) +
1919
- "/" + std::to_string(stats_.pieces_total) + " pieces (" +
1920
- std::to_string(stats_.bytes_done) + " bytes)");
1921
- }
1922
-
1923
- return true;
1924
- }
1925
-
1926
- bool Torrent::try_load_resume_data() {
1927
- std::string path;
1928
- {
1929
- std::lock_guard<std::mutex> lock(mutex_);
1930
- path = get_resume_file_path(config_.get_resume_path(), info_hash_);
1931
- }
1932
-
1933
- std::string error;
1934
- auto resume_data = read_resume_data_file(path, &error);
1935
-
1936
- if (!resume_data) {
1937
- LOG_DEBUG("Torrent", "No resume data found for '" + name_ + "': " + error);
1938
- return false;
1939
- }
1940
-
1941
- return load_resume_data(*resume_data);
1942
- }
1943
-
1944
- //=============================================================================
1945
- // File Verification (Recheck)
1946
- //=============================================================================
1947
-
1948
- void Torrent::verify_piece_hash_sync(uint32_t piece, bool& valid) {
1949
- // Note: This reads the piece synchronously and computes hash
1950
- // For use during file checking, not during normal download
1951
- if (!info_) {
1952
- valid = false;
1953
- return;
1954
- }
1955
-
1956
- uint32_t actual_length = info_->piece_size(piece);
1957
- auto mappings = get_file_mappings();
1958
-
1959
- // Calculate piece offset in torrent
1960
- uint64_t piece_offset = static_cast<uint64_t>(piece) * info_->piece_length();
1961
-
1962
- // Read the piece data
1963
- std::vector<uint8_t> piece_data(actual_length);
1964
- size_t bytes_read = 0;
1965
-
1966
- // Map to files and read
1967
- for (const auto& mapping : mappings) {
1968
- if (bytes_read >= actual_length) break;
1969
-
1970
- // Check if this file contains part of our piece
1971
- uint64_t file_start = mapping.torrent_offset;
1972
- uint64_t file_end = file_start + mapping.length;
1973
-
1974
- if (piece_offset + bytes_read >= file_end) continue;
1975
- if (piece_offset + actual_length <= file_start) continue;
1976
-
1977
- // Calculate read range within this file
1978
- uint64_t read_start = 0;
1979
- uint64_t read_offset_in_file = 0;
1980
-
1981
- if (piece_offset + bytes_read >= file_start) {
1982
- read_offset_in_file = piece_offset + bytes_read - file_start;
1983
- } else {
1984
- read_start = file_start - piece_offset;
1985
- }
1986
-
1987
- size_t read_len = static_cast<size_t>(
1988
- (std::min)(static_cast<uint64_t>(actual_length - bytes_read),
1989
- file_end - (file_start + read_offset_in_file)));
1990
-
1991
- std::string file_path = combine_paths(config_.save_path, mapping.path);
1992
-
1993
- if (!file_exists(file_path)) {
1994
- valid = false;
1995
- return;
1996
- }
1997
-
1998
- if (!read_file_chunk(file_path, read_offset_in_file,
1999
- piece_data.data() + read_start, read_len)) {
2000
- valid = false;
2001
- return;
2002
- }
2003
-
2004
- bytes_read += read_len;
2005
- }
2006
-
2007
- if (bytes_read < actual_length) {
2008
- valid = false;
2009
- return;
2010
- }
2011
-
2012
- // Compute SHA1 hash
2013
- SHA1 sha1;
2014
- sha1.update(piece_data.data(), piece_data.size());
2015
- std::string calculated_hash = sha1.finalize();
2016
-
2017
- // Compare with expected hash
2018
- auto expected_hash = info_->piece_hash(piece);
2019
- std::string expected_hex;
2020
- for (uint8_t b : expected_hash) {
2021
- char hex[3];
2022
- snprintf(hex, sizeof(hex), "%02x", b);
2023
- expected_hex += hex;
2024
- }
2025
-
2026
- valid = (calculated_hash == expected_hex);
2027
- }
2028
-
2029
- void Torrent::check_files(std::function<void(uint32_t, uint32_t)> progress_callback) {
2030
- std::lock_guard<std::mutex> lock(mutex_);
2031
-
2032
- if (!info_ || !picker_) {
2033
- LOG_WARN("Torrent", "Cannot check files - no metadata");
2034
- return;
2035
- }
2036
-
2037
- LOG_INFO("Torrent", "Checking files for '" + name_ + "'...");
2038
-
2039
- uint32_t num_pieces = info_->num_pieces();
2040
- uint32_t pieces_have = 0;
2041
-
2042
- // Reset bitfield
2043
- have_pieces_ = Bitfield(num_pieces);
2044
- picker_->set_have_bitfield(have_pieces_);
2045
-
2046
- for (uint32_t i = 0; i < num_pieces; ++i) {
2047
- bool valid = false;
2048
- verify_piece_hash_sync(i, valid);
2049
-
2050
- if (valid) {
2051
- picker_->mark_have(i);
2052
- have_pieces_.set_bit(i);
2053
- ++pieces_have;
2054
- }
2055
-
2056
- if (progress_callback) {
2057
- progress_callback(i + 1, num_pieces);
2058
- }
2059
- }
2060
-
2061
- // Update stats
2062
- stats_.pieces_done = pieces_have;
2063
- stats_.bytes_done = 0;
2064
- for (uint32_t i = 0; i < num_pieces; ++i) {
2065
- if (have_pieces_.get_bit(i)) {
2066
- stats_.bytes_done += info_->piece_size(i);
2067
- }
2068
- }
2069
- stats_.progress = static_cast<float>(pieces_have) / static_cast<float>(num_pieces);
2070
-
2071
- LOG_INFO("Torrent", "File check complete: " + std::to_string(pieces_have) +
2072
- "/" + std::to_string(num_pieces) + " pieces verified");
2073
-
2074
- // If complete, switch to seeding
2075
- if (picker_->is_complete()) {
2076
- set_state(TorrentState::Seeding);
2077
- choker_.set_seed_mode(true);
2078
- }
2079
- }
2080
-
2081
- void Torrent::check_files_async(
2082
- std::function<void(uint32_t, uint32_t)> completion_callback,
2083
- std::function<void(uint32_t, uint32_t)> progress_callback) {
2084
-
2085
- // Run file check in a separate thread
2086
- auto weak_self = weak_from_this();
2087
-
2088
- std::thread([weak_self, completion_callback, progress_callback]() {
2089
- auto self = weak_self.lock();
2090
- if (!self) return;
2091
-
2092
- self->check_files(progress_callback);
2093
-
2094
- uint32_t have = 0, total = 0;
2095
- {
2096
- std::lock_guard<std::mutex> lock(self->mutex_);
2097
- have = self->stats_.pieces_done;
2098
- total = self->stats_.pieces_total;
2099
- }
2100
-
2101
- if (completion_callback) {
2102
- completion_callback(have, total);
2103
- }
2104
- }).detach();
2105
- }
2106
-
2107
- void Torrent::on_file_check_complete(uint32_t pieces_have) {
2108
- std::lock_guard<std::mutex> lock(mutex_);
2109
-
2110
- LOG_INFO("Torrent", "File check complete: " + std::to_string(pieces_have) + " pieces");
2111
-
2112
- if (picker_ && picker_->is_complete()) {
2113
- set_state(TorrentState::Seeding);
2114
- choker_.set_seed_mode(true);
2115
- } else if (state_ == TorrentState::CheckingFiles) {
2116
- set_state(TorrentState::Downloading);
2117
- }
2118
- }
2119
-
2120
- } // namespace librats