QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MockLinkFTP.cc
Go to the documentation of this file.
1#include "MockLinkFTP.h"
2#include "MockLink.h"
4
5#include <cerrno>
6
7#include <QtCore/QDataStream>
8#include <QtCore/QDir>
9#include <QtCore/QTemporaryFile>
10#include <QtCore/QThread>
11
12QGC_LOGGING_CATEGORY(MockLinkFTPLog, "Comms.MockLink.MockLinkFTP")
13
14MockLinkFTP::MockLinkFTP(uint8_t systemIdServer, uint8_t componentIdServer, MockLink *mockLink)
15 : QObject(mockLink)
16 , _systemIdServer(systemIdServer)
17 , _componentIdServer(componentIdServer)
18 , _mockLink(mockLink)
19{
20 // qCDebug(MockLinkFTPLog) << Q_FUNC_INFO << this;
21}
22
24{
25 if (!_paramPckTempFile.isEmpty()) {
26 QFile::remove(_paramPckTempFile);
27 }
28 for (const QString &tempPath : std::as_const(_logFileTempPaths)) {
29 QFile::remove(tempPath);
30 }
31}
32
33void MockLinkFTP::ensureNullTemination(MavlinkFTP::Request *request)
34{
35 if (request->hdr.size < sizeof(request->data)) {
36 request->data[request->hdr.size] = '\0';
37 } else {
38 request->data[sizeof(request->data) - 1] = '\0';
39 }
40}
41
42void MockLinkFTP::_listCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber, bool withTime)
43{
44 MavlinkFTP::Request ackResponse{};
45 ensureNullTemination(request);
46
47 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
49
50 if (withTime && !_listDirectoryWithTimeSupported) {
51 // Simulate a server which doesn't implement the command. The client should fall back to kCmdListDirectory.
52 _sendNak(senderSystemId, senderComponentId, _listDirectoryWithTimeNakError, outgoingSeqNumber, listOpCode);
53 return;
54 }
55
56 // We support the root path and the @MAV_LOG virtual log directory tree
57 const QString path = reinterpret_cast<char*>(&request->data[0]);
58 const QString logPrefix = QStringLiteral("@MAV_LOG/");
59 QStringList entries;
60 if (path.isEmpty() || path == QStringLiteral("/")) {
61 // Default mock listing: 6 fixed entries
62 for (uint32_t index = 0; index < 6; index++) {
63 QString entry = QStringLiteral("Ffile%1.txt\t%2").arg(index).arg(1024 + index);
64 if (withTime) {
65 entry += QStringLiteral("\t%1").arg(kMockModificationTime + index);
66 }
67 entries.append(entry);
68 }
69 } else if ((path == QStringLiteral("@MAV_LOG")) || path.startsWith(logPrefix)) {
70 const QString subdir = path.mid(logPrefix.length());
71 const std::optional<QStringList> logEntries = _logDirectoryEntries(subdir, withTime);
72 if (!logEntries) {
73 qCDebug(MockLinkFTPLog) << "list of unknown @MAV_LOG directory:" << path;
74 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
75 return;
76 }
77 entries = *logEntries;
78 } else {
79 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
80 return;
81 }
82
83 if (request->hdr.offset > 0) {
84 if (_errMode == errModeNakSecondResponse) {
85 // Nak error all subsequent requests
86 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, listOpCode);
87 return;
88 }
89
90 if (_errMode == errModeNoSecondResponse) {
91 // No response for all subsequent requests
92 return;
93 }
94
95 if (_errMode == errModeNoSecondResponseAllowRetry) {
96 // No response to this request, subsequent requests will succeed
97 _errMode = errModeNone;
98 return;
99 }
100 }
101
102 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
103 ackResponse.hdr.req_opcode = listOpCode;
104 ackResponse.hdr.session = 0;
105 ackResponse.hdr.offset = request->hdr.offset;
106 ackResponse.hdr.size = 0;
107
108 // Entry format is "F<name>\t<size>", plus a trailing "\t<modification time>" for kCmdListDirectoryWithTime.
109 // MockLink sends up to two directory entries per packet.
110 if (request->hdr.offset < static_cast<uint32_t>(entries.count())) {
111 char *bufPtr = reinterpret_cast<char*>(&ackResponse.data[0]);
112 for (uint32_t index = request->hdr.offset; (index < request->hdr.offset + 2) && (index < static_cast<uint32_t>(entries.count())); index++) {
113 const QByteArray dirEntry = entries[static_cast<qsizetype>(index)].toUtf8();
114 if (ackResponse.hdr.size + dirEntry.length() + 1 > static_cast<int>(sizeof(ackResponse.data))) {
115 break;
116 }
117 (void) strncpy(bufPtr, dirEntry.constData(), dirEntry.length());
118 ackResponse.hdr.size += dirEntry.length() + 1;
119 bufPtr += dirEntry.length() + 1;
120 }
121 } else {
122 ackResponse.hdr.opcode = MavlinkFTP::kRspNak;
123 ackResponse.data[0] = MavlinkFTP::kErrEOF;
124 ackResponse.hdr.size = 1;
125 }
126
127 _sendResponse(senderSystemId, senderComponentId, &ackResponse, outgoingSeqNumber);
128}
129
130void MockLinkFTP::_openCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
131{
132 MavlinkFTP::Request response{};
133 ensureNullTemination(request);
134 const QString path = reinterpret_cast<char*>(request->data);
135
136 _uploadSession.reset();
137
138 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
139
140 if (_singleSessionEnforced && _currentFile.isOpen()) {
141 qCDebug(MockLinkFTPLog) << "MockLinkFTP: OpenFileRO while session open, NAK No Sessions Available";
142 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrNoSessionsAvailable, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
143 return;
144 }
145
146 const size_t cchPath = strnlen(reinterpret_cast<char*>(request->data), sizeof(request->data));
147 Q_ASSERT(cchPath != sizeof(request->data));
148 Q_UNUSED(cchPath); // Fix initialized-but-not-referenced warning on release builds
149
150 _currentFile.close();
151
152 QString tmpFilename;
153 const QString sizePrefix = sizeFilenamePrefix;
154 if (path.startsWith(sizePrefix)) {
155 const QString sizeString = path.right(path.length() - sizePrefix.length());
156 tmpFilename = _createTestTempFile(sizeString.toInt());
157 } else if (path == "/general.json" || path == "general.json") {
158 tmpFilename = QStringLiteral(":MockLink/General.MetaData.json");
159 } else if (path == "/general.json.xz" || path == "general.json.xz") {
160 tmpFilename = QStringLiteral(":MockLink/General.MetaData.json.xz");
161 } else if (path == "/parameter.json" || path == "parameter.json") {
162 tmpFilename = QStringLiteral(":MockLink/Parameter.MetaData.json");
163 } else if (path == "/parameter.json.xz" || path == "parameter.json.xz") {
164 tmpFilename = QStringLiteral(":MockLink/Parameter.MetaData.json.xz");
165 } else if (path == "@PARAM/param.pck" || path.startsWith("@PARAM/param.pck?")) {
166 if (!_paramPckEnabled) {
167 _sendNakErrno(senderSystemId, senderComponentId, ENOENT, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
168 return;
169 }
170 const bool withDefaults = path.contains(QStringLiteral("withdefaults=1"));
171 tmpFilename = _generateParamPck(withDefaults);
172 } else if (path.startsWith(QStringLiteral("@MAV_LOG/"))) {
173 tmpFilename = _logFileTempPath(path.mid(QStringLiteral("@MAV_LOG/").length()));
174 }
175
176 if (!tmpFilename.isEmpty()) {
177 _currentFile.setFileName(tmpFilename);
178 if (!_currentFile.open(QIODevice::ReadOnly)) {
179 _sendNakErrno(senderSystemId, senderComponentId, _currentFile.error(), outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
180 return;
181 }
182 } else {
183 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFailFileNotFound, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileRO);
184 return;
185 }
186
187 response.hdr.opcode = MavlinkFTP::kRspAck;
188 response.hdr.req_opcode = MavlinkFTP::kCmdOpenFileRO;
189 response.hdr.session = _sessionId;
190
191 // Data contains file length
192 response.hdr.size = sizeof(uint32_t);
193
194 // Ardupilot sends constant wrong file size for parameter file due to dynamic on the fly generation
195 response.openFileLength = ((path == "@PARAM/param.pck" || path.startsWith("@PARAM/param.pck?")) ? qPow(1024, 2) : _currentFile.size());
196
197 _openFileROCount++;
198 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
199}
200
201void MockLinkFTP::_createFileCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
202{
203 ensureNullTemination(request);
204
205 const QString path = reinterpret_cast<char*>(request->data);
206 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
207
208 if (path.isEmpty()) {
209 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdCreateFile);
210 return;
211 }
212
213 _uploadSession.reset();
214 _uploadSession.active = true;
215 _uploadSession.remotePath = path;
216
217 MavlinkFTP::Request response{};
218 response.hdr.opcode = MavlinkFTP::kRspAck;
219 response.hdr.req_opcode = MavlinkFTP::kCmdCreateFile;
220 response.hdr.session = _sessionId;
221 response.hdr.size = 0;
222
223 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
224}
225
226void MockLinkFTP::_openFileWOCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
227{
228 ensureNullTemination(request);
229
230 const QString path = reinterpret_cast<char*>(request->data);
231 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
232
233 if (path.isEmpty()) {
234 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdOpenFileWO);
235 return;
236 }
237
238 _uploadSession.reset();
239 _uploadSession.active = true;
240 _uploadSession.remotePath = path;
241
242 MavlinkFTP::Request response{};
243 response.hdr.opcode = MavlinkFTP::kRspAck;
244 response.hdr.req_opcode = MavlinkFTP::kCmdOpenFileWO;
245 response.hdr.session = _sessionId;
246 response.hdr.size = 0;
247
248 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
249}
250
251void MockLinkFTP::_readCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
252{
253 MavlinkFTP::Request response{};
254 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
255 _readFileCount++;
256 _lastReadFileRequestSize = request->hdr.size;
257
258 if (request->hdr.session != _sessionId) {
259 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
260 return;
261 }
262
263 if (!_currentFile.isOpen()) {
264 qCDebug(MockLinkFTPLog) << "MockLinkFTP: ReadFile with no open session, NAK Invalid Session";
265 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
266 return;
267 }
268
269 const uint32_t readOffset = request->hdr.offset; // offset into file for reading
270 if (readOffset != 0) {
271 // If we get here it means the client is requesting additional data past the first request
272 if (_errMode == errModeNakSecondResponse) {
273 // Nak error all subsequent requests
274 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
275 return;
276 }
277
278 if (_errMode == errModeNoSecondResponse) {
279 // No rsponse for all subsequent requests
280 return;
281 }
282 }
283
284 if (readOffset >= _currentFile.size()) {
285 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdReadFile);
286 return;
287 }
288
289 // ArduPilot semantics: size 0 means the full payload, larger requests are clamped to it
290 const qint64 maxRead = qMin<qint64>(request->hdr.size ? request->hdr.size : sizeof(response.data), sizeof(response.data));
291 const uint8_t cBytesToRead = static_cast<uint8_t>(qMin(maxRead, _currentFile.size() - readOffset));
292 (void) _currentFile.seek(readOffset);
293 const QByteArray bytes = _currentFile.read(cBytesToRead);
294 (void) memcpy(response.data, bytes.constData(), cBytesToRead);
295
296 // We should always have written something, otherwise there is something wrong with the code above
297 Q_ASSERT(cBytesToRead);
298
299 response.hdr.session = _sessionId;
300 response.hdr.size = cBytesToRead;
301 response.hdr.offset = request->hdr.offset;
302 response.hdr.opcode = MavlinkFTP::kRspAck;
303 response.hdr.req_opcode = MavlinkFTP::kCmdReadFile;
304
305 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
306}
307
308void MockLinkFTP::_removeFileCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
309{
310 ensureNullTemination(request);
311 const QString path = reinterpret_cast<char*>(request->data);
312 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
313
314 const QString logPrefix = QStringLiteral("@MAV_LOG/");
315 if (path.startsWith(logPrefix)) {
316 const QString name = path.mid(logPrefix.length());
317 for (int i = 0; i < _logFiles.count(); i++) {
318 if (_logFiles[i].name == name) {
319 _logFiles.removeAt(i);
320
321 // Drop the cached temp file so the deleted log is no longer downloadable
322 const QString tempPath = _logFileTempPaths.take(name);
323 if (!tempPath.isEmpty()) {
324 QFile::remove(tempPath);
325 }
326
327 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdRemoveFile);
328 return;
329 }
330 }
331 }
332
333 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFailFileNotFound, outgoingSeqNumber, MavlinkFTP::kCmdRemoveFile);
334}
335
336void MockLinkFTP::_burstReadCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
337{
338 _lastBurstReadRequestSize = request->hdr.size;
339
340 if (_burstReadDelayMs > 0) {
341 QThread::msleep(_burstReadDelayMs);
342 }
343
344 MavlinkFTP::Request response{};
345 uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
346
347 if (request->hdr.session != _sessionId) {
348 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
349 return;
350 }
351
352 if (!_currentFile.isOpen()) {
353 qCDebug(MockLinkFTPLog) << "MockLinkFTP: BurstReadFile with no open session, NAK Invalid Session";
354 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
355 return;
356 }
357
358 if (_errMode == errModeNoSecondResponse) {
359 return;
360 }
361
362 constexpr int burstMax = 10;
363 int burstCount = 1;
364 uint32_t burstOffset = request->hdr.offset;
365 // ArduPilot semantics: size 0 means the full payload, larger requests are clamped to it
366 const qint64 maxRead = qMin<qint64>(request->hdr.size ? request->hdr.size : sizeof(response.data), sizeof(response.data));
367 MavlinkFTP::Request reorderedResponse{};
368 uint16_t reorderedSeqNumber = 0;
369 bool haveReorderedResponse = false;
370
371 while ((burstOffset < _currentFile.size()) && (burstCount++ < burstMax)) {
372 _currentFile.seek(burstOffset);
373
374 const uint8_t cBytes = static_cast<uint8_t>(qMin(maxRead, _currentFile.size() - burstOffset));
375 const QByteArray bytes = _currentFile.read(cBytes);
376 Q_ASSERT(cBytes); // We should always have written something, otherwise there is something wrong with the code above
377
378 (void) memcpy(response.data, bytes.constData(), cBytes);
379
380 response.hdr.session = _sessionId;
381 response.hdr.size = cBytes;
382 response.hdr.offset = burstOffset;
383 response.hdr.opcode = MavlinkFTP::kRspAck;
384 response.hdr.req_opcode = MavlinkFTP::kCmdBurstReadFile;
385 response.hdr.burstComplete = (burstCount == burstMax) ? 1 : 0;
386
387 if (_dropBurstPacketPending && (burstOffset == _dropBurstPacketOffset)) {
388 qCDebug(MockLinkFTPLog) << "MockLinkFTP: dropping burst packet at offset" << burstOffset;
389 _dropBurstPacketPending = false;
390 } else if (_reorderBurstPacketPending && (burstOffset == _reorderBurstPacketOffset)) {
391 qCDebug(MockLinkFTPLog) << "MockLinkFTP: holding back burst packet at offset" << burstOffset;
392 _reorderBurstPacketPending = false;
393 reorderedResponse = response;
394 reorderedSeqNumber = outgoingSeqNumber;
395 haveReorderedResponse = true;
396 } else {
397 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
398 }
399
400 outgoingSeqNumber = _nextSeqNumber(outgoingSeqNumber);
401 burstOffset += cBytes;
402 }
403
404 if (haveReorderedResponse) {
405 _sendResponse(senderSystemId, senderComponentId, &reorderedResponse, reorderedSeqNumber);
406 }
407
408 if (burstOffset >= _currentFile.size()) {
409 // Burst is fully complete
410 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrEOF, outgoingSeqNumber, MavlinkFTP::kCmdBurstReadFile);
411 }
412
413 if ((_expireSessionAfterBursts > 0) && (++_burstsServed >= _expireSessionAfterBursts)) {
414 qCDebug(MockLinkFTPLog) << "MockLinkFTP: expiring session after burst" << _burstsServed;
415 _currentFile.close();
416 _expireSessionAfterBursts = 0;
417 }
418}
419
420void MockLinkFTP::_terminateCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
421{
422 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
423
424 if (request->hdr.session != _sessionId) {
425 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
426 return;
427 }
428
429 _currentFile.close();
430 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdTerminateSession);
431
432 _finalizeActiveUpload();
433
435}
436
437void MockLinkFTP::_resetCommand(uint8_t senderSystemId, uint8_t senderComponentId, uint16_t seqNumber)
438{
440
441 if (_ignoreResetSessions) {
442 return;
443 }
444
445 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
446
447 _currentFile.close();
448 _sendAck(senderSystemId, senderComponentId, outgoingSeqNumber, MavlinkFTP::kCmdResetSessions);
449
450 _finalizeActiveUpload();
451}
452
454{
455 _currentFile.close();
456 _currentFile.setFileName(_createTestTempFile(1024));
457 (void) _currentFile.open(QIODevice::ReadOnly);
458}
459
460void MockLinkFTP::_writeCommand(uint8_t senderSystemId, uint8_t senderComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
461{
462 const uint16_t outgoingSeqNumber = _nextSeqNumber(seqNumber);
463
464 if ((request->hdr.session != _sessionId) || !_uploadSession.active) {
465 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrInvalidSession, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
466 return;
467 }
468
469 if (request->hdr.offset > static_cast<uint32_t>(_uploadSession.buffer.size())) {
470 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
471 return;
472 }
473
474 if (request->hdr.offset != 0) {
475 if (_errMode == errModeNakSecondResponse) {
476 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
477 return;
478 }
479
480 if (_errMode == errModeNoSecondResponse) {
481 return;
482 }
483
484 if (_errMode == errModeNoSecondResponseAllowRetry) {
485 _errMode = errModeNone;
486 return;
487 }
488 }
489
490 const uint32_t bytesToWrite = request->hdr.size;
491 if (bytesToWrite > sizeof(request->data)) {
492 _sendNak(senderSystemId, senderComponentId, MavlinkFTP::kErrFail, outgoingSeqNumber, MavlinkFTP::kCmdWriteFile);
493 return;
494 }
495
496 const uint32_t requiredSize = request->hdr.offset + bytesToWrite;
497 if (requiredSize > static_cast<uint32_t>(_uploadSession.buffer.size())) {
498 _uploadSession.buffer.resize(requiredSize);
499 }
500
501 if (bytesToWrite > 0) {
502 (void) memcpy(_uploadSession.buffer.data() + request->hdr.offset, request->data, bytesToWrite);
503 }
504
505 MavlinkFTP::Request response{};
506 response.hdr.opcode = MavlinkFTP::kRspAck;
507 response.hdr.req_opcode = MavlinkFTP::kCmdWriteFile;
508 response.hdr.session = _sessionId;
509 response.hdr.size = 0;
510 response.hdr.offset = request->hdr.offset;
511
512 _sendResponse(senderSystemId, senderComponentId, &response, outgoingSeqNumber);
513}
514
515void MockLinkFTP::_finalizeActiveUpload()
516{
517 if (!_uploadSession.active) {
518 return;
519 }
520
521 if (!_uploadSession.remotePath.isEmpty()) {
522 _uploadedFiles.insert(_uploadSession.remotePath, _uploadSession.buffer);
523 }
524
525 _uploadSession.reset();
526}
527
529{
530 if (message.msgid != MAVLINK_MSG_ID_FILE_TRANSFER_PROTOCOL) {
531 return;
532 }
533
534 mavlink_file_transfer_protocol_t requestFTP{};
535 mavlink_msg_file_transfer_protocol_decode(&message, &requestFTP);
536
537 if (requestFTP.target_system != _systemIdServer) {
538 return;
539 }
540
541 MavlinkFTP::Request *request = reinterpret_cast<MavlinkFTP::Request*>(&requestFTP.payload[0]);
542
543 // kCmdOpenFileRO and kCmdResetSessions don't support retry so we can't drop those
544 if (_randomDropsEnabled && (request->hdr.opcode != MavlinkFTP::kCmdOpenFileRO) && (request->hdr.opcode != MavlinkFTP::kCmdResetSessions)) {
545 if ((rand() % 5) == 0) {
546 qCDebug(MockLinkFTPLog) << "MockLinkFTP: Random drop of incoming packet";
547 return;
548 }
549 }
550
551 if (_lastReplyValid && (request->hdr.seqNumber == (_lastReplySequence - 1))) {
552 // This is the same request as the one we replied to last. It means the (n)ack got lost, and the GCS
553 // resent the request
554 qCDebug(MockLinkFTPLog) << "MockLinkFTP: resending response";
555 _mockLink->respondWithMavlinkMessage(_lastReply);
556 return;
557 }
558
559 const uint16_t incomingSeqNumber = request->hdr.seqNumber;
560 const uint16_t outgoingSeqNumber = _nextSeqNumber(incomingSeqNumber);
561
562 if ((request->hdr.opcode != MavlinkFTP::kCmdResetSessions) && (request->hdr.opcode != MavlinkFTP::kCmdTerminateSession)) {
563 if (_errMode == errModeNoResponse) {
564 // Don't respond to any requests, this shold cause the client to eventually timeout waiting for the ack
565 return;
566 }
567
568 if (_errMode == errModeNakResponse) {
569 // Nak all requests, the actual error send back doesn't really matter as long as it's an error
570 _sendNak(message.sysid, message.compid, MavlinkFTP::kErrFail, outgoingSeqNumber, static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode));
571 return;
572 }
573 }
574
575 MavlinkFTP::Request ackResponse{};
576 switch (request->hdr.opcode) {
578 // ignored, always acked
579 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
580 ackResponse.hdr.session = 0;
581 ackResponse.hdr.size = 0;
582 _sendResponse(message.sysid, message.compid, &ackResponse, outgoingSeqNumber);
583 break;
585 _listCommand(message.sysid, message.compid, request, incomingSeqNumber, false /* withTime */);
586 break;
588 _listCommand(message.sysid, message.compid, request, incomingSeqNumber, true /* withTime */);
589 break;
591 _openCommand(message.sysid, message.compid, request, incomingSeqNumber);
592 break;
594 _createFileCommand(message.sysid, message.compid, request, incomingSeqNumber);
595 break;
597 _openFileWOCommand(message.sysid, message.compid, request, incomingSeqNumber);
598 break;
600 _readCommand(message.sysid, message.compid, request, incomingSeqNumber);
601 break;
603 _burstReadCommand(message.sysid, message.compid, request, incomingSeqNumber);
604 break;
606 _removeFileCommand(message.sysid, message.compid, request, incomingSeqNumber);
607 break;
609 _writeCommand(message.sysid, message.compid, request, incomingSeqNumber);
610 break;
612 _terminateCommand(message.sysid, message.compid, request, incomingSeqNumber);
613 break;
615 _resetCommand(message.sysid, message.compid, incomingSeqNumber);
616 break;
617 default:
618 // nack for all NYI opcodes
619 _sendNak(message.sysid, message.compid, MavlinkFTP::kErrUnknownCommand, outgoingSeqNumber, static_cast<MavlinkFTP::OpCode_t>(request->hdr.opcode));
620 break;
621 }
622}
623
624void MockLinkFTP::_sendAck(uint8_t targetSystemId, uint8_t targetComponentId, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
625{
626 MavlinkFTP::Request ackResponse{};
627
628 ackResponse.hdr.opcode = MavlinkFTP::kRspAck;
629 ackResponse.hdr.req_opcode = reqOpcode;
630 ackResponse.hdr.session = _sessionId;
631 ackResponse.hdr.size = 0;
632
633 _sendResponse(targetSystemId, targetComponentId, &ackResponse, seqNumber);
634}
635
636void MockLinkFTP::_sendNak(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::ErrorCode_t error, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
637{
638 MavlinkFTP::Request nakResponse{};
639
640 nakResponse.hdr.opcode = MavlinkFTP::kRspNak;
641 nakResponse.hdr.req_opcode = reqOpcode;
642 nakResponse.hdr.session = _sessionId;
643 nakResponse.hdr.size = 1;
644 nakResponse.data[0] = error;
645
646 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
647}
648
649void MockLinkFTP::_sendNakErrno(uint8_t targetSystemId, uint8_t targetComponentId, uint8_t nakErrno, uint16_t seqNumber, MavlinkFTP::OpCode_t reqOpcode)
650{
651 MavlinkFTP::Request nakResponse{};
652
653 nakResponse.hdr.opcode = MavlinkFTP::kRspNak;
654 nakResponse.hdr.req_opcode = reqOpcode;
655 nakResponse.hdr.session = _sessionId;
656 nakResponse.hdr.size = 2;
657 nakResponse.data[0] = MavlinkFTP::kErrFailErrno;
658 nakResponse.data[1] = nakErrno;
659
660 _sendResponse(targetSystemId, targetComponentId, &nakResponse, seqNumber);
661}
662
663
664void MockLinkFTP::_sendResponse(uint8_t targetSystemId, uint8_t targetComponentId, MavlinkFTP::Request *request, uint16_t seqNumber)
665{
666 request->hdr.seqNumber = seqNumber;
667 _lastReplySequence = seqNumber;
668 _lastReplyValid = true;
669
670 (void) mavlink_msg_file_transfer_protocol_pack_chan(
671 _systemIdServer, // System ID
672 _componentIdServer, // Component ID
673 _mockLink->outgoingMavlinkChannel(),
674 &_lastReply, // Mavlink Message to pack into
675 0, // Target network
676 targetSystemId,
677 targetComponentId,
678 reinterpret_cast<uint8_t*>(request) // Payload
679 );
680
681 // kCmdOpenFileRO and kCmdResetSessions don't support retry so we can't drop those
682 if (_randomDropsEnabled && (request->hdr.req_opcode != MavlinkFTP::kCmdOpenFileRO) && (request->hdr.req_opcode != MavlinkFTP::kCmdResetSessions)) {
683 if ((rand() % 5) == 0) {
684 qCDebug(MockLinkFTPLog) << "MockLinkFTP: Random drop of outgoing packet";
685 return;
686 }
687 }
688
689 _mockLink->respondWithMavlinkMessage(_lastReply);
690}
691
692
693uint16_t MockLinkFTP::_nextSeqNumber(uint16_t seqNumber) const
694{
695 uint16_t outgoingSeqNumber = seqNumber + 1;
696
697 if (_errMode == errModeBadSequence) {
698 outgoingSeqNumber++;
699 }
700
701 return outgoingSeqNumber;
702}
703
704QString MockLinkFTP::_createTestTempFile(int size)
705{
706 QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral("/MockLinkFTPTestCaseXXXXXX"));
707 tmpFile.setAutoRemove(false);
708
709 if (tmpFile.open()) {
710 for (int i = 0; i < size; i++) {
711 (void) tmpFile.write(QByteArray(1, static_cast<char>(i % 255)));
712 }
713 tmpFile.close();
714 }
715
716 return tmpFile.fileName();
717}
718
719QByteArray MockLinkFTP::logFileContents(const QString &name) const
720{
721 for (const LogFile &logFile : _logFiles) {
722 if (logFile.name == name) {
723 return _generateLogFileContents(name, logFile.size);
724 }
725 }
726 return QByteArray();
727}
728
729QByteArray MockLinkFTP::_generateLogFileContents(const QString &name, int size)
730{
731 const uint seed = qHash(name);
732 QByteArray contents(size, Qt::Uninitialized);
733 for (int i = 0; i < size; i++) {
734 contents[i] = static_cast<char>((seed + static_cast<uint>(i)) & 0xFF);
735 }
736 return contents;
737}
738
739void MockLinkFTP::setLogFiles(const QList<LogFile> &logFiles)
740{
741 // Invalidate cached temp files so a reused name is regenerated with the new contents
742 for (const QString &tempPath : std::as_const(_logFileTempPaths)) {
743 QFile::remove(tempPath);
744 }
745 _logFileTempPaths.clear();
746
747 _logFiles = logFiles;
748}
749
750QString MockLinkFTP::_logFileTempPath(const QString &name)
751{
752 if (_logFileTempPaths.contains(name)) {
753 return _logFileTempPaths.value(name);
754 }
755
756 for (const LogFile &logFile : std::as_const(_logFiles)) {
757 if (logFile.name != name) {
758 continue;
759 }
760
761 QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral("/MockLinkFTPLogXXXXXX"));
762 tmpFile.setAutoRemove(false);
763 if (!tmpFile.open()) {
764 return QString();
765 }
766 (void) tmpFile.write(_generateLogFileContents(name, logFile.size));
767 tmpFile.close();
768
769 _logFileTempPaths.insert(name, tmpFile.fileName());
770 return tmpFile.fileName();
771 }
772
773 return QString();
774}
775
776std::optional<QStringList> MockLinkFTP::_logDirectoryEntries(const QString& subdir, bool withTime) const
777{
778 const auto dirEntry = [this, withTime](const QString& name) {
779 QString entry = QStringLiteral("D") + name;
780 if (withTime && _logDirEntriesWithTime) {
781 entry += QStringLiteral("\t0\t%1").arg(kMockModificationTime);
782 }
783 return entry;
784 };
785
786 QStringList entries;
787 if (_logDirDotEntries) {
788 entries.append(dirEntry(QStringLiteral(".")));
789 entries.append(dirEntry(QStringLiteral("..")));
790 }
791
792 const QString prefix = subdir.isEmpty() ? QString() : (subdir + QLatin1Char('/'));
793 bool found = subdir.isEmpty();
794 QStringList childDirs;
795 for (const LogFile& logFile : _logFiles) {
796 if (!logFile.name.startsWith(prefix)) {
797 continue;
798 }
799 found = true;
800
801 const QString relativeName = logFile.name.mid(prefix.length());
802 const qsizetype slashIdx = relativeName.indexOf(QLatin1Char('/'));
803 if (slashIdx >= 0) {
804 const QString childDir = relativeName.left(slashIdx);
805 if (!childDirs.contains(childDir)) {
806 childDirs.append(childDir);
807 entries.append(dirEntry(childDir));
808 }
809 continue;
810 }
811
812 QString entry = QStringLiteral("F%1\t%2").arg(relativeName).arg(logFile.size);
813 if (withTime) {
814 entry += QStringLiteral("\t%1").arg(logFile.mtime);
815 }
816 entries.append(entry);
817 }
818
819 if (!found) {
820 return std::nullopt;
821 }
822 return entries;
823}
824
825QString MockLinkFTP::_generateParamPck(bool withDefaults)
826{
827 // AP_PARAM types used in the binary param.pck format
828 enum APParamType : quint8 {
829 AP_PARAM_INT8 = 1,
830 AP_PARAM_INT16 = 2,
831 AP_PARAM_INT32 = 3,
832 AP_PARAM_FLOAT = 4,
833 };
834
835 // ArduPilot binary param.pck format magic numbers.
836 // See AP_Filesystem_Param.cpp in ArduPilot source (AP_PARAM_HEADER_MAGIC / AP_PARAM_HEADER_MAGIC_DEFAULT).
837 constexpr quint16 magicStandard = 0x671B; // param.pck without defaults
838 constexpr quint16 magicWithDefaults = 0x671C; // param.pck?withdefaults=1
839 constexpr int readSize = 239; // Max data bytes per MAVLink FTP burst: MAVLink payload (251 bytes) minus FTP header (12 bytes) = 239.
840 // This matches the MAVLink FTP spec / PX4-ArduPilot implementations and is critical to correct chunking and padding.
841
842 // Get params for component 1 (MAV_COMP_ID_AUTOPILOT1)
843 constexpr int compId = 1;
844 const auto &valueMap = _mockLink->_mapParamName2Value;
845 const auto &typeMap = _mockLink->_mapParamName2MavParamType;
846 if (!valueMap.contains(compId) || !typeMap.contains(compId)) {
847 qCWarning(MockLinkFTPLog) << "_generateParamPck: no parameters for component" << compId;
848 return QString();
849 }
850
851 const auto &values = valueMap[compId];
852 const auto &types = typeMap[compId];
853
854 // Sort parameter names alphabetically (matching ArduPilot behavior)
855 QStringList paramNames = values.keys();
856 paramNames.sort();
857
858 const quint16 numParams = static_cast<quint16>(paramNames.size());
859
860 // Map MAV_PARAM_TYPE to AP_PARAM type and value size
861 auto mapType = [](MAV_PARAM_TYPE mavType, quint8 &apType, int &valueSize) {
862 switch (mavType) {
863 case MAV_PARAM_TYPE_UINT8:
864 case MAV_PARAM_TYPE_INT8:
865 apType = AP_PARAM_INT8;
866 valueSize = 1;
867 break;
868 case MAV_PARAM_TYPE_UINT16:
869 case MAV_PARAM_TYPE_INT16:
870 apType = AP_PARAM_INT16;
871 valueSize = 2;
872 break;
873 case MAV_PARAM_TYPE_UINT32:
874 case MAV_PARAM_TYPE_INT32:
875 apType = AP_PARAM_INT32;
876 valueSize = 4;
877 break;
878 case MAV_PARAM_TYPE_REAL32:
879 apType = AP_PARAM_FLOAT;
880 valueSize = 4;
881 break;
882 default:
883 apType = AP_PARAM_FLOAT;
884 valueSize = 4;
885 break;
886 }
887 };
888
889 QByteArray data;
890 QDataStream stream(&data, QIODevice::WriteOnly);
891 stream.setByteOrder(QDataStream::LittleEndian);
892
893 // Write header
894 const quint16 magic = withDefaults ? magicWithDefaults : magicStandard;
895 stream << magic << numParams << numParams;
896
897 QByteArray previousName;
898 constexpr int headerSize = 6; // 2 bytes magic + 2 bytes numParams + 2 bytes totalParams
899
900 for (const QString &name : paramNames) {
901 const QVariant &value = values[name];
902 const MAV_PARAM_TYPE mavType = types[name];
903 const QByteArray nameBytes = name.toLatin1();
904
905 quint8 apType = 0;
906 int valueSize = 0;
907 mapType(mavType, apType, valueSize);
908
909 // Compute common prefix with previous parameter name
910 const int previousNameLen = previousName.size();
911 const int currentNameLen = nameBytes.size();
912 int commonLen = 0;
913 // common_len is limited to 15 because common_len + name_len <= 16 and name_len must be >= 1
914 const int maxCommon = std::min({previousNameLen, currentNameLen, 15});
915 while (commonLen < maxCommon && nameBytes[commonLen] == previousName[commonLen]) {
916 commonLen++;
917 }
918
919 int nameLen = currentNameLen - commonLen;
920 // name_len is encoded as (name_len - 1) in 4 bits, so it must be >= 1.
921 // If the entire name matched the common prefix, steal one char back.
922 if (nameLen == 0 && commonLen > 0) {
923 nameLen = 1;
924 commonLen--;
925 }
926 // Ensure name_len + common_len <= 16
927 if (nameLen + commonLen > 16) {
928 commonLen = 16 - nameLen;
929 }
930 // name_len must fit in 4 bits as (name_len - 1)
931 if (nameLen > 16) {
932 nameLen = 16;
933 commonLen = 0;
934 }
935
936 // For ArduPilot, calibration-indicator parameters have a firmware default of 0
937 // (uncalibrated). All other parameters use their current value as the default,
938 // since MockLink params are loaded from a snapshot and we have no separate
939 // defaults file.
940 const bool useZeroDefault = withDefaults
941 && (_mockLink->getFirmwareType() == MAV_AUTOPILOT_ARDUPILOTMEGA)
942 && MockLink::kAPMCalOffsetParams.contains(name);
943 const bool addDefault = withDefaults;
944 const quint8 flags = addDefault ? 0x01 : 0x00;
945 const int packedLen = 2 + nameLen + valueSize + (addDefault ? valueSize : 0);
946
947 // FTP reads data in fixed-size blocks (readSize = 239 bytes, the MAVLink FTP
948 // payload). If a multi-byte value straddles a block boundary, a retransmitted
949 // block could overwrite part of the value with stale data, corrupting it.
950 // To prevent this, ArduPilot inserts zero-byte padding before the entry so
951 // the value bytes land entirely within one block. The parser skips leading
952 // zeros between entries. See AP_Filesystem_Param::pack_param.
953 if (valueSize > 1) {
954 const quint32 dataOfs = static_cast<quint32>(data.size()) - headerSize;
955 const quint32 endOfs = dataOfs + static_cast<quint32>(packedLen);
956 const quint32 endMod = endOfs % readSize;
957 if (endMod > 0 && endMod < static_cast<quint32>(valueSize)) {
958 const int pad = valueSize - static_cast<int>(endMod);
959 for (int i = 0; i < pad; i++) {
960 stream << static_cast<quint8>(0);
961 }
962 }
963 }
964
965 // Type + flags byte
966 stream << static_cast<quint8>(apType | (flags << 4));
967 // Name encoding byte: upper 4 bits = (name_len - 1), lower 4 bits = common_len
968 stream << static_cast<quint8>(((nameLen - 1) << 4) | commonLen);
969 // Write the unique suffix of the name
970 stream.writeRawData(nameBytes.constData() + commonLen, nameLen);
971
972 // Write parameter value
973 switch (apType) {
974 case AP_PARAM_INT8: {
975 const qint8 v = static_cast<qint8>(value.toInt());
976 stream << v;
977 if (addDefault) stream << (useZeroDefault ? qint8(0) : v);
978 break;
979 }
980 case AP_PARAM_INT16: {
981 const qint16 v = static_cast<qint16>(value.toInt());
982 stream << v;
983 if (addDefault) stream << (useZeroDefault ? qint16(0) : v);
984 break;
985 }
986 case AP_PARAM_INT32: {
987 const qint32 v = value.toInt();
988 stream << v;
989 if (addDefault) stream << (useZeroDefault ? qint32(0) : v);
990 break;
991 }
992 case AP_PARAM_FLOAT: {
993 const float f = value.toFloat();
994 qint32 raw;
995 memcpy(&raw, &f, sizeof(raw));
996 stream << raw;
997 if (addDefault) {
998 if (useZeroDefault) {
999 stream << qint32(0);
1000 } else {
1001 stream << raw;
1002 }
1003 }
1004 break;
1005 }
1006 }
1007
1008 previousName = nameBytes;
1009 }
1010
1011 // Clean up previous temp file
1012 if (!_paramPckTempFile.isEmpty()) {
1013 QFile::remove(_paramPckTempFile);
1014 _paramPckTempFile.clear();
1015 }
1016
1017 // Write to temp file
1018 QTemporaryFile tmpFile(QDir::temp().filePath(QStringLiteral("MockLinkParamPckXXXXXX")));
1019 tmpFile.setAutoRemove(false);
1020
1021 if (!tmpFile.open()) {
1022 qCWarning(MockLinkFTPLog) << "_generateParamPck: failed to create temp file";
1023 return QString();
1024 }
1025
1026 tmpFile.write(data);
1027 tmpFile.close();
1028 _paramPckTempFile = tmpFile.fileName();
1029
1030 qCDebug(MockLinkFTPLog) << "_generateParamPck: generated" << numParams << "params,"
1031 << data.size() << "bytes, withDefaults:" << withDefaults
1032 << "file:" << tmpFile.fileName();
1033
1034 return tmpFile.fileName();
1035}
uint qHash(const FirmwareUpgradeController::FirmwareIdentifier &firmwareId)
Error error
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
ErrorCode_t
Error codes returned in Nak response PayloadHeader.data[0].
Definition MAVLinkFTP.h:62
@ kErrNoSessionsAvailable
All available Sessions in use.
Definition MAVLinkFTP.h:68
@ kErrFail
Unknown failure.
Definition MAVLinkFTP.h:64
@ kErrEOF
Offset past end of file for List and Read commands.
Definition MAVLinkFTP.h:69
@ kErrUnknownCommand
Unknown command opcode.
Definition MAVLinkFTP.h:70
@ kErrFailErrno
errno sent back in PayloadHeader.data[1]
Definition MAVLinkFTP.h:65
@ kErrInvalidSession
Session is not currently open.
Definition MAVLinkFTP.h:67
@ kErrFailFileNotFound
Definition MAVLinkFTP.h:73
@ kCmdRemoveFile
Remove file at <path>
Definition MAVLinkFTP.h:47
@ kCmdWriteFile
Writes <size> bytes to <offset> in <session>
Definition MAVLinkFTP.h:46
@ kCmdCreateFile
Creates file at <path> for writing, returns <session>
Definition MAVLinkFTP.h:45
@ kCmdOpenFileRO
Opens file at <path> for reading, returns <session>
Definition MAVLinkFTP.h:43
@ kCmdListDirectoryWithTime
List directory as kCmdListDirectory, each entry additionally carrying trailing \t<modification time>
Definition MAVLinkFTP.h:55
@ kCmdReadFile
Reads <size> bytes from <offset> in <session>
Definition MAVLinkFTP.h:44
@ kCmdOpenFileWO
Opens file at <path> for writing, returns <session>
Definition MAVLinkFTP.h:50
@ kCmdResetSessions
Terminates all open Read sessions.
Definition MAVLinkFTP.h:41
@ kCmdNone
ignored, always acked
Definition MAVLinkFTP.h:39
@ kRspAck
Ack response.
Definition MAVLinkFTP.h:57
@ kCmdBurstReadFile
Burst download session file.
Definition MAVLinkFTP.h:54
@ kCmdTerminateSession
Terminates open Read session.
Definition MAVLinkFTP.h:40
@ kRspNak
Nak response.
Definition MAVLinkFTP.h:58
@ kCmdListDirectory
List files in <path> from <offset>
Definition MAVLinkFTP.h:42
Mock implementation of Mavlink FTP server.
Definition MockLinkFTP.h:18
void resetCommandReceived()
You can connect to this signal to be notified when the server receives a Reset command.
static constexpr uint32_t kMockModificationTime
void terminateCommandReceived()
You can connect to this signal to be notified when the server receives a Terminate command.
QList< LogFile > logFiles() const
Returns the log files served from the @MAV_LOG virtual directory.
Definition MockLinkFTP.h:41
void setLogFiles(const QList< LogFile > &logFiles)
void openStaleSessionForTest()
Opens a read session server-side without any client request, simulating a session left behind by a cr...
static constexpr const char * sizeFilenamePrefix
void mavlinkMessageReceived(const mavlink_message_t &message)
Called to handle an FTP message.
QByteArray logFileContents(const QString &name) const
@ errModeBadSequence
Return response with bad sequence number.
@ errModeNoSecondResponseAllowRetry
No response to subsequent request to initial command, error will be cleared after this so retry will ...
@ errModeNakSecondResponse
Nak subsequent request to initial command.
@ errModeNoResponse
No response to any request, client should eventually time out with no Ack.
@ errModeNone
No error, respond correctly.
@ errModeNoSecondResponse
No response to subsequent request to initial command.
@ errModeNakResponse
Nak all requests.
Describes a simulated onboard log file served from the @MAV_LOG virtual directory.
Definition MockLinkFTP.h:30