@cetapod/react-native-smb 1.0.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 (137) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +72 -0
  3. package/ReactNativeSmb.podspec +59 -0
  4. package/cpp/ios/OnLoad.mm +24 -0
  5. package/cpp/shared/HybridSMB.cpp +324 -0
  6. package/cpp/shared/HybridSMB.hpp +102 -0
  7. package/cpp/shared/ReactNativeSmb.hpp +428 -0
  8. package/cpp/shared/connection/ContextRequest.hpp +27 -0
  9. package/cpp/shared/connection/ContextRequestQueue.cpp +85 -0
  10. package/cpp/shared/connection/ContextRequestQueue.hpp +28 -0
  11. package/cpp/shared/connection/MetadataContextLease.hpp +33 -0
  12. package/cpp/shared/connection/PoolContextHandle.cpp +35 -0
  13. package/cpp/shared/connection/PoolContextHandle.hpp +44 -0
  14. package/cpp/shared/connection/PoolSlot.cpp +73 -0
  15. package/cpp/shared/connection/PoolSlot.hpp +46 -0
  16. package/cpp/shared/connection/PoolTypes.hpp +41 -0
  17. package/cpp/shared/connection/SmbConnection.cpp +385 -0
  18. package/cpp/shared/connection/SmbConnection.hpp +93 -0
  19. package/cpp/shared/connection/SmbConnectionPool.cpp +316 -0
  20. package/cpp/shared/connection/SmbConnectionPool.hpp +84 -0
  21. package/cpp/shared/core/CancellationToken.hpp +22 -0
  22. package/cpp/shared/core/OperatorContext.hpp +24 -0
  23. package/cpp/shared/core/SmbEnums.hpp +126 -0
  24. package/cpp/shared/core/SmbTask.cpp +131 -0
  25. package/cpp/shared/core/SmbTask.hpp +52 -0
  26. package/cpp/shared/core/SmbTaskCore.cpp +222 -0
  27. package/cpp/shared/core/SmbTaskCore.hpp +107 -0
  28. package/cpp/shared/core/SmbTypes.hpp +35 -0
  29. package/cpp/shared/core/TaskObserverHub.cpp +166 -0
  30. package/cpp/shared/core/TaskObserverHub.hpp +66 -0
  31. package/cpp/shared/core/TaskSnapshotCodec.cpp +32 -0
  32. package/cpp/shared/core/TaskSnapshotCodec.hpp +41 -0
  33. package/cpp/shared/io/SmbCopyTree.hpp +114 -0
  34. package/cpp/shared/io/SmbDirScan.hpp +80 -0
  35. package/cpp/shared/io/SmbFileIO.cpp +1355 -0
  36. package/cpp/shared/io/SmbFileIO.hpp +45 -0
  37. package/cpp/shared/io/SmbPathUtil.hpp +75 -0
  38. package/cpp/shared/io/SmbSecurity.cpp +308 -0
  39. package/cpp/shared/io/SmbSecurity.hpp +32 -0
  40. package/cpp/shared/io/SmbTreeOps.hpp +112 -0
  41. package/cpp/shared/operators/Operator.hpp +35 -0
  42. package/cpp/shared/operators/OperatorBase.cpp +57 -0
  43. package/cpp/shared/operators/OperatorBase.hpp +57 -0
  44. package/cpp/shared/operators/connection/ConnectOperator.cpp +19 -0
  45. package/cpp/shared/operators/connection/ConnectOperator.hpp +22 -0
  46. package/cpp/shared/operators/connection/ConnectShareOperator.cpp +19 -0
  47. package/cpp/shared/operators/connection/ConnectShareOperator.hpp +21 -0
  48. package/cpp/shared/operators/connection/DisconnectOperator.cpp +12 -0
  49. package/cpp/shared/operators/connection/DisconnectOperator.hpp +15 -0
  50. package/cpp/shared/operators/connection/InitializeOperator.cpp +12 -0
  51. package/cpp/shared/operators/connection/InitializeOperator.hpp +22 -0
  52. package/cpp/shared/operators/connection/ListSharesOperator.cpp +13 -0
  53. package/cpp/shared/operators/connection/ListSharesOperator.hpp +18 -0
  54. package/cpp/shared/operators/listing/GetPathInfoOperator.cpp +43 -0
  55. package/cpp/shared/operators/listing/GetPathInfoOperator.hpp +21 -0
  56. package/cpp/shared/operators/listing/GetSecurityDescriptorOperator.cpp +202 -0
  57. package/cpp/shared/operators/listing/GetSecurityDescriptorOperator.hpp +22 -0
  58. package/cpp/shared/operators/listing/ListDirectoryOperator.cpp +20 -0
  59. package/cpp/shared/operators/listing/ListDirectoryOperator.hpp +24 -0
  60. package/cpp/shared/operators/mutate/CopyItemOperator.cpp +19 -0
  61. package/cpp/shared/operators/mutate/CopyItemOperator.hpp +25 -0
  62. package/cpp/shared/operators/mutate/CreateDirectoryOperator.cpp +28 -0
  63. package/cpp/shared/operators/mutate/CreateDirectoryOperator.hpp +20 -0
  64. package/cpp/shared/operators/mutate/DeleteItemOperator.cpp +96 -0
  65. package/cpp/shared/operators/mutate/DeleteItemOperator.hpp +23 -0
  66. package/cpp/shared/operators/mutate/DuplicateItemOperator.cpp +48 -0
  67. package/cpp/shared/operators/mutate/DuplicateItemOperator.hpp +23 -0
  68. package/cpp/shared/operators/mutate/MoveItemOperator.cpp +32 -0
  69. package/cpp/shared/operators/mutate/MoveItemOperator.hpp +21 -0
  70. package/cpp/shared/operators/mutate/RenameItemOperator.cpp +37 -0
  71. package/cpp/shared/operators/mutate/RenameItemOperator.hpp +21 -0
  72. package/cpp/shared/operators/transfer/DownloadFileOperator.cpp +31 -0
  73. package/cpp/shared/operators/transfer/DownloadFileOperator.hpp +21 -0
  74. package/cpp/shared/operators/transfer/UploadFileOperator.cpp +27 -0
  75. package/cpp/shared/operators/transfer/UploadFileOperator.hpp +21 -0
  76. package/cpp/shared/util/SmbErrorMapper.hpp +229 -0
  77. package/cpp/shared/util/SmbLog.hpp +20 -0
  78. package/docs/API.md +105 -0
  79. package/docs/ARCHITECTURE.md +70 -0
  80. package/docs/TASK_MODEL.md +149 -0
  81. package/ios/include/smb2/libsmb2-dcerpc-lsa.h +172 -0
  82. package/ios/include/smb2/libsmb2-dcerpc-srvsvc.h +180 -0
  83. package/ios/include/smb2/libsmb2-dcerpc.h +155 -0
  84. package/ios/include/smb2/libsmb2-raw.h +497 -0
  85. package/ios/include/smb2/libsmb2.h +1384 -0
  86. package/ios/include/smb2/smb2-errors.h +549 -0
  87. package/ios/include/smb2/smb2.h +1251 -0
  88. package/ios/libs/device/libsmb2-device.a +0 -0
  89. package/ios/libs/libsmb2.xcframework/Info.plist +48 -0
  90. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/libsmb2-dcerpc-lsa.h +172 -0
  91. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/libsmb2-dcerpc-srvsvc.h +180 -0
  92. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/libsmb2-dcerpc.h +155 -0
  93. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/libsmb2-raw.h +497 -0
  94. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/libsmb2.h +1384 -0
  95. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/smb2-errors.h +549 -0
  96. package/ios/libs/libsmb2.xcframework/ios-arm64/Headers/smb2/smb2.h +1251 -0
  97. package/ios/libs/libsmb2.xcframework/ios-arm64/libsmb2.a +0 -0
  98. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/libsmb2-dcerpc-lsa.h +172 -0
  99. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/libsmb2-dcerpc-srvsvc.h +180 -0
  100. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/libsmb2-dcerpc.h +155 -0
  101. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/libsmb2-raw.h +497 -0
  102. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/libsmb2.h +1384 -0
  103. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/smb2-errors.h +549 -0
  104. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/Headers/smb2/smb2.h +1251 -0
  105. package/ios/libs/libsmb2.xcframework/ios-arm64_x86_64-simulator/libsmb2.a +0 -0
  106. package/ios/libs/simulator/libsmb2-simulator.a +0 -0
  107. package/lib/ReactNativeSmb.d.ts +200 -0
  108. package/lib/ReactNativeSmb.js +1 -0
  109. package/lib/bridge/decode.d.ts +4 -0
  110. package/lib/bridge/decode.js +92 -0
  111. package/lib/core/SMB.d.ts +3 -0
  112. package/lib/core/SMB.js +4 -0
  113. package/lib/core/SmbClient.d.ts +35 -0
  114. package/lib/core/SmbClient.js +99 -0
  115. package/lib/core/SmbTask.d.ts +41 -0
  116. package/lib/core/SmbTask.js +74 -0
  117. package/lib/core/TransferStore.d.ts +22 -0
  118. package/lib/core/TransferStore.js +88 -0
  119. package/lib/core/ids.d.ts +1 -0
  120. package/lib/core/ids.js +3 -0
  121. package/lib/core/taskResult.d.ts +3 -0
  122. package/lib/core/taskResult.js +52 -0
  123. package/lib/hooks/useSubscribe.d.ts +20 -0
  124. package/lib/hooks/useSubscribe.js +36 -0
  125. package/lib/hooks/useTransfers.d.ts +7 -0
  126. package/lib/hooks/useTransfers.js +29 -0
  127. package/lib/index.d.ts +7 -0
  128. package/lib/index.js +7 -0
  129. package/lib/labels.d.ts +6 -0
  130. package/lib/labels.js +71 -0
  131. package/lib/types.d.ts +141 -0
  132. package/lib/types.js +59 -0
  133. package/lib/utils.d.ts +79 -0
  134. package/lib/utils.js +153 -0
  135. package/package.json +63 -0
  136. package/scripts/build-libsmb2.sh +143 -0
  137. package/scripts/create-xcframework.sh +84 -0
@@ -0,0 +1,1355 @@
1
+ #include "SmbFileIO.hpp" // sibling
2
+
3
+ #include <fcntl.h>
4
+ #include <poll.h>
5
+ #include <smb2/smb2.h>
6
+ #include <smb2/libsmb2.h>
7
+
8
+ #include <algorithm>
9
+ #include <cerrno>
10
+ #include <cstdio>
11
+ #include <fstream>
12
+ #include <stdexcept>
13
+ #include <utility>
14
+
15
+ #include "../connection/SmbConnection.hpp"
16
+ #include "../core/CancellationToken.hpp"
17
+ #include "../util/SmbErrorMapper.hpp"
18
+ #include "../util/SmbLog.hpp"
19
+
20
+ namespace react_native_smb {
21
+
22
+ // Progress coalescing: emit only when >=256 KiB or >=0.5% have elapsed since
23
+ // the last emission. Declared here so both the legacy sync path and the
24
+ // async pipelined paths can use it.
25
+ static constexpr int64_t kProgressMinBytes = 256 * 1024; // 256 KiB
26
+ static constexpr double kProgressMinFraction = 0.005; // 0.5%
27
+
28
+ // Keep the async pipeline responsive to cancellation. The slot can only be
29
+ // returned after already-submitted SMB requests finish, so cap per-request
30
+ // bytes instead of tearing down the connection on cancel.
31
+ static constexpr uint32_t kAsyncChunkSizeCap = 256 * 1024; // 256 KiB
32
+
33
+ inline bool shouldEmitProgress(int64_t current, int64_t total, int64_t& lastEmitted) {
34
+ if (total <= 0) return false;
35
+ if (current - lastEmitted >= kProgressMinBytes) {
36
+ lastEmitted = current;
37
+ return true;
38
+ }
39
+ double delta = static_cast<double>(current - lastEmitted) / static_cast<double>(total);
40
+ if (delta >= kProgressMinFraction) {
41
+ lastEmitted = current;
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ inline uint32_t capAsyncChunkSize(uint32_t negotiatedSize) {
48
+ if (negotiatedSize == 0) negotiatedSize = 1024 * 1024;
49
+ return std::min(negotiatedSize, kAsyncChunkSizeCap);
50
+ }
51
+
52
+ int64_t smbReadFile(void* ctxVoid, const std::string& remotePath, const std::string& localPath, std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
53
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
54
+
55
+ smb2fh* fh = smb2_open(ctx, remotePath.c_str(), O_RDONLY);
56
+ if (!fh) {
57
+ std::string error = smb2_get_error(ctx);
58
+ std::string msg = "Download Failed: Could not open remote file for reading: " + remotePath + ". Error: " + error;
59
+ throw std::runtime_error(msg);
60
+ }
61
+
62
+ struct smb2_stat_64 stat;
63
+ const int fstatResult = smb2_fstat(ctx, fh, &stat);
64
+ if (fstatResult < 0) {
65
+ smb2_close(ctx, fh);
66
+ std::string error = smb2_get_error(ctx);
67
+ std::string msg = "Download Failed: Could not get stats for file: " + remotePath + ". Error: " + error;
68
+ throw std::runtime_error(msg);
69
+ }
70
+
71
+ int64_t fileSize = static_cast<int64_t>(stat.smb2_size);
72
+ int64_t totalBytesRead = 0;
73
+
74
+ std::ofstream localFile(localPath, std::ios::binary | std::ios::trunc);
75
+ if (!localFile.is_open()) {
76
+ smb2_close(ctx, fh);
77
+ std::string msg = "Download Failed: Could not create local file at: " + localPath + ". Check permissions.";
78
+ throw std::runtime_error(msg);
79
+ }
80
+
81
+ auto cleanup = [&]() {
82
+ localFile.close();
83
+ smb2_close(ctx, fh);
84
+ };
85
+
86
+ try {
87
+ constexpr size_t BUFFER_SIZE = 1024 * 1024;
88
+ std::vector<uint8_t> buffer(BUFFER_SIZE);
89
+ ssize_t bytesRead;
90
+
91
+ if (fileSize > 0) {
92
+ if (progressHandler) {
93
+ progressHandler(0.0, static_cast<double>(fileSize));
94
+ }
95
+
96
+ int64_t lastEmittedDl = 0;
97
+ while ((bytesRead = smb2_read(ctx, fh, buffer.data(), BUFFER_SIZE)) > 0) {
98
+ // Check for cancellation
99
+ if (cancel.cancelled()) {
100
+ cleanup();
101
+ std::remove(localPath.c_str());
102
+ return 0;
103
+ }
104
+
105
+ localFile.write(reinterpret_cast<const char*>(buffer.data()), bytesRead);
106
+ if (localFile.fail()) {
107
+ std::string msg = "Failed to write to local file '" + localPath + "'";
108
+ throw std::runtime_error(msg);
109
+ }
110
+
111
+ totalBytesRead += bytesRead;
112
+
113
+ if (progressHandler && shouldEmitProgress(totalBytesRead, fileSize, lastEmittedDl)) {
114
+ progressHandler(static_cast<double>(totalBytesRead), static_cast<double>(fileSize));
115
+ }
116
+
117
+ if (totalBytesRead > fileSize) {
118
+ std::string msg = "Read more bytes than expected file size";
119
+ throw std::runtime_error(msg);
120
+ }
121
+ }
122
+
123
+ if (bytesRead < 0) {
124
+ std::string error = smb2_get_error(ctx);
125
+ std::string msg = "Failed to read from remote file '" + remotePath + "': " + error;
126
+ throw std::runtime_error(msg);
127
+ }
128
+
129
+ localFile.flush();
130
+ if (localFile.fail()) {
131
+ std::string msg = "Failed to flush data to local file '" + localPath + "'";
132
+ throw std::runtime_error(msg);
133
+ }
134
+ }
135
+
136
+ if (totalBytesRead != fileSize) {
137
+ std::string msg = "Downloaded file size mismatch. Expected: " + std::to_string(fileSize) + ", Got: " + std::to_string(totalBytesRead);
138
+ throw std::runtime_error(msg);
139
+ }
140
+
141
+ cleanup();
142
+
143
+ if (progressHandler) {
144
+ progressHandler(static_cast<double>(fileSize), static_cast<double>(fileSize));
145
+ }
146
+ } catch (const std::exception& e) {
147
+ cleanup();
148
+ try {
149
+ std::remove(localPath.c_str());
150
+ } catch (const std::exception& removeError) {
151
+ // Failed to remove incomplete file, ignoring to preserve original error
152
+ }
153
+ throw;
154
+ }
155
+
156
+ return totalBytesRead;
157
+ }
158
+
159
+ int64_t smbWriteFile(void* ctxVoid, const std::string& localPath, const std::string& remotePath, std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
160
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
161
+
162
+ std::ifstream localFile(localPath, std::ios::binary | std::ios::ate);
163
+ if (!localFile.is_open()) {
164
+ std::string msg = "Upload Failed: Could not open local file for reading: " + localPath;
165
+ throw std::runtime_error(msg);
166
+ }
167
+
168
+ std::streamsize localFileSize = localFile.tellg();
169
+ localFile.seekg(0, std::ios::beg);
170
+
171
+ if (localFileSize < 0) {
172
+ std::string msg = "Upload Failed: Could not determine size of local file: " + localPath;
173
+ throw std::runtime_error(msg);
174
+ }
175
+
176
+ smb2fh* fh = smb2_open(ctx, remotePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC);
177
+ if (!fh) {
178
+ std::string error = smb2_get_error(ctx);
179
+ std::string msg = "Upload Failed: Could not create remote file: " + remotePath + ". Error: " + error;
180
+ throw std::runtime_error(msg);
181
+ }
182
+
183
+ auto cleanup = [&]() {
184
+ localFile.close();
185
+ smb2_close(ctx, fh);
186
+ };
187
+
188
+ try {
189
+ constexpr size_t BUFFER_SIZE = 1024 * 1024;
190
+ std::vector<uint8_t> buffer(BUFFER_SIZE);
191
+ int64_t totalBytesWritten = 0;
192
+
193
+ if (progressHandler) {
194
+ progressHandler(0.0, static_cast<double>(localFileSize));
195
+ }
196
+
197
+ while (localFile.read(reinterpret_cast<char*>(buffer.data()), BUFFER_SIZE) || localFile.gcount() > 0) {
198
+ // Check for cancellation
199
+ if (cancel.cancelled()) {
200
+ cleanup();
201
+ smb2_unlink(ctx, remotePath.c_str());
202
+ return 0;
203
+ }
204
+
205
+ auto bytesToWrite = static_cast<uint32_t>(localFile.gcount());
206
+ if (localFile.bad()) {
207
+ std::string msg = "Error reading from local file '" + localPath + "'";
208
+ throw std::runtime_error(msg);
209
+ }
210
+
211
+ ssize_t bytesWritten = smb2_write(ctx, fh, buffer.data(), bytesToWrite);
212
+ if (bytesWritten < 0) {
213
+ std::string error = smb2_get_error(ctx);
214
+ std::string msg = "Failed to write to remote file '" + remotePath + "': " + error;
215
+ throw std::runtime_error(msg);
216
+ }
217
+
218
+ if (static_cast<uint32_t>(bytesWritten) != bytesToWrite) {
219
+ std::string msg = "Incomplete write to remote file '" + remotePath + "'";
220
+ throw std::runtime_error(msg);
221
+ }
222
+
223
+ totalBytesWritten += bytesWritten;
224
+
225
+ if (progressHandler) {
226
+ progressHandler(static_cast<double>(totalBytesWritten), static_cast<double>(localFileSize));
227
+ }
228
+
229
+ if (totalBytesWritten > localFileSize) {
230
+ std::string msg = "Written more bytes than local file size";
231
+ throw std::runtime_error(msg);
232
+ }
233
+ }
234
+
235
+ if (smb2_fsync(ctx, fh) < 0) {
236
+ }
237
+
238
+ if (totalBytesWritten != localFileSize) {
239
+ std::string msg = "Uploaded file size mismatch. Expected: " + std::to_string(localFileSize) + ", Uploaded: " + std::to_string(totalBytesWritten);
240
+ throw std::runtime_error(msg);
241
+ }
242
+
243
+ if (progressHandler) {
244
+ progressHandler(static_cast<double>(localFileSize), static_cast<double>(localFileSize));
245
+ }
246
+
247
+ cleanup();
248
+ return totalBytesWritten;
249
+ } catch (const std::exception& e) {
250
+ cleanup();
251
+ try {
252
+ smb2_unlink(ctx, remotePath.c_str());
253
+ } catch (const std::exception& unlinkError) {
254
+ }
255
+
256
+ throw;
257
+ }
258
+ }
259
+
260
+ int64_t smbCopyFile(void* ctxVoid, const std::string& fromPath, const std::string& toPath, std::shared_ptr<int64_t> totalBytesCopied, int64_t totalSize,
261
+ std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
262
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
263
+
264
+ // Open source file
265
+ smb2fh* sourceFh = smb2_open(ctx, fromPath.c_str(), O_RDONLY);
266
+ if (!sourceFh) {
267
+ std::string error = smb2_get_error(ctx);
268
+ std::string msg = "Failed to open source file '" + fromPath + "': " + error;
269
+ throw std::runtime_error(msg);
270
+ }
271
+
272
+ // Open destination file
273
+ smb2fh* destFh = smb2_open(ctx, toPath.c_str(), O_WRONLY | O_CREAT | O_EXCL);
274
+ if (!destFh) {
275
+ smb2_close(ctx, sourceFh);
276
+ std::string error = smb2_get_error(ctx);
277
+ std::string msg = "Failed to create destination file '" + toPath + "': " + error;
278
+ throw std::runtime_error(msg);
279
+ }
280
+
281
+ auto cleanup = [&]() {
282
+ smb2_close(ctx, sourceFh);
283
+ smb2_close(ctx, destFh);
284
+ };
285
+
286
+ try {
287
+ constexpr size_t BUFFER_SIZE = 64 * 1024;
288
+ std::vector<uint8_t> buffer(BUFFER_SIZE);
289
+ int64_t fileBytescopied = 0;
290
+
291
+ ssize_t bytesRead;
292
+ while ((bytesRead = smb2_read(ctx, sourceFh, buffer.data(), BUFFER_SIZE)) > 0) {
293
+ // Check for cancellation
294
+ if (cancel.cancelled()) {
295
+ cleanup();
296
+ smb2_unlink(ctx, toPath.c_str());
297
+ return 0;
298
+ }
299
+
300
+ ssize_t bytesWritten = smb2_write(ctx, destFh, buffer.data(), static_cast<uint32_t>(bytesRead));
301
+ if (bytesWritten < 0) {
302
+ std::string error = smb2_get_error(ctx);
303
+ std::string msg = "Failed to write to destination file '" + toPath + "': " + error;
304
+ throw std::runtime_error(msg);
305
+ }
306
+ if (bytesWritten != bytesRead) {
307
+ std::string msg = "Incomplete write to destination file '" + toPath + "'";
308
+ throw std::runtime_error(msg);
309
+ }
310
+
311
+ fileBytescopied += bytesWritten;
312
+
313
+ // Update cumulative progress if counter is provided
314
+ if (totalBytesCopied) {
315
+ *totalBytesCopied += bytesWritten;
316
+ }
317
+
318
+ // Report progress if handler is provided
319
+ if (progressHandler && totalSize > 0) {
320
+ int64_t currentProgress = totalBytesCopied ? *totalBytesCopied : fileBytescopied;
321
+ progressHandler(static_cast<double>(currentProgress), static_cast<double>(totalSize));
322
+ }
323
+ }
324
+
325
+ if (bytesRead < 0) {
326
+ std::string error = smb2_get_error(ctx);
327
+ std::string msg = "Failed to read from source file '" + fromPath + "': " + error;
328
+ throw std::runtime_error(msg);
329
+ }
330
+
331
+ // Sync the destination file
332
+ if (smb2_fsync(ctx, destFh) < 0) {
333
+ // Sync failed, but continue
334
+ }
335
+
336
+ cleanup();
337
+
338
+ return fileBytescopied;
339
+ } catch (const std::exception& e) {
340
+ cleanup();
341
+ // Try to delete the incomplete destination file
342
+ try {
343
+ smb2_unlink(ctx, toPath.c_str());
344
+ } catch (const std::exception& unlinkError) {
345
+ }
346
+ throw;
347
+ }
348
+ }
349
+
350
+ // ═══════════════════════════════════════════════════════════════════════════════
351
+ // Async pipelined file I/O
352
+ //
353
+ // These use smb2_pread_async / smb2_pwrite_async to keep up to
354
+ // kMaxInFlight chunks on the wire simultaneously, exploiting SMB2
355
+ // credit-based pipelining for significantly higher throughput.
356
+ //
357
+ // Lifecycle (synchronous, on caller's Nitro worker thread under mutex):
358
+ // 1. Caller opens the remote file, computes file size & chunk size.
359
+ // 2. Caller kicks off the first batch of smb2_pread_async / pwrite_async.
360
+ // 3. Caller drives a local poll()/smb2_service() loop. Each
361
+ // smb2_service() call fires readCb/writeCb on the same thread; those
362
+ // callbacks copy bytes to/from the local file and submit the next
363
+ // chunk.
364
+ // 4. The poll loop exits when all chunks completed / errored / cancelled.
365
+ // 5. Caller returns the byte count, or throws on error.
366
+ //
367
+ // Safety:
368
+ // - State pointer passed as cb_data lives on the caller's stack frame
369
+ // (the function does not return until the poll loop completes), so
370
+ // callbacks always have a valid `this`.
371
+ // - All callbacks run on the calling thread, while it holds
372
+ // connectionMutex_, so libsmb2 sees a single-threaded user.
373
+ // ═══════════════════════════════════════════════════════════════════════════════
374
+
375
+ namespace {
376
+
377
+ // Number of chunks kept in flight on a single smb2_context.
378
+ static constexpr int kMaxInFlight = 8;
379
+
380
+ // Progress coalescing thresholds. Avoid invoking the JS-side progress
381
+ // callback on every chunk completion — that callback crosses the JNI / JSI
382
+ // bridge and can back-pressure the transfer thread while connectionMutex_
383
+ // is held.
384
+ // - Always emit when at least kProgressMinBytes have elapsed since the
385
+ // last emission, OR
386
+ // - the relative progress moved at least kProgressMinFraction (0.5%).
387
+ // Final 100% is emitted explicitly by the caller of the async functions.
388
+
389
+ // ─── Async pipelined read ────────────────────────────────────────────────────
390
+ //
391
+ // Slot lifecycle:
392
+ // 1. submitNextRead() picks an idle slot, fills (buf, baseOffset, requested,
393
+ // received=0) and issues smb2_pread_async on the slot's full buffer.
394
+ // 2. readCb() may receive a SHORT READ (status < requested). When this
395
+ // happens the slot stays busy: we re-submit smb2_pread_async on the
396
+ // tail (buf + received, requested - received, baseOffset + received).
397
+ // The slot is freed only when received == requested.
398
+ // 3. findSlot() locates a slot by range containment because the in-flight
399
+ // buffer pointer may be in the middle of the slot's buffer (tail re-issue).
400
+
401
+ struct AsyncReadState {
402
+ smb2_context* ctx = nullptr;
403
+ smb2fh* fh = nullptr;
404
+ FILE* outFile = nullptr;
405
+
406
+ int64_t fileSize = 0;
407
+ uint32_t chunkSize = 0;
408
+
409
+ uint64_t nextOffset = 0;
410
+ int64_t bytesCompleted = 0;
411
+ int64_t lastEmittedBytes = 0;
412
+ int inFlight = 0;
413
+ bool errored = false;
414
+ std::string errorMsg;
415
+
416
+ struct ChunkSlot {
417
+ std::vector<uint8_t> buf;
418
+ uint64_t baseOffset = 0; // file offset corresponding to buf[0]
419
+ uint32_t requested = 0; // total bytes requested for this chunk
420
+ uint32_t received = 0; // bytes delivered so far (across re-submits)
421
+ bool available = true;
422
+ };
423
+ ChunkSlot slots[kMaxInFlight];
424
+
425
+ std::function<void(double, double)> progressHandler;
426
+ CancellationToken cancel;
427
+ std::string localPath;
428
+
429
+ bool isDone() const { return errored || bytesCompleted >= fileSize; }
430
+
431
+ void markCancelled() {
432
+ if (!errored) {
433
+ errored = true;
434
+ errorMsg = "Download cancelled";
435
+ }
436
+ }
437
+
438
+ // Safety: every byte has been requested, nothing is in flight, all
439
+ // slots are free, but bytesCompleted < fileSize. Without this guard,
440
+ // a residual short-read deficit would leave the poll loop hanging.
441
+ bool isStuck() const {
442
+ if (errored || inFlight > 0) return false;
443
+ if (nextOffset < static_cast<uint64_t>(fileSize)) return false;
444
+ for (int i = 0; i < kMaxInFlight; i++) {
445
+ if (!slots[i].available) return false;
446
+ }
447
+ return bytesCompleted < fileSize;
448
+ }
449
+
450
+ // Locate the slot whose buffer range contains bufPtr (the in-flight
451
+ // smb2 callback may point into the middle of the slot's buffer when
452
+ // the slot is mid-recovery from a short read).
453
+ int findSlot(const uint8_t* bufPtr) {
454
+ for (int i = 0; i < kMaxInFlight; i++) {
455
+ if (slots[i].available) continue;
456
+ const uint8_t* base = slots[i].buf.data();
457
+ if (bufPtr >= base && bufPtr < base + slots[i].buf.size()) return i;
458
+ }
459
+ return -1;
460
+ }
461
+
462
+ void submitNextRead() {
463
+ if (errored || nextOffset >= static_cast<uint64_t>(fileSize)) return;
464
+ if (cancel.cancelled()) {
465
+ markCancelled();
466
+ return;
467
+ }
468
+
469
+ int slot = -1;
470
+ for (int i = 0; i < kMaxInFlight; i++) {
471
+ if (slots[i].available) {
472
+ slot = i;
473
+ break;
474
+ }
475
+ }
476
+ if (slot < 0) return;
477
+
478
+ uint64_t remaining = static_cast<uint64_t>(fileSize) - nextOffset;
479
+ uint32_t len = static_cast<uint32_t>(std::min(static_cast<uint64_t>(chunkSize), remaining));
480
+
481
+ slots[slot].buf.resize(len);
482
+ slots[slot].baseOffset = nextOffset;
483
+ slots[slot].requested = len;
484
+ slots[slot].received = 0;
485
+ slots[slot].available = false;
486
+
487
+ uint64_t offset = nextOffset;
488
+ nextOffset += len;
489
+
490
+ int ret = smb2_pread_async(ctx, fh, slots[slot].buf.data(), len, offset, readCb, this);
491
+ if (ret < 0) {
492
+ errored = true;
493
+ errorMsg = std::string("Download Failed: smb2_pread_async: ") + smb2_get_error(ctx);
494
+ slots[slot].available = true;
495
+ nextOffset -= len;
496
+ return;
497
+ }
498
+ inFlight++;
499
+ }
500
+
501
+ static void readCb(smb2_context* smb2, int status, void* command_data, void* cb_data) {
502
+ auto* st = static_cast<AsyncReadState*>(cb_data);
503
+ st->inFlight--;
504
+
505
+ auto* cbd = static_cast<smb2_read_cb_data*>(command_data);
506
+
507
+ // ── Locate the slot for this completion (range-contains lookup) ──
508
+ int slotIdx = (cbd ? st->findSlot(cbd->buf) : -1);
509
+
510
+ // ── Error / already-errored path ──
511
+ if (st->errored) {
512
+ if (slotIdx >= 0) st->slots[slotIdx].available = true;
513
+ return;
514
+ }
515
+ if (status < 0) {
516
+ st->errored = true;
517
+ st->errorMsg = std::string("Download Failed: read error: ") + smb2_get_error(smb2);
518
+ if (slotIdx >= 0) st->slots[slotIdx].available = true;
519
+ return;
520
+ }
521
+ if (slotIdx < 0) {
522
+ st->errored = true;
523
+ st->errorMsg = "Download Failed: internal slot lookup failed";
524
+ return;
525
+ }
526
+
527
+ auto& slot = st->slots[slotIdx];
528
+
529
+ // File offset where THIS returned chunk starts (slot may already
530
+ // hold `received` bytes from prior short-read recoveries).
531
+ const uint64_t writeOffset = slot.baseOffset + slot.received;
532
+
533
+ // ── Persist the received bytes to local file ──
534
+ if (st->outFile && status > 0) {
535
+ if (fseeko(st->outFile, static_cast<off_t>(writeOffset), SEEK_SET) != 0 || fwrite(cbd->buf, 1, static_cast<size_t>(status), st->outFile) != static_cast<size_t>(status)) {
536
+ st->errored = true;
537
+ st->errorMsg = "Download Failed: local file write error";
538
+ slot.available = true;
539
+ return;
540
+ }
541
+ }
542
+
543
+ slot.received += static_cast<uint32_t>(status);
544
+ st->bytesCompleted += status;
545
+
546
+ // ── L1: short-read recovery ─────────────────────────────────────
547
+ // If the server returned fewer bytes than requested for this slot,
548
+ // re-submit the tail on the SAME slot without freeing it.
549
+ if (slot.received < slot.requested) {
550
+ if (status == 0) {
551
+ // Zero-byte completion before fileSize → unrecoverable.
552
+ st->errored = true;
553
+ st->errorMsg = "Download Failed: unexpected EOF (zero-byte read)";
554
+ slot.available = true;
555
+ return;
556
+ }
557
+ uint32_t tailLen = slot.requested - slot.received;
558
+ uint64_t tailOffset = slot.baseOffset + slot.received;
559
+ SMB_LOG("AsyncReadState: short read got=%d requested=%u at offset=%llu — re-issuing tail %u bytes at %llu", status, slot.requested, static_cast<unsigned long long>(slot.baseOffset),
560
+ tailLen, static_cast<unsigned long long>(tailOffset));
561
+ int ret = smb2_pread_async(st->ctx, st->fh, slot.buf.data() + slot.received, tailLen, tailOffset, readCb, st);
562
+ if (ret < 0) {
563
+ st->errored = true;
564
+ st->errorMsg = std::string("Download Failed: short-read recovery smb2_pread_async: ") + smb2_get_error(st->ctx);
565
+ slot.available = true;
566
+ return;
567
+ }
568
+ st->inFlight++;
569
+ return;
570
+ }
571
+
572
+ // ── Slot fully satisfied ──
573
+ slot.available = true;
574
+
575
+ if (st->progressHandler && st->bytesCompleted < st->fileSize && shouldEmitProgress(st->bytesCompleted, st->fileSize, st->lastEmittedBytes)) {
576
+ st->progressHandler(static_cast<double>(st->bytesCompleted), static_cast<double>(st->fileSize));
577
+ }
578
+
579
+ st->submitNextRead();
580
+ }
581
+
582
+ void cleanup() {
583
+ if (outFile) {
584
+ fflush(outFile);
585
+ fclose(outFile);
586
+ outFile = nullptr;
587
+ }
588
+ if (fh && ctx) {
589
+ smb2_close(ctx, fh);
590
+ fh = nullptr;
591
+ }
592
+ }
593
+ };
594
+
595
+ // ─── Async pipelined write ───────────────────────────────────────────────────
596
+ //
597
+ // Symmetric to AsyncReadState: a slot is kept busy until its full requested
598
+ // byte range has been acknowledged by the server. Short writes are recovered
599
+ // by re-issuing smb2_pwrite_async on the un-written tail of the same slot.
600
+
601
+ struct AsyncWriteState {
602
+ smb2_context* ctx = nullptr;
603
+ smb2fh* fh = nullptr;
604
+ FILE* inFile = nullptr;
605
+
606
+ int64_t fileSize = 0;
607
+ uint32_t chunkSize = 0;
608
+
609
+ uint64_t nextOffset = 0;
610
+ int64_t bytesCompleted = 0;
611
+ int64_t lastEmittedBytes = 0;
612
+ int inFlight = 0;
613
+ bool errored = false;
614
+ std::string errorMsg;
615
+
616
+ struct ChunkSlot {
617
+ std::vector<uint8_t> buf;
618
+ uint64_t baseOffset = 0;
619
+ uint32_t requested = 0;
620
+ uint32_t written = 0;
621
+ bool available = true;
622
+ };
623
+ ChunkSlot slots[kMaxInFlight];
624
+
625
+ std::function<void(double, double)> progressHandler;
626
+ CancellationToken cancel;
627
+ std::string remotePath;
628
+
629
+ bool isDone() const { return errored || bytesCompleted >= fileSize; }
630
+
631
+ bool isStuck() const {
632
+ if (errored || inFlight > 0) return false;
633
+ if (nextOffset < static_cast<uint64_t>(fileSize)) return false;
634
+ for (int i = 0; i < kMaxInFlight; i++) {
635
+ if (!slots[i].available) return false;
636
+ }
637
+ return bytesCompleted < fileSize;
638
+ }
639
+
640
+ // Range-contains lookup (tail re-issues use a mid-buffer pointer).
641
+ int findSlot(const uint8_t* bufPtr) {
642
+ for (int i = 0; i < kMaxInFlight; i++) {
643
+ if (slots[i].available) continue;
644
+ const uint8_t* base = slots[i].buf.data();
645
+ if (bufPtr >= base && bufPtr < base + slots[i].buf.size()) return i;
646
+ }
647
+ return -1;
648
+ }
649
+
650
+ void submitNextWrite() {
651
+ if (errored || nextOffset >= static_cast<uint64_t>(fileSize)) return;
652
+ if (cancel.cancelled()) {
653
+ errored = true;
654
+ errorMsg = "Upload cancelled";
655
+ return;
656
+ }
657
+
658
+ int slot = -1;
659
+ for (int i = 0; i < kMaxInFlight; i++) {
660
+ if (slots[i].available) {
661
+ slot = i;
662
+ break;
663
+ }
664
+ }
665
+ if (slot < 0) return;
666
+
667
+ uint64_t remaining = static_cast<uint64_t>(fileSize) - nextOffset;
668
+ uint32_t len = static_cast<uint32_t>(std::min(static_cast<uint64_t>(chunkSize), remaining));
669
+
670
+ slots[slot].buf.resize(len);
671
+
672
+ // Read chunk from local file
673
+ if (fseeko(inFile, static_cast<off_t>(nextOffset), SEEK_SET) != 0 || fread(slots[slot].buf.data(), 1, len, inFile) != len) {
674
+ errored = true;
675
+ errorMsg = "Upload Failed: could not read from local file";
676
+ return;
677
+ }
678
+
679
+ slots[slot].baseOffset = nextOffset;
680
+ slots[slot].requested = len;
681
+ slots[slot].written = 0;
682
+ slots[slot].available = false;
683
+ uint64_t offset = nextOffset;
684
+ nextOffset += len;
685
+
686
+ int ret = smb2_pwrite_async(ctx, fh, slots[slot].buf.data(), len, offset, writeCb, this);
687
+ if (ret < 0) {
688
+ errored = true;
689
+ errorMsg = std::string("Upload Failed: smb2_pwrite_async: ") + smb2_get_error(ctx);
690
+ slots[slot].available = true;
691
+ nextOffset -= len;
692
+ return;
693
+ }
694
+ inFlight++;
695
+ }
696
+
697
+ static void writeCb(smb2_context* smb2, int status, void* command_data, void* cb_data) {
698
+ auto* st = static_cast<AsyncWriteState*>(cb_data);
699
+ st->inFlight--;
700
+
701
+ auto* cbd = static_cast<smb2_write_cb_data*>(command_data);
702
+ int slotIdx = (cbd ? st->findSlot(cbd->buf) : -1);
703
+
704
+ if (st->errored) {
705
+ if (slotIdx >= 0) st->slots[slotIdx].available = true;
706
+ return;
707
+ }
708
+ if (status < 0) {
709
+ st->errored = true;
710
+ st->errorMsg = std::string("Upload Failed: write error: ") + smb2_get_error(smb2);
711
+ if (slotIdx >= 0) st->slots[slotIdx].available = true;
712
+ return;
713
+ }
714
+ if (slotIdx < 0) {
715
+ st->errored = true;
716
+ st->errorMsg = "Upload Failed: internal slot lookup failed";
717
+ return;
718
+ }
719
+
720
+ auto& slot = st->slots[slotIdx];
721
+ slot.written += static_cast<uint32_t>(status);
722
+ st->bytesCompleted += status;
723
+
724
+ // ── L1: short-write recovery ──
725
+ if (slot.written < slot.requested) {
726
+ if (status == 0) {
727
+ st->errored = true;
728
+ st->errorMsg = "Upload Failed: server accepted zero bytes";
729
+ slot.available = true;
730
+ return;
731
+ }
732
+ uint32_t tailLen = slot.requested - slot.written;
733
+ uint64_t tailOffset = slot.baseOffset + slot.written;
734
+ SMB_LOG("AsyncWriteState: short write got=%d requested=%u at offset=%llu — re-issuing tail %u bytes at %llu", status, slot.requested, static_cast<unsigned long long>(slot.baseOffset),
735
+ tailLen, static_cast<unsigned long long>(tailOffset));
736
+ int ret = smb2_pwrite_async(st->ctx, st->fh, slot.buf.data() + slot.written, tailLen, tailOffset, writeCb, st);
737
+ if (ret < 0) {
738
+ st->errored = true;
739
+ st->errorMsg = std::string("Upload Failed: short-write recovery smb2_pwrite_async: ") + smb2_get_error(st->ctx);
740
+ slot.available = true;
741
+ return;
742
+ }
743
+ st->inFlight++;
744
+ return;
745
+ }
746
+
747
+ slot.available = true;
748
+
749
+ if (st->progressHandler && st->bytesCompleted < st->fileSize && shouldEmitProgress(st->bytesCompleted, st->fileSize, st->lastEmittedBytes)) {
750
+ st->progressHandler(static_cast<double>(st->bytesCompleted), static_cast<double>(st->fileSize));
751
+ }
752
+
753
+ st->submitNextWrite();
754
+ }
755
+
756
+ void cleanup() {
757
+ if (fh && ctx) {
758
+ smb2_fsync(ctx, fh);
759
+ smb2_close(ctx, fh);
760
+ fh = nullptr;
761
+ }
762
+ if (inFile) {
763
+ fclose(inFile);
764
+ inFile = nullptr;
765
+ }
766
+ }
767
+
768
+ void cleanupOnError() {
769
+ if (fh && ctx) {
770
+ smb2_close(ctx, fh);
771
+ fh = nullptr;
772
+ }
773
+ if (inFile) {
774
+ fclose(inFile);
775
+ inFile = nullptr;
776
+ }
777
+ if (ctx && !remotePath.empty()) {
778
+ smb2_unlink(ctx, remotePath.c_str());
779
+ }
780
+ }
781
+ };
782
+
783
+ } // anonymous namespace
784
+
785
+ // ── Public async entry points ────────────────────────────────────────────────
786
+ //
787
+ // Both functions expect to be called while the manager's connectionMutex_ is
788
+ // held (i.e. from inside a Handle::submitSync lambda). They drive their own
789
+ // poll()/smb2_service() loop on the calling thread.
790
+
791
+ namespace {
792
+
793
+ template <typename DoneFn, typename TickFn>
794
+ bool drivePollLoop(smb2_context* ctx, DoneFn&& done, TickFn&& tick) {
795
+ struct pollfd pfd;
796
+ while (!done()) {
797
+ tick();
798
+ if (done()) break;
799
+ pfd.fd = smb2_get_fd(ctx);
800
+ pfd.events = smb2_which_events(ctx);
801
+ pfd.revents = 0;
802
+ if (pfd.fd < 0 || pfd.events == 0) {
803
+ // Nothing to wait on (e.g. all chunks already submitted and
804
+ // libsmb2 has no pending work) — yield briefly.
805
+ ::poll(nullptr, 0, 10);
806
+ continue;
807
+ }
808
+ int ret = ::poll(&pfd, 1, 100 /*ms*/);
809
+ if (ret < 0) {
810
+ if (errno == EINTR) continue;
811
+ return false;
812
+ }
813
+ if (ret == 0) continue; // timeout, retry
814
+ int svc = smb2_service(ctx, pfd.revents);
815
+ if (svc < 0) return false;
816
+ }
817
+ return true;
818
+ }
819
+
820
+ // Drive the smb2 poll/service loop on the calling thread until `done()` is
821
+ // true. Used by async read/write/copy paths to pump pipelined chunks.
822
+ // Returns false if poll() failed with anything other than EINTR.
823
+ template <typename DoneFn>
824
+ bool drivePollLoop(smb2_context* ctx, DoneFn&& done) {
825
+ return drivePollLoop(ctx, std::forward<DoneFn>(done), []() {});
826
+ }
827
+
828
+ } // anonymous namespace
829
+
830
+ int64_t smbReadFileAsync(void* ctxVoid, const std::string& remotePath, const std::string& localPath, std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
831
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
832
+
833
+ AsyncReadState state;
834
+ state.ctx = ctx;
835
+ state.progressHandler = std::move(progressHandler);
836
+ state.cancel = cancel;
837
+ state.localPath = localPath;
838
+
839
+ // Open remote file
840
+ state.fh = smb2_open(ctx, remotePath.c_str(), O_RDONLY);
841
+ if (!state.fh) {
842
+ throw std::runtime_error("Download Failed: Could not open remote file '" + remotePath + "': " + smb2_get_error(ctx));
843
+ }
844
+
845
+ // Get file size
846
+ struct smb2_stat_64 st;
847
+ if (smb2_fstat(ctx, state.fh, &st) < 0) {
848
+ std::string err = smb2_get_error(ctx);
849
+ smb2_close(ctx, state.fh);
850
+ throw std::runtime_error("Download Failed: Could not stat '" + remotePath + "': " + err);
851
+ }
852
+ state.fileSize = static_cast<int64_t>(st.smb2_size);
853
+
854
+ // Handle empty file
855
+ if (state.fileSize == 0) {
856
+ FILE* f = fopen(localPath.c_str(), "wb");
857
+ if (f) fclose(f);
858
+ smb2_close(ctx, state.fh);
859
+ state.fh = nullptr;
860
+ if (state.progressHandler) state.progressHandler(0.0, 0.0);
861
+ return 0;
862
+ }
863
+
864
+ // Determine chunk size from server negotiation, capped for cancel responsiveness.
865
+ state.chunkSize = capAsyncChunkSize(smb2_get_max_read_size(ctx));
866
+
867
+ // Open local file for random-access writing
868
+ state.outFile = fopen(localPath.c_str(), "wb");
869
+ if (!state.outFile) {
870
+ smb2_close(ctx, state.fh);
871
+ state.fh = nullptr;
872
+ throw std::runtime_error("Download Failed: Could not create local file '" + localPath + "'");
873
+ }
874
+
875
+ if (state.progressHandler) state.progressHandler(0.0, static_cast<double>(state.fileSize));
876
+
877
+ // Kick off the first batch of async reads
878
+ for (int i = 0; i < kMaxInFlight && !state.errored; i++) {
879
+ state.submitNextRead();
880
+ }
881
+
882
+ // Drive the poll loop until all chunks complete (or error / cancel / stuck).
883
+ // isStuck() is a defence-in-depth check: if a callback path ever fails
884
+ // to recover from a short transfer, the loop will exit instead of hanging.
885
+ bool pollOk = drivePollLoop(
886
+ ctx, [&]() { return (state.isDone() && state.inFlight == 0) || state.isStuck(); },
887
+ [&]() {
888
+ if (cancel.cancelled() && !state.errored) {
889
+ state.markCancelled();
890
+ }
891
+ });
892
+ if (state.isStuck()) {
893
+ SMB_LOG("smbReadFileAsync: stuck-state safety net triggered (bytesCompleted=%lld fileSize=%lld)", static_cast<long long>(state.bytesCompleted), static_cast<long long>(state.fileSize));
894
+ state.errored = true;
895
+ state.errorMsg = "Download Failed: I/O pipeline stalled (unrecoverable short transfer)";
896
+ }
897
+ if (!pollOk && !state.errored) {
898
+ state.errored = true;
899
+ state.errorMsg = std::string("Download Failed: poll/service: ") + smb2_get_error(ctx);
900
+ }
901
+
902
+ state.cleanup();
903
+
904
+ if (state.errored) {
905
+ std::remove(localPath.c_str());
906
+ throw std::runtime_error(state.errorMsg);
907
+ }
908
+ if (state.bytesCompleted != state.fileSize) {
909
+ std::remove(localPath.c_str());
910
+ throw std::runtime_error("Downloaded file size mismatch. Expected: " + std::to_string(state.fileSize) + ", Got: " + std::to_string(state.bytesCompleted));
911
+ }
912
+
913
+ if (state.progressHandler) state.progressHandler(static_cast<double>(state.fileSize), static_cast<double>(state.fileSize));
914
+ return state.bytesCompleted;
915
+ }
916
+
917
+ int64_t smbWriteFileAsync(void* ctxVoid, const std::string& localPath, const std::string& remotePath, std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
918
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
919
+
920
+ AsyncWriteState state;
921
+ state.ctx = ctx;
922
+ state.progressHandler = std::move(progressHandler);
923
+ state.cancel = cancel;
924
+ state.remotePath = remotePath;
925
+
926
+ // Open local file for reading and measure size.
927
+ state.inFile = fopen(localPath.c_str(), "rb");
928
+ if (!state.inFile) {
929
+ throw std::runtime_error("Upload Failed: Could not open local file '" + localPath + "'");
930
+ }
931
+ fseeko(state.inFile, 0, SEEK_END);
932
+ int64_t localFileSize = static_cast<int64_t>(ftello(state.inFile));
933
+ fseeko(state.inFile, 0, SEEK_SET);
934
+ if (localFileSize < 0) {
935
+ fclose(state.inFile);
936
+ state.inFile = nullptr;
937
+ throw std::runtime_error("Upload Failed: Could not determine size of '" + localPath + "'");
938
+ }
939
+ state.fileSize = localFileSize;
940
+
941
+ // Handle empty file
942
+ if (state.fileSize == 0) {
943
+ smb2fh* fh = smb2_open(ctx, remotePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC);
944
+ if (fh) smb2_close(ctx, fh);
945
+ fclose(state.inFile);
946
+ state.inFile = nullptr;
947
+ if (state.progressHandler) state.progressHandler(0.0, 0.0);
948
+ return 0;
949
+ }
950
+
951
+ // Open remote file for writing
952
+ state.fh = smb2_open(ctx, remotePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC);
953
+ if (!state.fh) {
954
+ std::string err = smb2_get_error(ctx);
955
+ fclose(state.inFile);
956
+ state.inFile = nullptr;
957
+ throw std::runtime_error("Upload Failed: Could not create remote file '" + remotePath + "': " + err);
958
+ }
959
+
960
+ // Determine chunk size from server negotiation, capped for cancel responsiveness.
961
+ state.chunkSize = capAsyncChunkSize(smb2_get_max_write_size(ctx));
962
+
963
+ if (state.progressHandler) state.progressHandler(0.0, static_cast<double>(state.fileSize));
964
+
965
+ // Kick off the first batch of async writes
966
+ for (int i = 0; i < kMaxInFlight && !state.errored; i++) {
967
+ state.submitNextWrite();
968
+ }
969
+
970
+ bool pollOk = drivePollLoop(
971
+ ctx, [&]() { return (state.isDone() && state.inFlight == 0) || state.isStuck(); },
972
+ [&]() {
973
+ if (cancel.cancelled() && !state.errored) {
974
+ state.errored = true;
975
+ state.errorMsg = "Upload cancelled";
976
+ }
977
+ });
978
+ if (state.isStuck()) {
979
+ SMB_LOG("smbWriteFileAsync: stuck-state safety net triggered (bytesCompleted=%lld fileSize=%lld)", static_cast<long long>(state.bytesCompleted), static_cast<long long>(state.fileSize));
980
+ state.errored = true;
981
+ state.errorMsg = "Upload Failed: I/O pipeline stalled (unrecoverable short transfer)";
982
+ }
983
+ if (!pollOk && !state.errored) {
984
+ state.errored = true;
985
+ state.errorMsg = std::string("Upload Failed: poll/service: ") + smb2_get_error(ctx);
986
+ }
987
+
988
+ if (state.errored) {
989
+ state.cleanupOnError();
990
+ throw std::runtime_error(state.errorMsg);
991
+ }
992
+
993
+ state.cleanup();
994
+
995
+ if (state.bytesCompleted != state.fileSize) {
996
+ smb2_unlink(ctx, remotePath.c_str());
997
+ throw std::runtime_error("Uploaded file size mismatch. Expected: " + std::to_string(state.fileSize) + ", Uploaded: " + std::to_string(state.bytesCompleted));
998
+ }
999
+
1000
+ if (state.progressHandler) state.progressHandler(static_cast<double>(state.fileSize), static_cast<double>(state.fileSize));
1001
+ return state.bytesCompleted;
1002
+ }
1003
+
1004
+ // ─── Async pipelined SMB→SMB copy ────────────────────────────────────────────
1005
+ //
1006
+ // One smb2_context, two open file handles (src + dst). Up to kMaxInFlight
1007
+ // chunks are bouncing between Reading and Writing on the wire at any time;
1008
+ // when a read completes the same buffer is immediately submitted as a write,
1009
+ // freeing read bandwidth for the next chunk only when its write completes.
1010
+ // Drives one poll/service loop on the calling thread under connectionMutex_.
1011
+
1012
+ namespace {
1013
+
1014
+ struct AsyncCopyState {
1015
+ smb2_context* ctx = nullptr;
1016
+ smb2fh* srcFh = nullptr;
1017
+ smb2fh* dstFh = nullptr;
1018
+
1019
+ int64_t fileSize = 0;
1020
+ uint32_t chunkSize = 0;
1021
+
1022
+ uint64_t nextReadOffset = 0;
1023
+ int64_t bytesCopied = 0; // bytes that finished writing (for progress)
1024
+ int64_t lastEmittedBytes = 0;
1025
+ int inFlight = 0;
1026
+ bool errored = false;
1027
+ std::string errorMsg;
1028
+
1029
+ struct ChunkSlot {
1030
+ enum State { Idle, Reading, Writing };
1031
+ State state = Idle;
1032
+ std::vector<uint8_t> buf;
1033
+ uint64_t baseOffset = 0; // file offset corresponding to buf[0]
1034
+ uint32_t requested = 0; // total chunk size to copy (== buf.size())
1035
+ uint32_t read = 0; // src bytes received so far (≤ requested)
1036
+ uint32_t written = 0; // dst bytes acknowledged so far (≤ read)
1037
+ };
1038
+ ChunkSlot slots[kMaxInFlight];
1039
+
1040
+ struct CbData {
1041
+ AsyncCopyState* st;
1042
+ int slotIdx;
1043
+ };
1044
+ CbData cbDatas[kMaxInFlight];
1045
+
1046
+ // Shared cumulative counter for multi-file (recursive) copies.
1047
+ std::shared_ptr<int64_t> totalBytesCopied;
1048
+ int64_t totalSize = 0;
1049
+ std::function<void(double, double)> progressHandler;
1050
+ CancellationToken cancel;
1051
+ std::string toPath;
1052
+
1053
+ void init() {
1054
+ for (int i = 0; i < kMaxInFlight; i++) {
1055
+ cbDatas[i] = {this, i};
1056
+ }
1057
+ }
1058
+
1059
+ bool isDone() const {
1060
+ if (errored) return true;
1061
+ if (nextReadOffset < static_cast<uint64_t>(fileSize)) return false;
1062
+ for (int i = 0; i < kMaxInFlight; i++) {
1063
+ if (slots[i].state != ChunkSlot::Idle) return false;
1064
+ }
1065
+ return true;
1066
+ }
1067
+
1068
+ // Safety net: ranges fully requested, no in-flight work, all slots
1069
+ // idle, but not all bytes copied. Prevents the poll loop from
1070
+ // exiting cleanly with a truncated destination.
1071
+ bool isStuck() const {
1072
+ if (errored || inFlight > 0) return false;
1073
+ if (nextReadOffset < static_cast<uint64_t>(fileSize)) return false;
1074
+ for (int i = 0; i < kMaxInFlight; i++) {
1075
+ if (slots[i].state != ChunkSlot::Idle) return false;
1076
+ }
1077
+ return bytesCopied < fileSize;
1078
+ }
1079
+
1080
+ void submitNextRead() {
1081
+ if (errored || nextReadOffset >= static_cast<uint64_t>(fileSize)) return;
1082
+ if (cancel.cancelled()) {
1083
+ errored = true;
1084
+ errorMsg = "Copy cancelled";
1085
+ return;
1086
+ }
1087
+
1088
+ int slot = -1;
1089
+ for (int i = 0; i < kMaxInFlight; i++) {
1090
+ if (slots[i].state == ChunkSlot::Idle) {
1091
+ slot = i;
1092
+ break;
1093
+ }
1094
+ }
1095
+ if (slot < 0) return;
1096
+
1097
+ uint64_t remaining = static_cast<uint64_t>(fileSize) - nextReadOffset;
1098
+ uint32_t len = static_cast<uint32_t>(std::min(static_cast<uint64_t>(chunkSize), remaining));
1099
+
1100
+ slots[slot].buf.resize(len);
1101
+ slots[slot].baseOffset = nextReadOffset;
1102
+ slots[slot].requested = len;
1103
+ slots[slot].read = 0;
1104
+ slots[slot].written = 0;
1105
+ slots[slot].state = ChunkSlot::Reading;
1106
+ nextReadOffset += len;
1107
+
1108
+ int ret = smb2_pread_async(ctx, srcFh, slots[slot].buf.data(), len, slots[slot].baseOffset, readCb, &cbDatas[slot]);
1109
+ if (ret < 0) {
1110
+ errored = true;
1111
+ errorMsg = std::string("Copy Failed: smb2_pread_async: ") + smb2_get_error(ctx);
1112
+ slots[slot].state = ChunkSlot::Idle;
1113
+ nextReadOffset -= len;
1114
+ return;
1115
+ }
1116
+ inFlight++;
1117
+ }
1118
+
1119
+ // Issue (or re-issue) a write for the un-written tail of this slot.
1120
+ void submitWriteForSlot(int slotIdx) {
1121
+ if (errored) {
1122
+ slots[slotIdx].state = ChunkSlot::Idle;
1123
+ return;
1124
+ }
1125
+ auto& slot = slots[slotIdx];
1126
+ slot.state = ChunkSlot::Writing;
1127
+ uint32_t tailLen = slot.read - slot.written;
1128
+ uint64_t tailOffset = slot.baseOffset + slot.written;
1129
+ int ret = smb2_pwrite_async(ctx, dstFh, slot.buf.data() + slot.written, tailLen, tailOffset, writeCb, &cbDatas[slotIdx]);
1130
+ if (ret < 0) {
1131
+ errored = true;
1132
+ errorMsg = std::string("Copy Failed: smb2_pwrite_async: ") + smb2_get_error(ctx);
1133
+ slot.state = ChunkSlot::Idle;
1134
+ return;
1135
+ }
1136
+ inFlight++;
1137
+ }
1138
+
1139
+ static void readCb(smb2_context* smb2, int status, void* /*command_data*/, void* cb_data) {
1140
+ auto* d = static_cast<CbData*>(cb_data);
1141
+ auto* st = d->st;
1142
+ int slotIdx = d->slotIdx;
1143
+ st->inFlight--;
1144
+
1145
+ if (st->errored) {
1146
+ st->slots[slotIdx].state = ChunkSlot::Idle;
1147
+ return;
1148
+ }
1149
+ if (status < 0) {
1150
+ st->errored = true;
1151
+ st->errorMsg = std::string("Copy Failed: read error: ") + smb2_get_error(smb2);
1152
+ st->slots[slotIdx].state = ChunkSlot::Idle;
1153
+ return;
1154
+ }
1155
+
1156
+ auto& slot = st->slots[slotIdx];
1157
+ slot.read += static_cast<uint32_t>(status);
1158
+
1159
+ // ── L1: short-read recovery (re-issue tail on same slot) ──
1160
+ if (slot.read < slot.requested) {
1161
+ if (status == 0) {
1162
+ st->errored = true;
1163
+ st->errorMsg = "Copy Failed: unexpected EOF on source (zero-byte read)";
1164
+ slot.state = ChunkSlot::Idle;
1165
+ return;
1166
+ }
1167
+ uint32_t tailLen = slot.requested - slot.read;
1168
+ uint64_t tailOffset = slot.baseOffset + slot.read;
1169
+ SMB_LOG("AsyncCopyState: short read got=%d requested=%u at offset=%llu — re-issuing tail %u bytes at %llu", status, slot.requested, static_cast<unsigned long long>(slot.baseOffset),
1170
+ tailLen, static_cast<unsigned long long>(tailOffset));
1171
+ int ret = smb2_pread_async(st->ctx, st->srcFh, slot.buf.data() + slot.read, tailLen, tailOffset, readCb, &st->cbDatas[slotIdx]);
1172
+ if (ret < 0) {
1173
+ st->errored = true;
1174
+ st->errorMsg = std::string("Copy Failed: short-read recovery smb2_pread_async: ") + smb2_get_error(st->ctx);
1175
+ slot.state = ChunkSlot::Idle;
1176
+ return;
1177
+ }
1178
+ st->inFlight++;
1179
+ return;
1180
+ }
1181
+
1182
+ // Slot fully read → start the write phase.
1183
+ st->submitWriteForSlot(slotIdx);
1184
+ }
1185
+
1186
+ static void writeCb(smb2_context* smb2, int status, void* /*command_data*/, void* cb_data) {
1187
+ auto* d = static_cast<CbData*>(cb_data);
1188
+ auto* st = d->st;
1189
+ int slotIdx = d->slotIdx;
1190
+ st->inFlight--;
1191
+
1192
+ if (st->errored) {
1193
+ st->slots[slotIdx].state = ChunkSlot::Idle;
1194
+ return;
1195
+ }
1196
+ if (status < 0) {
1197
+ st->errored = true;
1198
+ st->errorMsg = std::string("Copy Failed: write error: ") + smb2_get_error(smb2);
1199
+ st->slots[slotIdx].state = ChunkSlot::Idle;
1200
+ return;
1201
+ }
1202
+
1203
+ auto& slot = st->slots[slotIdx];
1204
+ slot.written += static_cast<uint32_t>(status);
1205
+
1206
+ // ── L1: short-write recovery (re-issue tail on same slot) ──
1207
+ if (slot.written < slot.read) {
1208
+ if (status == 0) {
1209
+ st->errored = true;
1210
+ st->errorMsg = "Copy Failed: destination accepted zero bytes";
1211
+ slot.state = ChunkSlot::Idle;
1212
+ return;
1213
+ }
1214
+ SMB_LOG("AsyncCopyState: short write got=%d total=%u at offset=%llu — re-issuing tail %u bytes", status, slot.read, static_cast<unsigned long long>(slot.baseOffset),
1215
+ slot.read - slot.written);
1216
+ st->submitWriteForSlot(slotIdx);
1217
+ return;
1218
+ }
1219
+
1220
+ // Slot fully written → free it and credit progress.
1221
+ uint32_t copied = slot.requested;
1222
+ slot.state = ChunkSlot::Idle;
1223
+ st->bytesCopied += copied;
1224
+ if (st->totalBytesCopied) *st->totalBytesCopied += copied;
1225
+
1226
+ if (st->progressHandler && st->totalSize > 0) {
1227
+ int64_t cur = st->totalBytesCopied ? *st->totalBytesCopied : st->bytesCopied;
1228
+ if (cur < st->totalSize && shouldEmitProgress(cur, st->totalSize, st->lastEmittedBytes)) {
1229
+ st->progressHandler(static_cast<double>(cur), static_cast<double>(st->totalSize));
1230
+ }
1231
+ }
1232
+
1233
+ // Slot is free; submit next read to keep the pipeline saturated.
1234
+ st->submitNextRead();
1235
+ }
1236
+
1237
+ void cleanup() {
1238
+ if (srcFh && ctx) {
1239
+ smb2_close(ctx, srcFh);
1240
+ srcFh = nullptr;
1241
+ }
1242
+ if (dstFh && ctx) {
1243
+ smb2_fsync(ctx, dstFh);
1244
+ smb2_close(ctx, dstFh);
1245
+ dstFh = nullptr;
1246
+ }
1247
+ }
1248
+
1249
+ void cleanupOnError() {
1250
+ if (srcFh && ctx) {
1251
+ smb2_close(ctx, srcFh);
1252
+ srcFh = nullptr;
1253
+ }
1254
+ if (dstFh && ctx) {
1255
+ smb2_close(ctx, dstFh);
1256
+ dstFh = nullptr;
1257
+ }
1258
+ if (ctx && !toPath.empty()) {
1259
+ smb2_unlink(ctx, toPath.c_str());
1260
+ }
1261
+ }
1262
+ };
1263
+
1264
+ } // anonymous namespace
1265
+
1266
+ int64_t smbCopyFileAsync(void* ctxVoid, const std::string& fromPath, const std::string& toPath, std::shared_ptr<int64_t> totalBytesCopied, int64_t totalSize,
1267
+ std::function<void(double, double)> progressHandler, const CancellationToken& cancel) {
1268
+ smb2_context* ctx = static_cast<smb2_context*>(ctxVoid);
1269
+
1270
+ AsyncCopyState state;
1271
+ state.ctx = ctx;
1272
+ state.totalBytesCopied = totalBytesCopied;
1273
+ state.totalSize = totalSize;
1274
+ state.progressHandler = std::move(progressHandler);
1275
+ state.cancel = cancel;
1276
+ state.toPath = toPath;
1277
+ state.init();
1278
+
1279
+ // Open source.
1280
+ state.srcFh = smb2_open(ctx, fromPath.c_str(), O_RDONLY);
1281
+ if (!state.srcFh) {
1282
+ std::string err = smb2_get_error(ctx);
1283
+ throw std::runtime_error("Copy Failed: Could not open source '" + fromPath + "': " + err);
1284
+ }
1285
+
1286
+ // Size source.
1287
+ struct smb2_stat_64 st;
1288
+ if (smb2_fstat(ctx, state.srcFh, &st) < 0) {
1289
+ std::string err = smb2_get_error(ctx);
1290
+ smb2_close(ctx, state.srcFh);
1291
+ throw std::runtime_error("Copy Failed: Could not stat source '" + fromPath + "': " + err);
1292
+ }
1293
+ state.fileSize = static_cast<int64_t>(st.smb2_size);
1294
+
1295
+ // Open destination (exclusive create).
1296
+ state.dstFh = smb2_open(ctx, toPath.c_str(), O_WRONLY | O_CREAT | O_EXCL);
1297
+ if (!state.dstFh) {
1298
+ std::string err = smb2_get_error(ctx);
1299
+ smb2_close(ctx, state.srcFh);
1300
+ state.srcFh = nullptr;
1301
+ throw std::runtime_error("Copy Failed: Could not create destination '" + toPath + "': " + err);
1302
+ }
1303
+
1304
+ // Handle empty file: nothing to read/write, just close.
1305
+ if (state.fileSize == 0) {
1306
+ smb2_fsync(ctx, state.dstFh);
1307
+ smb2_close(ctx, state.dstFh);
1308
+ smb2_close(ctx, state.srcFh);
1309
+ state.srcFh = state.dstFh = nullptr;
1310
+ return 0;
1311
+ }
1312
+
1313
+ state.chunkSize = capAsyncChunkSize(smb2_get_max_read_size(ctx));
1314
+ uint32_t maxW = capAsyncChunkSize(smb2_get_max_write_size(ctx));
1315
+ if (maxW < state.chunkSize) state.chunkSize = maxW;
1316
+
1317
+ // Kick off first batch of reads.
1318
+ for (int i = 0; i < kMaxInFlight && !state.errored; i++) {
1319
+ state.submitNextRead();
1320
+ }
1321
+
1322
+ bool pollOk = drivePollLoop(
1323
+ ctx, [&]() { return (state.isDone() && state.inFlight == 0) || state.isStuck(); },
1324
+ [&]() {
1325
+ if (cancel.cancelled() && !state.errored) {
1326
+ state.errored = true;
1327
+ state.errorMsg = "Copy cancelled";
1328
+ }
1329
+ });
1330
+ if (state.isStuck()) {
1331
+ SMB_LOG("smbCopyFileAsync: stuck-state safety net triggered (bytesCopied=%lld fileSize=%lld)", static_cast<long long>(state.bytesCopied), static_cast<long long>(state.fileSize));
1332
+ state.errored = true;
1333
+ state.errorMsg = "Copy Failed: I/O pipeline stalled (unrecoverable short transfer)";
1334
+ }
1335
+ if (!pollOk && !state.errored) {
1336
+ state.errored = true;
1337
+ state.errorMsg = std::string("Copy Failed: poll/service: ") + smb2_get_error(ctx);
1338
+ }
1339
+
1340
+ if (state.errored) {
1341
+ state.cleanupOnError();
1342
+ throw std::runtime_error(state.errorMsg);
1343
+ }
1344
+
1345
+ state.cleanup();
1346
+
1347
+ if (state.bytesCopied != state.fileSize) {
1348
+ smb2_unlink(ctx, toPath.c_str());
1349
+ throw std::runtime_error("Copied file size mismatch. Expected: " + std::to_string(state.fileSize) + ", Got: " + std::to_string(state.bytesCopied));
1350
+ }
1351
+
1352
+ return state.bytesCopied;
1353
+ }
1354
+
1355
+ } // namespace react_native_smb