Skip to content

Commit 1781cf6

Browse files
authored
Merge pull request #45 from us-irs/dest-handler-fixes-eof-cancel-handling
fixes for EOF cancel handling
2 parents 6144327 + 7f19a03 commit 1781cf6

5 files changed

Lines changed: 130 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919

2020
- Replaced `*Cfg*` abbreviation with `*Config*`
2121

22+
## Fixed
23+
24+
- Corrections for EOF (Cancel) Handling: Perform proper checks on whether the file is actually
25+
completed using the supplied file size and checksum.
26+
2227
# [v0.5.1] 2025-02-10
2328

2429
- Bump allowed `spacepackets` to v0.28.0

src/cfdppy/handler/dest.py

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ def _eof_ack_pdu_done(self) -> None:
616616
):
617617
self._start_deferred_lost_segment_handling()
618618
return
619-
self._checksum_verify()
619+
assert self._params.fp.crc32 is not None
620620
self.states.step = TransactionStep.TRANSFER_COMPLETION
621621

622622
def _start_transaction_first_packet_file_data(self, fd_pdu: FileDataPdu) -> None:
@@ -902,7 +902,13 @@ def _deferred_lost_segment_handling(self) -> None:
902902
and not self._params.acked_params.metadata_missing
903903
):
904904
# We are done and have received everything.
905-
self._checksum_verify()
905+
assert self._params.fp.crc32 is not None
906+
if self._checksum_verify(self._params.fp.progress, self._params.fp.crc32):
907+
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
908+
self._params.finished_params.condition_code = ConditionCode.NO_ERROR
909+
else:
910+
self._params.finished_params.delivery_code = DeliveryCode.DATA_INCOMPLETE
911+
self._params.finished_params.condition_code = ConditionCode.FILE_CHECKSUM_FAILURE
906912
self.states.step = TransactionStep.TRANSFER_COMPLETION
907913
self._params.acked_params.deferred_lost_segment_detection_active = False
908914
return
@@ -923,6 +929,7 @@ def _deferred_lost_segment_handling(self) -> None:
923929
and self._params.acked_params.nak_activity_counter + 1
924930
== self._params.remote_cfg.nak_timer_expiration_limit
925931
):
932+
self._params.finished_params.delivery_code = DeliveryCode.DATA_INCOMPLETE
926933
self._declare_fault(ConditionCode.NAK_LIMIT_REACHED)
927934
return
928935
# This is not the first NAK issuance and the timer expired.
@@ -983,8 +990,18 @@ def _handle_cancel_eof(self, eof_pdu: EofPdu) -> None:
983990
eof_pdu.condition_code,
984991
EntityIdTlv(self._params.remote_cfg.entity_id.as_bytes),
985992
)
986-
# Store this as progress for the checksum calculation.
987-
self._params.fp.progress = self._params.fp.file_size
993+
# Store this as progress for the checksum calculation as well.
994+
self._params.fp.progress = eof_pdu.file_size
995+
if self._params.acked_params.metadata_missing:
996+
self._params.finished_params.delivery_code = DeliveryCode.DATA_INCOMPLETE
997+
return
998+
if self._params.fp.file_size == 0:
999+
# Empty file, no file data PDU.
1000+
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
1001+
return
1002+
if self._checksum_verify(self._params.fp.progress, self._params.fp.crc32):
1003+
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
1004+
return
9881005
self._params.finished_params.delivery_code = DeliveryCode.DATA_INCOMPLETE
9891006

9901007
def _handle_no_error_eof(self) -> bool:
@@ -1004,15 +1021,12 @@ def _handle_no_error_eof(self) -> bool:
10041021
)
10051022
if (
10061023
self.transmission_mode == TransmissionMode.UNACKNOWLEDGED
1007-
and not self._checksum_verify()
1024+
and not self._checksum_verify(self._params.fp.progress, self._params.fp.crc32) # type: ignore
10081025
):
1009-
if (
1010-
self._declare_fault(ConditionCode.FILE_CHECKSUM_FAILURE)
1011-
!= FaultHandlerCode.IGNORE_ERROR
1012-
):
1013-
return False
10141026
self._start_check_limit_handling()
10151027
return False
1028+
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
1029+
self._params.finished_params.condition_code = ConditionCode.NO_ERROR
10161030
return True
10171031

10181032
def _start_deferred_lost_segment_handling(self) -> None:
@@ -1035,27 +1049,21 @@ def _prepare_eof_ack_packet(self) -> None:
10351049
)
10361050
self._add_packet_to_be_sent(ack_pdu)
10371051

1038-
def _checksum_verify(self) -> bool:
1039-
file_delivery_complete = False
1052+
def _checksum_verify(self, verify_len: int, expected_crc32: bytes) -> bool:
10401053
if (
10411054
self._params.checksum_type == ChecksumType.NULL_CHECKSUM
10421055
or self._params.fp.metadata_only
10431056
):
1044-
file_delivery_complete = True
1045-
else:
1046-
crc32 = self.user.vfs.calculate_checksum(
1047-
self._params.checksum_type,
1048-
self._params.fp.file_name,
1049-
self._params.fp.progress,
1050-
)
1051-
if crc32 == self._params.fp.crc32:
1052-
file_delivery_complete = True
1053-
else:
1054-
self._declare_fault(ConditionCode.FILE_CHECKSUM_FAILURE)
1055-
if file_delivery_complete:
1056-
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
1057-
self._params.finished_params.condition_code = ConditionCode.NO_ERROR
1058-
return file_delivery_complete
1057+
return True
1058+
crc32 = self.user.vfs.calculate_checksum(
1059+
self._params.checksum_type,
1060+
self._params.fp.file_name,
1061+
verify_len,
1062+
)
1063+
if crc32 == expected_crc32:
1064+
return True
1065+
self._declare_fault(ConditionCode.FILE_CHECKSUM_FAILURE)
1066+
return False
10591067

10601068
def _file_transfer_complete_transition(self) -> None:
10611069
if self.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
@@ -1126,8 +1134,11 @@ def _add_packet_to_be_sent(self, packet: GenericPduPacket) -> None:
11261134
def _check_limit_handling(self) -> None:
11271135
assert self._params.check_timer is not None
11281136
assert self._params.remote_cfg is not None
1137+
assert self._params.fp.crc32 is not None
11291138
if self._params.check_timer.timed_out():
1130-
if self._checksum_verify():
1139+
if self._checksum_verify(self._params.fp.progress, self._params.fp.crc32):
1140+
self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
1141+
self._params.finished_params.condition_code = ConditionCode.NO_ERROR
11311142
self._file_transfer_complete_transition()
11321143
return
11331144
if self._params.current_check_count + 1 >= self._params.remote_cfg.check_limit:

tests/test_dest_handler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def _generic_finished_pdu_check_acked(
264264
expected_condition_code: ConditionCode,
265265
expected_file_status: FileStatus = FileStatus.FILE_RETAINED,
266266
expected_fault_location: EntityIdTlv | None = None,
267+
empty_file: bool = False,
267268
) -> FinishedPdu:
268269
return self._generic_finished_pdu_check(
269270
fsm_res,
@@ -272,6 +273,7 @@ def _generic_finished_pdu_check_acked(
272273
expected_file_status,
273274
expected_condition_code,
274275
expected_fault_location=expected_fault_location,
276+
empty_file=empty_file,
275277
)
276278

277279
def _generic_no_error_finished_pdu_check_acked(
@@ -308,6 +310,7 @@ def _generic_finished_pdu_check(
308310
expected_file_status: FileStatus = FileStatus.FILE_RETAINED,
309311
expected_condition_code: ConditionCode = ConditionCode.NO_ERROR,
310312
expected_fault_location: EntityIdTlv | None = None,
313+
empty_file: bool = False,
311314
) -> FinishedPdu:
312315
self._state_checker(fsm_res, 1, expected_state, expected_step)
313316
self.assertTrue(fsm_res.states.packets_ready)
@@ -319,7 +322,7 @@ def _generic_finished_pdu_check(
319322
finished_pdu = next_pdu.to_finished_pdu()
320323
self.assertEqual(finished_pdu.condition_code, expected_condition_code)
321324
self.assertEqual(finished_pdu.file_status, expected_file_status)
322-
if expected_condition_code == ConditionCode.NO_ERROR:
325+
if expected_condition_code == ConditionCode.NO_ERROR or empty_file:
323326
self.assertEqual(finished_pdu.delivery_code, DeliveryCode.DATA_COMPLETE)
324327
else:
325328
self.assertEqual(finished_pdu.delivery_code, DeliveryCode.DATA_INCOMPLETE)

tests/test_dest_handler_acked.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,53 @@ def test_acked_small_file_transfer(self):
5757
self._generic_verify_transfer_completion(fsm_res, file_content)
5858
self._generic_insert_finished_pdu_ack(finished_pdu)
5959

60+
def test_cancelled_file_transfer_empty(self):
61+
# Basic acknowledged empty file transfer.
62+
self._generic_regular_transfer_init(0)
63+
# Cancel the transfer by sending an EOF PDU with the appropriate parameters.
64+
eof_pdu = EofPdu(
65+
file_size=0,
66+
file_checksum=NULL_CHECKSUM_U32,
67+
pdu_conf=self.src_pdu_conf,
68+
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
69+
)
70+
fsm_res = self.dest_handler.state_machine(eof_pdu)
71+
# Should contain an ACK PDU now.
72+
self._generic_verify_eof_ack_packet(
73+
fsm_res,
74+
TransactionStep.WAITING_FOR_FINISHED_ACK,
75+
condition_code_of_acked_pdu=ConditionCode.CANCEL_REQUEST_RECEIVED,
76+
)
77+
finished_pdu = self._generic_finished_pdu_check_acked(
78+
fsm_res,
79+
expected_condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
80+
expected_fault_location=EntityIdTlv(self.src_entity_id.as_bytes),
81+
empty_file=True,
82+
)
83+
# Complete, because this is just an empty file.
84+
self._generic_verify_transfer_completion(
85+
fsm_res,
86+
expected_file_data=None,
87+
expected_finished_params=FinishedParams(
88+
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
89+
delivery_code=DeliveryCode.DATA_COMPLETE,
90+
file_status=FileStatus.FILE_RETAINED,
91+
fault_location=EntityIdTlv(self.src_entity_id.as_bytes),
92+
),
93+
)
94+
self._generic_insert_finished_pdu_ack(finished_pdu)
95+
6096
def test_cancelled_file_transfer(self):
6197
file_content = b"Hello World!"
6298
with open(self.src_file_path, "wb") as of:
6399
of.write(file_content)
100+
crc32 = fastcrc.crc32.iso_hdlc(file_content)
64101
# Basic acknowledged empty file transfer.
65102
self._generic_regular_transfer_init(len(file_content))
66103
# Cancel the transfer by sending an EOF PDU with the appropriate parameters.
67104
eof_pdu = EofPdu(
68-
file_size=0,
69-
file_checksum=NULL_CHECKSUM_U32,
105+
file_size=len(file_content),
106+
file_checksum=crc32,
70107
pdu_conf=self.src_pdu_conf,
71108
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
72109
)
@@ -77,7 +114,6 @@ def test_cancelled_file_transfer(self):
77114
TransactionStep.WAITING_FOR_FINISHED_ACK,
78115
condition_code_of_acked_pdu=ConditionCode.CANCEL_REQUEST_RECEIVED,
79116
)
80-
# fsm_res = self.dest_handler.state_machine()
81117
finished_pdu = self._generic_finished_pdu_check_acked(
82118
fsm_res,
83119
expected_condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,

tests/test_dest_handler_naked.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,58 @@ def test_check_timer_mechanism(self):
150150
TransactionStep.IDLE,
151151
)
152152

153-
def test_cancelled_transfer_via_eof_pdu(self):
153+
def test_cancelled_transfer_via_eof_pdu_complete(self):
154154
data = b"Hello World\n"
155155
with open(self.src_file_path, "wb") as of:
156156
of.write(data)
157157
file_size = self.src_file_path.stat().st_size
158+
crc32 = struct.pack("!I", fastcrc.crc32.iso_hdlc(data))
158159
self._generic_regular_transfer_init(
159160
file_size=file_size,
160161
)
161162
self._insert_file_segment(segment=data, offset=0)
162163
# Cancel the transfer by sending an EOF PDU with the appropriate parameters.
163164
eof_pdu = EofPdu(
164-
file_size=0,
165-
file_checksum=NULL_CHECKSUM_U32,
165+
file_size=len(data),
166+
file_checksum=crc32,
167+
pdu_conf=self.src_pdu_conf,
168+
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
169+
)
170+
fsm_res = self.dest_handler.state_machine(eof_pdu)
171+
self._generic_eof_recv_indication_check(fsm_res)
172+
if self.closure_requested:
173+
self._generic_finished_pdu_check(
174+
fsm_res,
175+
expected_state=CfdpState.IDLE,
176+
expected_step=TransactionStep.IDLE,
177+
expected_condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
178+
expected_fault_location=EntityIdTlv(self.src_entity_id.as_bytes),
179+
)
180+
# The data is still complete, checksum was verified successfully.
181+
self._generic_verify_transfer_completion(
182+
fsm_res,
183+
expected_file_data=None,
184+
expected_finished_params=FinishedParams(
185+
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
186+
delivery_code=DeliveryCode.DATA_COMPLETE,
187+
file_status=FileStatus.FILE_RETAINED,
188+
fault_location=EntityIdTlv(self.src_entity_id.as_bytes),
189+
),
190+
)
191+
192+
def test_cancelled_transfer_via_eof_pdu_incomplete(self):
193+
data = b"Hello World\n"
194+
with open(self.src_file_path, "wb") as of:
195+
of.write(data)
196+
file_size = self.src_file_path.stat().st_size
197+
crc32 = struct.pack("!I", fastcrc.crc32.iso_hdlc(data))
198+
self._generic_regular_transfer_init(
199+
file_size=file_size,
200+
)
201+
# Cancel the transfer by sending an EOF PDU with the appropriate parameters.
202+
eof_pdu = EofPdu(
203+
file_size=len(data),
204+
file_checksum=crc32,
166205
pdu_conf=self.src_pdu_conf,
167206
condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
168207
)
@@ -176,6 +215,7 @@ def test_cancelled_transfer_via_eof_pdu(self):
176215
expected_condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
177216
expected_fault_location=EntityIdTlv(self.src_entity_id.as_bytes),
178217
)
218+
# Data segment missing, checksum fails, data incomplete.
179219
self._generic_verify_transfer_completion(
180220
fsm_res,
181221
expected_file_data=None,

0 commit comments

Comments
 (0)