minor fixes, add tests

This commit is contained in:
Hannes Mehnert 2024-03-14 20:55:37 +01:00
parent b770728db5
commit ca7658230d
4 changed files with 124 additions and 21 deletions

5
dune
View file

@ -2,3 +2,8 @@
(name ohex) (name ohex)
(public_name ohex) (public_name ohex)
(modules ohex)) (modules ohex))
(test
(name tests)
(modules tests)
(libraries alcotest ohex))

39
ohex.ml
View file

@ -8,25 +8,32 @@ let is_space = function
| ' ' | '\n' | '\r' | '\t' -> true | ' ' | '\n' | '\r' | '\t' -> true
| _ -> false | _ -> false
let count_hex_chars ?(skip_whitespace = true) src = let digit = function
if skip_whitespace then | '0'..'9' as c -> int_of_char c - 0x30
| 'A'..'F' as c -> int_of_char c - 0x41 + 10
| 'a'..'f' as c -> int_of_char c - 0x61 + 10
| _ -> invalid_arg "bad character"
let required_length ?(skip_whitespace = true) src =
let req =
string_fold (fun r c -> string_fold (fun r c ->
if is_space c then r else succ r) if skip_whitespace && is_space c then
0 src / 2 r
else (
ignore (digit c);
succ r))
0 src
in
if req mod 2 = 0 then
req / 2
else else
String.length src / 2 invalid_arg "leftover byte in hex string"
let decode_into ?(skip_whitespace = true) src tgt ?(off = 0) () = let decode_into ?(skip_whitespace = true) src tgt ?(off = 0) () =
let fold f acc str = let fold f acc str =
let st = ref acc in let st = ref acc in
String.iter (fun c -> st := f !st c) str; String.iter (fun c -> st := f !st c) str;
!st !st
and digit c =
match c with
| '0'..'9' -> int_of_char c - 0x30
| 'A'..'F' -> int_of_char c - 0x41 + 10
| 'a'..'f' -> int_of_char c - 0x61 + 10
| _ -> invalid_arg "bad character"
in in
let chars, leftover = let chars, leftover =
fold (fun (chars, leftover) c -> fold (fun (chars, leftover) c ->
@ -45,19 +52,21 @@ let decode_into ?(skip_whitespace = true) src tgt ?(off = 0) () =
List.iteri (fun idx c -> Bytes.set_uint8 tgt (off + idx) c) chars List.iteri (fun idx c -> Bytes.set_uint8 tgt (off + idx) c) chars
let decode ?(skip_whitespace = true) src = let decode ?(skip_whitespace = true) src =
let len = count_hex_chars ~skip_whitespace src in let len = required_length ~skip_whitespace src in
let buf = Bytes.create len in let buf = Bytes.create len in
decode_into ~skip_whitespace src buf (); decode_into ~skip_whitespace src buf ();
Bytes.unsafe_to_string buf Bytes.unsafe_to_string buf
let hex_map = "0123456789abcdef"
let encode_into src tgt ?(off = 0) () = let encode_into src tgt ?(off = 0) () =
String.iteri (fun idx c -> String.iteri (fun idx c ->
let hi, lo = let hi, lo =
let i = int_of_char c in let i = int_of_char c in
i lsr 4, i land 0xFF hex_map.[i lsr 4], hex_map.[i land 0x0F]
in in
Bytes.set_uint8 tgt (idx * 2 + off) hi; Bytes.set tgt (idx * 2 + off) hi;
Bytes.set_uint8 tgt (idx * 2 + off + 1) lo) Bytes.set tgt (idx * 2 + off + 1) lo)
src src
let encode src = let encode src =

View file

@ -1,10 +1,14 @@
(** Convert from and to hex representation. *) (** Convert from and to hexadecimal representation. *)
val count_hex_chars : ?skip_whitespace:bool -> string -> int val required_length : ?skip_whitespace:bool -> string -> int
(** [count_hex_chars ~skip_whitespace s] counts the amount of hex characters in (** [required_length ~skip_whitespace s] returns the length needed when the
the string [s]. The argument [skip_whitespace] defaults to [true], and skips hex string [s] would be decoded into a sequence of octets. The argument
any whitespace characters (' ', '\n', '\r', '\t'). This function is useful [skip_whitespace] defaults to [true], and skips any whitespace characters
for estimating the space required for [decode_into]. *) (' ', '\n', '\r', '\t'). This function is useful for estimating the space
required for [decode_into].
@raise Invalid_argument if any character in [s] is not a hex character, or
an odd amount of characters are present. *)
val decode : ?skip_whitespace:bool -> string -> string val decode : ?skip_whitespace:bool -> string -> string
(** [decode ~skip_whitespace s] decodes a hex string [s] into a sequence of (** [decode ~skip_whitespace s] decodes a hex string [s] into a sequence of

85
tests.ml Normal file
View file

@ -0,0 +1,85 @@
let tests = [
"", 0, "";
"41", 1, "A";
"41 41", 2, "AA";
" 41 41 ", 2, "AA";
" 414 1", 2, "AA";
]
let len_dec_tests =
List.mapi (fun i (s, len, v) ->
string_of_int i ^ " is correct", `Quick,
(fun () ->
Alcotest.(check int "required length" len (Ohex.required_length s));
Alcotest.(check string "decode works fine" v (Ohex.decode s))))
tests
let bad_char_input = [ "W" ; "AAWW" ; "WWAA" ]
let leftover_input = [ "AAA" ; "A" ]
let bad_input_ws = [ " "; " AA" ; "AA " ; "A A" ]
let bad_len_dec_tests =
(List.mapi (fun i s ->
string_of_int i ^ " fails (bad character)", `Quick,
(fun () ->
Alcotest.(check_raises "required length raises"
(Invalid_argument "bad character")
(fun () -> ignore (Ohex.required_length s)));
Alcotest.(check_raises "decode raises"
(Invalid_argument "bad character")
(fun () -> ignore (Ohex.decode s)))))
bad_char_input) @
(List.mapi (fun i s ->
string_of_int i ^ " fails (leftover)", `Quick,
(fun () ->
Alcotest.(check_raises "required length raises"
(Invalid_argument "leftover byte in hex string")
(fun () -> ignore (Ohex.required_length ~skip_whitespace:false s)));
Alcotest.(check_raises "decode raises"
(Invalid_argument "leftover byte in hex string")
(fun () -> ignore (Ohex.decode ~skip_whitespace:false s)))))
leftover_input) @
(List.mapi (fun i s ->
string_of_int i ^ " fails (skip_whitespace = false)", `Quick,
(fun () ->
Alcotest.(check_raises "required length raises"
(Invalid_argument "bad character")
(fun () -> ignore (Ohex.required_length ~skip_whitespace:false s)));
Alcotest.(check_raises "decode raises"
(Invalid_argument "bad character")
(fun () -> ignore (Ohex.decode ~skip_whitespace:false s)))))
bad_input_ws)
let dec_enc () =
let random_string () =
let size = Random.int 128 in
let buf = Bytes.create size in
for i = 0 to size - 1 do
Bytes.set_uint8 buf i (Random.int 256)
done;
Bytes.unsafe_to_string buf
in
for i = 0 to 10_000 do
let input = random_string () in
Alcotest.(check string ("dec (enc s) = s " ^ string_of_int i)
input Ohex.(decode (encode input)));
Alcotest.(check string ("dec ~skip_ws:false (enc s) = s " ^ string_of_int i)
input Ohex.(decode ~skip_whitespace:false (encode input)));
let buf = Bytes.create (String.length input * 2) in
Ohex.encode_into input buf ~off:0 ();
let out = Bytes.create (String.length input) in
Ohex.decode_into (Bytes.unsafe_to_string buf) out ~off:0 ();
Alcotest.(check string ("dec_into (enc_into s) = s " ^ string_of_int i)
input (Bytes.unsafe_to_string out))
done
let suites = [
"length and decode pass", len_dec_tests ;
"bad input", bad_len_dec_tests ;
"decode encode", [ "decode (encode s) = s", `Quick, dec_enc ];
]
let () = Alcotest.run "hex tests" suites