GCC Code Coverage Report


Directory: ./
File: Svc/Ccsds/CfdpManager/Engine.cpp
Date: 2026-11-03 22:10:14
Exec Total Coverage
Lines: 0 568 0.0%
Functions: 0 44 0.0%
Branches: 0 374 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title Engine.cpp
3 // \brief CFDP Engine implementation
4 //
5 // This file is a port of CFDP engine operations from the following files
6 // from the NASA Core Flight System (cFS) CFDP (CF) Application, version 3.0.0,
7 // adapted for use within the F-Prime (F') framework:
8 // - cf_cfdp.c (CFDP PDU validation, processing, and engine operations)
9 //
10 // This file contains two sets of functions. The first is what is needed
11 // to deal with CFDP PDUs. Specifically validating them for correctness
12 // and ensuring the byte-order is correct for the target. The second
13 // is incoming and outgoing CFDP PDUs pass through here. All receive
14 // CFDP PDU logic is performed here and the data is passed to the
15 // R (rx) and S (tx) logic.
16 //
17 // ======================================================================
18 //
19 // NASA Docket No. GSC-18,447-1
20 //
21 // Copyright (c) 2019 United States Government as represented by the
22 // Administrator of the National Aeronautics and Space Administration.
23 // All Rights Reserved.
24 //
25 // Licensed under the Apache License, Version 2.0 (the "License"); you may
26 // not use this file except in compliance with the License. You may obtain
27 // a copy of the License at
28 //
29 // http://www.apache.org/licenses/LICENSE-2.0
30 //
31 // Unless required by applicable law or agreed to in writing, software
32 // distributed under the License is distributed on an "AS IS" BASIS,
33 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34 // See the License for the specific language governing permissions and
35 // limitations under the License.
36 //
37 // ======================================================================
38
39 #include <string.h>
40 #include <new>
41
42 #include <Fw/Types/StringUtils.hpp>
43 #include <Os/FilePathUtils.hpp>
44 #include <Os/FileSystem.hpp>
45
46 #include <Svc/Ccsds/CfdpManager/CfdpManager.hpp>
47 #include <Svc/Ccsds/CfdpManager/Channel.hpp>
48 #include <Svc/Ccsds/CfdpManager/Engine.hpp>
49 #include <Svc/Ccsds/CfdpManager/Transaction.hpp>
50 #include <Svc/Ccsds/CfdpManager/Types/PduBase.hpp>
51 #include <Svc/Ccsds/CfdpManager/Utils.hpp>
52
53 namespace Svc {
54 namespace Ccsds {
55 namespace Cfdp {
56
57 // ----------------------------------------------------------------------
58 // Construction and destruction
59 // ----------------------------------------------------------------------
60
61 ✗ Engine::Engine(CfdpManager* manager) : m_manager(manager), m_seqNum(0), m_allocator(nullptr), m_allocatorId(0) {
62 ✗ for (U8 i = 0; i < Cfdp::NumChannels; ++i) {
63 ✗ m_channels[i] = nullptr;
64 }
65 ✗ }
66
67 ✗ Engine::~Engine() {
68 ✗ FW_ASSERT(m_allocator != nullptr, 0); // init() must have been called
69
70 ✗ for (U8 i = 0; i < Cfdp::NumChannels; ++i) {
71 ✗ if (m_channels[i] != nullptr) {
72 // Clean up Channel's internal arrays first
73 ✗ m_channels[i]->cleanup(*m_allocator, m_allocatorId);
74
75 // Call destructor
76 ✗ m_channels[i]->~Channel();
77
78 // Deallocate the Channel object itself
79 ✗ m_allocator->deallocate(m_allocatorId, m_channels[i]);
80 ✗ m_channels[i] = nullptr;
81 }
82 }
83 ✗ }
84
85 // ----------------------------------------------------------------------
86 // Public interface methods
87 // ----------------------------------------------------------------------
88
89 ✗ void Engine::init(Fw::MemAllocator& allocator, FwEnumStoreType memId) {
90 // Store allocator for cleanup in destructor
91 ✗ m_allocator = &allocator;
92 ✗ m_allocatorId = memId;
93
94 // Allocate and construct all channels using the allocator
95 ✗ for (U8 i = 0; i < Cfdp::NumChannels; ++i) {
96 ✗ FwSizeType channelSize = sizeof(Channel);
97 ✗ void* channelMem = allocator.allocate(memId, channelSize);
98 ✗ FW_ASSERT(channelMem != nullptr);
99
100 // Use placement new to construct Channel in allocated memory
101 ✗ m_channels[i] = new (channelMem) Channel(this, i, this->m_manager, allocator, memId);
102 }
103 ✗ }
104
105 ✗ void Engine::armAckTimer(Transaction* txn) {
106 ✗ txn->m_ack_timer.setTimer(txn->m_cfdpManager->getAckTimerParam(txn->m_chan_num));
107 ✗ txn->m_flags.com.ack_timer_armed = true;
108 ✗ }
109
110 ✗ void Engine::armInactTimer(Transaction* txn) {
111 ✗ U32 timerDuration = 0;
112
113 // select timeout based on the state
114 ✗ if (GetTxnStatus(txn) == AckTxnStatus::ACK_TXN_STATUS_ACTIVE) {
115 // in an active transaction, we expect traffic so use the normal inactivity timer
116 ✗ timerDuration = txn->m_cfdpManager->getInactivityTimerParam(txn->m_chan_num);
117 } else {
118 // in an inactive transaction, we do NOT expect traffic, and this timer is now used
119 // just in case any late straggler PDUs dp get delivered. In this case the
120 // time should be longer than the retransmit time (ack timer) but less than the full
121 // inactivity timer (because again, we are not expecting traffic, so waiting the full
122 // timeout would hold resources longer than needed). Using double the ack timer should
123 // ensure that if the remote retransmitted anything, we will see it, and avoids adding
124 // another config option just for this.
125 ✗ timerDuration = txn->m_cfdpManager->getAckTimerParam(txn->m_chan_num) * 2;
126 }
127
128 ✗ txn->m_inactivity_timer.setTimer(timerDuration);
129 ✗ }
130
131 ✗ void Engine::dispatchRecv(Transaction* txn, const Fw::Buffer& buffer) {
132 // Loop to handle state transitions without recursion
133 // The loop allows recvInit to transition to R2 state and re-dispatch
134 ✗ bool needsDispatch = true;
135 ✗ while (needsDispatch) {
136 ✗ needsDispatch = false; // Assume single dispatch unless state handler requests re-dispatch
137
138 // Dispatch based on transaction state
139 ✗ switch (txn->m_state) {
140 ✗ case TxnState::TXN_STATE_INIT:
141 ✗ needsDispatch = this->recvInit(txn, buffer);
142 ✗ break;
143 ✗ case TxnState::TXN_STATE_R1:
144 ✗ txn->r1Recv(buffer);
145 ✗ break;
146 ✗ case TxnState::TXN_STATE_S1:
147 ✗ txn->s1Recv(buffer);
148 ✗ break;
149 ✗ case TxnState::TXN_STATE_R2:
150 ✗ txn->r2Recv(buffer);
151 ✗ break;
152 ✗ case TxnState::TXN_STATE_S2:
153 ✗ txn->s2Recv(buffer);
154 ✗ break;
155 ✗ case TxnState::TXN_STATE_DROP:
156 ✗ this->recvDrop(txn, buffer);
157 ✗ break;
158 ✗ case TxnState::TXN_STATE_HOLD:
159 ✗ this->recvHold(txn, buffer);
160 ✗ break;
161 ✗ default:
162 // Invalid or undefined state
163 ✗ break;
164 }
165 }
166
167 ✗ this->armInactTimer(txn); // whenever a packet was received by the other size, always arm its inactivity timer
168 ✗ }
169
170 ✗ void Engine::dispatchTx(Transaction* txn) {
171 static const TxnSendDispatchTable state_fns = {{
172 nullptr, // TxnState::TXN_STATE_UNDEF
173 nullptr, // TxnState::TXN_STATE_INIT
174 nullptr, // TxnState::TXN_STATE_R1
175 &Transaction::s1Tx, // TxnState::TXN_STATE_S1
176 nullptr, // TxnState::TXN_STATE_R2
177 &Transaction::s2Tx, // TxnState::TXN_STATE_S2
178 nullptr, // TxnState::TXN_STATE_DROP
179 nullptr // TxnState::TXN_STATE_HOLD
180 }};
181
182 ✗ txn->txStateDispatch(&state_fns);
183 ✗ }
184
185 ✗ Status::T Engine::sendMd(Transaction* txn) {
186 ✗ FW_ASSERT((txn->m_state == TxnState::TXN_STATE_S1) || (txn->m_state == TxnState::TXN_STATE_S2),
187 static_cast<U8>(txn->m_state));
188 ✗ FW_ASSERT(txn->m_chan != nullptr);
189
190 // Create and initialize Metadata PDU
191 ✗ MetadataPdu md;
192
193 // Set closure requested flag based on transaction class
194 // Class 1: closure not requested (0), Class 2: closure requested (1)
195 ✗ U8 closureRequested = (txn->m_state == TxnState::TXN_STATE_S2) ? 1 : 0;
196
197 // Direction is toward receiver for metadata PDU sent by sender
198 ✗ Cfdp::PduDirection direction = PduDirection::DIRECTION_TOWARD_RECEIVER;
199
200 ✗ md.initialize(direction,
201 txn->getClass(), // transmission mode (Class 1 or 2)
202 ✗ m_manager->getLocalEidParam(), // source EID
203 ✗ txn->m_history->seq_num, // transaction sequence number
204 ✗ txn->m_history->peer_eid, // destination EID
205 txn->m_fsize, // file size
206 ✗ txn->m_history->fnames.src_filename, // source filename
207 ✗ txn->m_history->fnames.dst_filename, // destination filename
208 ChecksumType::CHECKSUM_TYPE_MODULAR, // checksum type
209 closureRequested // closure requested flag
210 );
211
212 ✗ return serializeAndSendPdu(txn, md);
213 ✗ }
214
215 ✗ Status::T Engine::sendFd(Transaction* txn, FileDataPdu& fdPdu) {
216 ✗ Status::T status = serializeAndSendPdu(txn, fdPdu);
217 ✗ if (status == Cfdp::Status::SUCCESS) {
218 ✗ m_manager->addSentFileDataBytes(txn->getChannelId(), fdPdu.getDataSize());
219 }
220 ✗ return status;
221 }
222
223 ✗ Status::T Engine::sendEof(Transaction* txn) {
224 // Create and initialize EOF PDU
225 ✗ EofPdu eof;
226
227 // Direction is toward receiver for EOF sent by sender
228 ✗ Cfdp::PduDirection direction = PduDirection::DIRECTION_TOWARD_RECEIVER;
229 ✗ ConditionCode conditionCode = static_cast<ConditionCode>(TxnStatusToConditionCode(txn->m_history->txn_stat));
230
231 // Increment sent EOF counters based on condition code
232 ✗ if (conditionCode == ConditionCode::CONDITION_CODE_CANCEL_REQUEST_RECEIVED) {
233 ✗ this->m_manager->incrementSentEofCanceled(txn->getChannelId());
234 ✗ } else if (conditionCode != ConditionCode::CONDITION_CODE_NO_ERROR) {
235 ✗ this->m_manager->incrementFaultTxEofError(txn->getChannelId());
236 }
237
238 ✗ eof.initialize(direction,
239 txn->getClass(), // transmission mode
240 ✗ m_manager->getLocalEidParam(), // source EID
241 ✗ txn->m_history->seq_num, // transaction sequence number
242 ✗ txn->m_history->peer_eid, // destination EID
243 conditionCode, // condition code
244 txn->m_crc.getValue(), // checksum
245 txn->m_fsize // file size
246 );
247
248 // Add entity ID TLV on error conditions (optional per CCSDS spec)
249 ✗ if (conditionCode != ConditionCode::CONDITION_CODE_NO_ERROR) {
250 ✗ Cfdp::Tlv tlv;
251 ✗ tlv.initialize(m_manager->getLocalEidParam()); // Local entity ID
252 ✗ eof.appendTlv(tlv);
253 }
254
255 ✗ return serializeAndSendPdu(txn, eof);
256 ✗ }
257
258 ✗ Status::T Engine::sendAck(Transaction* txn,
259 AckTxnStatus ts,
260 FileDirective dir_code,
261 ConditionCode cc,
262 EntityId peer_eid,
263 TransactionSeq tsn) {
264 ✗ FW_ASSERT(
265 (dir_code == FileDirective::FILE_DIRECTIVE_END_OF_FILE) || (dir_code == FileDirective::FILE_DIRECTIVE_FIN),
266 static_cast<U8>(dir_code));
267
268 // Determine source and destination EIDs based on transaction direction
269 EntityId src_eid;
270 EntityId dst_eid;
271 ✗ if (txn->getHistory()->dir == Direction::DIRECTION_TX) {
272 ✗ src_eid = m_manager->getLocalEidParam();
273 ✗ dst_eid = peer_eid;
274 } else {
275 ✗ src_eid = peer_eid;
276 ✗ dst_eid = m_manager->getLocalEidParam();
277 }
278
279 // Create and initialize ACK PDU
280 ✗ AckPdu ack;
281
282 // Direction: toward sender for EOF ACK, toward receiver for FIN ACK
283 ✗ Cfdp::PduDirection direction = (dir_code == FileDirective::FILE_DIRECTIVE_END_OF_FILE)
284 ✗ ? Cfdp::PduDirection::DIRECTION_TOWARD_SENDER
285 : Cfdp::PduDirection::DIRECTION_TOWARD_RECEIVER;
286
287 ✗ ack.initialize(direction,
288 txn->getClass(), // transmission mode
289 src_eid, // source EID
290 tsn, // transaction sequence number
291 dst_eid, // destination EID
292 dir_code, // directive being acknowledged
293 1, // directive subtype code (always 1)
294 cc, // condition code
295 ts // transaction status
296 );
297
298 ✗ return serializeAndSendPdu(txn, ack);
299 ✗ }
300
301 ✗ Status::T Engine::sendFin(Transaction* txn, FinDeliveryCode dc, FinFileStatus fs, ConditionCode cc) {
302 // Create and initialize FIN PDU
303 ✗ FinPdu fin;
304
305 // Direction is toward sender for FIN sent by receiver
306 ✗ Cfdp::PduDirection direction = PduDirection::DIRECTION_TOWARD_SENDER;
307
308 ✗ fin.initialize(direction,
309 txn->getClass(), // transmission mode
310 ✗ txn->m_history->peer_eid, // source EID (receiver)
311 ✗ txn->m_history->seq_num, // transaction sequence number
312 ✗ m_manager->getLocalEidParam(), // destination EID (sender)
313 cc, // condition code
314 static_cast<FinDeliveryCode>(dc), // delivery code
315 static_cast<FinFileStatus>(fs) // file status
316 );
317
318 // Add entity ID TLV on error conditions (optional per CCSDS spec)
319 ✗ if (cc != ConditionCode::CONDITION_CODE_NO_ERROR) {
320 ✗ Cfdp::Tlv tlv;
321 ✗ tlv.initialize(m_manager->getLocalEidParam()); // Local entity ID
322 ✗ fin.appendTlv(tlv);
323 }
324
325 ✗ return serializeAndSendPdu(txn, fin);
326 ✗ }
327
328 ✗ Status::T Engine::sendNak(Transaction* txn, NakPdu& nakPdu) {
329 // Verify this is a Class 2 transaction (NAK only used in Class 2)
330 ✗ Class::T tx_class = txn->getClass();
331 ✗ FW_ASSERT(tx_class == Cfdp::Class::CLASS_2, tx_class);
332
333 ✗ return serializeAndSendPdu(txn, nakPdu);
334 }
335
336 ✗ Status::T Engine::serializeAndSendPdu(Transaction* txn, PduBase& pdu) {
337 // Delegate to the channel-based helper; a transaction always carries its channel.
338 ✗ return this->serializeAndSendPduOnChannel(*txn->m_chan, pdu);
339 }
340
341 ✗ Status::T Engine::serializeAndSendPduOnChannel(Channel& chan, PduBase& pdu) {
342 ✗ Fw::Buffer buffer;
343 ✗ Status::T status = Cfdp::Status::SUCCESS;
344
345 // Allocate buffer with space for packet descriptor
346 ✗ const FwSizeType bufferSize = pdu.getBufferSize() + CfdpManager::PACKET_DESCRIPTOR_SIZE;
347 ✗ status = m_manager->getPduBuffer(buffer, chan, bufferSize);
348
349 ✗ if (status == Cfdp::Status::SUCCESS) {
350 // Serialize to buffer at offset to leave room for descriptor
351 ✗ Fw::SerialBuffer sb(buffer.getData() + CfdpManager::PACKET_DESCRIPTOR_SIZE,
352 ✗ buffer.getSize() - CfdpManager::PACKET_DESCRIPTOR_SIZE);
353 ✗ Fw::SerializeStatus serStatus = pdu.serializeTo(sb);
354
355 ✗ if (serStatus != Fw::FW_SERIALIZE_OK) {
356 // Log generic PDU serialization error with PDU type
357 ✗ m_manager->log_WARNING_LO_FailPduSerialization(chan.getChannelId(), pdu.getType(),
358 static_cast<I32>(serStatus));
359 ✗ m_manager->returnPduBuffer(chan, buffer);
360 ✗ status = Cfdp::Status::ERROR;
361 } else {
362 // Update buffer size to actual serialized size plus descriptor
363 ✗ buffer.setSize(sb.getSize() + CfdpManager::PACKET_DESCRIPTOR_SIZE);
364 ✗ m_manager->sendPduBuffer(chan, buffer);
365 ✗ m_manager->incrementSentPdu(chan.getChannelId());
366 }
367 ✗ }
368
369 ✗ return status;
370 ✗ }
371
372 ✗ Status::T Engine::sendFinAckStateless(Channel& chan,
373 TransactionSeq tsn,
374 EntityId finSrcEid,
375 EntityId finDstEid,
376 ConditionCode cc) {
377 // A FIN has arrived for a downlink transaction we sourced, but no live transaction
378 // remains (it already completed and was recycled). Per CFDP the sender must still
379 // acknowledge a retransmitted FIN; we do so statelessly from the FIN header.
380 //
381 // The FIN was sent toward us (the sender), so its header carries
382 // sourceEid = the transaction source = this local entity,
383 // destEid = the peer (receiver).
384 // The ACK(FIN) we emit travels toward the receiver, mirroring Engine::sendAck's
385 // DIRECTION_TX case: src = local entity, dst = peer.
386 ✗ AckPdu ack;
387 ✗ ack.initialize(Cfdp::PduDirection::DIRECTION_TOWARD_RECEIVER,
388 Cfdp::Class::CLASS_2, // FIN/ACK only exist in class 2
389 finSrcEid, // source EID (this local entity)
390 tsn, // transaction sequence number
391 finDstEid, // destination EID (the peer/receiver)
392 FileDirective::FILE_DIRECTIVE_FIN, // directive being acknowledged
393 1, // directive subtype code (always 1)
394 cc, // echo the FIN's condition code
395 AckTxnStatus::ACK_TXN_STATUS_UNRECOGNIZED // we no longer recognize this transaction
396 );
397
398 ✗ return this->serializeAndSendPduOnChannel(chan, ack);
399 ✗ }
400
401 ✗ bool Engine::validateRxDestPath(U8 chan_num, Fw::String& path, RxDestPathRejectReason& reason) {
402 ✗ Fw::String rxDir = this->m_manager->getRxDirParam(chan_num);
403 ✗ if (rxDir.length() == 0) {
404 // Sandbox not configured for this channel: accept path as-is (documented fail-open behavior)
405 ✗ return true;
406 }
407
408 // Canonicalize the receive directory (relative rx_dir resolves against CWD) into the
409 // trailing-'/' form that checkContainment requires; shared with Os::SandboxedFile
410 char root[Os::FilePathUtils::MAX_PATH_LENGTH];
411 ✗ if (Os::FilePathUtils::resolveDirectoryFromCwd(rxDir.toChar(), root, sizeof(root)) != Os::FilePathUtils::VALID) {
412 ✗ reason = RxDestPathRejectReason::RX_DIR_UNRESOLVABLE;
413 ✗ return false;
414 }
415
416 // Resolve the received path: relative paths land inside the receive directory, `..` segments
417 // are collapsed textually (symlinks are not followed; see Os::SandboxedFile threat model)
418 char resolved[Os::FilePathUtils::MAX_PATH_LENGTH];
419 ✗ if (Os::FilePathUtils::resolvePath(path.toChar(), root, resolved, sizeof(resolved)) != Os::FilePathUtils::VALID) {
420 ✗ reason = RxDestPathRejectReason::PATH_UNRESOLVABLE;
421 ✗ return false;
422 }
423 ✗ if (Os::FilePathUtils::checkContainment(resolved, root) != Os::FilePathUtils::VALID) {
424 ✗ reason = RxDestPathRejectReason::OUTSIDE_RX_DIR;
425 ✗ return false;
426 }
427 // The canonical path is stored in the transaction and reported in events sized to
428 // MaxFilePathSize; reject rather than silently truncate a longer result
429 ✗ if (Fw::StringUtils::string_length(resolved, sizeof(resolved)) > Cfdp::MaxFilePathSize) {
430 ✗ reason = RxDestPathRejectReason::TOO_LONG;
431 ✗ return false;
432 }
433
434 ✗ path = resolved;
435 ✗ return true;
436 ✗ }
437
438 ✗ Status::T Engine::recvMd(Transaction* txn, const MetadataPdu& md) {
439 /* the destination path comes from the remote entity: canonicalize it and confirm it lies within
440 * the configured receive directory BEFORE anything from this PDU is committed to the transaction.
441 * On rejection nothing from the PDU (file size, source name, destination) is stored; the only
442 * change to the transaction is its status, set below. */
443 ✗ Fw::String dst = md.getDestFilename();
444 ✗ RxDestPathRejectReason reason;
445 ✗ if (!this->validateRxDestPath(txn->m_chan_num, dst, reason)) {
446 ✗ this->m_manager->log_WARNING_HI_RxDestPathRejected(txn->m_chan_num, txn->m_history->src_eid,
447 ✗ txn->m_history->seq_num, dst,
448 ✗ this->m_manager->getRxDirParam(txn->m_chan_num), reason);
449 ✗ this->m_manager->incrementFaultFileOpen(txn->m_chan_num);
450 // Refused destination = filestore rejection, so finishTransaction reports a failed reception.
451 // Whether the sender learns of it depends on the caller; see the recvMd doc in Engine.hpp.
452 ✗ this->setTxnStatus(txn, TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
453 ✗ return Status::PDU_METADATA_ERROR;
454 }
455
456 /* store the expected file size in transaction */
457 ✗ txn->m_fsize = md.getFileSize();
458
459 /* structural validation (length, non-empty) already done during deserialization */
460 ✗ txn->m_history->fnames.src_filename = md.getSourceFilename();
461 ✗ txn->m_history->fnames.dst_filename = dst;
462
463 ✗ this->m_manager->log_ACTIVITY_LO_MetadataReceived(txn->m_history->fnames.src_filename,
464 ✗ txn->m_history->fnames.dst_filename, txn->m_history->seq_num);
465 ✗ return Status::SUCCESS;
466 ✗ }
467
468 ✗ Status::T Engine::recvFd(Transaction* txn, const FileDataPdu& fd) {
469 ✗ Status::T ret = Cfdp::Status::SUCCESS;
470
471 // Extract header
472 ✗ const Cfdp::PduHeader& header = fd.asHeader();
473
474 // Check for segment metadata flag (not currently supported)
475 ✗ if (header.hasSegmentMetadata()) {
476 /* If recv PDU has the "segment_meta_flag" set, this is not currently handled in CF. */
477 ✗ this->m_manager->log_WARNING_LO_FileDataSegmentMetadata();
478 ✗ this->setTxnStatus(txn, TxnStatus::TXN_STATUS_PROTOCOL_ERROR);
479 ✗ this->m_manager->incrementRecvErrors(txn->getChannelId());
480 ✗ ret = Cfdp::Status::ERROR;
481 }
482
483 ✗ return ret;
484 }
485
486 ✗ Status::T Engine::recvEof(Transaction* txn, const EofPdu& eofPdu) {
487 // EOF PDU has been validated during fromBuffer()
488
489 // Process TLVs if present
490 ✗ const Cfdp::TlvList& tlvList = eofPdu.getTlvList();
491 ✗ for (U8 i = 0; i < tlvList.getNumTlv(); i++) {
492 ✗ const Cfdp::Tlv& tlv = tlvList.getTlv(i);
493 ✗ if (tlv.getType() == Cfdp::TlvType::TLV_TYPE_ENTITY_ID) {
494 // Entity ID TLV present - validation not currently performed
495 // Future enhancement: Add validation or logging if required
496 }
497 // Other TLV types can be processed here in the future
498 }
499
500 ✗ return Cfdp::Status::SUCCESS;
501 }
502
503 ✗ Status::T Engine::recvFin(Transaction* txn, const FinPdu& finPdu) {
504 // FIN PDU has been validated during fromBuffer()
505
506 // Process TLVs if present
507 ✗ const Cfdp::TlvList& tlvList = finPdu.getTlvList();
508 ✗ for (U8 i = 0; i < tlvList.getNumTlv(); i++) {
509 ✗ const Cfdp::Tlv& tlv = tlvList.getTlv(i);
510 ✗ if (tlv.getType() == Cfdp::TlvType::TLV_TYPE_ENTITY_ID) {
511 // Entity ID TLV present - validation not currently performed
512 // Future enhancement: Add validation or logging if required
513 }
514 // Other TLV types can be processed here in the future
515 }
516
517 ✗ return Cfdp::Status::SUCCESS;
518 }
519
520 ✗ Status::T Engine::recvNak(Transaction* txn, const NakPdu& pdu) {
521 // NAK PDU has been validated during fromBuffer()
522 ✗ return Cfdp::Status::SUCCESS;
523 }
524
525 ✗ void Engine::recvDrop(Transaction* txn, const Fw::Buffer& buffer) {
526 ✗ this->m_manager->incrementRecvDropped(txn->getChannelId());
527 (void)buffer; // Unused - we're just dropping the PDU
528 ✗ }
529
530 ✗ void Engine::recvHold(Transaction* txn, const Fw::Buffer& buffer) {
531 // anything received in this state is considered spurious
532 ✗ this->m_manager->incrementRecvSpurious(txn->getChannelId());
533
534 //
535 // Normally we do not expect PDUs for a transaction in holdover, because
536 // from the local point of view it is completed and done. But the reason
537 // for the holdover is because the remote side might not have gotten all
538 // the acks and could still be [re-]sending us PDUs for anything it does
539 // not know we got already.
540 //
541 // If an R2 sent FIN, it's possible that the peer missed the
542 // FIN-ACK and is sending another FIN. In that case we need to send
543 // another ACK.
544 //
545
546 // currently the only thing we will re-ack is the FIN.
547
548 // Use peekPduType to determine the PDU type
549 ✗ Cfdp::PduTypeEnum::T pduType = Cfdp::peekPduType(buffer);
550
551 // Check if this is a FIN PDU for a Class 2 transaction
552 ✗ if (pduType == Cfdp::PduTypeEnum::FINISHED && txn->getClass() == Cfdp::Class::CLASS_2) {
553 // Deserialize FIN PDU
554 ✗ FinPdu fin;
555 ✗ Fw::SerialBuffer sb2(const_cast<U8*>(buffer.getData()), buffer.getSize());
556 ✗ sb2.setBuffLen(buffer.getSize());
557
558 ✗ Fw::SerializeStatus deserStatus = fin.deserializeFrom(sb2);
559 ✗ if (deserStatus == Fw::FW_SERIALIZE_OK) {
560 // Re-send the FIN-ACK
561 ✗ this->sendAck(txn, AckTxnStatus::ACK_TXN_STATUS_TERMINATED, FileDirective::FILE_DIRECTIVE_FIN,
562 ✗ fin.getConditionCode(), txn->m_history->peer_eid, txn->m_history->seq_num);
563 }
564 // Note: Deserialization errors are silently ignored in hold state
565 // as we're just trying to be helpful by re-acknowledging FIN if we can
566 ✗ }
567 ✗ }
568
569 ✗ bool Engine::recvInit(Transaction* txn, const Fw::Buffer& buffer) {
570 // Use peekPduType to determine the PDU type before deserializing
571 ✗ Cfdp::PduTypeEnum::T pduType = Cfdp::peekPduType(buffer);
572
573 // First parse header to get transaction information
574 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
575 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
576 ✗ sb.setBuffLen(buffer.getSize());
577
578 Cfdp::PduHeader header;
579 ✗ Fw::SerializeStatus status = header.fromSerialBuffer(sb);
580
581 ✗ if (status == Fw::FW_SERIALIZE_OK) {
582 ✗ TransactionSeq transactionSeq = header.getTransactionSeq();
583 ✗ EntityId sourceEid = header.getSourceEid();
584 ✗ Class::T txmMode = header.getTxmMode();
585
586 // only RX transactions dare tread here
587 ✗ txn->m_history->seq_num = transactionSeq;
588
589 // peer_eid is always the remote partner. src_eid is always the transaction source.
590 // in this case, they are the same
591 ✗ txn->m_history->peer_eid = sourceEid;
592 ✗ txn->m_history->src_eid = sourceEid;
593
594 // all RX transactions will need a chunk list to track file segments
595 ✗ if (txn->m_chunks == nullptr) {
596 ✗ txn->m_chunks = txn->m_chan->findUnusedChunks(Direction::DIRECTION_RX);
597 }
598 ✗ if (txn->m_chunks == nullptr) {
599 ✗ this->m_manager->log_WARNING_LO_ChunklistUnavailable(transactionSeq);
600 } else {
601 ✗ if (pduType == Cfdp::PduTypeEnum::FILE_DATA) {
602 // file data PDU
603 // being idle and receiving a file data PDU means that no active transaction knew
604 // about the transaction in progress, so most likely PDUs were missed.
605
606 ✗ if (txmMode == Cfdp::Class::CLASS_1) {
607 // R1, can't do anything without metadata first
608 ✗ txn->m_state = TxnState::TXN_STATE_DROP; // drop all incoming
609 // use inactivity timer to ultimately free the state
610 } else {
611 // R2 can handle missing metadata, so go ahead and create a temp file
612 ✗ txn->m_state = TxnState::TXN_STATE_R2;
613 ✗ txn->m_txn_class = Cfdp::Class::CLASS_2;
614 ✗ txn->rInit();
615 ✗ return true; // Request re-dispatch to enter r2 state handler
616 }
617 ✗ } else if (pduType == Cfdp::PduTypeEnum::METADATA) {
618 // file directive PDU with metadata - this is the expected case for starting a new RX transaction
619 ✗ MetadataPdu md;
620 ✗ Fw::SerialBuffer sb2(const_cast<U8*>(buffer.getData()), buffer.getSize());
621 ✗ sb2.setBuffLen(buffer.getSize());
622
623 ✗ Fw::SerializeStatus deserStatus = md.deserializeFrom(sb2);
624 ✗ if (deserStatus == Fw::FW_SERIALIZE_OK) {
625 ✗ if (this->recvMd(txn, md) == Status::SUCCESS) {
626 // NOTE: whether or not class 1 or 2, get a free chunks. It's cheap, and simplifies cleanup
627 // path
628 ✗ txn->m_state =
629 ✗ txmMode == Cfdp::Class::CLASS_1 ? TxnState::TXN_STATE_R1 : TxnState::TXN_STATE_R2;
630 ✗ txn->m_txn_class = txmMode;
631 ✗ txn->m_flags.rx.md_recv = true;
632 ✗ txn->rInit(); // initialize R
633 }
634 // else: destination path rejected and the transaction marked FILESTORE_REJECTION.
635 // State stays INIT so the transaction is finished below as a failed reception
636 // (parked in HOLD until the inactivity timer recycles it) without any file
637 // ever being opened.
638 } else {
639 ✗ m_manager->log_WARNING_LO_FailMetadataPduDeserialization(txn->getChannelId(),
640 static_cast<I32>(deserStatus));
641 }
642 ✗ } else {
643 // Unexpected PDU type in init state
644 ✗ this->m_manager->log_WARNING_LO_UnhandledPduInIdleState();
645 ✗ this->m_manager->incrementRecvErrors(txn->getChannelId());
646 }
647 }
648
649 ✗ if (txn->m_state == TxnState::TXN_STATE_INIT) {
650 // state was not changed, so free the transaction
651 ✗ this->finishTransaction(txn, false);
652 }
653 } else {
654 ✗ m_manager->log_WARNING_LO_FailPduHeaderDeserialization(txn->getChannelId(), status);
655 }
656 ✗ return false; // No re-dispatch needed
657 ✗ }
658
659 ✗ void Engine::receivePdu(U8 chan_id, const Fw::Buffer& buffer) {
660 ✗ Transaction* txn = nullptr;
661 ✗ Channel* chan = nullptr;
662
663 ✗ FW_ASSERT(chan_id < Cfdp::NumChannels, chan_id, Cfdp::NumChannels);
664
665 ✗ chan = m_channels[chan_id];
666 ✗ FW_ASSERT(chan != nullptr);
667
668 // Parse the header to get transaction routing info
669 // Avoid full PDU deserialization here to defer it until the appropriate handler
670 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
671 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
672 ✗ sb.setBuffLen(buffer.getSize());
673
674 Cfdp::PduHeader header;
675 ✗ Fw::SerializeStatus status = header.fromSerialBuffer(sb);
676
677 ✗ if (status == Fw::FW_SERIALIZE_OK) {
678 // Increment received PDU counter for PDUs with valid headers
679 ✗ this->m_manager->incrementRecvPdu(chan_id);
680
681 ✗ TransactionSeq transactionSeq = header.getTransactionSeq();
682 ✗ EntityId sourceEid = header.getSourceEid();
683 ✗ EntityId destEid = header.getDestEid();
684
685 // Look up transaction by sequence number
686 ✗ txn = chan->findTransactionBySequenceNumber(transactionSeq, sourceEid);
687
688 ✗ if (txn == nullptr) {
689 // A retransmitted FIN can arrive for a downlink transaction we sourced after
690 // that transaction has already completed and been recycled (e.g. the peer
691 // kept retransmitting FIN across a lossy/quiet link). Per CFDP the sender
692 // must still acknowledge it, so re-ACK statelessly from the FIN header.
693 // Such a FIN carries sourceEid == our local entity (we were the source) and
694 // destEid == the peer, so it would otherwise be dropped as InvalidDestinationEid.
695 ✗ if (Cfdp::peekPduType(buffer) == Cfdp::PduTypeEnum::FINISHED &&
696 ✗ sourceEid == this->m_manager->getLocalEidParam()) {
697 ✗ FinPdu fin;
698 ✗ Fw::SerialBuffer finSb(const_cast<U8*>(buffer.getData()), buffer.getSize());
699 ✗ finSb.setBuffLen(buffer.getSize());
700 ✗ if (fin.deserializeFrom(finSb) == Fw::FW_SERIALIZE_OK) {
701 ✗ this->sendFinAckStateless(*chan, transactionSeq, sourceEid, destEid, fin.getConditionCode());
702 ✗ this->m_manager->log_DIAGNOSTIC_TxLateFinAcked(sourceEid, transactionSeq);
703 } else {
704 ✗ this->m_manager->log_WARNING_LO_FailFinPduDeserialization(
705 chan_id, static_cast<I32>(Fw::FW_DESERIALIZE_FORMAT_ERROR));
706 }
707 ✗ }
708 // if no match found, then it must be the case that we would be the destination entity id, so verify it
709 ✗ else if (destEid == this->m_manager->getLocalEidParam()) {
710 // we didn't find a match, so assign it to a transaction
711 // assume this is initiating an RX transaction, as TX transactions are only commanded
712 ✗ txn = this->startRxTransaction(chan->getChannelId());
713 ✗ if (txn == nullptr) {
714 ✗ this->m_manager->log_WARNING_LO_RxTransactionLimitReached(sourceEid, transactionSeq);
715 }
716 } else {
717 ✗ this->m_manager->log_WARNING_LO_InvalidDestinationEid(destEid);
718 }
719 }
720
721 ✗ if (txn != nullptr) {
722 // found one! Send it to the transaction state processor
723 ✗ this->dispatchRecv(txn, buffer);
724 } else {
725 // Transaction limit reached - EVR already emitted by findOrStartRxTransaction
726 }
727 } else {
728 // Invalid PDU header, drop packet
729 ✗ m_manager->log_WARNING_LO_FailPduHeaderDeserialization(chan_id, static_cast<I32>(status));
730 }
731 ✗ }
732
733 ✗ void Engine::setChannelFlowState(U8 channelId, Flow::T flowState) {
734 ✗ FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels);
735 ✗ m_channels[channelId]->setFlowState(flowState);
736 ✗ }
737
738 ✗ Status::T Engine::setSuspendResumeTransaction(U8 channelId,
739 TransactionSeq transactionSeq,
740 EntityId entityId,
741 SuspendResume::T action) {
742 ✗ Status::T status = Status::ERROR;
743
744 ✗ FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels);
745
746 ✗ Channel* chan = m_channels[channelId];
747 ✗ Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId);
748
749 ✗ if (txn != nullptr) {
750 ✗ txn->m_flags.com.suspended = (action == SuspendResume::SUSPEND);
751 ✗ status = Status::SUCCESS;
752 }
753
754 ✗ return status;
755 }
756
757 ✗ Status::T Engine::cancelTransactionBySeq(U8 channelId, TransactionSeq transactionSeq, EntityId entityId) {
758 ✗ Status::T status = Status::ERROR;
759
760 ✗ FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels);
761
762 ✗ Channel* chan = m_channels[channelId];
763 ✗ Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId);
764
765 ✗ if (txn != nullptr) {
766 ✗ this->cancelTransaction(txn);
767 ✗ status = Status::SUCCESS;
768 }
769
770 ✗ return status;
771 }
772
773 ✗ Status::T Engine::abandonTransaction(U8 channelId, TransactionSeq transactionSeq, EntityId entityId) {
774 ✗ Status::T status = Status::ERROR;
775
776 ✗ FW_ASSERT(channelId < Cfdp::NumChannels, channelId, Cfdp::NumChannels);
777
778 ✗ Channel* chan = m_channels[channelId];
779 ✗ Transaction* txn = chan->findTransactionBySequenceNumber(transactionSeq, entityId);
780
781 // A finished transaction stays findable in HOLD until its inactivity timer recycles it.
782 // There is nothing left to abandon, so report it the same as a transaction that is not found.
783 ✗ if ((txn != nullptr) && (txn->m_state != TxnState::TXN_STATE_HOLD)) {
784 ✗ this->finishTransaction(txn, false);
785 ✗ status = Status::SUCCESS;
786 }
787
788 ✗ return status;
789 }
790
791 ✗ void Engine::txFileInitiate(Transaction* txn,
792 Class::T cfdp_class,
793 Keep::T keep,
794 U8 chan,
795 U8 priority,
796 EntityId dest_id) {
797 ✗ txn->initTxFile(cfdp_class, keep, chan, priority);
798
799 // Increment sequence number for new transaction
800 ✗ ++this->m_seqNum;
801
802 // Capture info for history
803 ✗ txn->m_history->seq_num = this->m_seqNum;
804 ✗ txn->m_history->src_eid = m_manager->getLocalEidParam();
805 ✗ txn->m_history->peer_eid = dest_id;
806
807 ✗ txn->m_chan->insertSortPrio(txn, QueueId::PEND);
808 ✗ }
809
810 ✗ Status::T Engine::txFile(const Fw::String& src_filename,
811 const Fw::String& dst_filename,
812 Class::T cfdp_class,
813 Keep::T keep,
814 U8 chan_num,
815 U8 priority,
816 EntityId dest_id,
817 TransactionInitType initType) {
818 Transaction* txn;
819 ✗ Channel* chan = nullptr;
820
821 ✗ FW_ASSERT(chan_num < Cfdp::NumChannels, chan_num, Cfdp::NumChannels);
822 ✗ chan = m_channels[chan_num];
823
824 ✗ Status::T ret = Cfdp::Status::SUCCESS;
825
826 ✗ if (chan->getNumCmdTx() < MaxCommandedPlaybackFilesPerChan) {
827 ✗ txn = chan->findUnusedTransaction(Direction::DIRECTION_TX);
828 } else {
829 ✗ txn = nullptr;
830 }
831
832 ✗ if (txn == nullptr) {
833 ✗ this->m_manager->log_WARNING_LO_MaxTxTransactionsReached();
834 ✗ ret = Cfdp::Status::ERROR;
835 } else {
836 // NOTE: the caller of this function ensures the provided src and dst filenames are nullptr terminated
837
838 ✗ txn->m_history->fnames.src_filename = src_filename;
839 ✗ txn->m_history->fnames.dst_filename = dst_filename;
840 ✗ this->txFileInitiate(txn, cfdp_class, keep, chan_num, priority, dest_id);
841
842 ✗ chan->incrementCmdTxCounter();
843 ✗ txn->m_flags.tx.cmd_tx = true;
844
845 // Set transaction initiation type
846 ✗ txn->m_initType = initType;
847
848 // Log transaction queued event
849 ✗ this->m_manager->log_ACTIVITY_LO_TxFileQueued(txn->m_history->fnames.src_filename, txn->m_history->seq_num);
850 }
851
852 ✗ return ret;
853 }
854
855 ✗ Transaction* Engine::startRxTransaction(U8 chan_num) {
856 ✗ Channel* chan = nullptr;
857 Transaction* txn;
858
859 ✗ FW_ASSERT(chan_num < Cfdp::NumChannels, chan_num, Cfdp::NumChannels);
860 ✗ chan = m_channels[chan_num];
861
862 // if (CF_AppData.hk.Payload.channel_hk[chan_num].q_size[QueueId::RX] < CF_MAX_SIMULTANEOUS_RX)
863 // {
864 // txn = chan->findUnusedTransaction(Direction::DIRECTION_RX);
865 // }
866 // else
867 // {
868 // txn = nullptr;
869 // }
870 // Receive transactions are limited by MaxRxTransactions parameter
871 ✗ txn = chan->findUnusedTransaction(Direction::DIRECTION_RX);
872
873 ✗ if (txn != nullptr) {
874 // set default FIN status
875 ✗ txn->m_state_data.receive.r2.dc = FinDeliveryCode::FIN_DELIVERY_CODE_INCOMPLETE;
876 ✗ txn->m_state_data.receive.r2.fs = FinFileStatus::FIN_FILE_STATUS_DISCARDED;
877
878 ✗ txn->m_flags.com.q_index = QueueId::RX;
879 ✗ chan->insertBackInQueue(static_cast<QueueId::T>(txn->m_flags.com.q_index), &txn->m_cl_node);
880 }
881
882 ✗ return txn;
883 }
884
885 ✗ Status::T Engine::playbackDirInitiate(Playback* pb,
886 const Fw::String& src_filename,
887 const Fw::String& dst_filename,
888 Class::T cfdp_class,
889 Keep::T keep,
890 U8 chan,
891 U8 priority,
892 EntityId dest_id) {
893 ✗ Status::T status = Cfdp::Status::SUCCESS;
894 Os::Directory::Status dirStatus;
895
896 // make sure the directory can be open
897 ✗ dirStatus = pb->dir.open(src_filename.toChar(), Os::Directory::READ);
898 ✗ if (dirStatus != Os::Directory::OP_OK) {
899 ✗ this->m_manager->log_WARNING_LO_PlaybackDirOpenFailed(src_filename, dirStatus);
900 ✗ this->m_manager->incrementFaultDirectoryRead(chan);
901 ✗ status = Cfdp::Status::ERROR;
902 } else {
903 ✗ pb->diropen = true;
904 ✗ pb->busy = true;
905 ✗ pb->keep = keep;
906 ✗ pb->priority = priority;
907 ✗ pb->dest_id = dest_id;
908 ✗ pb->cfdp_class = cfdp_class;
909
910 // NOTE: the caller of this function ensures the provided src and dst filenames are nullptr terminated
911 ✗ pb->fnames.src_filename = src_filename;
912 ✗ pb->fnames.dst_filename = dst_filename;
913 }
914
915 // the executor will start the transfer next cycle
916 ✗ return status;
917 }
918
919 ✗ Status::T Engine::playbackDir(const Fw::String& src_filename,
920 const Fw::String& dst_filename,
921 Class::T cfdp_class,
922 Keep::T keep,
923 U8 chan,
924 U8 priority,
925 EntityId dest_id) {
926 U32 i;
927 Playback* pb;
928 Status::T status;
929
930 // Loop through the channel's playback directories to find an open slot
931 ✗ for (i = 0; i < MaxCommandedPlaybackDirectoriesPerChan; ++i) {
932 ✗ pb = m_channels[chan]->getPlayback(i);
933 ✗ if (!pb->busy) {
934 ✗ break;
935 }
936 }
937
938 ✗ if (i == MaxCommandedPlaybackDirectoriesPerChan) {
939 ✗ this->m_manager->log_WARNING_LO_PlaybackDirSlotUnavailable();
940 ✗ status = Cfdp::Status::ERROR;
941 } else {
942 ✗ status = this->playbackDirInitiate(pb, src_filename, dst_filename, cfdp_class, keep, chan, priority, dest_id);
943 }
944
945 ✗ return status;
946 }
947
948 ✗ Status::T Engine::startPollDir(U8 chanId,
949 U8 pollId,
950 const Fw::String& srcDir,
951 const Fw::String& dstDir,
952 Class::T cfdp_class,
953 U8 priority,
954 EntityId destEid,
955 U32 intervalSec) {
956 ✗ Status::T status = Cfdp::Status::SUCCESS;
957 ✗ CfdpPollDir* pd = nullptr;
958
959 ✗ FW_ASSERT(chanId < Cfdp::NumChannels, chanId, Cfdp::NumChannels);
960 ✗ FW_ASSERT(pollId < MaxPollingDirPerChan, pollId, MaxPollingDirPerChan);
961
962 // First check if the poll directory is already in use
963 ✗ pd = m_channels[chanId]->getPollDir(pollId);
964 ✗ if (pd->enabled == Fw::Enabled::DISABLED) {
965 // Populate arguments
966 ✗ pd->intervalSec = intervalSec;
967 ✗ pd->priority = priority;
968 ✗ pd->cfdpClass = cfdp_class;
969 ✗ pd->destEid = destEid;
970 ✗ pd->srcDir = srcDir;
971 ✗ pd->dstDir = dstDir;
972
973 // Set timer and enable polling
974 ✗ pd->intervalTimer.setTimer(pd->intervalSec);
975 ✗ pd->enabled = Fw::Enabled::ENABLED;
976 } else {
977 // Poll directory slot already in use
978 ✗ this->m_manager->log_WARNING_LO_PollDirBusy(chanId, pollId);
979 ✗ status = Cfdp::Status::ERROR;
980 }
981
982 ✗ return status;
983 }
984
985 ✗ Status::T Engine::stopPollDir(U8 chanId, U8 pollId) {
986 ✗ Status::T status = Cfdp::Status::SUCCESS;
987 ✗ CfdpPollDir* pd = nullptr;
988
989 ✗ FW_ASSERT(chanId < Cfdp::NumChannels, chanId, Cfdp::NumChannels);
990 ✗ FW_ASSERT(pollId < MaxPollingDirPerChan, pollId, MaxPollingDirPerChan);
991
992 // Check if the poll directory is in use
993 ✗ pd = m_channels[chanId]->getPollDir(pollId);
994 ✗ if (pd->enabled == Fw::Enabled::ENABLED) {
995 // Clear poll directory arguments
996 ✗ pd->intervalSec = 0;
997 ✗ pd->priority = 0;
998 ✗ pd->cfdpClass = static_cast<Class::T>(0);
999 ✗ pd->destEid = static_cast<EntityId>(0);
1000 ✗ pd->srcDir = "";
1001 ✗ pd->dstDir = "";
1002
1003 // Disable timer and polling
1004 ✗ pd->intervalTimer.disableTimer();
1005 ✗ pd->enabled = Fw::Enabled::DISABLED;
1006 } else {
1007 // Poll directory not active - cannot stop
1008 ✗ this->m_manager->log_WARNING_LO_PollDirNotActive(chanId, pollId);
1009 ✗ status = Cfdp::Status::ERROR;
1010 }
1011
1012 ✗ return status;
1013 }
1014
1015 ✗ void Engine::cycle(void) {
1016 U32 i;
1017
1018 ✗ for (i = 0; i < Cfdp::NumChannels; ++i) {
1019 ✗ Channel* chan = m_channels[i];
1020 ✗ FW_ASSERT(chan != nullptr);
1021
1022 ✗ chan->resetOutgoingCounter();
1023
1024 ✗ if (chan->getFlowState() == Cfdp::Flow::NOT_FROZEN) {
1025 // handle ticks before tx cycle. Do this because there may be a limited number of TX messages available
1026 // this cycle, and it's important to respond to class 2 ACK/NAK more than it is to send new filedata
1027 // PDUs.
1028
1029 // cycle all transactions (tick)
1030 ✗ chan->tickTransactions();
1031
1032 // cycle the current tx transaction
1033 ✗ chan->cycleTx();
1034
1035 ✗ chan->processPlaybackDirectories();
1036 ✗ chan->processPollingDirectories();
1037 }
1038 }
1039 ✗ }
1040
1041 ✗ void Engine::finishTransaction(Transaction* txn, bool keep_history) {
1042 ✗ if (txn->m_flags.com.q_index == QueueId::FREE) {
1043 ✗ this->m_manager->log_DIAGNOSTIC_ResetFreedTransaction();
1044 ✗ return;
1045 }
1046
1047 // A transaction in HOLD has already been finished and is waiting for its inactivity timer
1048 // to recycle it. Finishing it again would repeat the completion bookkeeping (the commanded
1049 // TX and playback counters, completion events, and the port notification), so do nothing.
1050 ✗ if (txn->m_state == TxnState::TXN_STATE_HOLD) {
1051 ✗ return;
1052 }
1053
1054 // this should always be
1055 ✗ FW_ASSERT(txn->m_chan != nullptr);
1056
1057 // If this was on the TXA queue (transmit side) then we need to move it out
1058 // so the tick processor will stop trying to actively transmit something -
1059 // it should move on to the next transaction.
1060 //
1061 // RX transactions can stay on the RX queue, that does not hurt anything
1062 // because they are only triggered when a PDU comes in matching that seq_num
1063 // (RX queue is not separated into A/W parts)
1064 ✗ if (txn->m_flags.com.q_index == QueueId::TXA) {
1065 ✗ txn->m_chan->dequeueTransaction(txn);
1066 ✗ txn->m_chan->insertSortPrio(txn, QueueId::TXW);
1067 }
1068
1069 ✗ if (true == txn->m_fd.isOpen()) {
1070 ✗ txn->m_fd.close();
1071
1072 ✗ if (!txn->m_keep) {
1073 ✗ this->handleNotKeepFile(txn);
1074 }
1075 }
1076
1077 ✗ if (txn->m_history != nullptr) {
1078 // Emit completion events for successful transactions
1079 ✗ if (!TxnStatusIsError(txn->m_history->txn_stat)) {
1080 ✗ if (txn->m_history->dir == Direction::DIRECTION_TX) {
1081 ✗ this->m_manager->log_ACTIVITY_HI_TxFileTransferCompleted(
1082 ✗ txn->m_txn_class, txn->m_history->seq_num, txn->m_history->src_eid,
1083 ✗ txn->m_history->fnames.src_filename, txn->m_history->peer_eid, txn->m_history->fnames.dst_filename,
1084 ✗ static_cast<U32>(txn->m_fsize));
1085 ✗ } else if (txn->m_history->dir == Direction::DIRECTION_RX) {
1086 ✗ this->m_manager->log_ACTIVITY_HI_RxFileTransferCompleted(
1087 ✗ txn->m_txn_class, txn->m_history->seq_num, txn->m_history->src_eid,
1088 ✗ txn->m_history->fnames.src_filename, m_manager->getLocalEidParam(),
1089 ✗ txn->m_history->fnames.dst_filename, static_cast<U32>(txn->m_fsize));
1090 }
1091 } else {
1092 // Log failure events for failed transactions
1093 ✗ if (txn->m_history->dir == Direction::DIRECTION_TX) {
1094 ✗ this->m_manager->log_WARNING_LO_TxFileTransferFailed(
1095 ✗ txn->m_txn_class, txn->m_history->seq_num, txn->m_history->src_eid,
1096 ✗ txn->m_history->fnames.src_filename, txn->m_history->peer_eid, txn->m_history->fnames.dst_filename,
1097 ✗ static_cast<U8>(txn->m_history->txn_stat));
1098 ✗ } else if (txn->m_history->dir == Direction::DIRECTION_RX) {
1099 ✗ this->m_manager->log_WARNING_LO_RxFileTransferFailed(
1100 ✗ txn->m_txn_class, txn->m_history->seq_num, txn->m_history->src_eid,
1101 ✗ txn->m_history->fnames.src_filename, txn->m_history->peer_eid, txn->m_history->fnames.dst_filename,
1102 ✗ static_cast<U8>(txn->m_history->txn_stat));
1103 }
1104 }
1105
1106 // extra bookkeeping for tx direction only
1107 ✗ if (txn->m_history->dir == Direction::DIRECTION_TX && txn->m_flags.tx.cmd_tx) {
1108 ✗ txn->m_chan->decrementCmdTxCounter();
1109 }
1110
1111 // Notify via port if this was a port-initiated transfer
1112 ✗ if (txn->m_initType == TransactionInitType::INIT_BY_PORT) {
1113 // Map transaction status to SendFileStatus
1114 Svc::SendFileStatus::T status;
1115 ✗ if (TxnStatusIsError(txn->m_history->txn_stat)) {
1116 ✗ status = Svc::SendFileStatus::STATUS_ERROR;
1117 } else {
1118 ✗ status = Svc::SendFileStatus::STATUS_OK;
1119 }
1120
1121 // Invoke the file complete notification
1122 ✗ this->m_manager->sendFileComplete(status);
1123 }
1124
1125 ✗ txn->m_flags.com.keep_history = keep_history;
1126 }
1127
1128 ✗ if (txn->m_pb) {
1129 // a playback's transaction is now done, decrement the playback counter
1130 ✗ FW_ASSERT(txn->m_pb->num_ts);
1131 ✗ --txn->m_pb->num_ts;
1132 }
1133
1134 ✗ txn->m_chan->clearCurrentIfMatch(txn);
1135
1136 // Put this transaction into the holdover state, inactivity timer will recycle it
1137 ✗ txn->m_state = TxnState::TXN_STATE_HOLD;
1138 ✗ this->armInactTimer(txn);
1139 }
1140
1141 ✗ void Engine::setTxnStatus(Transaction* txn, TxnStatus txn_stat) {
1142 ✗ if (!TxnStatusIsError(txn->m_history->txn_stat)) {
1143 ✗ txn->m_history->txn_stat = txn_stat;
1144 }
1145 ✗ }
1146
1147 ✗ void Engine::cancelTransaction(Transaction* txn) {
1148 ✗ void (Transaction::* fns[static_cast<U32>(Direction::DIRECTION_NUM)])() = {nullptr};
1149
1150 ✗ fns[static_cast<U32>(Direction::DIRECTION_RX)] = &Transaction::rCancel;
1151 ✗ fns[static_cast<U32>(Direction::DIRECTION_TX)] = &Transaction::sCancel;
1152
1153 ✗ if (!txn->m_flags.com.canceled) {
1154 ✗ txn->m_flags.com.canceled = true;
1155 ✗ this->setTxnStatus(txn, TxnStatus::TXN_STATUS_CANCEL_REQUEST_RECEIVED);
1156
1157 // this should always be true, just confirming before indexing into array
1158 ✗ if (txn->m_history->dir < Direction::DIRECTION_NUM) {
1159 ✗ (txn->*fns[static_cast<U32>(txn->m_history->dir)])();
1160 }
1161 }
1162 ✗ }
1163
1164 ✗ bool Engine::isPollingDir(const Fw::StringBase& src_file, U8 chan_num) {
1165 ✗ bool return_code = false;
1166 ✗ Fw::String src_dir;
1167 CfdpPollDir* pd;
1168 U32 i;
1169
1170 // Extract directory portion (everything before last '/')
1171 ✗ FwSizeType lastSlashPos = 0;
1172 ✗ bool foundSlash = false;
1173 ✗ for (FwSizeType pos = 0; pos < src_file.length(); ++pos) {
1174 ✗ if (src_file.toChar()[pos] == '/') {
1175 ✗ lastSlashPos = pos;
1176 ✗ foundSlash = true;
1177 }
1178 }
1179
1180 ✗ if (foundSlash) {
1181 ✗ src_dir.format("%.*s", static_cast<int>(lastSlashPos), src_file.toChar());
1182 }
1183
1184 ✗ for (i = 0; i < MaxPollingDirPerChan; ++i) {
1185 ✗ pd = m_channels[chan_num]->getPollDir(i);
1186 ✗ if (src_dir == pd->srcDir) {
1187 ✗ return_code = true;
1188 ✗ break;
1189 }
1190 }
1191
1192 ✗ return return_code;
1193 ✗ }
1194
1195 ✗ void Engine::handleNotKeepFile(Transaction* txn) {
1196 ✗ Os::FileSystem::Status fileStatus = Os::FileSystem::OTHER_ERROR;
1197 ✗ Fw::String failDir;
1198 ✗ Fw::String moveDir;
1199
1200 // Sender
1201 ✗ if (txn->getHistory()->dir == Direction::DIRECTION_TX) {
1202 ✗ if (!TxnStatusIsError(txn->getHistory()->txn_stat)) {
1203 // If move directory is defined attempt move
1204 ✗ moveDir = m_manager->getMoveDirParam(txn->getChannelId());
1205 ✗ if (moveDir.length() > 0) {
1206 ✗ fileStatus = Os::FileSystem::moveFile(txn->m_history->fnames.src_filename.toChar(), moveDir.toChar());
1207 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1208 ✗ m_manager->log_WARNING_LO_FailKeepFileMove(txn->m_history->fnames.src_filename, moveDir,
1209 fileStatus);
1210 }
1211 }
1212
1213 // If move_dir is empty or move failed, delete the file
1214 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1215 ✗ fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.src_filename.toChar());
1216 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1217 ✗ m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.src_filename, fileStatus);
1218 }
1219 }
1220 } else {
1221 // file inside a polling directory
1222 ✗ if (this->isPollingDir(txn->m_history->fnames.src_filename, txn->getChannelId())) {
1223 // If fail directory is defined attempt move
1224 ✗ failDir = m_manager->getFailDirParam(txn->getChannelId());
1225 ✗ if (failDir.length() > 0) {
1226 fileStatus =
1227 ✗ Os::FileSystem::moveFile(txn->m_history->fnames.src_filename.toChar(), failDir.toChar());
1228 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1229 ✗ m_manager->log_WARNING_LO_FailPollFileMove(txn->m_history->fnames.src_filename, failDir,
1230 fileStatus);
1231 }
1232 }
1233
1234 // If fail_dir is empty or move failed, delete the file
1235 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1236 ✗ fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.src_filename.toChar());
1237 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1238 ✗ m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.src_filename, fileStatus);
1239 }
1240 }
1241 }
1242 }
1243 }
1244 // Not Sender
1245 else {
1246 ✗ fileStatus = Os::FileSystem::removeFile(txn->m_history->fnames.dst_filename.toChar());
1247 ✗ if (fileStatus != Os::FileSystem::OP_OK) {
1248 ✗ m_manager->log_WARNING_LO_FileRemoveFailed(txn->m_history->fnames.dst_filename, fileStatus);
1249 }
1250 }
1251 ✗ }
1252
1253 ✗ Cfdp::ChannelTelemetry& Engine::getChannelTelemetryRef(U8 channelId) {
1254 ✗ return this->m_manager->getChannelTelemetryRef(channelId);
1255 }
1256
1257 } // namespace Cfdp
1258 } // namespace Ccsds
1259 } // namespace Svc
1260