commit 25372c91b4eadd79c3b4fbdb7af783765b773b91 Author: Hannes Mehnert Date: Thu Mar 14 13:04:14 2024 +0100 initial commit diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c7e176b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,23 @@ +Copyright (c) 2017, 2018, Hannes Mehnert +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/dune b/dune new file mode 100644 index 0000000..9c7bf61 --- /dev/null +++ b/dune @@ -0,0 +1,4 @@ +(library + (name ohex) + (public_name ohex) + (modules ohex)) diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..ba6caab --- /dev/null +++ b/dune-project @@ -0,0 +1,3 @@ +(lang dune 2.7) +(name ohex) +(formatting disabled) diff --git a/ohex.ml b/ohex.ml new file mode 100644 index 0000000..3d33253 --- /dev/null +++ b/ohex.ml @@ -0,0 +1,102 @@ + +let string_fold f acc str = + let st = ref acc in + String.iter (fun c -> st := f !st c) str; + !st + +let is_space = function + | ' ' | '\n' | '\r' | '\t' -> true + | _ -> false + +let count_hex_chars ?(skip_whitespace = true) src = + string_fold (fun r c -> + if skip_whitespace && is_space c then + r + else + succ r) + 0 src / 2 + +let decode_into ?(skip_whitespace = true) src tgt ?(off = 0) () = + let fold f acc str = + let st = ref acc in + String.iter (fun c -> st := f !st c) str; + !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 + let chars, leftover = + fold (fun (chars, leftover) c -> + if skip_whitespace && is_space c then + chars, leftover + else + let c = digit c in + match leftover with + | None -> chars, Some (c lsl 4) + | Some c' -> (c' lor c) :: chars, None) + ([], None) src + in + let chars = List.rev chars in + if leftover <> None then + invalid_arg "leftover byte in hex string"; + List.iteri (fun idx c -> Bytes.set_uint8 tgt (off + idx) c) chars + +let decode ?(skip_whitespace = true) src = + let len = count_hex_chars ~skip_whitespace src in + let buf = Bytes.create len in + decode_into ~skip_whitespace src buf (); + Bytes.unsafe_to_string buf + +let encode_into src tgt ?(off = 0) () = + String.iteri (fun idx c -> + let hi, lo = + let i = int_of_char c in + i lsr 4, i land 0xFF + in + Bytes.set_uint8 tgt (idx * 2 + off) hi; + Bytes.set_uint8 tgt (idx * 2 + off + 1) lo) + src + +let encode src = + let buf = Bytes.create (String.length src * 2) in + encode_into src buf (); + Bytes.unsafe_to_string buf + +let printable_ascii c = + let i = int_of_char c in + not (i < 0x20 || i >= 0x7f) + +let pp ?(row_numbers = true) ?(chars = true) () ppf s = + String.iteri (fun idx c -> + if idx mod 16 = 0 && row_numbers then + Format.fprintf ppf "%06x " idx; + Format.fprintf ppf "%02x" (int_of_char c); + if idx mod 2 = 1 then + Format.pp_print_string ppf " "; + if idx mod 8 = 7 then + Format.pp_print_string ppf " "; + if idx mod 16 = 15 && chars then + String.iter (fun c -> + Format.pp_print_char ppf (if printable_ascii c then c else '.')) + (String.sub s (idx - 15) 16); + if idx mod 16 = 15 then + Format.pp_print_string ppf "\n") + s; + (if chars then + let last_n, pad = + let l = String.length s in + let pad = 16 - (l mod 16) in + let pad = if pad = 16 then 0 else pad in + String.sub s (l - (l mod 16)) (l mod 16), + pad + in + let pad_chars = pad * 2 + (pad + 1) / 2 + (if pad > 8 then 1 else 0) + 1 in + Format.pp_print_string ppf (String.make pad_chars ' '); + String.iter (fun c -> + Format.pp_print_char ppf (if printable_ascii c then c else '.')) + last_n); + if String.length s mod 16 <> 0 then + Format.pp_print_string ppf "\n" diff --git a/ohex.mli b/ohex.mli new file mode 100644 index 0000000..cfd6f74 --- /dev/null +++ b/ohex.mli @@ -0,0 +1,44 @@ +(** Convert from and to hex representation. *) + +val count_hex_chars : ?skip_whitespace:bool -> string -> int +(** [count_hex_chars ~skip_whitespace s] counts the amount of hex characters in + the string [s]. The argument [skip_whitespace] defaults to [true], and skips + any whitespace characters (' ', '\n', '\r', '\t'). This function is useful + for estimating the space required for [decode_into]. *) + +val decode : ?skip_whitespace:bool -> string -> string +(** [decode ~skip_whitespace s] decodes a hex string [s] into a sequence of + octets. The argument [skip_whitespace] defaults to [true], and skips any + whitespace characters in [s] (' ', '\n', '\r', '\t'). An example: + [decode "4142" = "AB"]. + + @raise Invalid_argument if any character in [s] is not a hex character, or + an odd amount of characters are present. *) + +val decode_into : ?skip_whitespace:bool -> string -> bytes -> ?off:int -> unit + -> unit +(** [decode_into ~skip_whitespace s dst ~off ()] decodes [s] into [dst] + starting at [off] (defaults to 0). The argument [skip_whitespace] defaults + to [true] and skips any whitespace characters. + + @raise Invalid_argument if any character in [s] is not a hex character, an + odd amount of characters are present, or [dst] does not contain enough + space. *) + +val encode : string -> string +(** [encode s] encodes [s] into a freshly allocated string of double size, where + each character in [s] is encoded as two hex digits in the returned string. *) + +val encode_into : string -> bytes -> ?off:int -> unit -> unit +(** [encode_into s dst ~off ()] encodes [s] into [dst] starting at [off] + (defaults to 0). Each character is encoded as two hex digits in [dst]. + + @raise Invalid_argument if [dst] does not contain enough space. *) + +val pp : ?row_numbers:bool -> ?chars:bool -> unit -> + Format.formatter -> string -> unit +(** [pp ~row_numbers ~chars () ppf s] pretty-prints the string [s] in + hexadecimal (similar to [hexdump -C]). If [row_numbers] is provided + (defaults to [true]), each output line is prefixed with the row number. + If [chars] is provided (defaults to [true]), in the last column the ASCII + string is printed (non-printable characters are printed as '.'). *) diff --git a/ohex.opam b/ohex.opam new file mode 100644 index 0000000..54c6e97 --- /dev/null +++ b/ohex.opam @@ -0,0 +1,22 @@ +opam-version: "2.0" +maintainer: "Hannes Mehnert " +authors: "Hannes Mehnert " +license: "ISC" +homepage: "https://git.robur.coop/robur/ohex" +doc: "https://robur-coop.github.io/ohex/doc" +bug-reports: "https://git.robur.coop/robur/ohex/issues" +depends: [ + "ocaml" {>= "4.08.0"} + "dune" + "alcotest" {with-test} +] +build: [ + ["dune" "subst"] {dev} + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] +dev-repo: "git+https://git.robur.coop/robur/ohex.git" +synopsis: "Hexadecimal encoding and decoding" +description: """ +A library to encode and decode hexadecimal byte sequences. +"""