librats 1.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/README.md +145 -331
  2. package/binding.gyp +16 -3
  3. package/lib/index.d.ts +288 -696
  4. package/lib/index.js +407 -44
  5. package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
  6. package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
  7. package/native-src/CMakeLists.txt +404 -179
  8. package/native-src/LICENSE +1 -1
  9. package/native-src/src/librats/bindings/rats.cpp +762 -0
  10. package/native-src/src/librats/bindings/rats.h +380 -0
  11. package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
  12. package/native-src/src/librats/bittorrent/bencode.h +176 -0
  13. package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
  14. package/native-src/src/librats/bittorrent/bitfield.h +76 -0
  15. package/native-src/src/librats/bittorrent/byte_io.h +58 -0
  16. package/native-src/src/librats/bittorrent/choker.cpp +25 -0
  17. package/native-src/src/librats/bittorrent/choker.h +46 -0
  18. package/native-src/src/librats/bittorrent/client.cpp +413 -0
  19. package/native-src/src/librats/bittorrent/client.h +227 -0
  20. package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
  21. package/native-src/src/librats/bittorrent/disk_io.h +150 -0
  22. package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
  23. package/native-src/src/librats/bittorrent/extensions.h +94 -0
  24. package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
  25. package/native-src/src/librats/bittorrent/file_storage.h +79 -0
  26. package/native-src/src/librats/bittorrent/log.h +42 -0
  27. package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
  28. package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
  29. package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
  30. package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
  31. package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
  32. package/native-src/src/librats/bittorrent/peer_list.h +75 -0
  33. package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
  34. package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
  35. package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
  36. package/native-src/src/librats/bittorrent/reactor.h +89 -0
  37. package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
  38. package/native-src/src/librats/bittorrent/resume_data.h +41 -0
  39. package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
  40. package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
  41. package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
  42. package/native-src/src/librats/bittorrent/torrent.h +260 -0
  43. package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
  44. package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
  45. package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
  46. package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
  47. package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
  48. package/native-src/src/librats/bittorrent/tracker.h +108 -0
  49. package/native-src/src/librats/bittorrent/types.cpp +206 -0
  50. package/native-src/src/librats/bittorrent/types.h +86 -0
  51. package/native-src/src/librats/core/address.cpp +35 -0
  52. package/native-src/src/librats/core/address.h +78 -0
  53. package/native-src/src/librats/core/bytes.h +69 -0
  54. package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
  55. package/native-src/src/librats/core/chained_send_buffer.h +183 -0
  56. package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
  57. package/native-src/src/librats/core/endpoint_parse.h +31 -0
  58. package/native-src/src/librats/core/event_bus.h +70 -0
  59. package/native-src/src/librats/core/host_endpoint.h +56 -0
  60. package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
  61. package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
  62. package/native-src/src/librats/core/ip_address.cpp +120 -0
  63. package/native-src/src/librats/core/ip_address.h +109 -0
  64. package/native-src/src/librats/core/mpsc_queue.h +47 -0
  65. package/native-src/src/librats/core/notifier.h +74 -0
  66. package/native-src/src/librats/core/receive_buffer.cpp +219 -0
  67. package/native-src/src/librats/core/receive_buffer.h +171 -0
  68. package/native-src/src/librats/core/service_registry.h +58 -0
  69. package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
  70. package/native-src/src/librats/core/socket.h +496 -0
  71. package/native-src/src/librats/core/timer_queue.h +105 -0
  72. package/native-src/src/librats/core/types.cpp +43 -0
  73. package/native-src/src/librats/core/types.h +103 -0
  74. package/native-src/src/librats/core/wakeup_pipe.h +83 -0
  75. package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
  76. package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
  77. package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
  78. package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
  79. package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
  80. package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
  81. package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
  82. package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
  83. package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
  84. package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
  85. package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
  86. package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
  87. package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
  88. package/native-src/src/librats/crypto/hkdf.c +266 -0
  89. package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
  90. package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
  91. package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
  92. package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
  93. package/native-src/src/librats/crypto/poly1305.h +37 -0
  94. package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
  95. package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
  96. package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
  97. package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
  98. package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
  99. package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
  100. package/native-src/src/librats/dht/announce.cpp +37 -0
  101. package/native-src/src/librats/dht/announce.h +41 -0
  102. package/native-src/src/librats/dht/bep42.cpp +109 -0
  103. package/native-src/src/librats/dht/bep42.h +48 -0
  104. package/native-src/src/librats/dht/dht.cpp +501 -0
  105. package/native-src/src/librats/dht/dht.h +119 -0
  106. package/native-src/src/librats/dht/dht_runner.cpp +103 -0
  107. package/native-src/src/librats/dht/dht_runner.h +71 -0
  108. package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
  109. package/native-src/src/librats/dht/dos_blocker.h +47 -0
  110. package/native-src/src/librats/dht/find_peers.cpp +52 -0
  111. package/native-src/src/librats/dht/find_peers.h +73 -0
  112. package/native-src/src/librats/dht/id.h +167 -0
  113. package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
  114. package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
  115. package/native-src/src/librats/dht/log.h +38 -0
  116. package/native-src/src/librats/dht/node.cpp +473 -0
  117. package/native-src/src/librats/dht/node.h +164 -0
  118. package/native-src/src/librats/dht/node_entry.h +81 -0
  119. package/native-src/src/librats/dht/observer.h +72 -0
  120. package/native-src/src/librats/dht/persistence.cpp +90 -0
  121. package/native-src/src/librats/dht/persistence.h +32 -0
  122. package/native-src/src/librats/dht/routing_table.cpp +559 -0
  123. package/native-src/src/librats/dht/routing_table.h +185 -0
  124. package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
  125. package/native-src/src/librats/dht/rpc_manager.h +77 -0
  126. package/native-src/src/librats/dht/storage.cpp +92 -0
  127. package/native-src/src/librats/dht/storage.h +74 -0
  128. package/native-src/src/librats/dht/transport.h +27 -0
  129. package/native-src/src/librats/dht/traversal.cpp +326 -0
  130. package/native-src/src/librats/dht/traversal.h +120 -0
  131. package/native-src/src/librats/dht/udp_transport.cpp +49 -0
  132. package/native-src/src/librats/dht/udp_transport.h +51 -0
  133. package/native-src/src/librats/mdns/log.h +22 -0
  134. package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
  135. package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
  136. package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
  137. package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
  138. package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
  139. package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
  140. package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
  141. package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
  142. package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
  143. package/native-src/src/librats/node/circuit_service.h +84 -0
  144. package/native-src/src/librats/node/config.h +110 -0
  145. package/native-src/src/librats/node/dial_service.h +54 -0
  146. package/native-src/src/librats/node/dialer.cpp +264 -0
  147. package/native-src/src/librats/node/dialer.h +188 -0
  148. package/native-src/src/librats/node/host_events.h +26 -0
  149. package/native-src/src/librats/node/identify.cpp +130 -0
  150. package/native-src/src/librats/node/identify.h +71 -0
  151. package/native-src/src/librats/node/nat_status.cpp +103 -0
  152. package/native-src/src/librats/node/nat_status.h +118 -0
  153. package/native-src/src/librats/node/node.cpp +865 -0
  154. package/native-src/src/librats/node/node.h +344 -0
  155. package/native-src/src/librats/node/node_context.h +33 -0
  156. package/native-src/src/librats/node/peer_network.h +91 -0
  157. package/native-src/src/librats/peer/peer.h +49 -0
  158. package/native-src/src/librats/peer/peer_book.cpp +181 -0
  159. package/native-src/src/librats/peer/peer_book.h +88 -0
  160. package/native-src/src/librats/peer/peer_id.cpp +72 -0
  161. package/native-src/src/librats/peer/peer_id.h +62 -0
  162. package/native-src/src/librats/peer/peer_info.h +37 -0
  163. package/native-src/src/librats/peer/peer_table.cpp +170 -0
  164. package/native-src/src/librats/peer/peer_table.h +148 -0
  165. package/native-src/src/librats/security/handshaker.h +66 -0
  166. package/native-src/src/librats/security/identity.h +43 -0
  167. package/native-src/src/librats/security/noise_security.cpp +122 -0
  168. package/native-src/src/librats/security/noise_security.h +37 -0
  169. package/native-src/src/librats/security/plaintext_security.h +106 -0
  170. package/native-src/src/librats/security/session.h +37 -0
  171. package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
  172. package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
  173. package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
  174. package/native-src/src/librats/subsystems/bittorrent.h +136 -0
  175. package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
  176. package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
  177. package/native-src/src/librats/subsystems/dht_service.h +36 -0
  178. package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
  179. package/native-src/src/librats/subsystems/file_transfer.h +367 -0
  180. package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
  181. package/native-src/src/librats/subsystems/hole_punch.h +290 -0
  182. package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
  183. package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
  184. package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
  185. package/native-src/src/librats/subsystems/message_json.cpp +112 -0
  186. package/native-src/src/librats/subsystems/message_json.h +88 -0
  187. package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
  188. package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
  189. package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
  190. package/native-src/src/librats/subsystems/ping_service.h +66 -0
  191. package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
  192. package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
  193. package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
  194. package/native-src/src/librats/subsystems/pubsub.h +175 -0
  195. package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
  196. package/native-src/src/librats/subsystems/reconnection.h +126 -0
  197. package/native-src/src/librats/subsystems/relay.cpp +1142 -0
  198. package/native-src/src/librats/subsystems/relay.h +211 -0
  199. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  200. package/native-src/src/librats/transport/connection.cpp +343 -0
  201. package/native-src/src/librats/transport/connection.h +283 -0
  202. package/native-src/src/librats/transport/link.h +96 -0
  203. package/native-src/src/librats/transport/reactor.cpp +588 -0
  204. package/native-src/src/librats/transport/reactor.h +262 -0
  205. package/native-src/src/librats/transport/reactor_pool.h +81 -0
  206. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  207. package/native-src/src/librats/transport/relay_link.h +303 -0
  208. package/native-src/src/librats/transport/tcp_link.cpp +49 -0
  209. package/native-src/src/librats/transport/tcp_link.h +43 -0
  210. package/native-src/src/librats/transport/udp_mux.cpp +617 -0
  211. package/native-src/src/librats/transport/udp_mux.h +363 -0
  212. package/native-src/src/librats/transport/udp_packet.cpp +121 -0
  213. package/native-src/src/librats/transport/udp_packet.h +190 -0
  214. package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
  215. package/native-src/src/librats/transport/udp_stream.h +614 -0
  216. package/native-src/src/librats/util/features.h.in +51 -0
  217. package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
  218. package/native-src/src/librats/util/fs.h +136 -0
  219. package/native-src/src/librats/util/json.cpp +1002 -0
  220. package/native-src/src/librats/util/json.h +444 -0
  221. package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
  222. package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
  223. package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
  224. package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
  225. package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
  226. package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
  227. package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
  228. package/native-src/src/librats/util/rats_export.h +69 -0
  229. package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
  230. package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
  231. package/native-src/src/librats/wire/frame.cpp +76 -0
  232. package/native-src/src/librats/wire/frame.h +111 -0
  233. package/native-src/src/librats/wire/message_router.cpp +45 -0
  234. package/native-src/src/librats/wire/message_router.h +47 -0
  235. package/package.json +5 -4
  236. package/scripts/build-librats.js +1 -0
  237. package/scripts/postinstall.js +3 -3
  238. package/scripts/prepare-package.js +4 -4
  239. package/scripts/verify-installation.js +63 -105
  240. package/src/librats_node.cpp +1067 -1323
  241. package/native-src/src/bencode.cpp +0 -485
  242. package/native-src/src/bencode.h +0 -145
  243. package/native-src/src/bittorrent.cpp +0 -14
  244. package/native-src/src/bittorrent.h +0 -74
  245. package/native-src/src/bt_bitfield.cpp +0 -372
  246. package/native-src/src/bt_bitfield.h +0 -316
  247. package/native-src/src/bt_choker.cpp +0 -228
  248. package/native-src/src/bt_choker.h +0 -147
  249. package/native-src/src/bt_client.cpp +0 -1047
  250. package/native-src/src/bt_client.h +0 -445
  251. package/native-src/src/bt_create_torrent.cpp +0 -677
  252. package/native-src/src/bt_create_torrent.h +0 -473
  253. package/native-src/src/bt_extension.cpp +0 -469
  254. package/native-src/src/bt_extension.h +0 -309
  255. package/native-src/src/bt_file_storage.cpp +0 -261
  256. package/native-src/src/bt_file_storage.h +0 -298
  257. package/native-src/src/bt_handshake.cpp +0 -134
  258. package/native-src/src/bt_handshake.h +0 -157
  259. package/native-src/src/bt_messages.cpp +0 -364
  260. package/native-src/src/bt_messages.h +0 -324
  261. package/native-src/src/bt_network.cpp +0 -1007
  262. package/native-src/src/bt_network.h +0 -417
  263. package/native-src/src/bt_peer_connection.cpp +0 -742
  264. package/native-src/src/bt_peer_connection.h +0 -592
  265. package/native-src/src/bt_piece_picker.cpp +0 -786
  266. package/native-src/src/bt_piece_picker.h +0 -473
  267. package/native-src/src/bt_resume_data.cpp +0 -410
  268. package/native-src/src/bt_resume_data.h +0 -249
  269. package/native-src/src/bt_torrent.cpp +0 -2120
  270. package/native-src/src/bt_torrent.h +0 -641
  271. package/native-src/src/bt_torrent_info.cpp +0 -659
  272. package/native-src/src/bt_torrent_info.h +0 -418
  273. package/native-src/src/bt_types.h +0 -621
  274. package/native-src/src/chained_send_buffer.cpp +0 -75
  275. package/native-src/src/chained_send_buffer.h +0 -137
  276. package/native-src/src/crypto/hkdf.c +0 -266
  277. package/native-src/src/crypto/poly1305.h +0 -36
  278. package/native-src/src/dht.cpp +0 -3311
  279. package/native-src/src/dht.h +0 -717
  280. package/native-src/src/disk_io.cpp +0 -632
  281. package/native-src/src/disk_io.h +0 -315
  282. package/native-src/src/file_transfer.cpp +0 -1415
  283. package/native-src/src/file_transfer.h +0 -286
  284. package/native-src/src/fs.h +0 -108
  285. package/native-src/src/gossipsub.cpp +0 -1139
  286. package/native-src/src/gossipsub.h +0 -403
  287. package/native-src/src/ice.cpp +0 -893
  288. package/native-src/src/ice.h +0 -559
  289. package/native-src/src/json.hpp +0 -25526
  290. package/native-src/src/librats.cpp +0 -2378
  291. package/native-src/src/librats.h +0 -2324
  292. package/native-src/src/librats_bittorrent.cpp +0 -601
  293. package/native-src/src/librats_c.cpp +0 -1557
  294. package/native-src/src/librats_c.h +0 -323
  295. package/native-src/src/librats_discovery.cpp +0 -402
  296. package/native-src/src/librats_encryption.cpp +0 -275
  297. package/native-src/src/librats_file_transfer.cpp +0 -144
  298. package/native-src/src/librats_gossipsub.cpp +0 -289
  299. package/native-src/src/librats_ice.cpp +0 -213
  300. package/native-src/src/librats_log_macros.h +0 -36
  301. package/native-src/src/librats_logging.cpp +0 -173
  302. package/native-src/src/librats_mdns.cpp +0 -166
  303. package/native-src/src/librats_persistence.cpp +0 -796
  304. package/native-src/src/librats_portmap.cpp +0 -419
  305. package/native-src/src/librats_reconnection.cpp +0 -218
  306. package/native-src/src/librats_statistic.cpp +0 -105
  307. package/native-src/src/librats_storage.cpp +0 -189
  308. package/native-src/src/rats_export.h +0 -17
  309. package/native-src/src/receive_buffer.cpp +0 -82
  310. package/native-src/src/receive_buffer.h +0 -127
  311. package/native-src/src/socket.h +0 -228
  312. package/native-src/src/threadmanager.cpp +0 -105
  313. package/native-src/src/threadmanager.h +0 -53
  314. package/native-src/src/tracker.cpp +0 -1264
  315. package/native-src/src/tracker.h +0 -319
  316. package/native-src/src/turn.cpp +0 -762
  317. package/native-src/src/turn.h +0 -460
  318. package/native-src/src/wakeup_pipe.h +0 -60
  319. /package/native-src/src/{os.h → librats/util/os.h} +0 -0
@@ -0,0 +1,972 @@
1
+ #include "librats/subsystems/file_transfer.h"
2
+ #include "librats/node/node_context.h"
3
+ #include "librats/util/fs.h"
4
+ #include "librats/util/logger.h"
5
+
6
+ #include <algorithm>
7
+ #include <cstring>
8
+ #include <vector>
9
+
10
+ namespace librats {
11
+
12
+ namespace {
13
+
14
+ enum : uint8_t {
15
+ OP_OFFER = 1, OP_RESPONSE = 2, OP_CHUNK = 3, OP_FILE_END = 4,
16
+ OP_PROGRESS = 5, OP_COMPLETE = 6, OP_CANCEL = 7, OP_PAUSE = 8, OP_RESUME = 9,
17
+ };
18
+
19
+ void put_u16(Bytes& b, uint16_t v) { b.push_back(v >> 8); b.push_back(v & 0xFF); }
20
+ void put_u32(Bytes& b, uint32_t v) { for (int i = 3; i >= 0; --i) b.push_back((v >> (i * 8)) & 0xFF); }
21
+ void put_u64(Bytes& b, uint64_t v) { for (int i = 7; i >= 0; --i) b.push_back((v >> (i * 8)) & 0xFF); }
22
+
23
+ struct Reader {
24
+ const uint8_t* p;
25
+ const uint8_t* end;
26
+ bool ok = true;
27
+ uint8_t u8() { if (p >= end) { ok = false; return 0; } return *p++; }
28
+ uint16_t u16() { if (end - p < 2) { ok = false; return 0; } uint16_t v = (uint16_t(p[0]) << 8) | p[1]; p += 2; return v; }
29
+ uint32_t u32() { if (end - p < 4) { ok = false; return 0; } uint32_t v = 0; for (int i = 0; i < 4; ++i) v = (v << 8) | *p++; return v; }
30
+ uint64_t u64() { if (end - p < 8) { ok = false; return 0; } uint64_t v = 0; for (int i = 0; i < 8; ++i) v = (v << 8) | *p++; return v; }
31
+ ByteView bytes(size_t n) { if (size_t(end - p) < n) { ok = false; return {}; } ByteView v(p, n); p += n; return v; }
32
+ ByteView rest() { ByteView v(p, size_t(end - p)); p = end; return v; }
33
+ size_t remaining() const { return size_t(end - p); }
34
+ };
35
+
36
+ // Smallest possible OFFER manifest entry on the wire: [path_len:u16][size:u64]
37
+ // with a zero-length path.
38
+ constexpr uint64_t kMinManifestEntry = 2 + 8;
39
+
40
+ // Recursively collect regular files under abs_dir, recording POSIX relative paths.
41
+ void scan_directory(const std::string& abs_dir, const std::string& rel_prefix,
42
+ std::vector<FileTransfer::FileEntry>& files, std::vector<std::string>& sources) {
43
+ std::vector<DirectoryEntry> entries;
44
+ if (!list_directory(abs_dir.c_str(), entries)) return;
45
+ for (const auto& e : entries) {
46
+ const std::string rel = rel_prefix.empty() ? e.name : rel_prefix + "/" + e.name;
47
+ if (e.is_directory) {
48
+ scan_directory(e.path, rel, files, sources);
49
+ } else {
50
+ const int64_t sz = get_file_size(e.path.c_str());
51
+ files.push_back({rel, sz > 0 ? static_cast<uint64_t>(sz) : 0});
52
+ sources.push_back(e.path);
53
+ }
54
+ }
55
+ }
56
+
57
+ } // namespace
58
+
59
+ // A relative path coming from a peer must not escape the destination directory.
60
+ bool FileTransfer::is_safe_relative_path(const std::string& p) {
61
+ if (p.empty()) return false;
62
+ if (p.front() == '/' || p.front() == '\\') return false;
63
+ if (p.size() >= 2 && p[1] == ':') return false; // Windows drive letter
64
+ size_t start = 0;
65
+ for (size_t i = 0; i <= p.size(); ++i) {
66
+ if (i == p.size() || p[i] == '/' || p[i] == '\\') {
67
+ std::string comp = p.substr(start, i - start);
68
+ if (comp.empty() || comp == "." || comp == "..") return false;
69
+ start = i + 1;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ FileTransfer::FileTransfer(std::string temp_dir) { config_.temp_directory = std::move(temp_dir); }
76
+ FileTransfer::FileTransfer(Config config) : config_(std::move(config)) {}
77
+ FileTransfer::~FileTransfer() { stop(); }
78
+
79
+ void FileTransfer::attach(NodeContext& ctx) {
80
+ network_ = &ctx.network;
81
+ network_->on(MessageType::FileChunk,
82
+ [this](const Peer& peer, ByteView payload) { on_message(peer, payload); });
83
+ network_->on_peer_disconnected([this](const PeerId& id) {
84
+ // Fail every in-flight transfer with the departed peer and reclaim temps.
85
+ std::vector<std::shared_ptr<Outgoing>> out;
86
+ std::vector<std::shared_ptr<Incoming>> in;
87
+ {
88
+ std::lock_guard<std::mutex> lock(mutex_);
89
+ for (auto& [tid, t] : outgoing_) if (t->peer == id) out.push_back(t);
90
+ auto pit = incoming_.find(id);
91
+ if (pit != incoming_.end())
92
+ for (auto& [tid, t] : pit->second) in.push_back(t);
93
+ }
94
+ for (auto& t : out) finish_outgoing(t, false);
95
+ for (auto& t : in) finish_incoming(t, false, "peer disconnected");
96
+ });
97
+ }
98
+
99
+ void FileTransfer::start() {
100
+ if (running_.exchange(true)) return;
101
+ const uint32_t n = std::max<uint32_t>(1, config_.worker_threads);
102
+ for (uint32_t i = 0; i < n; ++i) workers_.emplace_back(&FileTransfer::worker_loop, this);
103
+ const uint32_t d = std::max<uint32_t>(1, config_.disk_threads);
104
+ for (uint32_t i = 0; i < d; ++i) disk_workers_.emplace_back(&FileTransfer::disk_worker_loop, this);
105
+ maintenance_thread_ = std::thread(&FileTransfer::maintenance_loop, this);
106
+ }
107
+
108
+ void FileTransfer::stop() {
109
+ if (!running_.exchange(false)) return;
110
+
111
+ queue_cv_.notify_all();
112
+ maintenance_cv_.notify_all();
113
+ disk_cv_.notify_all();
114
+
115
+ // Wake any worker blocked on a transfer's window/wait so run_send returns.
116
+ {
117
+ std::lock_guard<std::mutex> lock(mutex_);
118
+ for (auto& [id, t] : outgoing_) { std::lock_guard<std::mutex> lk(t->mtx); t->status = Status::Cancelled; t->cv.notify_all(); }
119
+ }
120
+ for (auto& w : workers_) if (w.joinable()) w.join();
121
+ workers_.clear();
122
+ // Join the disk pool before dropping any Incoming, so no writer touches a temp
123
+ // file while ~Incoming reclaims it.
124
+ for (auto& w : disk_workers_) if (w.joinable()) w.join();
125
+ disk_workers_.clear();
126
+ if (maintenance_thread_.joinable()) maintenance_thread_.join();
127
+
128
+ // Drop remaining transfers. ~Incoming closes any open handle and reclaims
129
+ // un-finalized temp files.
130
+ std::unordered_map<PeerId, std::unordered_map<uint64_t, std::shared_ptr<Incoming>>, PeerId::Hash> in;
131
+ {
132
+ std::lock_guard<std::mutex> lock(mutex_);
133
+ outgoing_.clear();
134
+ in.swap(incoming_);
135
+ }
136
+ in.clear();
137
+ }
138
+
139
+ FileTransfer::Stats FileTransfer::stats() const {
140
+ std::lock_guard<std::mutex> lock(stats_mutex_);
141
+ return stats_;
142
+ }
143
+
144
+ // ── lookups ───────────────────────────────────────────────────────────────────
145
+
146
+ std::shared_ptr<FileTransfer::Outgoing> FileTransfer::find_outgoing(uint64_t id) const {
147
+ std::lock_guard<std::mutex> lock(mutex_);
148
+ auto it = outgoing_.find(id);
149
+ return it == outgoing_.end() ? nullptr : it->second;
150
+ }
151
+
152
+ std::shared_ptr<FileTransfer::Incoming> FileTransfer::find_incoming(const PeerId& peer, uint64_t id) const {
153
+ std::lock_guard<std::mutex> lock(mutex_);
154
+ auto pit = incoming_.find(peer);
155
+ if (pit == incoming_.end()) return nullptr;
156
+ auto it = pit->second.find(id);
157
+ return it == pit->second.end() ? nullptr : it->second;
158
+ }
159
+
160
+ // ── small senders ─────────────────────────────────────────────────────────────
161
+
162
+ void FileTransfer::send_simple(const PeerId& peer, uint8_t op, uint64_t id) {
163
+ Bytes m; m.push_back(op); put_u64(m, id); send_to(peer, m);
164
+ }
165
+ void FileTransfer::send_complete(const PeerId& peer, uint64_t id, bool ok) {
166
+ Bytes m; m.push_back(OP_COMPLETE); put_u64(m, id); m.push_back(ok ? 1 : 0); send_to(peer, m);
167
+ }
168
+
169
+ // ── Sender: build + offer ───────────────────────────────────────────────────
170
+
171
+ uint64_t FileTransfer::send_file(const PeerId& to, const std::string& path) {
172
+ if (!is_file(path.c_str())) { LOG_WARN("filexfer", "send_file: not a file: " << path); return 0; }
173
+ const int64_t size = get_file_size(path.c_str());
174
+ if (size < 0) { LOG_WARN("filexfer", "send_file: cannot stat " << path); return 0; }
175
+
176
+ auto t = std::make_shared<Outgoing>();
177
+ t->peer = to;
178
+ t->name = get_filename_from_path(path.c_str());
179
+ t->root = path;
180
+ t->is_directory = false;
181
+ t->files.push_back({t->name, static_cast<uint64_t>(size)});
182
+ t->sources.push_back(path);
183
+ t->total_bytes = static_cast<uint64_t>(size);
184
+ return start_send(std::move(t));
185
+ }
186
+
187
+ uint64_t FileTransfer::send_directory(const PeerId& to, const std::string& dir_path) {
188
+ if (!is_directory(dir_path.c_str())) { LOG_WARN("filexfer", "send_directory: not a directory: " << dir_path); return 0; }
189
+
190
+ auto t = std::make_shared<Outgoing>();
191
+ t->name = get_filename_from_path(dir_path.c_str());
192
+ t->root = dir_path;
193
+ t->is_directory = true;
194
+ scan_directory(dir_path, "", t->files, t->sources);
195
+ for (const auto& f : t->files) t->total_bytes += f.size;
196
+ t->peer = to;
197
+ return start_send(std::move(t));
198
+ }
199
+
200
+ uint64_t FileTransfer::start_send(std::shared_ptr<Outgoing> t) {
201
+ const uint64_t id = next_id_.fetch_add(1);
202
+ t->id = id;
203
+ t->last_activity = std::chrono::steady_clock::now();
204
+ t->status = Status::Pending;
205
+ {
206
+ std::lock_guard<std::mutex> lock(mutex_);
207
+ outgoing_.emplace(id, t);
208
+ }
209
+
210
+ Bytes m;
211
+ m.push_back(OP_OFFER);
212
+ put_u64(m, id);
213
+ m.push_back(t->is_directory ? 1 : 0);
214
+ put_u64(m, t->total_bytes);
215
+ put_u16(m, static_cast<uint16_t>(t->name.size()));
216
+ m.insert(m.end(), t->name.begin(), t->name.end());
217
+ put_u32(m, static_cast<uint32_t>(t->files.size()));
218
+ for (const auto& f : t->files) {
219
+ put_u16(m, static_cast<uint16_t>(f.relative_path.size()));
220
+ m.insert(m.end(), f.relative_path.begin(), f.relative_path.end());
221
+ put_u64(m, f.size);
222
+ }
223
+ send_to(t->peer, m);
224
+ LOG_INFO("filexfer", "Offering " << (t->is_directory ? "directory '" : "file '") << t->name
225
+ << "' (" << t->files.size() << " file(s), " << t->total_bytes << " B) [" << id << "]");
226
+ return id;
227
+ }
228
+
229
+ void FileTransfer::queue_send(uint64_t id) {
230
+ { std::lock_guard<std::mutex> lk(queue_mutex_); send_queue_.push(id); }
231
+ queue_cv_.notify_one();
232
+ }
233
+
234
+ void FileTransfer::worker_loop() {
235
+ while (running_.load()) {
236
+ uint64_t id;
237
+ {
238
+ std::unique_lock<std::mutex> lk(queue_mutex_);
239
+ queue_cv_.wait(lk, [this] { return !running_.load() || !send_queue_.empty(); });
240
+ if (!running_.load()) return;
241
+ id = send_queue_.front();
242
+ send_queue_.pop();
243
+ }
244
+ auto t = find_outgoing(id);
245
+ if (!t) continue;
246
+ {
247
+ std::lock_guard<std::mutex> lk(t->mtx);
248
+ if (t->worker_active) continue; // another worker owns it
249
+ t->worker_active = true;
250
+ }
251
+ run_send(t);
252
+ { std::lock_guard<std::mutex> lk(t->mtx); t->worker_active = false; }
253
+ }
254
+ }
255
+
256
+ void FileTransfer::run_send(const std::shared_ptr<Outgoing>& t) {
257
+ // One send buffer reused for every chunk: the fixed CHUNK header is written in
258
+ // place and the file data is read straight into the bytes that follow it, so a
259
+ // chunk's payload is never copied between the disk read and the wire (the only
260
+ // remaining copies are the transport's own framing + encryption). Reusing the
261
+ // buffer also drops the per-chunk allocation.
262
+ constexpr size_t kChunkHeader = 1 + 8 + 4 + 8; // op + id + file_index + offset
263
+ Bytes m;
264
+ m.reserve(kChunkHeader + config_.chunk_size);
265
+
266
+ // The source file is held open across all of its chunks (one open()/close()
267
+ // per file instead of one per chunk) and read sequentially straight into the
268
+ // reused send buffer.
269
+ FileStream fp;
270
+ size_t open_idx = SIZE_MAX;
271
+
272
+ while (running_.load()) {
273
+ size_t file_index;
274
+ uint64_t offset, file_size;
275
+ std::string source;
276
+ {
277
+ std::unique_lock<std::mutex> lk(t->mtx);
278
+ if (t->status != Status::Active) return; // paused / cancelled / done
279
+ if (t->cur_file >= t->files.size()) break; // all data streamed
280
+ file_index = t->cur_file;
281
+ offset = t->cur_offset;
282
+ file_size = t->files[file_index].size;
283
+ source = t->sources[file_index];
284
+ if (offset == 0) rats_sha256_reset(&t->hash);
285
+ }
286
+
287
+ // Empty file: no chunks, just the per-file SHA (of the empty input).
288
+ if (file_size == 0) {
289
+ rats_sha256_context_t e; rats_sha256_reset(&e);
290
+ uint8_t digest[RATS_SHA256_HASH_SIZE]; rats_sha256_finish(&e, digest);
291
+ Bytes fe; fe.push_back(OP_FILE_END); put_u64(fe, t->id);
292
+ put_u32(fe, static_cast<uint32_t>(file_index));
293
+ fe.insert(fe.end(), digest, digest + RATS_SHA256_HASH_SIZE);
294
+ send_to(t->peer, fe);
295
+ fp.close(); open_idx = SIZE_MAX;
296
+ std::lock_guard<std::mutex> lk(t->mtx);
297
+ t->cur_file++; t->cur_offset = 0; t->files_done++;
298
+ continue;
299
+ }
300
+
301
+ // (Re)open the source when we start a new file, seeking to the resume offset.
302
+ if (open_idx != file_index) {
303
+ fp.close();
304
+ if (!fp.open_read(source.c_str()) || !fp.seek(offset)) {
305
+ LOG_ERROR("filexfer", "open/seek error on " << source << " at " << offset);
306
+ send_complete(t->peer, t->id, false);
307
+ finish_outgoing(t, false);
308
+ return;
309
+ }
310
+ open_idx = file_index;
311
+ }
312
+
313
+ const uint32_t want = static_cast<uint32_t>(std::min<uint64_t>(config_.chunk_size, file_size - offset));
314
+ m.clear();
315
+ m.push_back(OP_CHUNK);
316
+ put_u64(m, t->id);
317
+ put_u32(m, static_cast<uint32_t>(file_index));
318
+ put_u64(m, offset);
319
+ m.resize(kChunkHeader + want); // no realloc: capacity was reserved up front
320
+ if (fp.read(m.data() + kChunkHeader, want) != want) {
321
+ LOG_ERROR("filexfer", "read error on " << source << " at " << offset);
322
+ send_complete(t->peer, t->id, false);
323
+ finish_outgoing(t, false);
324
+ return;
325
+ }
326
+ send_to(t->peer, m);
327
+
328
+ bool file_done = false;
329
+ uint8_t digest[RATS_SHA256_HASH_SIZE];
330
+ {
331
+ std::lock_guard<std::mutex> lk(t->mtx);
332
+ rats_sha256_update(&t->hash, m.data() + kChunkHeader, want);
333
+ t->cur_offset += want;
334
+ t->bytes_done += want;
335
+ t->last_activity = std::chrono::steady_clock::now();
336
+ if (t->cur_offset >= file_size) {
337
+ rats_sha256_finish(&t->hash, digest);
338
+ t->cur_file++; t->cur_offset = 0; t->files_done++;
339
+ file_done = true;
340
+ }
341
+ }
342
+ { std::lock_guard<std::mutex> lk(stats_mutex_); stats_.bytes_sent += want; }
343
+
344
+ if (file_done) {
345
+ Bytes e; e.push_back(OP_FILE_END); put_u64(e, t->id);
346
+ put_u32(e, static_cast<uint32_t>(file_index));
347
+ e.insert(e.end(), digest, digest + RATS_SHA256_HASH_SIZE);
348
+ send_to(t->peer, e);
349
+ }
350
+ emit_progress(t);
351
+
352
+ // Backpressure: stay within `window_bytes` of the receiver's last ack.
353
+ std::unique_lock<std::mutex> lk(t->mtx);
354
+ while (running_.load() && t->status == Status::Active &&
355
+ t->bytes_done - t->acked >= config_.window_bytes) {
356
+ t->cv.wait_for(lk, std::chrono::milliseconds(200));
357
+ }
358
+ }
359
+ // All data streamed; the transfer stays Active until the receiver's COMPLETE
360
+ // arrives (handled on the reactor thread), which calls finish_outgoing().
361
+ }
362
+
363
+ void FileTransfer::finish_outgoing(const std::shared_ptr<Outgoing>& t, bool success) {
364
+ bool cancelled;
365
+ {
366
+ std::lock_guard<std::mutex> lk(t->mtx);
367
+ if (t->finished) return;
368
+ t->finished = true;
369
+ cancelled = (t->status == Status::Cancelled);
370
+ if (!cancelled)
371
+ t->status = success ? Status::Completed : Status::Failed;
372
+ t->cv.notify_all();
373
+ }
374
+ {
375
+ std::lock_guard<std::mutex> lock(mutex_);
376
+ outgoing_.erase(t->id);
377
+ }
378
+ { std::lock_guard<std::mutex> lk(stats_mutex_); success ? ++stats_.completed : ++stats_.failed; }
379
+ emit_progress(t);
380
+ if (cancelled) LOG_INFO("filexfer", "Send [" << t->id << "] cancelled");
381
+ else if (success) LOG_INFO("filexfer", "Send [" << t->id << "] completed (" << t->root << ")");
382
+ else LOG_WARN("filexfer", "Send [" << t->id << "] failed");
383
+ if (complete_handler_) complete_handler_(t->id, success, t->root);
384
+ }
385
+
386
+ // ── Receiver ──────────────────────────────────────────────────────────────────
387
+
388
+ void FileTransfer::accept(const PeerId& from, uint64_t id, const std::string& dest_path) {
389
+ auto t = find_incoming(from, id);
390
+ if (!t) return;
391
+ bool empty_transfer = false;
392
+ {
393
+ std::lock_guard<std::mutex> lk(t->mtx);
394
+ if (t->status != Status::Pending) return;
395
+ t->dest_root = dest_path;
396
+ for (size_t i = 0; i < t->files.size(); ++i) {
397
+ IncomingFile& f = t->files[i];
398
+ f.final_path = t->is_directory ? combine_paths(dest_path, f.relative_path) : dest_path;
399
+ // Transfer ids are only unique per sender (every node's first offer is
400
+ // id 1), so the sender's PeerId is what keeps two peers' temp files
401
+ // apart. Use the full hex: short_hex() is 32 bits, which a peer could
402
+ // grind a keypair to collide with on purpose.
403
+ f.temp_path = combine_paths(config_.temp_directory,
404
+ from.to_hex() + "." + std::to_string(id) + "."
405
+ + std::to_string(i) + ".part");
406
+ }
407
+ t->status = Status::Active;
408
+ t->last_activity = std::chrono::steady_clock::now();
409
+ empty_transfer = t->files.empty();
410
+ }
411
+ create_directories(config_.temp_directory.c_str());
412
+ if (t->is_directory) create_directories(dest_path.c_str());
413
+
414
+ Bytes m; m.push_back(OP_RESPONSE); put_u64(m, id); m.push_back(1); send_to(from, m);
415
+ LOG_INFO("filexfer", "Accepted transfer [" << id << "] -> " << dest_path);
416
+
417
+ if (empty_transfer) { // e.g. an empty directory: done immediately
418
+ send_complete(from, id, true);
419
+ finish_incoming(t, true, "");
420
+ }
421
+ }
422
+
423
+ void FileTransfer::reject(const PeerId& from, uint64_t id) {
424
+ auto t = find_incoming(from, id);
425
+ {
426
+ std::lock_guard<std::mutex> lock(mutex_);
427
+ auto pit = incoming_.find(from);
428
+ if (pit != incoming_.end()) pit->second.erase(id);
429
+ }
430
+ Bytes m; m.push_back(OP_RESPONSE); put_u64(m, id); m.push_back(0); send_to(from, m);
431
+ if (t && complete_handler_) complete_handler_(id, false, "");
432
+ }
433
+
434
+ // ── Receive-side disk writer (off the reactor thread) ─────────────────────────
435
+ //
436
+ // The reactor validates a chunk and pushes it into t->wq; the writer pool drains
437
+ // that queue on its own thread. `t->out`, `t->hash`, `t->out_idx` and
438
+ // `t->hashing_file` are writer-thread-only: a transfer's `scheduled` flag keeps
439
+ // exactly one worker draining it at a time, so these need no lock even though the
440
+ // worker touches them outside t->mtx.
441
+
442
+ FileTransfer::Incoming::~Incoming() {
443
+ out.close(); // must precede the temp-file delete on Windows (can't unlink an open file)
444
+ for (auto& f : files) if (f.temp_created && !f.finalized) delete_file(f.temp_path.c_str());
445
+ }
446
+
447
+ void FileTransfer::schedule_writer(const std::shared_ptr<Incoming>& t) {
448
+ { std::lock_guard<std::mutex> lk(disk_mutex_); disk_ready_.push(t); }
449
+ disk_cv_.notify_one();
450
+ }
451
+
452
+ void FileTransfer::disk_worker_loop() {
453
+ while (running_.load()) {
454
+ std::shared_ptr<Incoming> t;
455
+ {
456
+ std::unique_lock<std::mutex> lk(disk_mutex_);
457
+ disk_cv_.wait(lk, [this] { return !running_.load() || !disk_ready_.empty(); });
458
+ if (!running_.load()) return;
459
+ t = std::move(disk_ready_.front());
460
+ disk_ready_.pop();
461
+ }
462
+ if (t) drain_writes(t);
463
+ }
464
+ }
465
+
466
+ void FileTransfer::drain_writes(const std::shared_ptr<Incoming>& t) {
467
+ for (;;) {
468
+ WriteJob job;
469
+ {
470
+ std::lock_guard<std::mutex> lk(t->mtx);
471
+ if (t->finished) { // torn down elsewhere: drop pending work
472
+ std::queue<WriteJob> empty; t->wq.swap(empty);
473
+ t->queued_bytes = 0; t->scheduled = false;
474
+ return;
475
+ }
476
+ if (t->wq.empty()) { t->scheduled = false; return; }
477
+ job = std::move(t->wq.front());
478
+ t->wq.pop();
479
+ }
480
+ if (job.is_file_end) process_file_end(t, job);
481
+ else process_data(t, job);
482
+ }
483
+ }
484
+
485
+ void FileTransfer::process_data(const std::shared_ptr<Incoming>& t, WriteJob& job) {
486
+ const uint32_t fidx = job.fidx;
487
+ std::string temp_path;
488
+ {
489
+ std::lock_guard<std::mutex> lk(t->mtx);
490
+ if (t->finished || fidx >= t->files.size()) return;
491
+ temp_path = t->files[fidx].temp_path;
492
+ }
493
+
494
+ // Open the temp file once per file, then write sequentially at the given offset.
495
+ if (t->out_idx != fidx) {
496
+ t->out.close();
497
+ if (!t->out.open_write(temp_path.c_str())) {
498
+ send_complete(t->peer, t->id, false);
499
+ finish_incoming(t, false, "cannot create temp file");
500
+ return;
501
+ }
502
+ t->out_idx = fidx;
503
+ { std::lock_guard<std::mutex> lk(t->mtx); t->files[fidx].temp_created = true; }
504
+ }
505
+ if (t->hashing_file != static_cast<int>(fidx)) { rats_sha256_reset(&t->hash); t->hashing_file = static_cast<int>(fidx); }
506
+
507
+ if (!t->out.write_at(job.offset, job.data.data(), job.data.size())) {
508
+ LOG_ERROR("filexfer", "transfer " << t->id << ": disk write failed");
509
+ send_complete(t->peer, t->id, false);
510
+ finish_incoming(t, false, "failed to write chunk to disk");
511
+ return;
512
+ }
513
+ rats_sha256_update(&t->hash, job.data.data(), job.data.size());
514
+
515
+ const uint64_t ack_interval =
516
+ std::min<uint64_t>(config_.progress_interval, std::max<uint32_t>(1, config_.window_bytes / 2));
517
+ bool send_ack = false; uint64_t ack_bytes = 0;
518
+ {
519
+ std::lock_guard<std::mutex> lk(t->mtx);
520
+ IncomingFile& f = t->files[fidx];
521
+ f.received += job.data.size();
522
+ t->bytes_done += job.data.size();
523
+ if (t->queued_bytes >= job.data.size()) t->queued_bytes -= job.data.size();
524
+ t->last_activity = std::chrono::steady_clock::now();
525
+ // Ack on the byte threshold, and always once a file's data is fully on disk
526
+ // (keeps the sender's window moving across file boundaries).
527
+ if (f.received >= f.size || t->bytes_done - t->last_ack >= ack_interval) {
528
+ t->last_ack = t->bytes_done; ack_bytes = t->bytes_done; send_ack = true;
529
+ }
530
+ }
531
+ { std::lock_guard<std::mutex> lk(stats_mutex_); stats_.bytes_received += job.data.size(); }
532
+
533
+ if (send_ack) { Bytes m; m.push_back(OP_PROGRESS); put_u64(m, t->id); put_u64(m, ack_bytes); send_to(t->peer, m); }
534
+ emit_progress(t);
535
+ }
536
+
537
+ void FileTransfer::process_file_end(const std::shared_ptr<Incoming>& t, WriteJob& job) {
538
+ const uint32_t fidx = job.fidx;
539
+ std::string final_path, temp_path, rel;
540
+ uint64_t fsize = 0;
541
+ {
542
+ std::lock_guard<std::mutex> lk(t->mtx);
543
+ if (t->finished || fidx >= t->files.size()) return;
544
+ IncomingFile& f = t->files[fidx];
545
+ if (f.finalized) return;
546
+ final_path = f.final_path; temp_path = f.temp_path; rel = f.relative_path; fsize = f.size;
547
+ }
548
+
549
+ // Whole-file SHA-256 over exactly the bytes written to disk (a file with no
550
+ // data — an empty file — hashes the empty input).
551
+ if (t->hashing_file != static_cast<int>(fidx)) { rats_sha256_reset(&t->hash); t->hashing_file = static_cast<int>(fidx); }
552
+ uint8_t local[RATS_SHA256_HASH_SIZE];
553
+ rats_sha256_finish(&t->hash, local);
554
+ t->hashing_file = -1;
555
+ if (config_.verify_integrity && std::memcmp(local, job.sha, RATS_SHA256_HASH_SIZE) != 0) {
556
+ LOG_ERROR("filexfer", "SHA-256 mismatch for " << rel << " on transfer " << t->id);
557
+ send_complete(t->peer, t->id, false);
558
+ finish_incoming(t, false, "SHA-256 mismatch");
559
+ return;
560
+ }
561
+
562
+ // Close the temp handle before moving it into place (Windows can't rename an
563
+ // open file).
564
+ if (t->out_idx == fidx) { t->out.close(); t->out_idx = SIZE_MAX; }
565
+
566
+ const std::string parent = get_parent_directory(final_path.c_str());
567
+ if (!parent.empty()) create_directories(parent.c_str());
568
+ if (file_exists(final_path.c_str())) delete_file(final_path.c_str());
569
+
570
+ bool ok;
571
+ if (fsize == 0) {
572
+ ok = create_file_with_size(final_path.c_str(), 0);
573
+ if (file_exists(temp_path.c_str())) delete_file(temp_path.c_str());
574
+ } else {
575
+ ok = rename_file(temp_path.c_str(), final_path.c_str());
576
+ if (!ok) { // rename fails across volumes; fall back to copy + delete
577
+ ok = copy_file(temp_path.c_str(), final_path.c_str());
578
+ if (ok) delete_file(temp_path.c_str());
579
+ }
580
+ }
581
+ if (!ok) {
582
+ LOG_ERROR("filexfer", "cannot place file " << final_path);
583
+ send_complete(t->peer, t->id, false);
584
+ finish_incoming(t, false, "cannot write destination file");
585
+ return;
586
+ }
587
+
588
+ bool all_done = false;
589
+ {
590
+ std::lock_guard<std::mutex> lk(t->mtx);
591
+ IncomingFile& f = t->files[fidx];
592
+ f.finalized = true;
593
+ t->files_done++;
594
+ all_done = true;
595
+ for (const auto& ff : t->files) if (!ff.finalized) { all_done = false; break; }
596
+ }
597
+ emit_progress(t);
598
+
599
+ if (all_done) {
600
+ send_complete(t->peer, t->id, true);
601
+ finish_incoming(t, true, "");
602
+ }
603
+ }
604
+
605
+ void FileTransfer::finish_incoming(const std::shared_ptr<Incoming>& t, bool success, const std::string& error) {
606
+ std::string dest;
607
+ bool cancelled;
608
+ {
609
+ std::lock_guard<std::mutex> lk(t->mtx);
610
+ if (t->finished) return;
611
+ t->finished = true;
612
+ cancelled = (t->status == Status::Cancelled);
613
+ if (!cancelled)
614
+ t->status = success ? Status::Completed : Status::Failed;
615
+ dest = t->dest_root;
616
+ // Partial temp files are reclaimed by ~Incoming, once the disk writer has
617
+ // released its handle — deleting here could race an in-flight write.
618
+ }
619
+ {
620
+ std::lock_guard<std::mutex> lock(mutex_);
621
+ auto pit = incoming_.find(t->peer);
622
+ if (pit != incoming_.end()) pit->second.erase(t->id);
623
+ }
624
+ { std::lock_guard<std::mutex> lk(stats_mutex_); success ? ++stats_.completed : ++stats_.failed; }
625
+ emit_progress(t);
626
+ if (cancelled) LOG_INFO("filexfer", "Receive [" << t->id << "] cancelled");
627
+ else if (success) LOG_INFO("filexfer", "Receive [" << t->id << "] completed -> " << dest);
628
+ else if (error.empty()) LOG_WARN("filexfer", "Receive [" << t->id << "] failed");
629
+ else LOG_WARN("filexfer", "Receive [" << t->id << "] failed: " << error);
630
+ if (complete_handler_) complete_handler_(t->id, success, dest);
631
+ }
632
+
633
+ // ── Message dispatch (reactor thread) ────────────────────────────────────────
634
+
635
+ void FileTransfer::on_message(const Peer& peer, ByteView payload) {
636
+ Reader r{payload.data(), payload.data() + payload.size()};
637
+ const uint8_t op = r.u8();
638
+ const PeerId from = peer.id();
639
+
640
+ switch (op) {
641
+ case OP_OFFER: {
642
+ const uint64_t id = r.u64();
643
+ const bool is_dir = r.u8() != 0;
644
+ const uint64_t total = r.u64();
645
+ const uint16_t nlen = r.u16();
646
+ ByteView nm = r.bytes(nlen);
647
+ const uint32_t count = r.u32();
648
+ if (!r.ok) return;
649
+ // `count` is the peer's word. Reserving on it directly lets a ~30-byte
650
+ // message ask for hundreds of GB; the resulting bad_alloc/length_error
651
+ // would unwind out of the reactor thread, which has no handler above it,
652
+ // and abort the process. Reject a count the rest of the payload cannot
653
+ // possibly hold, then cap the speculative reserve so memory stays
654
+ // proportional to the entries actually parsed.
655
+ if (static_cast<uint64_t>(count) * kMinManifestEntry > r.remaining()) {
656
+ LOG_WARN("filexfer", "Dropping offer " << id << " from " << from.short_hex()
657
+ << ": manifest claims " << count << " file(s) but only "
658
+ << r.remaining() << " B remain");
659
+ return;
660
+ }
661
+ std::string name(reinterpret_cast<const char*>(nm.data()), nm.size());
662
+ std::vector<FileEntry> files;
663
+ files.reserve(std::min<uint32_t>(count, 4096));
664
+ for (uint32_t i = 0; i < count; ++i) {
665
+ const uint16_t plen = r.u16();
666
+ ByteView pb = r.bytes(plen);
667
+ const uint64_t sz = r.u64();
668
+ if (!r.ok) return;
669
+ files.push_back({std::string(reinterpret_cast<const char*>(pb.data()), pb.size()), sz});
670
+ }
671
+ handle_offer(from, id, is_dir, total, std::move(name), std::move(files));
672
+ return;
673
+ }
674
+ case OP_RESPONSE: {
675
+ const uint64_t id = r.u64();
676
+ const uint8_t accepted = r.u8();
677
+ if (!r.ok) return;
678
+ auto t = find_outgoing(id);
679
+ if (!t) return;
680
+ if (!accepted) { finish_outgoing(t, false); return; }
681
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Pending) t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); }
682
+ queue_send(id);
683
+ return;
684
+ }
685
+ case OP_CHUNK: {
686
+ const uint64_t id = r.u64();
687
+ const uint32_t fidx = r.u32();
688
+ const uint64_t offset = r.u64();
689
+ ByteView data = r.rest();
690
+ if (!r.ok) return;
691
+ handle_chunk(from, id, fidx, offset, data);
692
+ return;
693
+ }
694
+ case OP_FILE_END: {
695
+ const uint64_t id = r.u64();
696
+ const uint32_t fidx = r.u32();
697
+ ByteView sha = r.bytes(RATS_SHA256_HASH_SIZE);
698
+ if (!r.ok) return;
699
+ handle_file_end(from, id, fidx, sha.data());
700
+ return;
701
+ }
702
+ case OP_PROGRESS: {
703
+ const uint64_t id = r.u64();
704
+ const uint64_t received = r.u64();
705
+ if (!r.ok) return;
706
+ auto t = find_outgoing(id);
707
+ if (!t) return;
708
+ { std::lock_guard<std::mutex> lk(t->mtx); t->acked = received; t->last_activity = std::chrono::steady_clock::now(); }
709
+ t->cv.notify_all();
710
+ return;
711
+ }
712
+ case OP_COMPLETE: {
713
+ const uint64_t id = r.u64();
714
+ const uint8_t ok = r.u8();
715
+ if (!r.ok) return;
716
+ if (auto t = find_outgoing(id)) finish_outgoing(t, ok != 0);
717
+ return;
718
+ }
719
+ case OP_CANCEL: {
720
+ const uint64_t id = r.u64();
721
+ if (!r.ok) return;
722
+ if (auto t = find_outgoing(id)) {
723
+ { std::lock_guard<std::mutex> lk(t->mtx); t->status = Status::Cancelled; t->cv.notify_all(); }
724
+ finish_outgoing(t, false);
725
+ }
726
+ if (auto t = find_incoming(from, id)) {
727
+ { std::lock_guard<std::mutex> lk(t->mtx); t->status = Status::Cancelled; }
728
+ finish_incoming(t, false, "cancelled by peer");
729
+ }
730
+ return;
731
+ }
732
+ case OP_PAUSE: {
733
+ const uint64_t id = r.u64();
734
+ if (!r.ok) return;
735
+ if (auto t = find_outgoing(id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) t->status = Status::Paused; t->cv.notify_all(); }
736
+ if (auto t = find_incoming(from, id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) t->status = Status::Paused; }
737
+ return;
738
+ }
739
+ case OP_RESUME: {
740
+ const uint64_t id = r.u64();
741
+ if (!r.ok) return;
742
+ if (auto t = find_outgoing(id)) {
743
+ bool requeue = false;
744
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); requeue = true; } t->cv.notify_all(); }
745
+ if (requeue) queue_send(id);
746
+ }
747
+ if (auto t = find_incoming(from, id)) { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); } }
748
+ return;
749
+ }
750
+ default: return;
751
+ }
752
+ }
753
+
754
+ void FileTransfer::handle_offer(const PeerId& from, uint64_t id, bool is_dir, uint64_t total,
755
+ std::string name, std::vector<FileEntry> files) {
756
+ if (find_incoming(from, id)) return; // duplicate offer
757
+
758
+ // Validate every peer-supplied relative path in a directory manifest before
759
+ // it can be used to build a destination path.
760
+ if (is_dir) {
761
+ for (const auto& f : files) {
762
+ if (!is_safe_relative_path(f.relative_path)) {
763
+ LOG_WARN("filexfer", "Rejecting offer " << id << " from " << from.short_hex()
764
+ << ": unsafe path in manifest ('" << f.relative_path << "')");
765
+ Bytes m; m.push_back(OP_RESPONSE); put_u64(m, id); m.push_back(0); send_to(from, m);
766
+ return;
767
+ }
768
+ }
769
+ }
770
+
771
+ auto t = std::make_shared<Incoming>();
772
+ t->id = id; t->peer = from; t->name = std::move(name); t->is_directory = is_dir;
773
+ t->last_activity = std::chrono::steady_clock::now();
774
+ t->files.reserve(files.size());
775
+ for (auto& f : files) {
776
+ IncomingFile inf; inf.relative_path = f.relative_path; inf.size = f.size;
777
+ t->files.push_back(std::move(inf));
778
+ }
779
+ Offer offer{from, id, t->name, total, is_dir, std::move(files)};
780
+ { std::lock_guard<std::mutex> lock(mutex_); incoming_[from].emplace(id, t); }
781
+
782
+ if (offer_handler_) offer_handler_(offer);
783
+ else reject(from, id); // no handler → auto-reject
784
+ }
785
+
786
+ // Reactor thread: validate ordering + copy the chunk into the transfer's write
787
+ // queue, then hand it to the disk-writer pool. No disk I/O or hashing happens
788
+ // here — those would block the reactor, which serves every peer on this shard.
789
+ void FileTransfer::handle_chunk(const PeerId& from, uint64_t id, uint32_t fidx, uint64_t offset,
790
+ ByteView data) {
791
+ auto t = find_incoming(from, id);
792
+ if (!t) return;
793
+
794
+ std::string fail;
795
+ bool need_schedule = false;
796
+ {
797
+ std::lock_guard<std::mutex> lk(t->mtx);
798
+ if (t->finished) return;
799
+ if (t->status != Status::Active && t->status != Status::Paused) return;
800
+ if (fidx >= t->files.size() || fidx != t->recv_file) {
801
+ fail = "out-of-order file index";
802
+ } else {
803
+ IncomingFile& f = t->files[fidx];
804
+ if (offset != f.enqueued) fail = "out-of-order chunk offset";
805
+ else if (offset + data.size() > f.size) fail = "chunk exceeds declared file size";
806
+ else {
807
+ WriteJob job;
808
+ job.fidx = fidx; job.offset = offset; job.data = data.to_bytes();
809
+ f.enqueued += data.size();
810
+ t->queued_bytes += data.size();
811
+ t->last_activity = std::chrono::steady_clock::now();
812
+ t->wq.push(std::move(job));
813
+ if (!t->scheduled) { t->scheduled = true; need_schedule = true; }
814
+ }
815
+ }
816
+ }
817
+ if (!fail.empty()) {
818
+ LOG_ERROR("filexfer", "transfer " << id << ": " << fail);
819
+ send_complete(from, id, false);
820
+ finish_incoming(t, false, fail);
821
+ return;
822
+ }
823
+ if (need_schedule) schedule_writer(t);
824
+ }
825
+
826
+ void FileTransfer::handle_file_end(const PeerId& from, uint64_t id, uint32_t fidx, const uint8_t* sha) {
827
+ auto t = find_incoming(from, id);
828
+ if (!t) return;
829
+ std::string fail;
830
+ bool need_schedule = false;
831
+ {
832
+ std::lock_guard<std::mutex> lk(t->mtx);
833
+ if (t->finished || fidx >= t->files.size()) return;
834
+ if (fidx != t->recv_file) {
835
+ fail = "file-end out of order";
836
+ } else {
837
+ IncomingFile& f = t->files[fidx];
838
+ if (f.enqueued != f.size) { // all of the file's data must be queued first
839
+ fail = "file-end before all data";
840
+ } else {
841
+ WriteJob job;
842
+ job.fidx = fidx; job.is_file_end = true;
843
+ std::memcpy(job.sha, sha, RATS_SHA256_HASH_SIZE);
844
+ t->wq.push(std::move(job));
845
+ t->recv_file++; // subsequent chunks belong to the next file
846
+ if (!t->scheduled) { t->scheduled = true; need_schedule = true; }
847
+ }
848
+ }
849
+ }
850
+ if (!fail.empty()) {
851
+ LOG_ERROR("filexfer", "transfer " << id << ": " << fail);
852
+ send_complete(from, id, false);
853
+ finish_incoming(t, false, fail);
854
+ return;
855
+ }
856
+ if (need_schedule) schedule_writer(t);
857
+ }
858
+
859
+ // ── Control API (works from either side) ──────────────────────────────────────
860
+
861
+ bool FileTransfer::cancel(const PeerId& peer, uint64_t id) {
862
+ bool acted = false;
863
+ if (auto t = find_outgoing(id)) {
864
+ { std::lock_guard<std::mutex> lk(t->mtx); if (!t->finished) { t->status = Status::Cancelled; t->cv.notify_all(); acted = true; } }
865
+ if (acted) { send_simple(t->peer, OP_CANCEL, id); finish_outgoing(t, false); }
866
+ }
867
+ if (auto t = find_incoming(peer, id)) {
868
+ bool a = false;
869
+ { std::lock_guard<std::mutex> lk(t->mtx); if (!t->finished) { t->status = Status::Cancelled; a = true; } }
870
+ if (a) { send_simple(peer, OP_CANCEL, id); finish_incoming(t, false, "cancelled"); acted = true; }
871
+ }
872
+ return acted;
873
+ }
874
+
875
+ bool FileTransfer::pause(const PeerId& peer, uint64_t id) {
876
+ if (auto t = find_outgoing(id)) {
877
+ bool ok = false;
878
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) { t->status = Status::Paused; ok = true; t->cv.notify_all(); } }
879
+ if (ok) { send_simple(t->peer, OP_PAUSE, id); return true; }
880
+ }
881
+ if (auto t = find_incoming(peer, id)) {
882
+ bool ok = false;
883
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Active) { t->status = Status::Paused; ok = true; } }
884
+ if (ok) { send_simple(peer, OP_PAUSE, id); return true; }
885
+ }
886
+ return false;
887
+ }
888
+
889
+ bool FileTransfer::resume(const PeerId& peer, uint64_t id) {
890
+ if (auto t = find_outgoing(id)) {
891
+ bool ok = false;
892
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); ok = true; t->cv.notify_all(); } }
893
+ if (ok) { send_simple(t->peer, OP_RESUME, id); queue_send(id); return true; }
894
+ }
895
+ if (auto t = find_incoming(peer, id)) {
896
+ bool ok = false;
897
+ { std::lock_guard<std::mutex> lk(t->mtx); if (t->status == Status::Paused) { t->status = Status::Active; t->last_activity = std::chrono::steady_clock::now(); ok = true; } }
898
+ if (ok) { send_simple(peer, OP_RESUME, id); return true; }
899
+ }
900
+ return false;
901
+ }
902
+
903
+ // ── Progress callbacks ────────────────────────────────────────────────────────
904
+
905
+ void FileTransfer::emit_progress(const std::shared_ptr<Outgoing>& t) {
906
+ if (!progress_handler_) return;
907
+ const auto now = std::chrono::steady_clock::now();
908
+ Progress p;
909
+ { std::lock_guard<std::mutex> lk(t->mtx);
910
+ t->rate.sample(t->bytes_done, now);
911
+ p.id = t->id; p.peer = t->peer; p.direction = Direction::Sending; p.status = t->status;
912
+ p.bytes_transferred = t->bytes_done; p.total_bytes = t->total_bytes;
913
+ p.files_completed = t->files_done; p.total_files = static_cast<uint32_t>(t->files.size());
914
+ t->rate.fill(p, t->bytes_done, t->total_bytes, now); }
915
+ progress_handler_(p);
916
+ }
917
+
918
+ void FileTransfer::emit_progress(const std::shared_ptr<Incoming>& t) {
919
+ if (!progress_handler_) return;
920
+ const auto now = std::chrono::steady_clock::now();
921
+ Progress p;
922
+ { std::lock_guard<std::mutex> lk(t->mtx);
923
+ uint64_t total = 0; for (auto& f : t->files) total += f.size;
924
+ t->rate.sample(t->bytes_done, now);
925
+ p.id = t->id; p.peer = t->peer; p.direction = Direction::Receiving; p.status = t->status;
926
+ p.bytes_transferred = t->bytes_done; p.total_bytes = total;
927
+ p.files_completed = t->files_done; p.total_files = static_cast<uint32_t>(t->files.size());
928
+ t->rate.fill(p, t->bytes_done, total, now); }
929
+ progress_handler_(p);
930
+ }
931
+
932
+ // ── Maintenance: idle timeout + purge ────────────────────────────────────────
933
+
934
+ void FileTransfer::maintenance_loop() {
935
+ while (running_.load()) {
936
+ {
937
+ std::unique_lock<std::mutex> lk(maintenance_mutex_);
938
+ maintenance_cv_.wait_for(lk, std::chrono::seconds(2), [this] { return !running_.load(); });
939
+ }
940
+ if (!running_.load()) return;
941
+
942
+ const auto now = std::chrono::steady_clock::now();
943
+ const uint32_t timeout = config_.transfer_timeout_secs;
944
+
945
+ std::vector<std::shared_ptr<Outgoing>> out;
946
+ std::vector<std::shared_ptr<Incoming>> in;
947
+ {
948
+ std::lock_guard<std::mutex> lock(mutex_);
949
+ for (auto& [id, t] : outgoing_) out.push_back(t);
950
+ for (auto& [peer, m] : incoming_) for (auto& [id, t] : m) in.push_back(t);
951
+ }
952
+
953
+ for (auto& t : out) {
954
+ bool stale = false;
955
+ { std::lock_guard<std::mutex> lk(t->mtx);
956
+ if (!t->finished && t->status != Status::Paused &&
957
+ std::chrono::duration_cast<std::chrono::seconds>(now - t->last_activity).count() > timeout)
958
+ stale = true; }
959
+ if (stale) { LOG_WARN("filexfer", "outgoing transfer " << t->id << " timed out"); send_complete(t->peer, t->id, false); finish_outgoing(t, false); }
960
+ }
961
+ for (auto& t : in) {
962
+ bool stale = false;
963
+ { std::lock_guard<std::mutex> lk(t->mtx);
964
+ if (!t->finished && t->status != Status::Paused && t->status != Status::Pending &&
965
+ std::chrono::duration_cast<std::chrono::seconds>(now - t->last_activity).count() > timeout)
966
+ stale = true; }
967
+ if (stale) { LOG_WARN("filexfer", "incoming transfer " << t->id << " timed out"); send_complete(t->peer, t->id, false); finish_incoming(t, false, "timed out"); }
968
+ }
969
+ }
970
+ }
971
+
972
+ } // namespace librats