Self-Delimiting Numeric Values (RFC 6256)
0
fork

Configure Feed

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

Merge commit '16f8025448e0a99613bae3df99bd127969d69b68' as 'ocaml-sdnv'

+500
+1
.ocamlformat
··· 1 + version = 0.28.1
+3
dune-project
··· 1 + (lang dune 3.0) 2 + (name sdnv) 3 + (formatting (enabled_for ocaml))
+4
lib/dune
··· 1 + (library 2 + (name sdnv) 3 + (public_name sdnv) 4 + (libraries bytesrw))
+122
lib/sdnv.ml
··· 1 + (** Self-Delimiting Numeric Values (RFC 6256). 2 + 3 + Variable-length integer encoding used by LTP (CCSDS 734.1-B-1), Bundle 4 + Protocol, and other space protocols. *) 5 + 6 + (** {1 Encoding} *) 7 + 8 + let encoded_length v = 9 + if v < 0L then invalid_arg "Sdnv.encoded_length: negative value" 10 + else if v = 0L then 1 11 + else 12 + let rec count v n = 13 + if v = 0L then n else count (Int64.shift_right_logical v 7) (n + 1) 14 + in 15 + count v 0 16 + 17 + let encode_to buf off v = 18 + if v < 0L then invalid_arg "Sdnv.encode_to: negative value" 19 + else if v = 0L then begin 20 + Bytes.set_uint8 buf off 0; 21 + 1 22 + end 23 + else begin 24 + let n = encoded_length v in 25 + if off + n > Bytes.length buf then 26 + invalid_arg "Sdnv.encode_to: buffer too small"; 27 + for i = n - 1 downto 0 do 28 + let shift = i * 7 in 29 + let byte = 30 + Int64.to_int (Int64.logand (Int64.shift_right_logical v shift) 0x7FL) 31 + in 32 + (* Set continuation bit on all but last byte *) 33 + let byte = if i > 0 then byte lor 0x80 else byte in 34 + Bytes.set_uint8 buf (off + (n - 1 - i)) byte 35 + done; 36 + n 37 + end 38 + 39 + let encode v = 40 + let n = encoded_length v in 41 + let buf = Bytes.create n in 42 + let _ = encode_to buf 0 v in 43 + buf 44 + 45 + (** {1 Decoding} *) 46 + 47 + type error = Truncated | Overflow 48 + 49 + let pp_error ppf = function 50 + | Truncated -> Format.fprintf ppf "truncated SDNV" 51 + | Overflow -> Format.fprintf ppf "SDNV overflow" 52 + 53 + let decode buf off = 54 + let len = Bytes.length buf in 55 + let rec loop acc bits_read pos = 56 + if bits_read >= 64 then Error Overflow 57 + else if pos >= len then Error Truncated 58 + else 59 + let byte = Bytes.get_uint8 buf pos in 60 + let value = byte land 0x7F in 61 + let acc = Int64.(logor (shift_left acc 7) (of_int value)) in 62 + if byte land 0x80 = 0 then Ok (acc, pos - off + 1) 63 + else loop acc (bits_read + 7) (pos + 1) 64 + in 65 + loop 0L 0 off 66 + 67 + let decode_exn buf off = 68 + match decode buf off with 69 + | Ok v -> v 70 + | Error Truncated -> invalid_arg "Sdnv.decode_exn: truncated" 71 + | Error Overflow -> invalid_arg "Sdnv.decode_exn: overflow" 72 + 73 + (** {1 Streaming with bytesrw} *) 74 + 75 + module Slice = Bytesrw.Bytes.Slice 76 + 77 + let read r = 78 + let rec loop acc bits_read = 79 + if bits_read >= 64 then Error Overflow 80 + else 81 + match Bytesrw.Bytes.Reader.read r with 82 + | slice when Slice.is_eod slice -> Error Truncated 83 + | slice -> 84 + let data = Slice.bytes slice in 85 + let first = Slice.first slice in 86 + let length = Slice.length slice in 87 + let rec process_slice acc bits_read i = 88 + if i >= first + length then 89 + (* Need more data from next slice *) 90 + loop acc bits_read 91 + else 92 + let byte = Bytes.get_uint8 data i in 93 + let value = byte land 0x7F in 94 + let acc = Int64.(logor (shift_left acc 7) (of_int value)) in 95 + if byte land 0x80 = 0 then begin 96 + (* Push back remaining bytes if any *) 97 + let consumed = i - first + 1 in 98 + if consumed < length then begin 99 + let remaining = 100 + Slice.make data ~first:(i + 1) ~length:(length - consumed) 101 + in 102 + Bytesrw.Bytes.Reader.push_back r remaining 103 + end; 104 + Ok acc 105 + end 106 + else if bits_read + 7 >= 64 then Error Overflow 107 + else process_slice acc (bits_read + 7) (i + 1) 108 + in 109 + process_slice acc bits_read first 110 + in 111 + loop 0L 0 112 + 113 + let read_exn r = 114 + match read r with 115 + | Ok v -> v 116 + | Error Truncated -> invalid_arg "Sdnv.read_exn: truncated" 117 + | Error Overflow -> invalid_arg "Sdnv.read_exn: overflow" 118 + 119 + let write w v = 120 + let buf = encode v in 121 + let slice = Slice.make buf ~first:0 ~length:(Bytes.length buf) in 122 + Bytesrw.Bytes.Writer.write w slice
+58
lib/sdnv.mli
··· 1 + (** Self-Delimiting Numeric Values (RFC 6256). 2 + 3 + Variable-length integer encoding used by LTP (CCSDS 734.1-B-1), Bundle 4 + Protocol, and other space protocols. Each byte's high bit indicates 5 + continuation (1=more bytes follow, 0=last byte), with 7 bits of data per 6 + byte in big-endian order. 7 + 8 + {b Examples:} 9 + - 0 encodes as [0x00] (1 byte) 10 + - 127 encodes as [0x7F] (1 byte) 11 + - 128 encodes as [0x81 0x00] (2 bytes) 12 + - 16383 encodes as [0xFF 0x7F] (2 bytes) 13 + - 16384 encodes as [0x81 0x80 0x00] (3 bytes) 14 + 15 + {b Reference}: {{:https://datatracker.ietf.org/doc/html/rfc6256}RFC 6256} *) 16 + 17 + (** {1 Encoding} *) 18 + 19 + val encode : int64 -> bytes 20 + (** [encode v] encodes [v] as SDNV bytes. 21 + @raise Invalid_argument if [v] is negative. *) 22 + 23 + val encode_to : bytes -> int -> int64 -> int 24 + (** [encode_to buf off v] writes SDNV-encoded [v] into [buf] starting at [off]. 25 + Returns the number of bytes written. 26 + @raise Invalid_argument if [v] is negative or buffer too small. *) 27 + 28 + val encoded_length : int64 -> int 29 + (** [encoded_length v] returns the number of bytes needed to encode [v]. 30 + @raise Invalid_argument if [v] is negative. *) 31 + 32 + (** {1 Decoding} *) 33 + 34 + type error = 35 + | Truncated (** Not enough bytes to complete decoding *) 36 + | Overflow (** Value would overflow int64 *) 37 + 38 + val pp_error : Format.formatter -> error -> unit 39 + 40 + val decode : bytes -> int -> (int64 * int, error) result 41 + (** [decode buf off] decodes an SDNV starting at [off] in [buf]. Returns the 42 + decoded value and the number of bytes consumed. *) 43 + 44 + val decode_exn : bytes -> int -> int64 * int 45 + (** [decode_exn buf off] is like [decode] but raises [Invalid_argument] on 46 + error. *) 47 + 48 + (** {1 Streaming with bytesrw} *) 49 + 50 + val read : Bytesrw.Bytes.Reader.t -> (int64, error) result 51 + (** [read r] reads and decodes an SDNV from bytesrw reader [r]. *) 52 + 53 + val read_exn : Bytesrw.Bytes.Reader.t -> int64 54 + (** [read_exn r] is like [read] but raises [Invalid_argument] on error. *) 55 + 56 + val write : Bytesrw.Bytes.Writer.t -> int64 -> unit 57 + (** [write w v] encodes and writes SDNV [v] to bytesrw writer [w]. 58 + @raise Invalid_argument if [v] is negative. *)
+22
sdnv.opam
··· 1 + opam-version: "2.0" 2 + synopsis: "Self-Delimiting Numeric Values (RFC 6256)" 3 + description: """ 4 + Variable-length integer encoding used by LTP (CCSDS 734.1-B-1), Bundle Protocol, 5 + and other space protocols. Each byte's high bit indicates continuation, with 7 6 + bits of data per byte in big-endian order. 7 + """ 8 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 9 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 10 + license: "ISC" 11 + homepage: "https://github.com/gazagnaire/ocaml-sdnv" 12 + bug-reports: "https://github.com/gazagnaire/ocaml-sdnv/issues" 13 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-sdnv.git" 14 + depends: [ 15 + "ocaml" {>= "4.14"} 16 + "dune" {>= "3.0"} 17 + "bytesrw" 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + ["dune" "build" "-p" name "-j" jobs] 22 + ]
+3
test/dune
··· 1 + (test 2 + (name test_sdnv) 3 + (libraries sdnv alcotest))
+287
test/test_sdnv.ml
··· 1 + (** SDNV test vectors from RFC 5326 and RFC 6256. 2 + 3 + Self-Delimiting Numeric Values (SDNV) test vectors from RFC 5326 section 3.1 4 + and RFC 6256. Each byte's high bit is a continuation flag (1=more bytes, 5 + 0=last). The lower 7 bits of each byte carry the value, MSB first. *) 6 + 7 + (* {1 Test Helpers} *) 8 + 9 + let bytes_of_hex s = 10 + let s = String.concat "" (String.split_on_char ' ' s) in 11 + let len = String.length s / 2 in 12 + let buf = Bytes.create len in 13 + for i = 0 to len - 1 do 14 + let hex = String.sub s (i * 2) 2 in 15 + Bytes.set buf i (Char.chr (int_of_string ("0x" ^ hex))) 16 + done; 17 + buf 18 + 19 + let hex_of_bytes buf = 20 + let len = Bytes.length buf in 21 + let hex = Buffer.create (len * 2) in 22 + for i = 0 to len - 1 do 23 + Buffer.add_string hex (Printf.sprintf "%02x" (Char.code (Bytes.get buf i))) 24 + done; 25 + Buffer.contents hex 26 + 27 + (* {1 RFC 5326 Test Vectors} 28 + 29 + From RFC 5326 section 3.1: 30 + - 0x7F (127) -> 0x7F 31 + - 0xABC (2748) -> 0x95 0x3C 32 + - 0x1234 (4660) -> 0xA4 0x34 33 + - 0x4234 (16948) -> 0x81 0x84 0x34 *) 34 + 35 + let test_rfc5326_0x7f () = 36 + (* 0x7F (127) fits in 7 bits -> single byte 01111111 *) 37 + let expected = bytes_of_hex "7f" in 38 + let actual = Sdnv.encode 0x7FL in 39 + Alcotest.(check string) 40 + "0x7F encodes to 7F" (hex_of_bytes expected) (hex_of_bytes actual); 41 + match Sdnv.decode actual 0 with 42 + | Ok (decoded, len) -> 43 + Alcotest.(check int64) "0x7F decodes back" 0x7FL decoded; 44 + Alcotest.(check int) "length" 1 len 45 + | Error _ -> Alcotest.fail "decode failed" 46 + 47 + let test_rfc5326_0xabc () = 48 + (* 0xABC (2748) = 10010101 00111100 (0x95 0x3C) *) 49 + let expected = bytes_of_hex "95 3c" in 50 + let actual = Sdnv.encode 0xABCL in 51 + Alcotest.(check string) 52 + "0xABC encodes to 95 3C" (hex_of_bytes expected) (hex_of_bytes actual); 53 + match Sdnv.decode actual 0 with 54 + | Ok (decoded, len) -> 55 + Alcotest.(check int64) "0xABC decodes back" 0xABCL decoded; 56 + Alcotest.(check int) "length" 2 len 57 + | Error _ -> Alcotest.fail "decode failed" 58 + 59 + let test_rfc5326_0x1234 () = 60 + (* 0x1234 (4660) = 10100100 00110100 (0xA4 0x34) *) 61 + let expected = bytes_of_hex "a4 34" in 62 + let actual = Sdnv.encode 0x1234L in 63 + Alcotest.(check string) 64 + "0x1234 encodes to A4 34" (hex_of_bytes expected) (hex_of_bytes actual); 65 + match Sdnv.decode actual 0 with 66 + | Ok (decoded, len) -> 67 + Alcotest.(check int64) "0x1234 decodes back" 0x1234L decoded; 68 + Alcotest.(check int) "length" 2 len 69 + | Error _ -> Alcotest.fail "decode failed" 70 + 71 + let test_rfc5326_0x4234 () = 72 + (* 0x4234 (16948) = 10000001 10000100 00110100 (0x81 0x84 0x34) *) 73 + let expected = bytes_of_hex "81 84 34" in 74 + let actual = Sdnv.encode 0x4234L in 75 + Alcotest.(check string) 76 + "0x4234 encodes to 81 84 34" (hex_of_bytes expected) (hex_of_bytes actual); 77 + match Sdnv.decode actual 0 with 78 + | Ok (decoded, len) -> 79 + Alcotest.(check int64) "0x4234 decodes back" 0x4234L decoded; 80 + Alcotest.(check int) "length" 3 len 81 + | Error _ -> Alcotest.fail "decode failed" 82 + 83 + (* {1 Boundary Tests} *) 84 + 85 + let test_zero () = 86 + (* 0 -> single byte 0x00 *) 87 + let expected = bytes_of_hex "00" in 88 + let actual = Sdnv.encode 0L in 89 + Alcotest.(check string) 90 + "0 encodes to 00" (hex_of_bytes expected) (hex_of_bytes actual); 91 + match Sdnv.decode actual 0 with 92 + | Ok (decoded, len) -> 93 + Alcotest.(check int64) "0 decodes back" 0L decoded; 94 + Alcotest.(check int) "length" 1 len 95 + | Error _ -> Alcotest.fail "decode failed" 96 + 97 + let test_boundary_127 () = 98 + (* 127 = max 1-byte value *) 99 + let actual = Sdnv.encode 127L in 100 + Alcotest.(check int) "127 is 1 byte" 1 (Bytes.length actual); 101 + Alcotest.(check int) "encoded length" 1 (Sdnv.encoded_length 127L); 102 + match Sdnv.decode actual 0 with 103 + | Ok (decoded, _) -> Alcotest.(check int64) "127 roundtrips" 127L decoded 104 + | Error _ -> Alcotest.fail "decode failed" 105 + 106 + let test_boundary_128 () = 107 + (* 128 (0x80) = first 2-byte value: 10000001 00000000 (0x81 0x00) *) 108 + let expected = bytes_of_hex "81 00" in 109 + let actual = Sdnv.encode 128L in 110 + Alcotest.(check string) 111 + "128 encodes to 81 00" (hex_of_bytes expected) (hex_of_bytes actual); 112 + Alcotest.(check int) "encoded length" 2 (Sdnv.encoded_length 128L); 113 + match Sdnv.decode actual 0 with 114 + | Ok (decoded, len) -> 115 + Alcotest.(check int64) "128 decodes back" 128L decoded; 116 + Alcotest.(check int) "length" 2 len 117 + | Error _ -> Alcotest.fail "decode failed" 118 + 119 + let test_boundary_16383 () = 120 + (* 16383 (0x3FFF) = max 2-byte value: 11111111 01111111 (0xFF 0x7F) *) 121 + let expected = bytes_of_hex "ff 7f" in 122 + let actual = Sdnv.encode 0x3FFFL in 123 + Alcotest.(check string) 124 + "0x3FFF encodes to FF 7F" (hex_of_bytes expected) (hex_of_bytes actual); 125 + match Sdnv.decode actual 0 with 126 + | Ok (decoded, len) -> 127 + Alcotest.(check int64) "0x3FFF decodes back" 0x3FFFL decoded; 128 + Alcotest.(check int) "length" 2 len 129 + | Error _ -> Alcotest.fail "decode failed" 130 + 131 + let test_boundary_16384 () = 132 + (* 16384 (0x4000) = first 3-byte value: 10000001 10000000 00000000 *) 133 + let actual = Sdnv.encode 16384L in 134 + Alcotest.(check int) "16384 is 3 bytes" 3 (Bytes.length actual); 135 + Alcotest.(check int) "encoded length" 3 (Sdnv.encoded_length 16384L); 136 + match Sdnv.decode actual 0 with 137 + | Ok (decoded, _) -> Alcotest.(check int64) "16384 roundtrips" 16384L decoded 138 + | Error _ -> Alcotest.fail "decode failed" 139 + 140 + (* {1 Large Value Tests} *) 141 + 142 + let test_large_value () = 143 + (* Large value: 0x123456789 (4886718345) - requires 5 bytes *) 144 + let value = 0x123456789L in 145 + let actual = Sdnv.encode value in 146 + Alcotest.(check int) "large value needs 5 bytes" 5 (Bytes.length actual); 147 + Alcotest.(check int) "encoded length" 5 (Sdnv.encoded_length value); 148 + match Sdnv.decode actual 0 with 149 + | Ok (decoded, _) -> 150 + Alcotest.(check int64) "large value roundtrips" value decoded 151 + | Error _ -> Alcotest.fail "decode failed" 152 + 153 + let test_max_int63 () = 154 + (* Max positive int64: 0x7FFFFFFFFFFFFFFF *) 155 + let value = Int64.max_int in 156 + let actual = Sdnv.encode value in 157 + Alcotest.(check int) "max int64 needs 9 bytes" 9 (Bytes.length actual); 158 + match Sdnv.decode actual 0 with 159 + | Ok (decoded, _) -> 160 + Alcotest.(check int64) "max int64 roundtrips" value decoded 161 + | Error _ -> Alcotest.fail "decode failed" 162 + 163 + (* {1 Error Tests} *) 164 + 165 + let test_truncated () = 166 + (* 0x80 indicates continuation but no following byte *) 167 + let data = bytes_of_hex "80" in 168 + match Sdnv.decode data 0 with 169 + | Error Sdnv.Truncated -> () 170 + | Error Sdnv.Overflow -> Alcotest.fail "expected Truncated, got Overflow" 171 + | Ok _ -> Alcotest.fail "should fail on truncated" 172 + 173 + let test_truncated_multi () = 174 + (* 0x81 0x80 indicates continuation but missing final byte *) 175 + let data = bytes_of_hex "81 80" in 176 + match Sdnv.decode data 0 with 177 + | Error Sdnv.Truncated -> () 178 + | Error _ -> Alcotest.fail "expected Truncated" 179 + | Ok _ -> Alcotest.fail "should fail on truncated" 180 + 181 + let test_negative_rejected () = 182 + (* Negative values should be rejected at encode time *) 183 + try 184 + let _ = Sdnv.encode (-1L) in 185 + Alcotest.fail "should reject negative" 186 + with Invalid_argument _ -> () 187 + 188 + (* {1 Offset Tests} *) 189 + 190 + let test_decode_with_offset () = 191 + (* Encode two values back-to-back and decode with offset *) 192 + let buf = Bytes.create 10 in 193 + let len1 = Sdnv.encode_to buf 0 0x7FL in 194 + let len2 = Sdnv.encode_to buf len1 0xABCL in 195 + Alcotest.(check int) "first len" 1 len1; 196 + Alcotest.(check int) "second len" 2 len2; 197 + match Sdnv.decode buf 0 with 198 + | Ok (v1, consumed1) -> ( 199 + Alcotest.(check int64) "first value" 0x7FL v1; 200 + Alcotest.(check int) "first consumed" 1 consumed1; 201 + match Sdnv.decode buf consumed1 with 202 + | Ok (v2, consumed2) -> 203 + Alcotest.(check int64) "second value" 0xABCL v2; 204 + Alcotest.(check int) "second consumed" 2 consumed2 205 + | Error _ -> Alcotest.fail "second decode failed") 206 + | Error _ -> Alcotest.fail "first decode failed" 207 + 208 + (* {1 Roundtrip Tests} *) 209 + 210 + let test_roundtrip_powers_of_two () = 211 + (* Test all powers of 2 up to 2^62 *) 212 + let rec test_power n = 213 + if n > 62 then () 214 + else begin 215 + let value = Int64.shift_left 1L n in 216 + let encoded = Sdnv.encode value in 217 + match Sdnv.decode encoded 0 with 218 + | Ok (decoded, _) -> 219 + Alcotest.(check int64) 220 + (Printf.sprintf "2^%d roundtrips" n) 221 + value decoded; 222 + test_power (n + 1) 223 + | Error _ -> Alcotest.fail (Printf.sprintf "decode failed for 2^%d" n) 224 + end 225 + in 226 + test_power 0 227 + 228 + let test_roundtrip_powers_of_two_minus_one () = 229 + (* Test 2^n - 1 values (all 1s in binary) *) 230 + let rec test_power n = 231 + if n > 62 then () 232 + else begin 233 + let value = Int64.sub (Int64.shift_left 1L n) 1L in 234 + let encoded = Sdnv.encode value in 235 + match Sdnv.decode encoded 0 with 236 + | Ok (decoded, _) -> 237 + Alcotest.(check int64) 238 + (Printf.sprintf "2^%d-1 roundtrips" n) 239 + value decoded; 240 + test_power (n + 1) 241 + | Error _ -> Alcotest.fail (Printf.sprintf "decode failed for 2^%d-1" n) 242 + end 243 + in 244 + test_power 1 245 + 246 + (* {1 Test Runner} *) 247 + 248 + let () = 249 + Alcotest.run "sdnv" 250 + [ 251 + ( "RFC 5326 vectors", 252 + [ 253 + Alcotest.test_case "0x7F" `Quick test_rfc5326_0x7f; 254 + Alcotest.test_case "0xABC" `Quick test_rfc5326_0xabc; 255 + Alcotest.test_case "0x1234" `Quick test_rfc5326_0x1234; 256 + Alcotest.test_case "0x4234" `Quick test_rfc5326_0x4234; 257 + ] ); 258 + ( "boundary values", 259 + [ 260 + Alcotest.test_case "zero" `Quick test_zero; 261 + Alcotest.test_case "127 (max 1-byte)" `Quick test_boundary_127; 262 + Alcotest.test_case "128 (first 2-byte)" `Quick test_boundary_128; 263 + Alcotest.test_case "16383 (max 2-byte)" `Quick test_boundary_16383; 264 + Alcotest.test_case "16384 (first 3-byte)" `Quick test_boundary_16384; 265 + ] ); 266 + ( "large values", 267 + [ 268 + Alcotest.test_case "0x123456789" `Quick test_large_value; 269 + Alcotest.test_case "max int64" `Quick test_max_int63; 270 + ] ); 271 + ( "errors", 272 + [ 273 + Alcotest.test_case "truncated single" `Quick test_truncated; 274 + Alcotest.test_case "truncated multi" `Quick test_truncated_multi; 275 + Alcotest.test_case "negative rejected" `Quick test_negative_rejected; 276 + ] ); 277 + ( "offset", 278 + [ 279 + Alcotest.test_case "decode with offset" `Quick test_decode_with_offset; 280 + ] ); 281 + ( "roundtrip", 282 + [ 283 + Alcotest.test_case "powers of 2" `Quick test_roundtrip_powers_of_two; 284 + Alcotest.test_case "powers of 2 minus 1" `Quick 285 + test_roundtrip_powers_of_two_minus_one; 286 + ] ); 287 + ]