Repository navigation
Expand file tree
/
Copy pathfsplugin.cpp
More file actions
408 lines (374 loc) · 15.3 KB
/
Copy pathfsplugin.cpp
File metadata and controls
408 lines (374 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// fsplugin.cpp -- the extern "C" export shim, and the ONLY translation
// unit besides tests/*.cpp in this project.
//
// This file owns exactly: process-global state (the callbacks DC hands
// FsInitW and the AdbClient/PluginCore pair) and UTF-16<->UTF-8
// conversion at the SDK boundary (utils.hpp). The find-handle lifecycle,
// progress/warning/error callback bridging, and the write-operation
// classification for FsStatusInfoW are all pure functions hoisted into
// fsplugin_impl.hpp (FindHandle, advanceFindData, reportWarning,
// reportError, makeProgressFn, isWriteOperation, isUnsetFileTime) so they
// are directly testable; this file only supplies its globals as their
// arguments. Every actual decision -- device naming, caching, the
// download write scheme (direct, or temp-then-rename when
// overwriting), mtime preservation, which shell commands the mutating
// operations run -- lives in PluginCore
// (fsplugin_impl.hpp). If a change here starts to look like a
// policy decision rather than a translation, it belongs there instead.
//
// No C++ exception may ever cross this file's extern "C" boundary -- an
// escaping exception terminates Double Commander, not just the plugin.
// Every export body is therefore `try { ... } catch (...) { return
// <the appropriate error value>; }`.
//
// Threading note: Double Commander can call a WFX plugin from more than
// one thread (its background-transfer modes). Making this file's global
// state thread-safe is out of scope for this task; the globals below are
// kept to exactly the ones FsInitW needs to hand out, and nothing here
// does anything to make concurrent use *more* broken than the single
// AdbClient/PluginCore pair already implies.
#include "sdk.h"
#include "adbclient.hpp"
#include "adbserver.hpp"
#include "fsplugin_impl.hpp"
#include "transport.hpp"
#include "utils.hpp"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <unistd.h>
#include <vector>
// compile_mac.sh/compile_mac_universal.sh build with -fvisibility=hidden,
// so every symbol is hidden from the shared library's export table by
// default -- this file's own helper functions and globals included. Each
// of the exports Double Commander actually calls is marked with this
// attribute to opt back in; scripts/check-exports.sh and
// tests/test_exports.cpp both verify the result names exactly the 14
// exports Double Commander requires, nothing else (previously, an
// un-hidden libc++ internal symbol showed up alongside them in
// `nm -gU`).
#if defined(__GNUC__) || defined(__clang__)
#define WFX_EXPORT __attribute__((visibility("default")))
#else
#define WFX_EXPORT
#endif
namespace {
// Host and port fsplugin.cpp's TransportFactory connects to. The port
// itself is resolved by adbServerPort() (ANDROID_ADB_SERVER_PORT,
// falling back to ADB_DEFAULT_PORT); only the loopback host is fixed here.
constexpr const char* ADB_SERVER_HOST = "127.0.0.1";
// The value FsGetDefRootName copies out, and the WFX root path's first
// component everywhere a device serial or model name isn't in play yet.
constexpr const char* ROOT_NAME = "ADB";
// -----------------------------------------------------------------
// Process-global state (see the file comment above for the threading
// caveat). FsInitW populates all five; every other export assumes they
// already exist, which the SDK's contract guarantees (DC always calls
// FsInitW before any other export).
// -----------------------------------------------------------------
int gPluginNr = 0;
tProgressProcW gProgressProcW = nullptr;
tLogProcW gLogProcW = nullptr;
tRequestProcW gRequestProcW = nullptr;
std::unique_ptr<AdbClient> gAdbClient;
std::unique_ptr<PluginCore> gPluginCore;
} // namespace
extern "C" {
WFX_EXPORT int DCPCALL FsInitW(int pluginNr, tProgressProcW progressProc, tLogProcW logProc,
tRequestProcW requestProc) {
try {
gPluginNr = pluginNr;
gProgressProcW = progressProc;
gLogProcW = logProc;
gRequestProcW = requestProc;
auto getEnv = [](const char* name) -> const char* { return std::getenv(name); };
auto isExecutable = [](const std::string& path) -> bool {
return ::access(path.c_str(), X_OK) == 0;
};
int port = adbServerPort(getEnv);
std::string adbBinary = findAdbBinary(getEnv, isExecutable);
if (!adbBinary.empty()) {
startAdbServer(adbBinary);
}
TransportFactory factory = [port](std::string* error) -> std::unique_ptr<Transport> {
return TcpTransport::connectTo(ADB_SERVER_HOST, port, error);
};
// Order matters on a second FsInitW: PluginCore holds an
// AdbClient& , so replacing the client first would leave the old
// core holding a dangling reference for the length of one
// statement. Tear the core down before the client it points at.
gPluginCore.reset();
gAdbClient = std::make_unique<AdbClient>(std::move(factory));
gPluginCore = std::make_unique<PluginCore>(*gAdbClient);
return 0;
} catch (...) {
return 0; // FsInitW has no error return in this SDK; nothing else to report through.
}
}
WFX_EXPORT HANDLE DCPCALL FsFindFirstW(WCHAR* path, WIN32_FIND_DATAW* findData) {
try {
if (!gPluginCore) {
return INVALID_HANDLE_VALUE;
}
std::string wfxDir = wideToUtf8(path);
auto handle = std::make_unique<FindHandle>();
std::string warning;
std::string error;
bool ok = gPluginCore->listDirectory(wfxDir, &handle->entries, &warning, &error);
if (!ok) {
reportError(gLogProcW, gPluginNr, error);
return INVALID_HANDLE_VALUE;
}
reportWarning(gRequestProcW, gPluginNr, warning);
if (!advanceFindData(handle.get(), findData)) {
return INVALID_HANDLE_VALUE; // genuinely empty (or every entry was unfittable)
}
return handle.release();
} catch (...) {
return INVALID_HANDLE_VALUE;
}
}
WFX_EXPORT BOOL DCPCALL FsFindNextW(HANDLE hdl, WIN32_FIND_DATAW* findData) {
try {
if (hdl == nullptr || hdl == INVALID_HANDLE_VALUE) {
return false;
}
auto* handle = static_cast<FindHandle*>(hdl);
return advanceFindData(handle, findData);
} catch (...) {
return false;
}
}
WFX_EXPORT int DCPCALL FsFindClose(HANDLE hdl) {
try {
if (hdl != nullptr && hdl != INVALID_HANDLE_VALUE) {
delete static_cast<FindHandle*>(hdl);
}
} catch (...) {
// Nothing to do -- FsFindClose has no error return, and delete on
// an already-valid pointer should never throw in the first place.
}
return 0;
}
WFX_EXPORT int DCPCALL FsGetFileW(WCHAR* remoteName, WCHAR* localName, int copyFlags, RemoteInfoStruct* ri) {
try {
(void)ri; // ri restates size/attributes DC already knows from FsFindFirstW; unused here
if (!gPluginCore) {
return FS_FILE_READERROR;
}
std::string wfxRemote = wideToUtf8(remoteName);
std::string localPath = wideToUtf8(localName);
std::string error;
int result = gPluginCore->getFile(
wfxRemote, localPath, copyFlags,
makeProgressFn(gProgressProcW, gPluginNr, remoteName, localName), &error);
// Unconditional, not gated on the result code: a move whose
// download succeeded but whose delete-on-the-device failed comes
// back as FS_FILE_OK *with* a message, and that message is the
// only way the user learns the file is still on the phone.
// reportError is a no-op on an empty message (USERABORT, EXISTS,
// NOTFOUND and every ordinary success), and getFile clears *error
// on entry and sets it on no other success path -- keep it that
// way, or a future success message starts raising error dialogs.
reportError(gLogProcW, gPluginNr, error);
return result;
} catch (...) {
return FS_FILE_READERROR;
}
}
WFX_EXPORT int DCPCALL FsPutFileW(WCHAR* localName, WCHAR* remoteName, int copyFlags) {
try {
if (!gPluginCore) {
return FS_FILE_WRITEERROR;
}
std::string localPath = wideToUtf8(localName);
std::string wfxRemote = wideToUtf8(remoteName);
std::string error;
int result = gPluginCore->putFile(
localPath, wfxRemote, copyFlags,
makeProgressFn(gProgressProcW, gPluginNr, localName, remoteName), &error);
// Unconditional for the same reason as in FsGetFileW above: an
// upload whose delete-of-the-local-source failed comes back as
// FS_FILE_OK with a message, and that message is the only way the
// user learns the file is still on the computer. reportError is a
// no-op on an empty message (USERABORT, EXISTS, ordinary success).
reportError(gLogProcW, gPluginNr, error);
return result;
} catch (...) {
return FS_FILE_WRITEERROR;
}
}
WFX_EXPORT int DCPCALL FsRenMovFileW(WCHAR* oldName, WCHAR* newName, BOOL move, BOOL overWrite,
RemoteInfoStruct* ri) {
try {
(void)ri;
if (!gPluginCore) {
return FS_FILE_WRITEERROR;
}
std::string wfxFrom = wideToUtf8(oldName);
std::string wfxTo = wideToUtf8(newName);
std::string error;
bool crossDevice = false;
bool targetExists = false;
bool ok = gPluginCore->renameOrMove(wfxFrom, wfxTo, move != 0, overWrite != 0, &error,
&crossDevice, &targetExists);
if (ok) {
return FS_FILE_OK;
}
reportError(gLogProcW, gPluginNr, error);
// One mapping serves both halves of this entry point: Double
// Commander sends move=true for an F6 inside one device and
// move=false for an F5 there (a copy that stays on the phone).
//
// A refused overwrite maps to FS_FILE_EXISTS -- the same code
// getFile/putFile already return for the identical situation
// (wfxplugin.h defines it for exactly "target exists and
// OverWrite was false") -- checked ahead of crossDevice since the
// two are mutually exclusive by construction (PluginCore checks
// cross-device before ever reaching the overwrite check). A
// cross-device rejection maps to FS_FILE_NOTSUPPORTED, telling DC
// to fall back to its own get+put+delete emulation -- the one
// remaining failure mode that fallback can actually fix (and for
// a cross-device copy it really can finish the job). Every
// other failure (a shell error, a dropped transport) maps to
// FS_FILE_WRITEERROR instead: retrying the identical operation as
// a download+upload+delete would not fix a genuine error, and for
// a multi-gigabyte file wastes minutes attributing the failure to
// the wrong step.
if (targetExists) {
return FS_FILE_EXISTS;
}
return crossDevice ? FS_FILE_NOTSUPPORTED : FS_FILE_WRITEERROR;
} catch (...) {
return FS_FILE_WRITEERROR;
}
}
WFX_EXPORT BOOL DCPCALL FsDeleteFileW(WCHAR* remoteName) {
try {
if (!gPluginCore) {
return false;
}
std::string wfxRemote = wideToUtf8(remoteName);
std::string error;
bool ok = gPluginCore->deleteFile(wfxRemote, &error);
if (!ok) {
reportError(gLogProcW, gPluginNr, error);
}
return ok;
} catch (...) {
return false;
}
}
WFX_EXPORT BOOL DCPCALL FsRemoveDirW(WCHAR* remoteName) {
try {
if (!gPluginCore) {
return false;
}
std::string wfxRemote = wideToUtf8(remoteName);
std::string error;
bool ok = gPluginCore->removeDir(wfxRemote, &error);
if (!ok) {
reportError(gLogProcW, gPluginNr, error);
}
return ok;
} catch (...) {
return false;
}
}
WFX_EXPORT BOOL DCPCALL FsMkDirW(WCHAR* path) {
try {
if (!gPluginCore) {
return false;
}
std::string wfxDir = wideToUtf8(path);
std::string error;
bool ok = gPluginCore->makeDir(wfxDir, &error);
if (!ok) {
reportError(gLogProcW, gPluginNr, error);
}
return ok;
} catch (...) {
return false;
}
}
// Non-negotiable: Double
// Commander gates copying file dates, in *both* directions, on
// Assigned(FsSetTime) or Assigned(FsSetTimeW). Without this export, DC
// silently strips caoCopyTime from every copy operation's flags -- so
// dates would be lost even when downloading, where this plugin plays no
// part in setting them. Must always be present in the export table;
// scripts/check-exports.sh checks for it by name.
WFX_EXPORT BOOL DCPCALL FsSetTimeW(WCHAR* remoteName, FILETIME* creationTime, FILETIME* lastAccessTime,
FILETIME* lastWriteTime) {
try {
(void)creationTime; // ignored: only LastWriteTime is applied
(void)lastAccessTime; // Android's toybox touch has no separate atime knob worth using
if (!gPluginCore) {
return false;
}
if (isUnsetFileTime(lastWriteTime)) {
return true; // nothing to set is not a failure
}
std::string wfxRemote = wideToUtf8(remoteName);
time_t mtime = fileTimeToTime(*lastWriteTime);
std::string error;
bool ok = gPluginCore->setModificationTime(wfxRemote, static_cast<int64_t>(mtime), &error);
if (!ok) {
reportError(gLogProcW, gPluginNr, error);
}
return ok;
} catch (...) {
return false;
}
}
WFX_EXPORT void DCPCALL FsStatusInfoW(WCHAR* remoteDir, int infoStartEnd, int infoOperation) {
try {
if (!gPluginCore) {
return;
}
if (infoStartEnd != FS_STATUS_START || !isWriteOperation(infoOperation)) {
return;
}
gPluginCore->cache().clear();
if (gLogProcW != nullptr) {
std::string message =
"ADB: starting a write operation on " + wideToUtf8(remoteDir) +
" -- clearing the directory listing cache";
std::vector<WCHAR> wideMessage = utf8ToWide(message);
gLogProcW(gPluginNr, MSGTYPE_DETAILS, wideMessage.data());
}
} catch (...) {
// FsStatusInfoW has no error return; nothing else to do.
}
}
WFX_EXPORT void DCPCALL FsGetDefRootName(char* defRootName, int maxlen) {
try {
if (defRootName == nullptr || maxlen <= 0) {
return;
}
size_t len = std::strlen(ROOT_NAME);
size_t copyLen = (len < static_cast<size_t>(maxlen - 1)) ? len
: static_cast<size_t>(maxlen - 1);
std::memcpy(defRootName, ROOT_NAME, copyLen);
defRootName[copyLen] = '\0';
} catch (...) {
if (defRootName != nullptr && maxlen > 0) {
defRootName[0] = '\0';
}
}
}
WFX_EXPORT BOOL DCPCALL FsDisconnectW(WCHAR* disconnectRoot) {
try {
(void)disconnectRoot; // one shared cache for every device; nothing to key this on
if (!gPluginCore) {
return true;
}
gPluginCore->cache().clear();
return true;
} catch (...) {
return false;
}
}
} // extern "C"