GCC Code Coverage Report


Directory: ./
File: TransactionRx.cpp
Date: 2026-09-23 22:12:37
Exec Total Coverage
Lines: 0 536 0.0%
Functions: 0 29 0.0%
Branches: 0 462 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title TransactionRx.cpp
3 // \brief cpp file for CFDP RX Transaction state machine
4 //
5 // This file is a port of RX transaction state machine 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_r.c (receive-file transaction state handling routines)
9 // - cf_cfdp_dispatch.c (RX state machine dispatch functions)
10 //
11 // This file contains various state handling routines for
12 // transactions which are receiving a file, as well as dispatch
13 // functions for RX state machines and top-level transaction dispatch.
14 //
15 // ======================================================================
16 //
17 // NASA Docket No. GSC-18,447-1
18 //
19 // Copyright (c) 2019 United States Government as represented by the
20 // Administrator of the National Aeronautics and Space Administration.
21 // All Rights Reserved.
22 //
23 // Licensed under the Apache License, Version 2.0 (the "License"); you may
24 // not use this file except in compliance with the License. You may obtain
25 // a copy of the License at
26 //
27 // http://www.apache.org/licenses/LICENSE-2.0
28 //
29 // Unless required by applicable law or agreed to in writing, software
30 // distributed under the License is distributed on an "AS IS" BASIS,
31 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32 // See the License for the specific language governing permissions and
33 // limitations under the License.
34 //
35 // ======================================================================
36
37 #include <stdio.h>
38 #include <string.h>
39
40 #include <limits>
41
42 #include <Fw/Types/SuccessEnumAc.hpp>
43 #include <Os/FileSystem.hpp>
44
45 #include <Svc/Ccsds/CfdpManager/CfdpManager.hpp>
46 #include <Svc/Ccsds/CfdpManager/Channel.hpp>
47 #include <Svc/Ccsds/CfdpManager/Chunk.hpp>
48 #include <Svc/Ccsds/CfdpManager/Engine.hpp>
49 #include <Svc/Ccsds/CfdpManager/Transaction.hpp>
50 #include <Svc/Ccsds/CfdpManager/Utils.hpp>
51
52 namespace Svc {
53 namespace Ccsds {
54 namespace Cfdp {
55
56 // ======================================================================
57 // Construction and Destruction
58 // ======================================================================
59
60 ✗ Transaction::Transaction(Channel* channel, U8 channelId, Engine* engine, CfdpManager* manager)
61 ✗ : m_state(TxnState::TXN_STATE_UNDEF),
62 ✗ m_txn_class(Cfdp::Class::CLASS_1),
63 ✗ m_history(nullptr),
64 ✗ m_chunks(nullptr),
65 ✗ m_inactivity_timer(),
66 ✗ m_ack_timer(),
67 ✗ m_fsize(0),
68 ✗ m_foffs(0),
69 ✗ m_fd(),
70 ✗ m_crc(),
71 ✗ m_keep(Cfdp::Keep::KEEP),
72 ✗ m_chan_num(channelId), // Initialize from parameter
73 ✗ m_priority(0),
74 ✗ m_initType(TransactionInitType::INIT_BY_COMMAND),
75 ✗ m_cl_node{},
76 ✗ m_pb(nullptr),
77 ✗ m_state_data{},
78 ✗ m_flags{},
79 ✗ m_cfdpManager(manager), // Initialize from parameter
80 ✗ m_chan(channel), // Initialize from parameter
81 ✗ m_engine(engine) // Initialize from parameter
82 {
83 // Fully zero the union storage
84 ✗ memset(&this->m_state_data, 0, sizeof(this->m_state_data));
85 ✗ memset(&this->m_flags, 0, sizeof(this->m_flags));
86 ✗ }
87
88 ✗ Transaction::~Transaction() {}
89
90 ✗ void Transaction::reset() {
91 // Reset transaction state to default values
92 ✗ this->m_state = TxnState::TXN_STATE_UNDEF;
93 ✗ this->m_txn_class = Cfdp::Class::CLASS_1;
94 ✗ this->m_fsize = 0;
95 ✗ this->m_foffs = 0;
96 ✗ this->m_keep = Cfdp::Keep::KEEP;
97 ✗ this->m_priority = 0;
98 ✗ this->m_initType = TransactionInitType::INIT_BY_COMMAND;
99 ✗ this->m_crc = CFDP::Checksum(0);
100 ✗ this->m_pb = nullptr;
101
102 // Fully zero the union storage
103 ✗ memset(&this->m_state_data, 0, sizeof(this->m_state_data));
104 ✗ memset(&this->m_flags, 0, sizeof(this->m_flags));
105
106 // Close the file if it is open
107 ✗ if (this->m_fd.isOpen()) {
108 ✗ this->m_fd.close();
109 }
110
111 // Disable timers to ensure clean state for next transaction
112 // This prevents stale timers from a previous transaction firing in a new context
113 ✗ this->m_inactivity_timer.disableTimer();
114 ✗ this->m_ack_timer.disableTimer();
115
116 // The following state information is PRESERVED across reset (NOT modified):
117 // - this->m_cfdpManager // Channel binding
118 // - this->m_chan // Channel binding
119 // - this->m_engine // Channel binding
120 // - this->m_chan_num // Channel binding
121 // - this->m_history // Assigned when transaction is activated
122 // - this->m_chunks // Assigned when transaction is activated
123 // - this->m_cl_node // Managed by queue operations in freeTransaction()
124 ✗ }
125
126 // ======================================================================
127 // RX State Machine - Public Methods
128 // ======================================================================
129
130 ✗ void Transaction::r1Recv(const Fw::Buffer& buffer) {
131 static const FileDirectiveDispatchTable r1_fdir_handlers = {{
132 nullptr, /* CFDP_FileDirective_INVALID_MIN */
133 nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */
134 nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */
135 nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */
136 &Transaction::r1SubstateRecvEof, /* CFDP_FileDirective_EOF */
137 nullptr, /* CFDP_FileDirective_FIN */
138 nullptr, /* CFDP_FileDirective_ACK */
139 nullptr, /* CFDP_FileDirective_METADATA */
140 nullptr, /* CFDP_FileDirective_NAK */
141 nullptr, /* CFDP_FileDirective_PROMPT */
142 nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */
143 nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */
144 nullptr, /* CFDP_FileDirective_KEEP_ALIVE */
145 }};
146
147 static const RSubstateDispatchTable substate_fns = {{
148 &r1_fdir_handlers, /* RxSubState::RX_SUB_STATE_FILEDATA */
149 &r1_fdir_handlers, /* RxSubState::RX_SUB_STATE_EOF */
150 &r1_fdir_handlers, /* RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC */
151 }};
152
153 ✗ this->rDispatchRecv(buffer, &substate_fns, &Transaction::r1SubstateRecvFileData);
154 ✗ }
155
156 ✗ void Transaction::r2Recv(const Fw::Buffer& buffer) {
157 static const FileDirectiveDispatchTable r2_fdir_handlers_normal = {{
158 nullptr, /* CFDP_FileDirective_INVALID_MIN */
159 nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */
160 nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */
161 nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */
162 &Transaction::r2SubstateRecvEof, /* CFDP_FileDirective_EOF */
163 nullptr, /* CFDP_FileDirective_FIN */
164 nullptr, /* CFDP_FileDirective_ACK */
165 &Transaction::r2RecvMd, /* CFDP_FileDirective_METADATA */
166 nullptr, /* CFDP_FileDirective_NAK */
167 nullptr, /* CFDP_FileDirective_PROMPT */
168 nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */
169 nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */
170 nullptr, /* CFDP_FileDirective_KEEP_ALIVE */
171 }};
172 static const FileDirectiveDispatchTable r2_fdir_handlers_finack = {{
173 nullptr, /* CFDP_FileDirective_INVALID_MIN */
174 nullptr, /* 1 is unused in the CFDP_FileDirective_t enum */
175 nullptr, /* 2 is unused in the CFDP_FileDirective_t enum */
176 nullptr, /* 3 is unused in the CFDP_FileDirective_t enum */
177 &Transaction::r2SubstateRecvEof, /* CFDP_FileDirective_EOF */
178 nullptr, /* CFDP_FileDirective_FIN */
179 &Transaction::r2RecvFinAck, /* CFDP_FileDirective_ACK */
180 nullptr, /* CFDP_FileDirective_METADATA */
181 nullptr, /* CFDP_FileDirective_NAK */
182 nullptr, /* CFDP_FileDirective_PROMPT */
183 nullptr, /* 10 is unused in the CFDP_FileDirective_t enum */
184 nullptr, /* 11 is unused in the CFDP_FileDirective_t enum */
185 nullptr, /* CFDP_FileDirective_KEEP_ALIVE */
186 }};
187
188 static const RSubstateDispatchTable substate_fns = {{
189 &r2_fdir_handlers_normal, /* RxSubState::RX_SUB_STATE_FILEDATA */
190 &r2_fdir_handlers_normal, /* RxSubState::RX_SUB_STATE_EOF */
191 &r2_fdir_handlers_finack, /* RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC */
192 }};
193
194 ✗ this->rDispatchRecv(buffer, &substate_fns, &Transaction::r2SubstateRecvFileData);
195 ✗ }
196
197 ✗ void Transaction::rAckTimerTick() {
198 ✗ U8 ack_limit = 0;
199
200 /* note: the ack timer is only ever armed on class 2 */
201 ✗ if (this->m_state != TxnState::TXN_STATE_R2 || !this->m_flags.com.ack_timer_armed) {
202 /* nothing to do */
203 ✗ return;
204 }
205
206 ✗ if (this->m_ack_timer.getStatus() == Timer::Status::RUNNING) {
207 ✗ this->m_ack_timer.run();
208 } else {
209 /* ACK timer expired, so check for completion */
210 ✗ if (!this->m_flags.rx.complete) {
211 ✗ this->r2Complete(true);
212 ✗ } else if (this->m_state_data.receive.sub_state == RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC) {
213 /* Increment acknak counter */
214 ✗ ++this->m_state_data.receive.r2.acknak_count;
215
216 /* Check limit and handle if needed */
217 ✗ ack_limit = this->m_cfdpManager->getAckLimitParam(this->m_chan_num);
218 ✗ if (this->m_state_data.receive.r2.acknak_count >= ack_limit) {
219 ✗ this->m_cfdpManager->log_WARNING_LO_RxAckLimitReached(this->getClass(), this->m_history->src_eid,
220 ✗ this->m_history->seq_num);
221 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_ACK_LIMIT_NO_FIN);
222 ✗ this->m_cfdpManager->incrementFaultAckLimit(this->m_chan_num);
223
224 /* give up on this */
225 ✗ this->m_engine->finishTransaction(this, true);
226 ✗ this->m_flags.com.ack_timer_armed = false;
227 } else {
228 ✗ this->m_flags.rx.send_fin = true;
229 }
230 }
231
232 /* re-arm the timer if it is still pending */
233 ✗ if (this->m_flags.com.ack_timer_armed) {
234 /* whether sending FIN or waiting for more filedata, need ACK timer armed */
235 ✗ this->m_engine->armAckTimer(this);
236 }
237 }
238 }
239
240 ✗ void Transaction::rTick(I32* cont /* unused */) {
241 /* Steven is not real happy with this function. There should be a better way to separate out
242 * the logic by state so that it isn't a bunch of if statements for different flags
243 */
244
245 Status::T sret;
246 bool pending_send;
247
248 ✗ if (!this->m_flags.com.inactivity_fired) {
249 ✗ if (this->m_inactivity_timer.getStatus() == Timer::Status::RUNNING) {
250 ✗ this->m_inactivity_timer.run();
251 // Check if timer just expired naturally (after run())
252 ✗ if (this->m_inactivity_timer.getStatus() == Timer::Status::EXPIRED) {
253 ✗ this->m_flags.com.inactivity_fired = true;
254
255 /* HOLD state is the normal path to recycle transaction objects, not an error */
256 /* Canceled transactions timing out is also normal */
257 /* inactivity is abnormal in any other state */
258 ✗ if (this->m_state != TxnState::TXN_STATE_HOLD && !this->m_flags.com.canceled) {
259 ✗ this->rSendInactivityEvent();
260
261 /* in class 2 this also triggers sending an early FIN response */
262 ✗ if (this->m_state == TxnState::TXN_STATE_R2) {
263 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_INACTIVITY_DETECTED);
264 }
265 }
266 }
267 }
268 }
269
270 ✗ pending_send = true; /* maybe; tbd */
271
272 /* rx maintenance: possibly process send_eof_ack, send_nak or send_fin */
273 ✗ if (this->m_flags.rx.send_eof_ack) {
274 ✗ sret = this->m_engine->sendAck(this, AckTxnStatus::ACK_TXN_STATUS_ACTIVE,
275 FileDirective::FILE_DIRECTIVE_END_OF_FILE,
276 ✗ static_cast<ConditionCode>(this->m_state_data.receive.r2.eof_cc),
277 ✗ this->m_history->peer_eid, this->m_history->seq_num);
278
279 /* if SUCCESS, move on. If NO_BUF_AVAIL, retry later. If ERROR, stop retrying. */
280 ✗ if (sret == Cfdp::Status::SUCCESS) {
281 ✗ this->m_flags.rx.send_eof_ack = false;
282 ✗ } else if (sret == Cfdp::Status::ERROR) {
283 /* Serialization failed - error already logged in serializeAndSendPdu */
284 /* Clear flag to avoid infinite retry loop */
285 ✗ this->m_flags.rx.send_eof_ack = false;
286 ✗ pending_send = false;
287 }
288 /* else NO_BUF_AVAIL: leave flag set to retry next tick */
289 ✗ } else if (this->m_flags.rx.send_nak) {
290 ✗ if (!this->rSubstateSendNak()) {
291 ✗ this->m_flags.rx.send_nak = false; /* will re-enter on error */
292 }
293 ✗ } else if (this->m_flags.rx.send_fin) {
294 ✗ if (!this->r2SubstateSendFin()) {
295 ✗ this->m_flags.rx.send_fin = false; /* will re-enter on error */
296 }
297 } else {
298 /* no pending responses to the sender */
299 ✗ pending_send = false;
300 }
301
302 /* if the inactivity timer ran out, then there is no sense
303 * pending for responses for anything. Send out anything
304 * that we need to send (i.e. the FIN) just in case the sender
305 * is still listening to us but do not expect any future ACKs.
306 *
307 * Bound the deferral like the TX path: after a small retry budget, recycle regardless. */
308 ✗ bool retries_exhausted = false;
309 ✗ if (this->m_flags.com.inactivity_fired && pending_send) {
310 ✗ if (this->m_flags.com.post_inactivity_send_retries >=
311 ✗ this->m_cfdpManager->getPostInactivitySendRetriesParam()) {
312 ✗ retries_exhausted = true;
313 } else {
314 ✗ this->m_flags.com.post_inactivity_send_retries++;
315 }
316 }
317 ✗ if (this->m_flags.com.inactivity_fired && (!pending_send || retries_exhausted)) {
318 /* the transaction is now recyclable - this means we will
319 * no longer have a record of this transaction seq. If the sender
320 * wakes up or if the network delivers severely delayed PDUs at
321 * some future point, then they will be seen as spurious. They
322 * will no longer be associable with this transaction at all */
323 ✗ this->m_chan->recycleTransaction(this);
324
325 /* NOTE: this must be the last thing in here. Do not use txn after this */
326 } else {
327 /* transaction still valid so process the ACK timer, if relevant */
328 ✗ this->rAckTimerTick();
329 }
330 ✗ }
331
332 ✗ void Transaction::rCancel() {
333 /* for cancel, only need to send FIN if R2 */
334 ✗ if ((this->m_state == TxnState::TXN_STATE_R2) &&
335 ✗ (this->m_state_data.receive.sub_state < RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC)) {
336 ✗ this->m_flags.rx.send_fin = true;
337 } else {
338 ✗ this->r1Reset(); /* if R1, just call it quits */
339 }
340 ✗ }
341
342 ✗ void Transaction::rInit() {
343 Os::File::Status status;
344 ✗ Fw::String tmpDir;
345 ✗ Fw::String dst;
346
347 ✗ if (this->m_state == TxnState::TXN_STATE_R2) {
348 ✗ if (!this->m_flags.rx.md_recv) {
349 ✗ tmpDir = this->m_cfdpManager->getTmpDirParam(this->m_chan_num);
350 /* we need to make a temp file and then do a NAK for md PDU */
351 /* the transaction already has a history, and that has a buffer that we can use to
352 * hold the temp filename which is defined by the sequence number and the source entity ID */
353
354 // Create destination filepath with format: <tmpDir>/<src_eid>:<seq_num>.tmp
355 ✗ dst.format("%s/%" CFDP_PRI_ENTITY_ID ":%" CFDP_PRI_TRANSACTION_SEQ ".tmp", tmpDir.toChar(),
356 ✗ this->m_history->src_eid, this->m_history->seq_num);
357
358 ✗ this->m_history->fnames.dst_filename = dst;
359
360 ✗ this->m_cfdpManager->log_ACTIVITY_LO_RxTempFileCreated(this->getClass(), this->m_history->src_eid,
361 ✗ this->m_history->seq_num,
362 ✗ this->m_history->fnames.dst_filename);
363 }
364
365 ✗ this->m_engine->armAckTimer(this);
366 }
367
368 ✗ status = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_CREATE, Os::File::OVERWRITE);
369 ✗ if (status != Os::File::OP_OK) {
370 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileCreateFailed(this->getClass(), this->m_history->src_eid,
371 ✗ this->m_history->seq_num,
372 ✗ this->m_history->fnames.dst_filename, status);
373 ✗ this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num);
374 ✗ if (this->m_state == TxnState::TXN_STATE_R2) {
375 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
376 } else {
377 ✗ this->r1Reset();
378 }
379 } else {
380 ✗ this->m_state_data.receive.sub_state = RxSubState::RX_SUB_STATE_FILEDATA;
381 }
382 ✗ }
383
384 ✗ void Transaction::r2SetFinTxnStatus(TxnStatus txn_stat) {
385 ✗ this->m_engine->setTxnStatus(this, txn_stat);
386 ✗ this->m_flags.rx.send_fin = true;
387 ✗ }
388
389 ✗ void Transaction::r1Reset() {
390 ✗ this->m_engine->finishTransaction(this, true);
391 ✗ }
392
393 ✗ void Transaction::r2Reset() {
394 ✗ if ((this->m_state_data.receive.sub_state == RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC) ||
395 ✗ (static_cast<U8>(this->m_state_data.receive.r2.eof_cc) !=
396 ✗ static_cast<U8>(ConditionCode::CONDITION_CODE_NO_ERROR)) ||
397 ✗ TxnStatusIsError(this->m_history->txn_stat) || this->m_flags.com.canceled) {
398 ✗ this->r1Reset(); /* it's done */
399 } else {
400 /* not waiting for FIN ACK, so trigger send FIN */
401 ✗ this->m_flags.rx.send_fin = true;
402 }
403 ✗ }
404
405 ✗ Status::T Transaction::rCheckCrc(U32 expected_crc) {
406 ✗ Status::T ret = Cfdp::Status::SUCCESS;
407 U32 crc_result;
408
409 // The F' version does not have an equivalent finalize call as it
410 // - Never stores a partial word internally
411 // - Never needs to "flush" anything
412 // - Always accounts for padding at update time
413 ✗ crc_result = this->m_crc.getValue();
414 ✗ if (crc_result != expected_crc) {
415 ✗ this->m_cfdpManager->log_WARNING_LO_RxCrcMismatch(this->getClass(), this->m_history->src_eid,
416 ✗ this->m_history->seq_num, expected_crc, crc_result);
417 ✗ this->m_cfdpManager->incrementFaultCrcMismatch(this->m_chan_num);
418 ✗ ret = Cfdp::Status::ERROR;
419 }
420
421 ✗ return ret;
422 }
423
424 ✗ void Transaction::r2Complete(I32 ok_to_send_nak) {
425 U32 ret;
426 ✗ bool send_nak = false;
427 ✗ bool send_fin = false;
428 ✗ U8 nack_limit = 0;
429 /* checking if r2 is complete. Check NAK list, and send NAK if appropriate */
430 /* if all data is present, then there will be no gaps in the chunk */
431
432 ✗ if (!TxnStatusIsError(this->m_history->txn_stat)) {
433 /* first, check if md is received. If not, send specialized NAK */
434 ✗ if (!this->m_flags.rx.md_recv) {
435 ✗ send_nak = true;
436 } else {
437 /* only look for 1 gap, since the goal here is just to know that there are gaps */
438 ✗ ret = this->m_chunks->chunks.computeGaps(1, this->m_fsize, 0, nullptr, nullptr);
439
440 ✗ if (ret) {
441 /* there is at least 1 gap, so send a NAK */
442 ✗ send_nak = true;
443 ✗ } else if (this->m_flags.rx.eof_recv) {
444 /* the EOF was received, and there are no NAKs -- process completion in send FIN state */
445 ✗ send_fin = true;
446 }
447 }
448
449 ✗ if (send_nak && ok_to_send_nak) {
450 /* Increment the acknak counter */
451 ✗ ++this->m_state_data.receive.r2.acknak_count;
452
453 /* Check limit and handle if needed */
454 ✗ nack_limit = this->m_cfdpManager->getNackLimitParam(this->m_chan_num);
455 ✗ if (this->m_state_data.receive.r2.acknak_count >= nack_limit) {
456 ✗ this->m_cfdpManager->log_WARNING_LO_RxNakLimitReached(this->getClass(), this->m_history->src_eid,
457 ✗ this->m_history->seq_num);
458 ✗ send_fin = true;
459 ✗ this->m_cfdpManager->incrementFaultNakLimit(this->m_chan_num);
460 /* don't use CFDP_R2_SetFinTxnStatus because many places in this function set send_fin */
461 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_NAK_LIMIT_REACHED);
462 ✗ this->m_state_data.receive.r2.acknak_count = 0; /* reset for fin/ack */
463 } else {
464 ✗ this->m_flags.rx.send_nak = true;
465 }
466 }
467
468 ✗ if (send_fin) {
469 ✗ this->m_flags.rx.complete = true; /* latch completeness, since send_fin is cleared later */
470
471 /* the transaction is now considered complete, but this will not overwrite an
472 * error status code if there was one set */
473 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_NO_ERROR);
474 }
475
476 /* always go to RxSubState::RX_SUB_STATE_FILEDATA, and let tick change state */
477 ✗ this->m_state_data.receive.sub_state = RxSubState::RX_SUB_STATE_FILEDATA;
478 }
479 ✗ }
480
481 // ======================================================================
482 // RX State Machine - Private Helper Methods
483 // ======================================================================
484
485 ✗ Status::T Transaction::rProcessFd(const Fw::Buffer& buffer) {
486 ✗ Status::T ret = Cfdp::Status::SUCCESS;
487
488 /* this function is only entered for data PDUs */
489 // Deserialize FileData PDU from buffer
490 ✗ FileDataPdu fd;
491 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
492 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
493 ✗ sb.setBuffLen(buffer.getSize());
494
495 ✗ Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb);
496 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
497 ✗ this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(),
498 static_cast<I32>(deserStatus));
499 ✗ ret = Cfdp::Status::ERROR;
500 }
501
502 /*
503 * NOTE: The decode routine should have left a direct pointer to the data and actual data length
504 * within the PDU. The length has already been verified, too. Should not need to make any
505 * adjustments here, just write it.
506 */
507
508 ✗ FileSize offset = fd.getOffset();
509 ✗ U16 dataSize = fd.getDataSize();
510 ✗ const U8* dataPtr = fd.getData();
511
512 // Reject file data past the declared file size, or past the addressable offset space when no
513 // metadata has been received yet (subtraction form avoids offset + dataSize overflow).
514 ✗ if (ret == Cfdp::Status::SUCCESS) {
515 ✗ const FileSize bound = this->m_flags.rx.md_recv ? this->m_fsize : std::numeric_limits<FileSize>::max();
516 ✗ if ((offset > bound) || ((bound - offset) < dataSize)) {
517 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileDataOutOfBounds(
518 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num, offset, dataSize, bound);
519 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
520 ✗ this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num);
521 ✗ ret = Cfdp::Status::ERROR;
522 }
523 }
524
525 // A zero-length FileData segment is syntactically valid (payload length equals the encoded
526 // offset length) but carries no bytes. There is nothing to seek to, write, or track as a gap,
527 // so treat it as a successful no-op. This avoids a pointless file I/O round trip and prevents
528 // a zero-size interval from ever reaching the chunk tracker.
529 ✗ if ((ret == Cfdp::Status::SUCCESS) && (dataSize == 0)) {
530 ✗ return ret;
531 }
532
533 // Seek to file offset if needed
534 ✗ if (ret == Cfdp::Status::SUCCESS) {
535 ✗ if (this->m_state_data.receive.cached_pos != offset) {
536 ✗ Os::File::Status status = this->m_fd.seek(offset, Os::File::SeekType::ABSOLUTE);
537 ✗ if (status != Os::File::OP_OK) {
538 ✗ this->m_cfdpManager->log_WARNING_LO_RxSeekFailed(this->getClass(), this->m_history->src_eid,
539 ✗ this->m_history->seq_num, offset, status);
540 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
541 ✗ this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num);
542 ✗ ret = Cfdp::Status::ERROR;
543 }
544 }
545 }
546
547 // Write file data
548 ✗ if (ret == Cfdp::Status::SUCCESS) {
549 ✗ FwSizeType write_size = dataSize;
550 ✗ Os::File::Status status = this->m_fd.write(dataPtr, write_size, Os::File::WaitType::WAIT);
551 ✗ if (status != Os::File::OP_OK) {
552 ✗ this->m_cfdpManager->log_WARNING_LO_RxWriteFailed(this->getClass(), this->m_history->src_eid,
553 ✗ this->m_history->seq_num, dataSize,
554 static_cast<I32>(write_size));
555 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
556 ✗ this->m_cfdpManager->incrementFaultFileWrite(this->m_chan_num);
557 ✗ ret = Cfdp::Status::ERROR;
558 } else {
559 ✗ this->m_state_data.receive.cached_pos = static_cast<FileSize>(dataSize) + offset;
560 ✗ this->m_cfdpManager->addRecvFileDataBytes(this->m_chan_num, dataSize);
561 }
562 }
563
564 ✗ return ret;
565 ✗ }
566
567 ✗ Status::T Transaction::rSubstateRecvEof(const Fw::Buffer& buffer) {
568 ✗ Status::T ret = Cfdp::Status::SUCCESS;
569
570 // Deserialize EOF PDU from buffer
571 ✗ EofPdu eof;
572 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
573 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
574 ✗ sb.setBuffLen(buffer.getSize());
575
576 ✗ Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb);
577 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
578 ✗ this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(),
579 static_cast<I32>(deserStatus));
580 ✗ ret = Cfdp::Status::REC_PDU_BAD_EOF_ERROR;
581 }
582
583 ✗ if (ret == Cfdp::Status::SUCCESS) {
584 // NOTE: Engine::recvEof() currently always returns SUCCESS, so the failure branch is
585 // omitted here. Once recvEof() performs real EOF-level validation (see the TLV
586 // "Future enhancement" TODO in Engine::recvEof) and can return an error status, add an
587 // else branch that emits log_WARNING_LO_RxInvalidEofPdu, increments recvErrors, and sets
588 // Cfdp::Status::REC_PDU_BAD_EOF_ERROR for the failure case.
589 ✗ if (!this->m_engine->recvEof(this, eof)) {
590 /* this function is only entered for PDUs identified as EOF type */
591 ✗ ConditionCode cc = eof.getConditionCode();
592
593 /* Only check size if MD received and EOF doesn't have a non-zero condition code (e.g., don't check size for
594 * canceled transactions) */
595 ✗ if (this->m_flags.rx.md_recv && (cc == ConditionCode::CONDITION_CODE_NO_ERROR) &&
596 ✗ (eof.getFileSize() != this->m_fsize)) {
597 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileSizeMismatch(this->getClass(), this->m_history->src_eid,
598 ✗ this->m_history->seq_num, this->m_fsize,
599 eof.getFileSize());
600 ✗ this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num);
601 ✗ ret = Cfdp::Status::REC_PDU_FSIZE_MISMATCH_ERROR;
602 }
603
604 /* Log condition code if non-zero (cancel or error) - applies to both Class 1 and Class 2 */
605 ✗ if (cc != ConditionCode::CONDITION_CODE_NO_ERROR) {
606 /* Set transaction status from condition code to prevent completion event */
607 ✗ this->m_engine->setTxnStatus(this, static_cast<TxnStatus>(static_cast<I32>(cc)));
608
609 ✗ if (cc == ConditionCode::CONDITION_CODE_CANCEL_REQUEST_RECEIVED) {
610 /* Increment receive EOF cancellation counter (normal operation) */
611 ✗ this->m_cfdpManager->incrementRecvEofCanceled(this->m_chan_num);
612
613 ✗ this->m_cfdpManager->log_ACTIVITY_HI_RxEofCancelReceived(this->getClass(), this->m_history->src_eid,
614 ✗ this->m_history->seq_num);
615 } else {
616 /* Increment RX EOF error counter (protocol error) */
617 ✗ this->m_cfdpManager->incrementFaultRxEofError(this->m_chan_num);
618
619 ✗ this->m_cfdpManager->log_WARNING_LO_RxEofWithError(this->getClass(), this->m_history->src_eid,
620 ✗ this->m_history->seq_num, static_cast<U8>(cc));
621 }
622 }
623 }
624 }
625
626 ✗ return ret;
627 ✗ }
628
629 ✗ void Transaction::r1SubstateRecvEof(const Fw::Buffer& buffer) {
630 // Deserialize EOF PDU from buffer
631 ✗ EofPdu eof;
632 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
633 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
634 ✗ sb.setBuffLen(buffer.getSize());
635
636 ✗ Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb);
637 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
638 // Bad EOF, reset transaction
639 ✗ this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(),
640 static_cast<I32>(deserStatus));
641 ✗ this->r1Reset();
642 ✗ return;
643 }
644
645 ✗ Status::T ret = this->rSubstateRecvEof(buffer);
646 ✗ U32 crc = eof.getChecksum();
647 ✗ ConditionCode cc = eof.getConditionCode();
648
649 ✗ if (ret == Cfdp::Status::SUCCESS) {
650 /* Only check CRC if no error condition code */
651 ✗ if (cc == ConditionCode::CONDITION_CODE_NO_ERROR) {
652 /* Verify CRC */
653 ✗ if (this->rCheckCrc(crc) == Cfdp::Status::SUCCESS) {
654 /* successfully processed the file */
655 ✗ this->m_keep = Cfdp::Keep::KEEP; /* save the file */
656 }
657 /* if file failed to process, there's nothing to do. CFDP_R_CheckCrc() generates an event on failure */
658 }
659 }
660
661 /* after exit, always reset since we are done */
662 /* reset even if the EOF failed -- class 1, so it won't come again! */
663 ✗ this->r1Reset();
664 ✗ }
665
666 ✗ void Transaction::r2SubstateRecvEof(const Fw::Buffer& buffer) {
667 Status::T ret;
668
669 ✗ if (!this->m_flags.rx.eof_recv) {
670 // Deserialize EOF PDU from buffer
671 ✗ EofPdu eof;
672 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
673 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
674 ✗ sb.setBuffLen(buffer.getSize());
675
676 ✗ Fw::SerializeStatus deserStatus = eof.deserializeFrom(sb);
677 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
678 // Bad EOF, return to FILEDATA substate
679 ✗ this->m_cfdpManager->log_WARNING_LO_FailEofPduDeserialization(this->getChannelId(),
680 static_cast<I32>(deserStatus));
681 ✗ this->m_state_data.receive.sub_state = RxSubState::RX_SUB_STATE_FILEDATA;
682 ✗ return;
683 }
684
685 ✗ ret = this->rSubstateRecvEof(buffer);
686
687 /* did receiving EOF succeed? */
688 ✗ if (ret == Cfdp::Status::SUCCESS) {
689 ✗ this->m_flags.rx.eof_recv = true;
690
691 /* need to remember the EOF CRC for later */
692 ✗ this->m_state_data.receive.r2.eof_crc = eof.getChecksum();
693 ✗ this->m_state_data.receive.r2.eof_size = eof.getFileSize();
694
695 /* always ACK the EOF, even if we're not done */
696 ✗ this->m_state_data.receive.r2.eof_cc = static_cast<U8>(eof.getConditionCode());
697 ✗ this->m_flags.rx.send_eof_ack = true; /* defer sending ACK to tick handling */
698
699 /* only check for complete if EOF with no errors */
700 ✗ if (static_cast<U8>(this->m_state_data.receive.r2.eof_cc) ==
701 static_cast<U8>(ConditionCode::CONDITION_CODE_NO_ERROR)) {
702 ✗ this->r2Complete(true); /* CFDP_R2_Complete() will change state */
703 } else {
704 /* All CFDP CC values directly correspond to a Transaction Status of the same numeric value */
705 ✗ this->m_engine->setTxnStatus(
706 ✗ this, static_cast<TxnStatus>(static_cast<I32>(this->m_state_data.receive.r2.eof_cc)));
707 ✗ this->r2Reset();
708 }
709 } else {
710 /* bad EOF sent? */
711 ✗ if (ret == Cfdp::Status::REC_PDU_FSIZE_MISMATCH_ERROR) {
712 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
713 } else {
714 /* can't do anything with this bad EOF, so return to FILEDATA */
715 ✗ this->m_state_data.receive.sub_state = RxSubState::RX_SUB_STATE_FILEDATA;
716 }
717 }
718 ✗ }
719 }
720
721 ✗ void Transaction::r1SubstateRecvFileData(const Fw::Buffer& buffer) {
722 Status::T ret;
723
724 // Deserialize FileData PDU from buffer
725 ✗ FileDataPdu fd;
726 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
727 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
728 ✗ sb.setBuffLen(buffer.getSize());
729
730 ✗ Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb);
731 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
732 // Bad file data PDU, reset transaction
733 ✗ this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(),
734 static_cast<I32>(deserStatus));
735 ✗ this->r1Reset();
736 ✗ return;
737 }
738
739 /* got file data PDU? */
740 ✗ ret = this->m_engine->recvFd(this, fd);
741 ✗ if (ret == Cfdp::Status::SUCCESS) {
742 ✗ ret = this->rProcessFd(buffer);
743 }
744
745 ✗ if (ret == Cfdp::Status::SUCCESS) {
746 /* class 1 digests CRC */
747 ✗ this->m_crc.update(fd.getData(), fd.getOffset(), static_cast<U32>(fd.getDataSize()));
748 } else {
749 /* Reset transaction on failure */
750 ✗ this->r1Reset();
751 }
752 ✗ }
753
754 ✗ void Transaction::r2SubstateRecvFileData(const Fw::Buffer& buffer) {
755 Status::T ret;
756
757 // If CRC calculation has started (file reopened in READ mode), ignore late FileData PDUs.
758 // This can happen if retransmitted FileData arrives after EOF was received and CRC began.
759 ✗ if (this->m_state_data.receive.r2.rx_crc_calc_bytes > 0) {
760 // Silently ignore - file is complete and we're calculating CRC
761 // No EVR needed - late retransmissions are expected in CFDP Class 2
762 ✗ return;
763 }
764
765 // Deserialize FileData PDU from buffer
766 ✗ FileDataPdu fd;
767 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
768 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
769 ✗ sb.setBuffLen(buffer.getSize());
770
771 ✗ Fw::SerializeStatus deserStatus = fd.deserializeFrom(sb);
772 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
773 // Bad file data PDU, reset transaction
774 ✗ this->m_cfdpManager->log_WARNING_LO_FailFileDataPduDeserialization(this->getChannelId(),
775 static_cast<I32>(deserStatus));
776 ✗ this->r2Reset();
777 ✗ return;
778 }
779
780 /* got file data PDU? */
781 ✗ ret = this->m_engine->recvFd(this, fd);
782 ✗ if (ret == Cfdp::Status::SUCCESS) {
783 ✗ ret = this->rProcessFd(buffer);
784 }
785
786 ✗ if (ret == Cfdp::Status::SUCCESS) {
787 /* class 2 does CRC at FIN, but track gaps */
788 ✗ this->m_chunks->chunks.add(fd.getOffset(), static_cast<FileSize>(fd.getDataSize()));
789
790 ✗ if (this->m_flags.rx.fd_nak_sent) {
791 ✗ this->r2Complete(false); /* once nak-retransmit received, start checking for completion at each fd */
792 }
793
794 ✗ if (!this->m_flags.rx.complete) {
795 ✗ this->m_engine->armAckTimer(this); /* re-arm ACK timer, since we got data */
796 }
797
798 ✗ this->m_state_data.receive.r2.acknak_count = 0;
799 } else {
800 /* Reset transaction on failure */
801 ✗ this->r2Reset();
802 }
803 ✗ }
804
805 ✗ void Transaction::r2GapCompute(const Chunk* chunk, NakPdu& nak) {
806 ✗ FW_ASSERT(chunk->size > 0, static_cast<FwAssertArgType>(chunk->size));
807
808 // Calculate segment offsets relative to scope start
809 ✗ FileSize offsetStart = chunk->offset - nak.getScopeStart();
810 ✗ FileSize offsetEnd = offsetStart + chunk->size;
811
812 // Add segment to NAK PDU (returns false if array is full)
813 ✗ nak.addSegment(offsetStart, offsetEnd);
814 ✗ }
815
816 ✗ void Transaction::r2GapComputeWrapper(const Chunk* chunk, void* opaque) {
817 struct GapComputeContext {
818 Transaction* txn;
819 NakPdu* nak;
820 };
821 ✗ GapComputeContext* ctx = static_cast<GapComputeContext*>(opaque);
822 ✗ ctx->txn->r2GapCompute(chunk, *ctx->nak);
823 ✗ }
824
825 ✗ Status::T Transaction::rSubstateSendNak() {
826 ✗ Status::T status = Cfdp::Status::SUCCESS;
827
828 // Create and initialize NAK PDU
829 ✗ NakPdu nakPdu;
830 ✗ Cfdp::PduDirection direction = PduDirection::DIRECTION_TOWARD_SENDER;
831
832 ✗ if (this->m_flags.rx.md_recv) {
833 // We have metadata, so send NAK with file data gaps
834 ✗ nakPdu.initialize(direction,
835 this->getClass(), // transmission mode
836 ✗ this->m_history->peer_eid, // source EID (receiver)
837 ✗ this->m_history->seq_num, // transaction sequence number
838 ✗ this->m_cfdpManager->getLocalEidParam(), // destination EID (sender)
839 0, // scope start
840 0 // scope end
841 );
842
843 // Compute gaps and add segments to NAK PDU
844 ✗ U32 chunkCount = this->m_chunks->chunks.getCount();
845 ✗ U32 maxChunks = this->m_chunks->chunks.getMaxChunks();
846 ✗ U32 gapLimit = (chunkCount < maxChunks) ? maxChunks : (maxChunks - 1);
847
848 // For each gap found, add it as a segment to the NAK PDU via callback
849 struct GapComputeContext {
850 Transaction* txn;
851 NakPdu* nak;
852 ✗ } gapCtx = {this, &nakPdu};
853
854 ✗ U32 gapCount = this->m_chunks->chunks.computeGaps(static_cast<ChunkIdx>(gapLimit), this->m_fsize, 0,
855 &Transaction::r2GapComputeWrapper, &gapCtx);
856
857 ✗ if (!gapCount) {
858 // No gaps left, file reception is complete
859 ✗ this->m_flags.rx.complete = true;
860 ✗ status = Cfdp::Status::SUCCESS;
861 } else {
862 // Gaps are present, send the NAK PDU
863 ✗ status = this->m_engine->sendNak(this, nakPdu);
864 ✗ if (status == Cfdp::Status::SUCCESS) {
865 ✗ this->m_flags.rx.fd_nak_sent = true;
866 ✗ this->m_cfdpManager->addSentNakSegmentRequests(this->m_chan_num, gapCount);
867 }
868 }
869 } else {
870 // Need to send NAK to request metadata PDU again
871 // Special case: scope start/end and segment[0] all zeros requests metadata
872 ✗ nakPdu.initialize(direction,
873 this->getClass(), // transmission mode
874 ✗ this->m_history->peer_eid, // source EID (receiver)
875 ✗ this->m_history->seq_num, // transaction sequence number
876 ✗ this->m_cfdpManager->getLocalEidParam(), // destination EID (sender)
877 0, // scope start (special value)
878 0 // scope end (special value)
879 );
880
881 // Add special segment [0,0] to request metadata
882 ✗ nakPdu.addSegment(0, 0);
883
884 ✗ status = this->m_engine->sendNak(this, nakPdu);
885 }
886
887 ✗ return status;
888 ✗ }
889
890 ✗ Status::T Transaction::r2CalcCrcChunk() {
891 U8 buf[R2CrcChunkSize];
892 FileSize count_bytes;
893 FileSize want_offs_size;
894 FwSizeType read_size;
895 Os::File::Status fileStatus;
896 ✗ Status::T ret = Cfdp::Status::SUCCESS;
897 ✗ FileSize rx_crc_calc_bytes_per_cycle = 0;
898
899 ✗ memset(buf, 0, sizeof(buf));
900
901 ✗ count_bytes = 0;
902
903 // Open file for CRC calculation if needed
904 ✗ if (ret == Cfdp::Status::SUCCESS) {
905 ✗ if (this->m_state_data.receive.r2.rx_crc_calc_bytes == 0) {
906 ✗ this->m_crc = CFDP::Checksum(0);
907
908 // For Class 2 RX, the file was opened in WRITE mode for receiving FileData PDUs.
909 // Now we need to READ it for CRC calculation. Close and reopen in READ mode.
910 ✗ if (this->m_fd.isOpen()) {
911 ✗ this->m_fd.close();
912 }
913
914 ✗ fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_READ);
915 ✗ if (fileStatus != Os::File::OP_OK) {
916 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
917 ✗ ret = Cfdp::Status::ERROR;
918 } else {
919 // Reset cached position since we just reopened the file
920 ✗ this->m_state_data.receive.cached_pos = 0;
921 }
922 }
923 }
924
925 // Process file in chunks
926 ✗ if (ret == Cfdp::Status::SUCCESS) {
927 ✗ rx_crc_calc_bytes_per_cycle = this->m_cfdpManager->getRxCrcCalcBytesPerCycleParam();
928
929 ✗ while ((ret == Cfdp::Status::SUCCESS) && (count_bytes < rx_crc_calc_bytes_per_cycle) &&
930 ✗ (this->m_state_data.receive.r2.rx_crc_calc_bytes < this->m_fsize)) {
931 ✗ want_offs_size = this->m_state_data.receive.r2.rx_crc_calc_bytes + static_cast<FileSize>(sizeof(buf));
932
933 ✗ if (want_offs_size > this->m_fsize) {
934 ✗ read_size = this->m_fsize - this->m_state_data.receive.r2.rx_crc_calc_bytes;
935 } else {
936 ✗ read_size = sizeof(buf);
937 }
938
939 ✗ if (this->m_state_data.receive.cached_pos != this->m_state_data.receive.r2.rx_crc_calc_bytes) {
940 fileStatus =
941 ✗ this->m_fd.seek(this->m_state_data.receive.r2.rx_crc_calc_bytes, Os::File::SeekType::ABSOLUTE);
942 ✗ if (fileStatus != Os::File::OP_OK) {
943 ✗ this->m_cfdpManager->log_WARNING_LO_RxSeekCrcFailed(
944 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
945 this->m_state_data.receive.r2.rx_crc_calc_bytes, fileStatus);
946 // this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
947 ✗ this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num);
948 ✗ ret = Cfdp::Status::ERROR;
949 }
950 }
951
952 ✗ if (ret == Cfdp::Status::SUCCESS) {
953 ✗ FwSizeType expected_read_size = read_size;
954 ✗ fileStatus = this->m_fd.read(buf, read_size, Os::File::WaitType::WAIT);
955 ✗ if (fileStatus != Os::File::OP_OK) {
956 ✗ this->m_cfdpManager->log_WARNING_LO_RxReadCrcFailed(
957 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
958 static_cast<U32>(expected_read_size), static_cast<I32>(read_size));
959 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
960 ✗ this->m_cfdpManager->incrementFaultFileRead(this->m_chan_num);
961 ✗ ret = Cfdp::Status::ERROR;
962 } else {
963 ✗ this->m_crc.update(buf, this->m_state_data.receive.r2.rx_crc_calc_bytes,
964 static_cast<U32>(read_size));
965 ✗ this->m_state_data.receive.r2.rx_crc_calc_bytes += static_cast<FileSize>(read_size);
966 ✗ this->m_state_data.receive.cached_pos = this->m_state_data.receive.r2.rx_crc_calc_bytes;
967 ✗ count_bytes += static_cast<FileSize>(read_size);
968
969 // Reset inactivity timer to indicate transaction is actively processing
970 ✗ this->m_engine->armInactTimer(this);
971 }
972 }
973 }
974 }
975
976 // Check final CRC if all bytes processed
977 ✗ if (ret == Cfdp::Status::SUCCESS) {
978 ✗ if (this->m_state_data.receive.r2.rx_crc_calc_bytes == this->m_fsize) {
979 /* all bytes calculated, so now check */
980 ✗ if (this->rCheckCrc(this->m_state_data.receive.r2.eof_crc) == Cfdp::Status::SUCCESS) {
981 /* CRC matched! We are happy */
982 ✗ this->m_keep = Cfdp::Keep::KEEP; /* save the file */
983
984 /* set FIN PDU status */
985 ✗ this->m_state_data.receive.r2.dc = FinDeliveryCode::FIN_DELIVERY_CODE_COMPLETE;
986 ✗ this->m_state_data.receive.r2.fs = FinFileStatus::FIN_FILE_STATUS_RETAINED;
987 } else {
988 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILE_CHECKSUM_FAILURE);
989 }
990
991 ✗ this->m_flags.com.crc_calc = true;
992 } else {
993 // Not all bytes processed yet, return ERROR to signal need to continue
994 ✗ ret = Cfdp::Status::ERROR;
995 }
996 }
997
998 ✗ return ret;
999 }
1000
1001 ✗ Status::T Transaction::r2SubstateSendFin() {
1002 Status::T sret;
1003 ✗ Status::T ret = Cfdp::Status::SUCCESS;
1004
1005 ✗ if (!TxnStatusIsError(this->m_history->txn_stat) && !this->m_flags.com.crc_calc) {
1006 /* no error, and haven't checked CRC -- so start checking it */
1007 ✗ if (this->r2CalcCrcChunk()) {
1008 ✗ ret = Cfdp::Status::ERROR; /* signal to caller to re-enter next tick */
1009 }
1010 }
1011
1012 ✗ if (ret != Cfdp::Status::ERROR) {
1013 ✗ sret = this->m_engine->sendFin(this, this->m_state_data.receive.r2.dc, this->m_state_data.receive.r2.fs,
1014 ✗ static_cast<ConditionCode>(TxnStatusToConditionCode(this->m_history->txn_stat)));
1015
1016 /* Serialization error already logged in serializeAndSendPdu if ERROR returned */
1017 ✗ this->m_state_data.receive.sub_state =
1018 RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC; /* whether or not FIN send successful, ok to transition state */
1019 ✗ if (sret != Cfdp::Status::SUCCESS) {
1020 ✗ ret = Cfdp::Status::ERROR;
1021 }
1022 }
1023
1024 /* if no message, then try again next time */
1025 ✗ return ret;
1026 }
1027
1028 ✗ void Transaction::r2RecvFinAck(const Fw::Buffer& buffer) {
1029 // Deserialize ACK PDU from buffer
1030 ✗ AckPdu ack;
1031 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1032 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1033 ✗ sb.setBuffLen(buffer.getSize());
1034
1035 ✗ Fw::SerializeStatus deserStatus = ack.deserializeFrom(sb);
1036 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
1037 // Bad ACK PDU
1038 ✗ this->m_cfdpManager->log_WARNING_LO_FailAckPduDeserialization(this->getChannelId(),
1039 static_cast<I32>(deserStatus));
1040 ✗ this->m_cfdpManager->incrementRecvErrors(this->m_chan_num);
1041 ✗ return;
1042 }
1043
1044 // ACK PDU has been validated during deserialization
1045 // Got fin-ack, so time to close the state
1046 ✗ this->r2Reset();
1047 ✗ }
1048
1049 ✗ void Transaction::r2RecvMd(const Fw::Buffer& buffer) {
1050 ✗ Fw::String fname;
1051 Os::File::Status fileStatus;
1052 Os::FileSystem::Status fileSysStatus;
1053 ✗ bool success = true;
1054
1055 /* it isn't an error to get another MD PDU, right? */
1056 ✗ if (!this->m_flags.rx.md_recv) {
1057 /* NOTE: this->m_flags.rx.md_recv always 1 in R1, so this is R2 only */
1058 /* parse the md PDU. this will overwrite the transaction's history, which contains our filename. so let's
1059 * save the filename in a local buffer so it can be used with moveFile upon successful parsing of
1060 * the md PDU */
1061 ✗ fname = this->m_history->fnames.dst_filename;
1062
1063 // Deserialize Metadata PDU from buffer
1064 ✗ MetadataPdu md;
1065 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1066 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1067 ✗ sb.setBuffLen(buffer.getSize());
1068
1069 ✗ Fw::SerializeStatus deserStatus = md.deserializeFrom(sb);
1070 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
1071 // Bad metadata PDU
1072 ✗ this->m_cfdpManager->log_WARNING_LO_FailMetadataPduDeserialization(this->getChannelId(),
1073 static_cast<I32>(deserStatus));
1074 ✗ return;
1075 }
1076
1077 // PDU validation already done during deserialization
1078 ✗ this->m_engine->recvMd(this, md);
1079
1080 /* successfully obtained md PDU */
1081 ✗ if (this->m_flags.rx.eof_recv) {
1082 /* EOF was received, so check that md and EOF sizes match */
1083 ✗ if (this->m_state_data.receive.r2.eof_size != this->m_fsize) {
1084 ✗ this->m_cfdpManager->log_WARNING_LO_RxEofMdSizeMismatch(this->getClass(), this->m_history->src_eid,
1085 ✗ this->m_history->seq_num, this->m_fsize,
1086 this->m_state_data.receive.r2.eof_size);
1087 ✗ this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num);
1088 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
1089 ✗ success = false;
1090 }
1091 }
1092
1093 ✗ if (success) {
1094 /* close and rename file */
1095 ✗ this->m_fd.close();
1096
1097 ✗ fileSysStatus = Os::FileSystem::moveFile(fname.toChar(), this->m_history->fnames.dst_filename.toChar());
1098 ✗ if (fileSysStatus != Os::FileSystem::OP_OK) {
1099 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileRenameFailed(
1100 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num, fname,
1101 ✗ this->m_history->fnames.dst_filename, fileSysStatus);
1102 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
1103 ✗ this->m_cfdpManager->incrementFaultFileRename(this->m_chan_num);
1104 ✗ success = false;
1105 } else {
1106 // File was successfully renamed, open for writing
1107 ✗ fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_WRITE);
1108 ✗ if (fileStatus != Os::File::OP_OK) {
1109 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileReopenFailed(
1110 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
1111 ✗ this->m_history->fnames.dst_filename, fileStatus);
1112 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
1113 ✗ this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num);
1114 ✗ success = false;
1115 }
1116 }
1117
1118 ✗ if (success) {
1119 ✗ this->m_state_data.receive.cached_pos = 0; /* reset psn due to open */
1120 ✗ this->m_flags.rx.md_recv = true;
1121 ✗ this->m_state_data.receive.r2.acknak_count = 0; /* in case part of NAK */
1122 ✗ this->r2Complete(true); /* check for completion now that md is received */
1123 }
1124 }
1125 ✗ }
1126 ✗ }
1127
1128 ✗ void Transaction::rSendInactivityEvent() {
1129 ✗ this->m_cfdpManager->log_WARNING_LO_RxInactivityTimeout(this->getClass(), this->m_history->src_eid,
1130 ✗ this->m_history->seq_num);
1131 ✗ this->m_cfdpManager->incrementFaultInactivityTimer(this->m_chan_num);
1132 ✗ }
1133
1134 // ======================================================================
1135 // Dispatch Methods
1136 // ======================================================================
1137
1138 ✗ void Transaction::rDispatchRecv(const Fw::Buffer& buffer, const RSubstateDispatchTable* dispatch, StateRecvFunc fd_fn) {
1139 StateRecvFunc selected_handler;
1140
1141 ✗ FW_ASSERT(this->m_state_data.receive.sub_state < RxSubState::RX_SUB_STATE_NUM_STATES,
1142 static_cast<U8>(this->m_state_data.receive.sub_state),
1143 static_cast<U8>(RxSubState::RX_SUB_STATE_NUM_STATES));
1144
1145 ✗ selected_handler = nullptr;
1146
1147 // Peek at PDU type from buffer
1148 ✗ Cfdp::PduTypeEnum::T pduType = Cfdp::peekPduType(buffer);
1149
1150 // Special handling for file data PDU
1151 ✗ if (pduType == Cfdp::PduTypeEnum::FILE_DATA) {
1152 /* For file data PDU, use the provided fd_fn */
1153 ✗ if (!TxnStatusIsError(this->m_history->txn_stat)) {
1154 ✗ selected_handler = fd_fn;
1155 }
1156 } else {
1157 // Not a file-data PDU - parse as a directive PDU to get the directive code.
1158 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1159 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1160 ✗ sb.setBuffLen(buffer.getSize());
1161
1162 Cfdp::PduHeader header;
1163 ✗ if (header.fromSerialBuffer(sb) == Fw::FW_SERIALIZE_OK) {
1164 // Read directive code (first byte after header for directive PDUs)
1165 U8 directiveCodeByte;
1166 ✗ if (sb.deserializeTo(directiveCodeByte) == Fw::FW_SERIALIZE_OK) {
1167 ✗ FileDirective directiveCode = static_cast<FileDirective>(directiveCodeByte);
1168
1169 ✗ if (directiveCode < FileDirective::FILE_DIRECTIVE_INVALID_MAX) {
1170 /* The CFDP_R_SubstateDispatchTable_t is only used with file directive PDU */
1171 ✗ if (dispatch->state[static_cast<U32>(this->m_state_data.receive.sub_state)] != nullptr) {
1172 ✗ selected_handler = dispatch->state[static_cast<U32>(this->m_state_data.receive.sub_state)]
1173 ✗ ->fdirective[static_cast<U32>(directiveCode)];
1174 }
1175 } else {
1176 ✗ this->m_cfdpManager->incrementRecvSpurious(this->m_chan_num);
1177 ✗ this->m_cfdpManager->log_WARNING_LO_RxInvalidDirectiveCode(
1178 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num, directiveCodeByte,
1179 ✗ static_cast<U8>(this->m_state_data.receive.sub_state));
1180 }
1181 }
1182 }
1183 ✗ }
1184
1185 /*
1186 * NOTE: if no handler is selected, this will drop packets on the floor here.
1187 */
1188 ✗ if (selected_handler != nullptr) {
1189 ✗ (this->*selected_handler)(buffer);
1190 } else {
1191 ✗ this->m_cfdpManager->incrementRecvDropped(this->m_chan_num);
1192 }
1193 ✗ }
1194
1195 } // namespace Cfdp
1196 } // namespace Ccsds
1197 } // namespace Svc
1198