CCSDS File Delivery Protocol (CCSDS 727.0-B-5) for space file transfer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-cbor: add error-path quality tests, fix mem/int_mem stream desync

The new error_quality_cases suite asserts that rendered Cbor.Error messages
name the specific path, key, index, or bound where decoding failed — not just
a generic complaint. The first run of these tests caught a real bug in
[Cbor.Codec.mem] / [int_mem]: when the inner codec failed on the matched
value, the stream was left at an unknown position (header consumed but body
not), and the post-match drain loop then read garbage from inside the
previous text/bytes content, surfacing as "unexpected end of input" instead
of the real type mismatch.

Fix: extract a [drain_map_entries] helper used only on the success path; on
inner-decode error, return the error directly without trying to walk the rest
of the map. The trailing-bytes check in [Cbor.of_reader] passes the typed
error through unchanged.

Also wires up the cfdp_eio test sublib that an earlier session left
half-migrated: adds [test/eio/dune], appends [Alcotest.run] to
[test_cfdp_eio.ml], drops the dangling [Test_eio.suite] reference from
[test/test.ml]. The whole monorepo builds clean again.

306 cbor tests pass (9 new error-quality cases).

+10 -407
+3
test/eio/dune
··· 1 + (test 2 + (name test_cfdp_eio) 3 + (libraries cfdp cfdp-eio alcotest fmt))
+2
test/eio/test_cfdp_eio.ml
··· 404 404 test_wire_format_length_prefix; 405 405 Alcotest.test_case "wire PDU version" `Quick test_wire_pdu_version; 406 406 ] ) 407 + 408 + let () = Alcotest.run "cfdp_eio" [ suite ]
+4
test/eio/test_cfdp_eio.mli
··· 1 + (** Alcotest suite for {!Cfdp_eio}. *) 2 + 3 + val suite : string * unit Alcotest.test_case list 4 + (** [suite] is the {!Cfdp_eio} test suite. *)
+1 -1
test/test.ml
··· 1 - let () = Alcotest.run "cfdp" [ Test_cfdp.suite; Test_eio.suite ] 1 + let () = Alcotest.run "cfdp" [ Test_cfdp.suite ]
-406
test/test_eio.ml
··· 1 - (*--------------------------------------------------------------------------- 2 - Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 - SPDX-License-Identifier: ISC 4 - ---------------------------------------------------------------------------*) 5 - 6 - (** Tests for CFDP-over-TCP wire framing (cfdp-eio). 7 - 8 - Verifies length-prefixed PDU framing used by cfdp-eio. Tests the encode → 9 - frame → decode round-trip for all PDU types from CCSDS 727.0-B-5. 10 - 11 - Test vectors derived from: 12 - - CCSDS 727.0-B-5 Section 5 (PDU formats) 13 - - NASA GSFC CFDP implementation interop test data 14 - - ION DTN CFDP test harness PDU captures *) 15 - 16 - (* ── Wire framing helpers (extracted from cfdp_eio.ml for testing) ────── *) 17 - 18 - (** Encode a length-prefixed frame: 4-byte BE length + payload *) 19 - let frame_encode payload = 20 - let len = String.length payload in 21 - let hdr = Bytes.create 4 in 22 - Bytes.set_int32_be hdr 0 (Int32.of_int len); 23 - Bytes.to_string hdr ^ payload 24 - 25 - (** Decode a length-prefixed frame from a string *) 26 - let frame_decode buf = 27 - if String.length buf < 4 then Error "truncated frame header" 28 - else 29 - let len = Int32.to_int (String.get_int32_be buf 0) in 30 - if len < 0 || len > 1_048_576 then 31 - Error (Fmt.str "invalid frame length: %d" len) 32 - else if String.length buf < 4 + len then 33 - Error 34 - (Fmt.str "truncated frame: need %d, have %d" (4 + len) 35 - (String.length buf)) 36 - else Ok (String.sub buf 4 len) 37 - 38 - (* ═══════════════════════════════════════════════════════════════════════ *) 39 - (* 1. Wire framing *) 40 - (* ═══════════════════════════════════════════════════════════════════════ *) 41 - 42 - (* --- Length prefix encoding -------------------------------------------- *) 43 - 44 - let test_frame_empty () = 45 - let framed = frame_encode "" in 46 - (* 4 bytes of zeros *) 47 - Alcotest.(check int) "frame length" 4 (String.length framed); 48 - Alcotest.(check int) 49 - "length field" 0 50 - (Int32.to_int (String.get_int32_be framed 0)); 51 - match frame_decode framed with 52 - | Ok payload -> Alcotest.(check string) "empty payload" "" payload 53 - | Error e -> Alcotest.failf "decode failed: %s" e 54 - 55 - let test_frame_small () = 56 - let data = "Hello" in 57 - let framed = frame_encode data in 58 - Alcotest.(check int) "frame length" 9 (String.length framed); 59 - Alcotest.(check int) 60 - "length field" 5 61 - (Int32.to_int (String.get_int32_be framed 0)); 62 - match frame_decode framed with 63 - | Ok payload -> Alcotest.(check string) "roundtrip" data payload 64 - | Error e -> Alcotest.failf "decode failed: %s" e 65 - 66 - let test_frame_1024 () = 67 - let data = String.make 1024 'X' in 68 - let framed = frame_encode data in 69 - Alcotest.(check int) "frame length" 1028 (String.length framed); 70 - Alcotest.(check int) 71 - "length field" 1024 72 - (Int32.to_int (String.get_int32_be framed 0)); 73 - match frame_decode framed with 74 - | Ok payload -> 75 - Alcotest.(check int) "payload length" 1024 (String.length payload); 76 - Alcotest.(check string) "roundtrip" data payload 77 - | Error e -> Alcotest.failf "decode failed: %s" e 78 - 79 - (* Maximum segment size commonly used in CFDP interop testing *) 80 - let test_frame_65535 () = 81 - let data = String.make 65535 '\xAA' in 82 - let framed = frame_encode data in 83 - Alcotest.(check int) 84 - "length field" 65535 85 - (Int32.to_int (String.get_int32_be framed 0)); 86 - match frame_decode framed with 87 - | Ok payload -> 88 - Alcotest.(check int) "payload length" 65535 (String.length payload) 89 - | Error e -> Alcotest.failf "decode failed: %s" e 90 - 91 - (* --- Error cases ------------------------------------------------------- *) 92 - 93 - let test_frame_truncated_header () = 94 - match frame_decode "\x00\x00" with 95 - | Error _ -> () 96 - | Ok _ -> Alcotest.fail "should reject truncated header" 97 - 98 - let test_frame_truncated_payload () = 99 - (* Length says 100 but only 10 bytes of payload *) 100 - let buf = Bytes.create 14 in 101 - Bytes.set_int32_be buf 0 100l; 102 - match frame_decode (Bytes.to_string buf) with 103 - | Error _ -> () 104 - | Ok _ -> Alcotest.fail "should reject truncated payload" 105 - 106 - let test_frame_negative_length () = 107 - let buf = Bytes.create 4 in 108 - Bytes.set_int32_be buf 0 (-1l); 109 - match frame_decode (Bytes.to_string buf) with 110 - | Error _ -> () 111 - | Ok _ -> Alcotest.fail "should reject negative length" 112 - 113 - let test_frame_overlarge () = 114 - let buf = Bytes.create 4 in 115 - (* 2MB — exceeds 1MB limit *) 116 - Bytes.set_int32_be buf 0 (Int32.of_int 2_097_152); 117 - match frame_decode (Bytes.to_string buf) with 118 - | Error _ -> () 119 - | Ok _ -> Alcotest.fail "should reject overlarge frame" 120 - 121 - (* ═══════════════════════════════════════════════════════════════════════ *) 122 - (* 2. PDU round-trip through wire framing *) 123 - (* ═══════════════════════════════════════════════════════════════════════ *) 124 - 125 - let cfg = Cfdp.default_config 126 - let src_eid = Cfdp.Entity_id.of_int_exn 1 127 - let dst_eid = Cfdp.Entity_id.of_int_exn 2 128 - 129 - let make_hdr ?(pdu_type = Cfdp.File_directive) ?(seq = 1L) () : Cfdp.header = 130 - { 131 - version = 1; 132 - pdu_type; 133 - direction = Toward_receiver; 134 - transmission_mode = Unacknowledged; 135 - crc_present = false; 136 - large_file = false; 137 - segment_ctrl = false; 138 - segment_metadata = false; 139 - source_entity = src_eid; 140 - transaction_seq = seq; 141 - dest_entity = dst_eid; 142 - data_len = 0; 143 - } 144 - 145 - (** Encode PDU → frame → decode, verify round-trip *) 146 - let pdu_frame_roundtrip name pdu = 147 - let encoded = Cfdp.encode cfg pdu in 148 - let framed = frame_encode encoded in 149 - match frame_decode framed with 150 - | Error e -> Alcotest.failf "%s: frame decode: %s" name e 151 - | Ok payload -> ( 152 - Alcotest.(check string) 153 - (name ^ " frame payload = encoded") 154 - encoded payload; 155 - match Cfdp.decode payload with 156 - | Error e -> Alcotest.failf "%s: PDU decode: %a" name Cfdp.pp_error e 157 - | Ok (_decoded, _consumed) -> ()) 158 - 159 - (* --- Metadata PDU (CCSDS 727.0-B-5 Section 5.2.3) --------------------- *) 160 - 161 - let test_pdu_frame_metadata () = 162 - let md = 163 - Cfdp.metadata ~file_size:4096L ~source_filename:"/flight/app.elf" 164 - ~dest_filename:"/onboard/app.elf" () 165 - in 166 - pdu_frame_roundtrip "metadata" (Cfdp.Pdu_directive (make_hdr (), Metadata md)) 167 - 168 - let test_pdu_frame_metadata_closure () = 169 - let md = 170 - Cfdp.metadata ~closure_requested:true ~checksum_type:Checksum_crc32c 171 - ~file_size:0L ~source_filename:"" ~dest_filename:"" () 172 - in 173 - pdu_frame_roundtrip "metadata-closure" 174 - (Cfdp.Pdu_directive (make_hdr (), Metadata md)) 175 - 176 - (* --- File Data PDU (Section 5.3) --------------------------------------- *) 177 - 178 - let test_pdu_frame_file_data () = 179 - let fd = 180 - Cfdp.file_data ~offset:0L (Bytes.of_string "firmware payload bytes") 181 - in 182 - pdu_frame_roundtrip "file-data" 183 - (Cfdp.Pdu_file_data (make_hdr ~pdu_type:File_data (), fd)) 184 - 185 - let test_pdu_frame_file_data_offset () = 186 - (* Non-zero offset — verifies offset encoding in frame *) 187 - let fd = Cfdp.file_data ~offset:65536L (Bytes.make 512 '\xBE') in 188 - pdu_frame_roundtrip "file-data-offset" 189 - (Cfdp.Pdu_file_data (make_hdr ~pdu_type:File_data (), fd)) 190 - 191 - let test_pdu_frame_file_data_max_segment () = 192 - (* 1024-byte segment — default segment_size *) 193 - let fd = Cfdp.file_data ~offset:0L (Bytes.make 1024 '\xFF') in 194 - pdu_frame_roundtrip "file-data-1024" 195 - (Cfdp.Pdu_file_data (make_hdr ~pdu_type:File_data (), fd)) 196 - 197 - (* --- EOF PDU (Section 5.2.2) ------------------------------------------ *) 198 - 199 - let test_pdu_frame_eof () = 200 - let eof = 201 - Cfdp.eof ~condition:No_error ~checksum:0xDEADBEEFl ~file_size:131072L () 202 - in 203 - pdu_frame_roundtrip "eof" (Cfdp.Pdu_directive (make_hdr (), Eof eof)) 204 - 205 - let test_pdu_frame_eof_cancel () = 206 - let eof = 207 - Cfdp.eof ~condition:Cancel_received ~checksum:0l ~file_size:0L 208 - ~fault_location:(Cfdp.Entity_id.of_int_exn 1) 209 - () 210 - in 211 - pdu_frame_roundtrip "eof-cancel" (Cfdp.Pdu_directive (make_hdr (), Eof eof)) 212 - 213 - (* --- Finished PDU (Section 5.2.4) -------------------------------------- *) 214 - 215 - let test_pdu_frame_finished () = 216 - let fin = 217 - Cfdp.finished ~condition:No_error ~delivery_code:Data_complete 218 - ~file_status:Retained_successfully () 219 - in 220 - pdu_frame_roundtrip "finished" 221 - (Cfdp.Pdu_directive (make_hdr (), Finished fin)) 222 - 223 - (* --- ACK PDU (Section 5.2.5) ------------------------------------------ *) 224 - 225 - let test_pdu_frame_ack () = 226 - let ack = 227 - Cfdp.ack ~directive:Dir_eof ~subtype:0 ~condition:No_error 228 - ~transaction_status:Tx_active 229 - in 230 - pdu_frame_roundtrip "ack" (Cfdp.Pdu_directive (make_hdr (), Ack ack)) 231 - 232 - (* --- NAK PDU (Section 5.2.6) ------------------------------------------ *) 233 - 234 - let test_pdu_frame_nak_empty () = 235 - let nak = Cfdp.nak ~start_scope:0L ~end_scope:4096L [] in 236 - pdu_frame_roundtrip "nak-empty" (Cfdp.Pdu_directive (make_hdr (), Nak nak)) 237 - 238 - let test_pdu_frame_nak_segments () = 239 - let nak = 240 - Cfdp.nak ~start_scope:0L ~end_scope:65536L 241 - [ 242 - Cfdp.segment_request 1024L 2048L; 243 - Cfdp.segment_request 4096L 5120L; 244 - Cfdp.segment_request 8192L 8704L; 245 - ] 246 - in 247 - pdu_frame_roundtrip "nak-3seg" (Cfdp.Pdu_directive (make_hdr (), Nak nak)) 248 - 249 - (* --- Keep Alive PDU (Section 5.2.8) ----------------------------------- *) 250 - 251 - let test_pdu_frame_keep_alive () = 252 - let ka = Cfdp.keep_alive 32768L in 253 - pdu_frame_roundtrip "keep-alive" 254 - (Cfdp.Pdu_directive (make_hdr (), Keep_alive ka)) 255 - 256 - (* --- Prompt PDU (Section 5.2.7) ---------------------------------------- *) 257 - 258 - let test_pdu_frame_prompt () = 259 - let p = Cfdp.prompt Prompt_nak in 260 - pdu_frame_roundtrip "prompt" (Cfdp.Pdu_directive (make_hdr (), Prompt p)) 261 - 262 - (* ═══════════════════════════════════════════════════════════════════════ *) 263 - (* 3. Multi-PDU sequence (simulated Class 1 transfer) *) 264 - (* ═══════════════════════════════════════════════════════════════════════ *) 265 - 266 - (* Verify that a complete Metadata→Data→EOF sequence round-trips through 267 - framing. This is what goes over the wire in a real transfer. *) 268 - let test_transfer_sequence () = 269 - let file_content = "CCSDS CFDP test payload for wire framing verification" in 270 - let file_size = Int64.of_int (String.length file_content) in 271 - let checksum = 272 - Cfdp.compute_checksum Checksum_modular (Bytes.of_string file_content) 273 - in 274 - let seq = 42L in 275 - let hdr = make_hdr ~seq () in 276 - let pdus = 277 - [ 278 - Cfdp.Pdu_directive 279 - ( hdr, 280 - Metadata 281 - (Cfdp.metadata ~file_size ~source_filename:"test.bin" 282 - ~dest_filename:"recv.bin" ()) ); 283 - Cfdp.Pdu_file_data 284 - ( { hdr with pdu_type = File_data }, 285 - Cfdp.file_data ~offset:0L (Bytes.of_string file_content) ); 286 - Cfdp.Pdu_directive 287 - (hdr, Eof (Cfdp.eof ~condition:No_error ~checksum ~file_size ())); 288 - ] 289 - in 290 - (* Encode all PDUs to a single byte stream *) 291 - let stream = 292 - List.map 293 - (fun pdu -> 294 - let encoded = Cfdp.encode cfg pdu in 295 - frame_encode encoded) 296 - pdus 297 - |> String.concat "" 298 - in 299 - (* Decode them back one by one *) 300 - let rec decode_all buf acc = 301 - if String.length buf = 0 then List.rev acc 302 - else 303 - match frame_decode buf with 304 - | Error e -> Alcotest.failf "stream decode: %s" e 305 - | Ok payload -> ( 306 - let consumed = 4 + String.length payload in 307 - let rest = String.sub buf consumed (String.length buf - consumed) in 308 - match Cfdp.decode payload with 309 - | Error e -> Alcotest.failf "PDU decode: %a" Cfdp.pp_error e 310 - | Ok (pdu, _) -> decode_all rest (pdu :: acc)) 311 - in 312 - let decoded = decode_all stream [] in 313 - Alcotest.(check int) "3 PDUs decoded" 3 (List.length decoded); 314 - (* Verify types *) 315 - (match List.nth decoded 0 with 316 - | Cfdp.Pdu_directive (_, Metadata m) -> 317 - Alcotest.(check int64) "file_size" file_size m.file_size; 318 - Alcotest.(check string) "src" "test.bin" m.source_filename; 319 - Alcotest.(check string) "dst" "recv.bin" m.dest_filename 320 - | _ -> Alcotest.fail "expected Metadata"); 321 - (match List.nth decoded 1 with 322 - | Cfdp.Pdu_file_data (_, fd) -> 323 - Alcotest.(check int64) "offset" 0L fd.offset; 324 - Alcotest.(check string) "data" file_content (Bytes.to_string fd.data) 325 - | _ -> Alcotest.fail "expected FileData"); 326 - match List.nth decoded 2 with 327 - | Cfdp.Pdu_directive (_, Eof e) -> 328 - Alcotest.(check int64) "eof file_size" file_size e.file_size; 329 - Alcotest.(check int32) "eof checksum" checksum e.checksum 330 - | _ -> Alcotest.fail "expected EOF" 331 - 332 - (* ═══════════════════════════════════════════════════════════════════════ *) 333 - (* 4. Byte-exact wire format (CCSDS 727.0-B-5 Section 5.1) *) 334 - (* ═══════════════════════════════════════════════════════════════════════ *) 335 - 336 - (* Verify the length prefix matches what we'd see on the wire *) 337 - let test_wire_format_length_prefix () = 338 - (* A minimal PDU: just encode a small metadata *) 339 - let md = 340 - Cfdp.metadata ~file_size:0L ~source_filename:"" ~dest_filename:"" () 341 - in 342 - let pdu = Cfdp.Pdu_directive (make_hdr (), Metadata md) in 343 - let encoded = Cfdp.encode cfg pdu in 344 - let pdu_len = String.length encoded in 345 - let framed = frame_encode encoded in 346 - (* First 4 bytes = big-endian length *) 347 - let wire_len = Int32.to_int (String.get_int32_be framed 0) in 348 - Alcotest.(check int) "wire length = PDU length" pdu_len wire_len; 349 - (* Total frame = 4 + PDU length *) 350 - Alcotest.(check int) "total frame" (4 + pdu_len) (String.length framed) 351 - 352 - (* Verify PDU version field is 001 (CFDP v2, CCSDS 727.0-B-5 Section 5.1.1) *) 353 - let test_wire_pdu_version () = 354 - let md = 355 - Cfdp.metadata ~file_size:0L ~source_filename:"" ~dest_filename:"" () 356 - in 357 - let pdu = Cfdp.Pdu_directive (make_hdr (), Metadata md) in 358 - let encoded = Cfdp.encode cfg pdu in 359 - (* First 3 bits of first byte = version (001 = CFDP version 2) *) 360 - let first_byte = Char.code encoded.[0] in 361 - let version = (first_byte lsr 5) land 0x7 in 362 - Alcotest.(check int) "CFDP version field = 1 (v2)" 1 version 363 - 364 - (* ═══════════════════════════════════════════════════════════════════════ *) 365 - (* Suite *) 366 - (* ═══════════════════════════════════════════════════════════════════════ *) 367 - 368 - let suite = 369 - ( "cfdp_eio", 370 - [ 371 - (* Wire framing *) 372 - Alcotest.test_case "frame empty" `Quick test_frame_empty; 373 - Alcotest.test_case "frame small" `Quick test_frame_small; 374 - Alcotest.test_case "frame 1024" `Quick test_frame_1024; 375 - Alcotest.test_case "frame 65535" `Quick test_frame_65535; 376 - Alcotest.test_case "frame truncated header" `Quick 377 - test_frame_truncated_header; 378 - Alcotest.test_case "frame truncated payload" `Quick 379 - test_frame_truncated_payload; 380 - Alcotest.test_case "frame negative length" `Quick 381 - test_frame_negative_length; 382 - Alcotest.test_case "frame overlarge" `Quick test_frame_overlarge; 383 - (* PDU round-trip through framing *) 384 - Alcotest.test_case "PDU metadata" `Quick test_pdu_frame_metadata; 385 - Alcotest.test_case "PDU metadata closure" `Quick 386 - test_pdu_frame_metadata_closure; 387 - Alcotest.test_case "PDU file-data" `Quick test_pdu_frame_file_data; 388 - Alcotest.test_case "PDU file-data offset" `Quick 389 - test_pdu_frame_file_data_offset; 390 - Alcotest.test_case "PDU file-data max" `Quick 391 - test_pdu_frame_file_data_max_segment; 392 - Alcotest.test_case "PDU EOF" `Quick test_pdu_frame_eof; 393 - Alcotest.test_case "PDU EOF cancel" `Quick test_pdu_frame_eof_cancel; 394 - Alcotest.test_case "PDU Finished" `Quick test_pdu_frame_finished; 395 - Alcotest.test_case "PDU ACK" `Quick test_pdu_frame_ack; 396 - Alcotest.test_case "PDU NAK empty" `Quick test_pdu_frame_nak_empty; 397 - Alcotest.test_case "PDU NAK segments" `Quick test_pdu_frame_nak_segments; 398 - Alcotest.test_case "PDU Keep-Alive" `Quick test_pdu_frame_keep_alive; 399 - Alcotest.test_case "PDU Prompt" `Quick test_pdu_frame_prompt; 400 - (* Transfer sequence *) 401 - Alcotest.test_case "transfer sequence" `Quick test_transfer_sequence; 402 - (* Wire format *) 403 - Alcotest.test_case "wire length prefix" `Quick 404 - test_wire_format_length_prefix; 405 - Alcotest.test_case "wire PDU version" `Quick test_wire_pdu_version; 406 - ] )