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.

Add cfdp-eio, osv, and sbom.license libraries

cfdp-eio: CFDP file transfer over TCP with Eio. Drives Cfdp state
machines with real I/O — Sender (Class 1) and Receiver with
length-prefixed PDU framing. Same architecture as Borealis CFDP
service but using the standalone cfdp encode/decode API.

osv: OSV.dev vulnerability database client. Queries by Package URL,
ecosystem/name/version, or commit hash. Uses requests library for
HTTP. Maps CVSS scores to severity levels, supports batch queries.

sbom.license: SPDX license expression parser and policy evaluator.
Full Annex D grammar (AND/OR/WITH/+/LicenseRef, precedence, parens).
Policy engine with correct OR semantics (passes if any alternative
is acceptable). Built-in GPL and strong-copyleft deny lists.
Integration with Spdx.document and Cyclonedx.bom.

28 tests for sbom.license verified against Python license-expression
30.4 reference implementation: 50+ SPDX identifiers, operator
precedence, 18 real-world expressions (Linux kernel, Rust, npm,
Maven, Qt), error cases, SBOM integration with nested components.

+442
+36
cfdp-eio.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "CFDP file transfer over TCP with Eio" 4 + description: 5 + "Drives CFDP state machines with real I/O over Eio TCP connections. Supports Class 1 (unacknowledged) file transfers with length-prefixed PDU framing. Same Sender/Receiver architecture as Borealis but using the standalone cfdp library." 6 + maintainer: ["Thomas Gazagnaire"] 7 + authors: ["Thomas Gazagnaire"] 8 + license: "ISC" 9 + homepage: "https://tangled.org/gazagnaire.org/ocaml-cfdp" 10 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-cfdp/issues" 11 + depends: [ 12 + "dune" {>= "3.21"} 13 + "ocaml" {>= "5.1"} 14 + "cfdp" {= version} 15 + "eio" {>= "1.0"} 16 + "fpath" {>= "0.7"} 17 + "fmt" {>= "0.9"} 18 + "logs" {>= "0.7"} 19 + "odoc" {with-doc} 20 + ] 21 + build: [ 22 + ["dune" "subst"] {dev} 23 + [ 24 + "dune" 25 + "build" 26 + "-p" 27 + name 28 + "-j" 29 + jobs 30 + "@install" 31 + "@runtest" {with-test} 32 + "@doc" {with-doc} 33 + ] 34 + ] 35 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-cfdp" 36 + x-maintenance-intent: ["(latest)"]
+13
dune-project
··· 25 25 (fmt (>= 0.9)) 26 26 (alcotest (and :with-test (>= 1.7))) 27 27 (crowbar (and :with-test (>= 0.2))))) 28 + 29 + (package 30 + (name cfdp-eio) 31 + (synopsis "CFDP file transfer over TCP with Eio") 32 + (description 33 + "Drives CFDP state machines with real I/O over Eio TCP connections. Supports Class 1 (unacknowledged) file transfers with length-prefixed PDU framing. Same Sender/Receiver architecture as Borealis but using the standalone cfdp library.") 34 + (depends 35 + (ocaml (>= 5.1)) 36 + (cfdp (= :version)) 37 + (eio (>= 1.0)) 38 + (fpath (>= 0.7)) 39 + (fmt (>= 0.9)) 40 + (logs (>= 0.7))))
+268
lib/eio/cfdp_eio.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let src = Logs.Src.create "cfdp.eio" ~doc:"CFDP TCP transport" 7 + 8 + module Log = (val Logs.src_log src : Logs.LOG) 9 + 10 + (* ── Config ────────────────────────────────────────────────────────────── *) 11 + 12 + type config = { 13 + local_entity : int; 14 + remote_entity : int; 15 + segment_size : int; 16 + pdu_config : Cfdp.pdu_config; 17 + } 18 + 19 + let default_config = 20 + { 21 + local_entity = 1; 22 + remote_entity = 2; 23 + segment_size = 1024; 24 + pdu_config = Cfdp.default_config; 25 + } 26 + 27 + (* ── Connection ────────────────────────────────────────────────────────── *) 28 + 29 + type t = { 30 + flow : [ `Generic ] Eio.Net.stream_socket_ty Eio.Resource.t; 31 + buf_r : Eio.Buf_read.t; 32 + config : config; 33 + mutable next_seq : int64; 34 + } 35 + 36 + let connect ~sw ~net ?(config = default_config) ~host ~port () = 37 + Log.info (fun f -> f "connecting to %s:%d" host port); 38 + let addr = `Tcp (Eio.Net.Ipaddr.of_raw host, port) in 39 + let flow = Eio.Net.connect ~sw net addr in 40 + let buf_r = Eio.Buf_read.of_flow ~max_size:(1024 * 1024) flow in 41 + { flow; buf_r; config; next_seq = 1L } 42 + 43 + let close t = 44 + Log.info (fun f -> f "closing connection"); 45 + Eio.Flow.close t.flow 46 + 47 + (* ── Wire framing ──────────────────────────────────────────────────────── *) 48 + 49 + let send_frame flow data = 50 + let len = String.length data in 51 + let hdr = Bytes.create 4 in 52 + Bytes.set_int32_be hdr 0 (Int32.of_int len); 53 + Eio.Flow.copy_string (Bytes.to_string hdr ^ data) flow 54 + 55 + let recv_frame buf_r = 56 + let hdr = Eio.Buf_read.take 4 buf_r in 57 + let len = Int32.to_int (String.get_int32_be hdr 0) in 58 + if len <= 0 || len > 1_048_576 then 59 + Error (Format.asprintf "invalid frame length: %d" len) 60 + else Ok (Eio.Buf_read.take len buf_r) 61 + 62 + (* ── PDU I/O ───────────────────────────────────────────────────────────── *) 63 + 64 + let send_pdu t pdu = 65 + let data = Cfdp.encode t.config.pdu_config pdu in 66 + send_frame t.flow data 67 + 68 + let recv_pdu t = 69 + match recv_frame t.buf_r with 70 + | Error _ as e -> e 71 + | Ok data -> ( 72 + match Cfdp.decode data with 73 + | Ok (pdu, _consumed) -> Ok pdu 74 + | Error e -> Error (Format.asprintf "%a" Cfdp.pp_error e)) 75 + 76 + (* ── Helpers ───────────────────────────────────────────────────────────── *) 77 + 78 + type progress = { 79 + bytes_sent : int64; 80 + bytes_total : int64; 81 + segments_sent : int; 82 + segments_total : int; 83 + } 84 + 85 + let alloc_seq t = 86 + let s = t.next_seq in 87 + t.next_seq <- Int64.add t.next_seq 1L; 88 + s 89 + 90 + let make_header t ~pdu_type ~direction ~mode ~seq = 91 + let src = Cfdp.Entity_id.of_int_exn t.config.local_entity in 92 + let dst = Cfdp.Entity_id.of_int_exn t.config.remote_entity in 93 + Cfdp. 94 + { 95 + version = 1; 96 + pdu_type; 97 + direction; 98 + transmission_mode = mode; 99 + crc_present = false; 100 + large_file = false; 101 + segment_ctrl = false; 102 + segment_metadata = false; 103 + source_entity = src; 104 + transaction_seq = seq; 105 + dest_entity = dst; 106 + data_len = 0; 107 + } 108 + 109 + let read_segment path offset len = 110 + let ic = open_in_bin (Fpath.to_string path) in 111 + Fun.protect ~finally:(fun () -> close_in ic) @@ fun () -> 112 + LargeFile.seek_in ic offset; 113 + let file_len = LargeFile.in_channel_length ic in 114 + let remaining = Int64.to_int (Int64.sub file_len offset) in 115 + let actual_len = min len remaining in 116 + let buf = Bytes.create actual_len in 117 + really_input ic buf 0 actual_len; 118 + buf 119 + 120 + (* ── Sender (Class 1) ──────────────────────────────────────────────────── *) 121 + 122 + module Sender = struct 123 + let send_file t ~src ~dst ?(on_progress = fun _ -> ()) () = 124 + let stat = Unix.LargeFile.stat (Fpath.to_string src) in 125 + let file_size = stat.Unix.LargeFile.st_size in 126 + let seg_size = t.config.segment_size in 127 + let total_segments = 128 + Int64.to_int 129 + (Int64.div 130 + (Int64.add file_size (Int64.of_int (seg_size - 1))) 131 + (Int64.of_int seg_size)) 132 + in 133 + let seq = alloc_seq t in 134 + let src_entity = Cfdp.Entity_id.of_int_exn t.config.local_entity in 135 + let tx_id : Cfdp.transaction_id = { source = src_entity; seq_nr = seq } in 136 + let mode = Cfdp.Unacknowledged in 137 + Log.info (fun f -> 138 + f "send_file: %a -> %s (%Ld bytes, %d segments)" Fpath.pp src dst 139 + file_size total_segments); 140 + (* Metadata *) 141 + let md = 142 + Cfdp.metadata ~file_size ~source_filename:(Fpath.to_string src) 143 + ~dest_filename:dst () 144 + in 145 + let hdr = 146 + make_header t ~pdu_type:File_directive ~direction:Toward_receiver ~mode 147 + ~seq 148 + in 149 + send_pdu t (Cfdp.Pdu_directive (hdr, Metadata md)); 150 + (* File data segments *) 151 + let data_hdr = 152 + make_header t ~pdu_type:File_data ~direction:Toward_receiver ~mode ~seq 153 + in 154 + let rec send_segments offset seg_num = 155 + if Int64.compare offset file_size >= 0 then () 156 + else begin 157 + let remaining = Int64.sub file_size offset in 158 + let chunk_size = Int64.to_int (min (Int64.of_int seg_size) remaining) in 159 + let data = read_segment src offset chunk_size in 160 + let fd = Cfdp.file_data ~offset data in 161 + send_pdu t 162 + (Cfdp.Pdu_file_data ({ data_hdr with data_len = chunk_size }, fd)); 163 + let new_offset = Int64.add offset (Int64.of_int chunk_size) in 164 + on_progress 165 + { 166 + bytes_sent = new_offset; 167 + bytes_total = file_size; 168 + segments_sent = seg_num + 1; 169 + segments_total = total_segments; 170 + }; 171 + send_segments new_offset (seg_num + 1) 172 + end 173 + in 174 + send_segments 0L 0; 175 + (* EOF with checksum *) 176 + let ic = open_in_bin (Fpath.to_string src) in 177 + let checksum = 178 + Fun.protect ~finally:(fun () -> close_in ic) @@ fun () -> 179 + let buf = Bytes.create 65536 in 180 + let rec loop acc = 181 + let n = input ic buf 0 65536 in 182 + if n = 0 then acc 183 + else 184 + let chunk = Bytes.sub buf 0 n in 185 + loop (Int32.add acc (Cfdp.compute_checksum Checksum_modular chunk)) 186 + in 187 + loop 0l 188 + in 189 + let eof_pdu = Cfdp.eof ~condition:No_error ~checksum ~file_size () in 190 + send_pdu t (Cfdp.Pdu_directive (hdr, Eof eof_pdu)); 191 + Log.info (fun f -> 192 + f "send_file: complete, tx=%a" Cfdp.pp_transaction_id tx_id); 193 + Ok tx_id 194 + end 195 + 196 + (* ── Receiver ──────────────────────────────────────────────────────────── *) 197 + 198 + module Receiver = struct 199 + type recv_result = 200 + | Continue 201 + | Finished of { dest_file : string; size : int64; checksum : int32 } 202 + | Recv_error of string 203 + 204 + let recv_file t ~dest_dir ?(on_progress = fun _ -> ()) () = 205 + Log.info (fun f -> f "recv_file: waiting for transfer"); 206 + let dest_name = ref "" in 207 + let file_size = ref 0L in 208 + let tx_id = ref None in 209 + let segments : (int64 * Bytes.t) list ref = ref [] in 210 + let total_received = ref 0L in 211 + let rec loop () = 212 + match recv_pdu t with 213 + | Error msg -> Error msg 214 + | Ok (Cfdp.Pdu_directive (hdr, Metadata md)) -> 215 + dest_name := md.dest_filename; 216 + file_size := md.file_size; 217 + tx_id := 218 + Some 219 + ({ Cfdp.source = hdr.source_entity; seq_nr = hdr.transaction_seq } 220 + : Cfdp.transaction_id); 221 + Log.info (fun f -> 222 + f "metadata: %s (%Ld bytes)" md.dest_filename md.file_size); 223 + loop () 224 + | Ok (Cfdp.Pdu_file_data (_hdr, fd)) -> 225 + segments := (fd.offset, fd.data) :: !segments; 226 + total_received := 227 + Int64.add !total_received (Int64.of_int (Bytes.length fd.data)); 228 + let total_segs = 229 + max 1 230 + (Int64.to_int 231 + (Int64.div 232 + (Int64.add !file_size 233 + (Int64.of_int (t.config.segment_size - 1))) 234 + (Int64.of_int t.config.segment_size))) 235 + in 236 + on_progress 237 + { 238 + bytes_sent = !total_received; 239 + bytes_total = !file_size; 240 + segments_sent = List.length !segments; 241 + segments_total = total_segs; 242 + }; 243 + loop () 244 + | Ok (Cfdp.Pdu_directive (_hdr, Eof _eof)) -> 245 + Log.info (fun f -> f "received EOF"); 246 + let out_path = Fpath.(dest_dir / !dest_name) in 247 + let oc = open_out_bin (Fpath.to_string out_path) in 248 + Fun.protect 249 + ~finally:(fun () -> close_out oc) 250 + (fun () -> 251 + let sorted = 252 + List.sort (fun (a, _) (b, _) -> Int64.compare a b) !segments 253 + in 254 + List.iter 255 + (fun (offset, data) -> 256 + LargeFile.seek_out oc offset; 257 + output_bytes oc data) 258 + sorted); 259 + let id = 260 + match !tx_id with 261 + | Some id -> id 262 + | None -> { Cfdp.source = Cfdp.Entity_id.of_int_exn 0; seq_nr = 0L } 263 + in 264 + Ok (out_path, id) 265 + | Ok _ -> loop () 266 + in 267 + loop () 268 + end
+121
lib/eio/cfdp_eio.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** CFDP file transfer over TCP with Eio. 7 + 8 + Drives the {!Cfdp} state machines with real I/O over Eio TCP connections. 9 + PDUs are length-prefixed on the wire (4-byte big-endian length + PDU). 10 + 11 + Follows the same Sender / Receiver / Manager architecture as the Borealis 12 + CFDP service, but uses the mono {!Cfdp.encode}/{!Cfdp.decode} API and Eio 13 + networking instead of Borealis Binary.Writer and Filestore. 14 + 15 + {2 Quick Start} 16 + 17 + {[ 18 + Eio_main.run @@ fun env -> 19 + Eio.Switch.run @@ fun sw -> 20 + let conn = 21 + Cfdp_eio.connect ~sw ~net:env#net ~host:"flatsat-1" ~port:1734 () 22 + in 23 + let result = 24 + Cfdp_eio.Sender.send_file conn ~src:(Fpath.v "firmware.elf") 25 + ~dst:"firmware.elf" 26 + ~on_progress:(fun p -> 27 + Printf.printf "\r%Ld/%Ld" p.bytes_sent p.bytes_total) 28 + () 29 + in 30 + match result with 31 + | Ok tid -> Printf.printf "\nDone: %a\n" Cfdp.pp_transaction_id tid 32 + | Error msg -> Printf.eprintf "Error: %s\n" msg 33 + ]} 34 + 35 + {2 Wire Format} 36 + 37 + {v 38 + ┌──────────┬──────────────────┐ 39 + │ Length │ CFDP PDU │ 40 + │ (4B BE) │ (variable) │ 41 + └──────────┴──────────────────┘ 42 + v} *) 43 + 44 + (** {1 Connection} *) 45 + 46 + type t 47 + (** A CFDP transport connection. *) 48 + 49 + type config = { 50 + local_entity : int; (** Local CFDP entity ID (default: 1). *) 51 + remote_entity : int; (** Remote CFDP entity ID (default: 2). *) 52 + segment_size : int; (** Max file data segment bytes (default: 1024). *) 53 + pdu_config : Cfdp.pdu_config; (** PDU encoding config. *) 54 + } 55 + (** Transport configuration. *) 56 + 57 + val default_config : config 58 + (** Entity 1→2, 1024-byte segments, 2-byte entity IDs, 4-byte seq nrs. *) 59 + 60 + val connect : 61 + sw:Eio.Switch.t -> 62 + net:[> [ `Generic ] Eio.Net.ty ] Eio.Resource.t -> 63 + ?config:config -> 64 + host:string -> 65 + port:int -> 66 + unit -> 67 + t 68 + (** Connect to a remote CFDP entity over TCP. 69 + @raise Eio.Io on connection failure. *) 70 + 71 + val close : t -> unit 72 + (** Close the connection. *) 73 + 74 + (** {1 Progress} *) 75 + 76 + type progress = { 77 + bytes_sent : int64; 78 + bytes_total : int64; 79 + segments_sent : int; 80 + segments_total : int; 81 + } 82 + 83 + (** {1 Sender} *) 84 + 85 + module Sender : sig 86 + val send_file : 87 + t -> 88 + src:Fpath.t -> 89 + dst:string -> 90 + ?on_progress:(progress -> unit) -> 91 + unit -> 92 + (Cfdp.transaction_id, string) result 93 + (** Class 1 (unacknowledged) file transfer. Sends Metadata + File Data 94 + segments + EOF. Returns the transaction ID on success. *) 95 + end 96 + 97 + (** {1 Receiver} *) 98 + 99 + module Receiver : sig 100 + type recv_result = 101 + | Continue 102 + | Finished of { dest_file : string; size : int64; checksum : int32 } 103 + | Recv_error of string 104 + 105 + val recv_file : 106 + t -> 107 + dest_dir:Fpath.t -> 108 + ?on_progress:(progress -> unit) -> 109 + unit -> 110 + (Fpath.t * Cfdp.transaction_id, string) Stdlib.result 111 + (** Receive a complete file. Blocks until Metadata + Data + EOF are received. 112 + Writes to [dest_dir/filename]. *) 113 + end 114 + 115 + (** {1 Low-level PDU I/O} *) 116 + 117 + val send_pdu : t -> Cfdp.pdu -> unit 118 + (** Encode and send a single PDU. *) 119 + 120 + val recv_pdu : t -> (Cfdp.pdu, string) result 121 + (** Receive and decode a single PDU. *)
+4
lib/eio/dune
··· 1 + (library 2 + (name cfdp_eio) 3 + (public_name cfdp-eio) 4 + (libraries cfdp eio fpath fmt logs))