GCC Code Coverage Report


Directory: ./
File: TransactionRx.cpp
Date: 2026-10-14 22:12:44
Exec Total Coverage
Lines: 0 537 0.0%
Functions: 0 29 0.0%
Branches: 0 465 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->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_PROTOCOL_ERROR);
777 ✗ this->r2Reset();
778 ✗ return;
779 }
780
781 /* got file data PDU? */
782 ✗ ret = this->m_engine->recvFd(this, fd);
783 ✗ if (ret == Cfdp::Status::SUCCESS) {
784 ✗ ret = this->rProcessFd(buffer);
785 }
786
787 ✗ if (ret == Cfdp::Status::SUCCESS) {
788 /* class 2 does CRC at FIN, but track gaps */
789 ✗ this->m_chunks->chunks.add(fd.getOffset(), static_cast<FileSize>(fd.getDataSize()));
790
791 ✗ if (this->m_flags.rx.fd_nak_sent) {
792 ✗ this->r2Complete(false); /* once nak-retransmit received, start checking for completion at each fd */
793 }
794
795 ✗ if (!this->m_flags.rx.complete) {
796 ✗ this->m_engine->armAckTimer(this); /* re-arm ACK timer, since we got data */
797 }
798
799 ✗ this->m_state_data.receive.r2.acknak_count = 0;
800 } else {
801 /* Reset transaction on failure */
802 ✗ this->r2Reset();
803 }
804 ✗ }
805
806 ✗ void Transaction::r2GapCompute(const Chunk* chunk, NakPdu& nak) {
807 ✗ FW_ASSERT(chunk->size > 0, static_cast<FwAssertArgType>(chunk->size));
808
809 // Calculate segment offsets relative to scope start
810 ✗ FileSize offsetStart = chunk->offset - nak.getScopeStart();
811 ✗ FileSize offsetEnd = offsetStart + chunk->size;
812
813 // Add segment to NAK PDU (returns false if array is full)
814 ✗ nak.addSegment(offsetStart, offsetEnd);
815 ✗ }
816
817 ✗ void Transaction::r2GapComputeWrapper(const Chunk* chunk, void* opaque) {
818 struct GapComputeContext {
819 Transaction* txn;
820 NakPdu* nak;
821 };
822 ✗ GapComputeContext* ctx = static_cast<GapComputeContext*>(opaque);
823 ✗ ctx->txn->r2GapCompute(chunk, *ctx->nak);
824 ✗ }
825
826 ✗ Status::T Transaction::rSubstateSendNak() {
827 ✗ Status::T status = Cfdp::Status::SUCCESS;
828
829 // Create and initialize NAK PDU
830 ✗ NakPdu nakPdu;
831 ✗ Cfdp::PduDirection direction = PduDirection::DIRECTION_TOWARD_SENDER;
832
833 ✗ if (this->m_flags.rx.md_recv) {
834 // We have metadata, so send NAK with file data gaps
835 ✗ nakPdu.initialize(direction,
836 this->getClass(), // transmission mode
837 ✗ this->m_history->peer_eid, // source EID (receiver)
838 ✗ this->m_history->seq_num, // transaction sequence number
839 ✗ this->m_cfdpManager->getLocalEidParam(), // destination EID (sender)
840 0, // scope start
841 0 // scope end
842 );
843
844 // Compute gaps and add segments to NAK PDU
845 ✗ U32 chunkCount = this->m_chunks->chunks.getCount();
846 ✗ U32 maxChunks = this->m_chunks->chunks.getMaxChunks();
847 ✗ U32 gapLimit = (chunkCount < maxChunks) ? maxChunks : (maxChunks - 1);
848
849 // For each gap found, add it as a segment to the NAK PDU via callback
850 struct GapComputeContext {
851 Transaction* txn;
852 NakPdu* nak;
853 ✗ } gapCtx = {this, &nakPdu};
854
855 ✗ U32 gapCount = this->m_chunks->chunks.computeGaps(static_cast<ChunkIdx>(gapLimit), this->m_fsize, 0,
856 &Transaction::r2GapComputeWrapper, &gapCtx);
857
858 ✗ if (!gapCount) {
859 // No gaps left, file reception is complete
860 ✗ this->m_flags.rx.complete = true;
861 ✗ status = Cfdp::Status::SUCCESS;
862 } else {
863 // Gaps are present, send the NAK PDU
864 ✗ status = this->m_engine->sendNak(this, nakPdu);
865 ✗ if (status == Cfdp::Status::SUCCESS) {
866 ✗ this->m_flags.rx.fd_nak_sent = true;
867 ✗ this->m_cfdpManager->addSentNakSegmentRequests(this->m_chan_num, gapCount);
868 }
869 }
870 } else {
871 // Need to send NAK to request metadata PDU again
872 // Special case: scope start/end and segment[0] all zeros requests metadata
873 ✗ nakPdu.initialize(direction,
874 this->getClass(), // transmission mode
875 ✗ this->m_history->peer_eid, // source EID (receiver)
876 ✗ this->m_history->seq_num, // transaction sequence number
877 ✗ this->m_cfdpManager->getLocalEidParam(), // destination EID (sender)
878 0, // scope start (special value)
879 0 // scope end (special value)
880 );
881
882 // Add special segment [0,0] to request metadata
883 ✗ nakPdu.addSegment(0, 0);
884
885 ✗ status = this->m_engine->sendNak(this, nakPdu);
886 }
887
888 ✗ return status;
889 ✗ }
890
891 ✗ Status::T Transaction::r2CalcCrcChunk() {
892 U8 buf[R2CrcChunkSize];
893 FileSize count_bytes;
894 FileSize want_offs_size;
895 FwSizeType read_size;
896 Os::File::Status fileStatus;
897 ✗ Status::T ret = Cfdp::Status::SUCCESS;
898 ✗ FileSize rx_crc_calc_bytes_per_cycle = 0;
899
900 ✗ memset(buf, 0, sizeof(buf));
901
902 ✗ count_bytes = 0;
903
904 // Open file for CRC calculation if needed
905 ✗ if (ret == Cfdp::Status::SUCCESS) {
906 ✗ if (this->m_state_data.receive.r2.rx_crc_calc_bytes == 0) {
907 ✗ this->m_crc = CFDP::Checksum(0);
908
909 // For Class 2 RX, the file was opened in WRITE mode for receiving FileData PDUs.
910 // Now we need to READ it for CRC calculation. Close and reopen in READ mode.
911 ✗ if (this->m_fd.isOpen()) {
912 ✗ this->m_fd.close();
913 }
914
915 ✗ fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_READ);
916 ✗ if (fileStatus != Os::File::OP_OK) {
917 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
918 ✗ ret = Cfdp::Status::ERROR;
919 } else {
920 // Reset cached position since we just reopened the file
921 ✗ this->m_state_data.receive.cached_pos = 0;
922 }
923 }
924 }
925
926 // Process file in chunks
927 ✗ if (ret == Cfdp::Status::SUCCESS) {
928 ✗ rx_crc_calc_bytes_per_cycle = this->m_cfdpManager->getRxCrcCalcBytesPerCycleParam();
929
930 ✗ while ((ret == Cfdp::Status::SUCCESS) && (count_bytes < rx_crc_calc_bytes_per_cycle) &&
931 ✗ (this->m_state_data.receive.r2.rx_crc_calc_bytes < this->m_fsize)) {
932 ✗ want_offs_size = this->m_state_data.receive.r2.rx_crc_calc_bytes + static_cast<FileSize>(sizeof(buf));
933
934 ✗ if (want_offs_size > this->m_fsize) {
935 ✗ read_size = this->m_fsize - this->m_state_data.receive.r2.rx_crc_calc_bytes;
936 } else {
937 ✗ read_size = sizeof(buf);
938 }
939
940 ✗ if (this->m_state_data.receive.cached_pos != this->m_state_data.receive.r2.rx_crc_calc_bytes) {
941 fileStatus =
942 ✗ this->m_fd.seek(this->m_state_data.receive.r2.rx_crc_calc_bytes, Os::File::SeekType::ABSOLUTE);
943 ✗ if (fileStatus != Os::File::OP_OK) {
944 ✗ this->m_cfdpManager->log_WARNING_LO_RxSeekCrcFailed(
945 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
946 this->m_state_data.receive.r2.rx_crc_calc_bytes, fileStatus);
947 // this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
948 ✗ this->m_cfdpManager->incrementFaultFileSeek(this->m_chan_num);
949 ✗ ret = Cfdp::Status::ERROR;
950 }
951 }
952
953 ✗ if (ret == Cfdp::Status::SUCCESS) {
954 ✗ FwSizeType expected_read_size = read_size;
955 ✗ fileStatus = this->m_fd.read(buf, read_size, Os::File::WaitType::WAIT);
956 ✗ if ((fileStatus != Os::File::OP_OK) || (read_size != expected_read_size)) {
957 ✗ this->m_cfdpManager->log_WARNING_LO_RxReadCrcFailed(
958 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
959 static_cast<U32>(expected_read_size), static_cast<I32>(read_size));
960 ✗ this->m_engine->setTxnStatus(this, TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
961 ✗ this->m_cfdpManager->incrementFaultFileRead(this->m_chan_num);
962 ✗ ret = Cfdp::Status::ERROR;
963 } else {
964 ✗ this->m_crc.update(buf, this->m_state_data.receive.r2.rx_crc_calc_bytes,
965 static_cast<U32>(read_size));
966 ✗ this->m_state_data.receive.r2.rx_crc_calc_bytes += static_cast<FileSize>(read_size);
967 ✗ this->m_state_data.receive.cached_pos = this->m_state_data.receive.r2.rx_crc_calc_bytes;
968 ✗ count_bytes += static_cast<FileSize>(read_size);
969
970 // Reset inactivity timer to indicate transaction is actively processing
971 ✗ this->m_engine->armInactTimer(this);
972 }
973 }
974 }
975 }
976
977 // Check final CRC if all bytes processed
978 ✗ if (ret == Cfdp::Status::SUCCESS) {
979 ✗ if (this->m_state_data.receive.r2.rx_crc_calc_bytes == this->m_fsize) {
980 /* all bytes calculated, so now check */
981 ✗ if (this->rCheckCrc(this->m_state_data.receive.r2.eof_crc) == Cfdp::Status::SUCCESS) {
982 /* CRC matched! We are happy */
983 ✗ this->m_keep = Cfdp::Keep::KEEP; /* save the file */
984
985 /* set FIN PDU status */
986 ✗ this->m_state_data.receive.r2.dc = FinDeliveryCode::FIN_DELIVERY_CODE_COMPLETE;
987 ✗ this->m_state_data.receive.r2.fs = FinFileStatus::FIN_FILE_STATUS_RETAINED;
988 } else {
989 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILE_CHECKSUM_FAILURE);
990 }
991
992 ✗ this->m_flags.com.crc_calc = true;
993 } else {
994 // Not all bytes processed yet, return ERROR to signal need to continue
995 ✗ ret = Cfdp::Status::ERROR;
996 }
997 }
998
999 ✗ return ret;
1000 }
1001
1002 ✗ Status::T Transaction::r2SubstateSendFin() {
1003 Status::T sret;
1004 ✗ Status::T ret = Cfdp::Status::SUCCESS;
1005
1006 ✗ if (!TxnStatusIsError(this->m_history->txn_stat) && !this->m_flags.com.crc_calc) {
1007 /* no error, and haven't checked CRC -- so start checking it */
1008 ✗ if (this->r2CalcCrcChunk()) {
1009 ✗ ret = Cfdp::Status::ERROR; /* signal to caller to re-enter next tick */
1010 }
1011 }
1012
1013 ✗ if (ret != Cfdp::Status::ERROR) {
1014 ✗ sret = this->m_engine->sendFin(this, this->m_state_data.receive.r2.dc, this->m_state_data.receive.r2.fs,
1015 ✗ static_cast<ConditionCode>(TxnStatusToConditionCode(this->m_history->txn_stat)));
1016
1017 /* Serialization error already logged in serializeAndSendPdu if ERROR returned */
1018 ✗ this->m_state_data.receive.sub_state =
1019 RxSubState::RX_SUB_STATE_CLOSEOUT_SYNC; /* whether or not FIN send successful, ok to transition state */
1020 ✗ if (sret != Cfdp::Status::SUCCESS) {
1021 ✗ ret = Cfdp::Status::ERROR;
1022 }
1023 }
1024
1025 /* if no message, then try again next time */
1026 ✗ return ret;
1027 }
1028
1029 ✗ void Transaction::r2RecvFinAck(const Fw::Buffer& buffer) {
1030 // Deserialize ACK PDU from buffer
1031 ✗ AckPdu ack;
1032 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1033 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1034 ✗ sb.setBuffLen(buffer.getSize());
1035
1036 ✗ Fw::SerializeStatus deserStatus = ack.deserializeFrom(sb);
1037 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
1038 // Bad ACK PDU
1039 ✗ this->m_cfdpManager->log_WARNING_LO_FailAckPduDeserialization(this->getChannelId(),
1040 static_cast<I32>(deserStatus));
1041 ✗ this->m_cfdpManager->incrementRecvErrors(this->m_chan_num);
1042 ✗ return;
1043 }
1044
1045 // ACK PDU has been validated during deserialization
1046 // Got fin-ack, so time to close the state
1047 ✗ this->r2Reset();
1048 ✗ }
1049
1050 ✗ void Transaction::r2RecvMd(const Fw::Buffer& buffer) {
1051 ✗ Fw::String fname;
1052 Os::File::Status fileStatus;
1053 Os::FileSystem::Status fileSysStatus;
1054 ✗ bool success = true;
1055
1056 /* it isn't an error to get another MD PDU, right? */
1057 ✗ if (!this->m_flags.rx.md_recv) {
1058 /* NOTE: this->m_flags.rx.md_recv always 1 in R1, so this is R2 only */
1059 /* parse the md PDU. this will overwrite the transaction's history, which contains our filename. so let's
1060 * save the filename in a local buffer so it can be used with moveFile upon successful parsing of
1061 * the md PDU */
1062 ✗ fname = this->m_history->fnames.dst_filename;
1063
1064 // Deserialize Metadata PDU from buffer
1065 ✗ MetadataPdu md;
1066 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1067 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1068 ✗ sb.setBuffLen(buffer.getSize());
1069
1070 ✗ Fw::SerializeStatus deserStatus = md.deserializeFrom(sb);
1071 ✗ if (deserStatus != Fw::FW_SERIALIZE_OK) {
1072 // Bad metadata PDU
1073 ✗ this->m_cfdpManager->log_WARNING_LO_FailMetadataPduDeserialization(this->getChannelId(),
1074 static_cast<I32>(deserStatus));
1075 ✗ return;
1076 }
1077
1078 // PDU validation already done during deserialization
1079 ✗ this->m_engine->recvMd(this, md);
1080
1081 /* successfully obtained md PDU */
1082 ✗ if (this->m_flags.rx.eof_recv) {
1083 /* EOF was received, so check that md and EOF sizes match */
1084 ✗ if (this->m_state_data.receive.r2.eof_size != this->m_fsize) {
1085 ✗ this->m_cfdpManager->log_WARNING_LO_RxEofMdSizeMismatch(this->getClass(), this->m_history->src_eid,
1086 ✗ this->m_history->seq_num, this->m_fsize,
1087 this->m_state_data.receive.r2.eof_size);
1088 ✗ this->m_cfdpManager->incrementFaultFileSizeMismatch(this->m_chan_num);
1089 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILE_SIZE_ERROR);
1090 ✗ success = false;
1091 }
1092 }
1093
1094 ✗ if (success) {
1095 /* close and rename file */
1096 ✗ this->m_fd.close();
1097
1098 ✗ fileSysStatus = Os::FileSystem::moveFile(fname.toChar(), this->m_history->fnames.dst_filename.toChar());
1099 ✗ if (fileSysStatus != Os::FileSystem::OP_OK) {
1100 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileRenameFailed(
1101 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num, fname,
1102 ✗ this->m_history->fnames.dst_filename, fileSysStatus);
1103 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
1104 ✗ this->m_cfdpManager->incrementFaultFileRename(this->m_chan_num);
1105 ✗ success = false;
1106 } else {
1107 // File was successfully renamed, open for writing
1108 ✗ fileStatus = this->m_fd.open(this->m_history->fnames.dst_filename.toChar(), Os::File::OPEN_WRITE);
1109 ✗ if (fileStatus != Os::File::OP_OK) {
1110 ✗ this->m_cfdpManager->log_WARNING_LO_RxFileReopenFailed(
1111 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num,
1112 ✗ this->m_history->fnames.dst_filename, fileStatus);
1113 ✗ this->r2SetFinTxnStatus(TxnStatus::TXN_STATUS_FILESTORE_REJECTION);
1114 ✗ this->m_cfdpManager->incrementFaultFileOpen(this->m_chan_num);
1115 ✗ success = false;
1116 }
1117 }
1118
1119 ✗ if (success) {
1120 ✗ this->m_state_data.receive.cached_pos = 0; /* reset psn due to open */
1121 ✗ this->m_flags.rx.md_recv = true;
1122 ✗ this->m_state_data.receive.r2.acknak_count = 0; /* in case part of NAK */
1123 ✗ this->r2Complete(true); /* check for completion now that md is received */
1124 }
1125 }
1126 ✗ }
1127 ✗ }
1128
1129 ✗ void Transaction::rSendInactivityEvent() {
1130 ✗ this->m_cfdpManager->log_WARNING_LO_RxInactivityTimeout(this->getClass(), this->m_history->src_eid,
1131 ✗ this->m_history->seq_num);
1132 ✗ this->m_cfdpManager->incrementFaultInactivityTimer(this->m_chan_num);
1133 ✗ }
1134
1135 // ======================================================================
1136 // Dispatch Methods
1137 // ======================================================================
1138
1139 ✗ void Transaction::rDispatchRecv(const Fw::Buffer& buffer, const RSubstateDispatchTable* dispatch, StateRecvFunc fd_fn) {
1140 StateRecvFunc selected_handler;
1141
1142 ✗ FW_ASSERT(this->m_state_data.receive.sub_state < RxSubState::RX_SUB_STATE_NUM_STATES,
1143 static_cast<U8>(this->m_state_data.receive.sub_state),
1144 static_cast<U8>(RxSubState::RX_SUB_STATE_NUM_STATES));
1145
1146 ✗ selected_handler = nullptr;
1147
1148 // Peek at PDU type from buffer
1149 ✗ Cfdp::PduTypeEnum::T pduType = Cfdp::peekPduType(buffer);
1150
1151 // Special handling for file data PDU
1152 ✗ if (pduType == Cfdp::PduTypeEnum::FILE_DATA) {
1153 /* For file data PDU, use the provided fd_fn */
1154 ✗ if (!TxnStatusIsError(this->m_history->txn_stat)) {
1155 ✗ selected_handler = fd_fn;
1156 }
1157 } else {
1158 // Not a file-data PDU - parse as a directive PDU to get the directive code.
1159 // const_cast: Fw::SerialBuffer requires non-const U8* even for deserialization (read-only)
1160 ✗ Fw::SerialBuffer sb(const_cast<U8*>(buffer.getData()), buffer.getSize());
1161 ✗ sb.setBuffLen(buffer.getSize());
1162
1163 Cfdp::PduHeader header;
1164 ✗ if (header.fromSerialBuffer(sb) == Fw::FW_SERIALIZE_OK) {
1165 // Read directive code (first byte after header for directive PDUs)
1166 U8 directiveCodeByte;
1167 ✗ if (sb.deserializeTo(directiveCodeByte) == Fw::FW_SERIALIZE_OK) {
1168 ✗ FileDirective directiveCode = static_cast<FileDirective>(directiveCodeByte);
1169
1170 ✗ if (directiveCode < FileDirective::FILE_DIRECTIVE_INVALID_MAX) {
1171 /* The CFDP_R_SubstateDispatchTable_t is only used with file directive PDU */
1172 ✗ if (dispatch->state[static_cast<U32>(this->m_state_data.receive.sub_state)] != nullptr) {
1173 ✗ selected_handler = dispatch->state[static_cast<U32>(this->m_state_data.receive.sub_state)]
1174 ✗ ->fdirective[static_cast<U32>(directiveCode)];
1175 }
1176 } else {
1177 ✗ this->m_cfdpManager->incrementRecvSpurious(this->m_chan_num);
1178 ✗ this->m_cfdpManager->log_WARNING_LO_RxInvalidDirectiveCode(
1179 ✗ this->getClass(), this->m_history->src_eid, this->m_history->seq_num, directiveCodeByte,
1180 ✗ static_cast<U8>(this->m_state_data.receive.sub_state));
1181 }
1182 }
1183 }
1184 ✗ }
1185
1186 /*
1187 * NOTE: if no handler is selected, this will drop packets on the floor here.
1188 */
1189 ✗ if (selected_handler != nullptr) {
1190 ✗ (this->*selected_handler)(buffer);
1191 } else {
1192 ✗ this->m_cfdpManager->incrementRecvDropped(this->m_chan_num);
1193 }
1194 ✗ }
1195
1196 } // namespace Cfdp
1197 } // namespace Ccsds
1198 } // namespace Svc
1199