OCaml library for Crockford's Base32

initial import

+2
.gitignore
···
+
_build
+
.claude
+22
LICENSE.md
···
+
MIT License
+
+
Copyright (c) 2024-2025 Front Matter
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+95
README.md
···
+
# Crockford Base32 Encoding for OCaml
+
+
An OCaml implementation of [Douglas Crockford's
+
Base32](https://www.crockford.com/base32.html) encoding with ISO 7064 checksum
+
support. Provides encoding and decoding of int64 values to URI-friendly base32
+
strings, with optional checksum validation, padding, splitting, and random ID
+
generation. Ported from <https://github.com/front-matter/commonmeta>.
+
+
## Installation
+
+
```bash
+
opam install crockford
+
```
+
+
Or add to your `dune-project`:
+
+
```scheme
+
(package
+
(depends
+
(crockford)))
+
```
+
+
## Usage
+
+
### Basic Encoding and Decoding
+
+
```ocaml
+
(* Encode a number *)
+
let encoded = Crockford.encode 1234567890L in
+
(* "16jkpa2" *)
+
+
(* Decode back to number *)
+
let decoded = Crockford.decode "16jkpa2" in
+
(* 1234567890L *)
+
```
+
+
### With Checksum
+
+
```ocaml
+
(* Encode with checksum *)
+
let encoded = Crockford.encode ~checksum:true 1234567890L in
+
(* "16jkpa2d" *)
+
+
(* Decode and validate checksum *)
+
let decoded = Crockford.decode ~checksum:true "16jkpa2d" in
+
(* 1234567890L - or raises Checksum_mismatch if invalid *)
+
```
+
+
### Formatted Output
+
+
```ocaml
+
(* Split with dashes for readability *)
+
let encoded = Crockford.encode ~split_every:4 1234567890L in
+
(* "16jk-pa2" *)
+
+
(* With minimum length (zero-padded) *)
+
let encoded = Crockford.encode ~min_length:10 1234L in
+
(* "000000016j" *)
+
```
+
+
### Random ID Generation
+
+
```ocaml
+
Random.self_init ();
+
+
(* Generate random IDs *)
+
let id = Crockford.generate ~length:8 ~checksum:true () in
+
(* e.g., "a3x7m9q5" *)
+
+
(* Generate formatted IDs *)
+
let id = Crockford.generate ~length:16 ~split_every:4 ~checksum:true () in
+
(* e.g., "7n2q-8xkm-5pwt-3hr9" *)
+
```
+
+
### Normalization
+
+
```ocaml
+
(* Handles common character confusions *)
+
let decoded = Crockford.decode "ILO" in (* Treated as "110" *)
+
let decoded = Crockford.decode "16-JK-PA" in (* Dashes ignored *)
+
```
+
+
## License
+
+
MIT License
+
+
## Author
+
+
Anil Madhavapeddy <anil@recoil.org>
+
(based on code from https://github.com/front-matter/commonmeta)
+
+
## Links
+
+
- [Homepage](https://tangled.org/@anil.recoil.org/ocaml-crockford)
+
- [Crockford Base32 Specification](https://www.crockford.com/base32.html)
+4
bin/dune
···
+
(executable
+
(name roguedoi)
+
(public_name roguedoi)
+
(libraries crockford cmdliner))
+32
bin/roguedoi.ml
···
+
(* roguedoi.ml - Generate random DOI identifiers with Crockford base32 encoding *)
+
+
let generate_doi prefix length split =
+
Random.self_init ();
+
let suffix = Crockford.generate ~length ~split_every:split ~checksum:true () in
+
Printf.printf "https://doi.org/%s/%s\n%!" prefix suffix
+
+
let () =
+
let open Cmdliner in
+
+
let prefix =
+
let doc = "DOI prefix to use (e.g., 10.59350)" in
+
Arg.(value & opt string "10.59350" & info ["p"; "prefix"] ~docv:"PREFIX" ~doc)
+
in
+
+
let length =
+
let doc = "Total length of the generated suffix (including checksum)" in
+
Arg.(value & opt int 10 & info ["l"; "length"] ~docv:"LENGTH" ~doc)
+
in
+
+
let split =
+
let doc = "Split the suffix every N characters with hyphens (0 = no splitting)" in
+
Arg.(value & opt int 5 & info ["s"; "split"] ~docv:"SPLIT" ~doc)
+
in
+
+
let generate_cmd =
+
let doc = "Generate a random DOI with Crockford base32 encoding" in
+
let info = Cmd.info "roguedoi" ~version:"0.1.0" ~doc in
+
Cmd.v info Term.(const generate_doi $ prefix $ length $ split)
+
in
+
+
exit (Cmd.eval generate_cmd)
+32
crockford.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Crockford Base32 encoding for OCaml"
+
description:
+
"An OCaml implementation of Douglas Crockford's Base32 encoding with ISO 7064 checksum support. Provides encoding and decoding of int64 values to URI-friendly base32 strings, with optional checksum validation, padding, splitting, and random ID generation."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.org/@anil.recoil.org/ocaml-crockford"
+
bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-crockford/issues"
+
depends: [
+
"dune" {>= "3.20"}
+
"ocaml" {>= "4.14.1"}
+
"odoc" {with-doc}
+
"alcotest" {with-test & >= "1.9.0"}
+
"cmdliner" {>= "1.1.0"}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]
+26
dune-project
···
+
(lang dune 3.20)
+
+
(name crockford)
+
+
(generate_opam_files true)
+
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.org/@anil.recoil.org/ocaml-crockford")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-crockford/issues")
+
(maintenance_intent "(latest)")
+
+
(package
+
(name crockford)
+
(synopsis "Crockford Base32 encoding for OCaml")
+
(description
+
"An OCaml implementation of Douglas Crockford's Base32 encoding with \
+
ISO 7064 checksum support. Provides encoding and decoding of int64 values \
+
to URI-friendly base32 strings, with optional checksum validation, padding, \
+
splitting, and random ID generation.")
+
(depends
+
(ocaml (>= 4.14.1))
+
(odoc :with-doc)
+
(alcotest (and :with-test (>= 1.9.0)))
+
(cmdliner (>= 1.1.0))))
+177
lib/crockford.ml
···
+
type invalid_length = { length: int; message: string }
+
type invalid_character = { char: char; message: string }
+
type invalid_checksum = { checksum: string; message: string }
+
type checksum_mismatch = { expected: int64; got: int64; identifier: string }
+
+
type decode_error =
+
| Invalid_length of invalid_length
+
| Invalid_character of invalid_character
+
| Invalid_checksum of invalid_checksum
+
| Checksum_mismatch of checksum_mismatch
+
+
exception Decode_error of decode_error
+
+
let pp_invalid_length fmt { length; message } =
+
Format.fprintf fmt "Invalid_length: length=%d, %s" length message
+
+
let pp_invalid_character fmt { char; message } =
+
Format.fprintf fmt "Invalid_character: char='%c', %s" char message
+
+
let pp_invalid_checksum fmt { checksum; message } =
+
Format.fprintf fmt "Invalid_checksum: checksum=%s, %s" checksum message
+
+
let pp_checksum_mismatch fmt { expected; got; identifier } =
+
Format.fprintf fmt "Checksum_mismatch: expected=%Ld, got=%Ld, identifier=%s"
+
expected got identifier
+
+
let pp_decode_error fmt = function
+
| Invalid_length e -> pp_invalid_length fmt e
+
| Invalid_character e -> pp_invalid_character fmt e
+
| Invalid_checksum e -> pp_invalid_checksum fmt e
+
| Checksum_mismatch e -> pp_checksum_mismatch fmt e
+
+
let encoding_chars = "0123456789abcdefghjkmnpqrstvwxyz"
+
+
let generate_checksum number =
+
Int64.(sub (add (sub 97L (rem (mul 100L number) 97L)) 1L) 0L)
+
+
let validate number ~checksum =
+
Int64.equal checksum (generate_checksum number)
+
+
let normalize str =
+
let len = String.length str in
+
let buf = Bytes.create len in
+
let rec process i j =
+
if i >= len then Bytes.sub_string buf 0 j
+
else
+
let c = String.get str i in
+
let c_lower = Char.lowercase_ascii c in
+
match c_lower with
+
| '-' -> process (i + 1) j
+
| 'i' | 'l' -> Bytes.set buf j '1'; process (i + 1) (j + 1)
+
| 'o' -> Bytes.set buf j '0'; process (i + 1) (j + 1)
+
| _ -> Bytes.set buf j c_lower; process (i + 1) (j + 1)
+
in
+
process 0 0
+
+
let encode ?(split_every=0) ?(min_length=0) ?(checksum=false) number =
+
let original_number = number in
+
+
(* Build base32 encoding *)
+
let rec build_encoding acc n =
+
if Int64.equal n 0L then acc
+
else
+
let remainder = Int64.to_int (Int64.rem n 32L) in
+
let n' = Int64.div n 32L in
+
build_encoding (encoding_chars.[remainder] :: acc) n'
+
in
+
+
let encoded_list =
+
if Int64.equal number 0L then ['0']
+
else build_encoding [] number
+
in
+
+
let encoded_str = String.concat "" (List.map (String.make 1) encoded_list) in
+
+
(* Adjust min_length if checksum is enabled *)
+
let adjusted_length =
+
if checksum && min_length > 2 then min_length - 2
+
else min_length
+
in
+
+
(* Pad with zeros if needed *)
+
let padded =
+
if adjusted_length > 0 && String.length encoded_str < adjusted_length then
+
String.make (adjusted_length - String.length encoded_str) '0' ^ encoded_str
+
else
+
encoded_str
+
in
+
+
(* Add checksum *)
+
let with_checksum =
+
if checksum then
+
let cs = generate_checksum original_number in
+
padded ^ Printf.sprintf "%02Ld" cs
+
else
+
padded
+
in
+
+
(* Split if requested *)
+
if split_every > 0 then
+
let len = String.length with_checksum in
+
let num_splits = (len + split_every - 1) / split_every in
+
let splits = Array.make num_splits "" in
+
for i = 0 to num_splits - 1 do
+
let start = i * split_every in
+
let chunk_len = min split_every (len - start) in
+
splits.(i) <- String.sub with_checksum start chunk_len
+
done;
+
String.concat "-" (Array.to_list splits)
+
else
+
with_checksum
+
+
let decode ?(checksum=false) str =
+
let encoded = normalize str in
+
+
let (encoded_part, checksum_value) =
+
if checksum then begin
+
if String.length encoded < 3 then
+
raise (Decode_error (Invalid_checksum {
+
checksum = encoded;
+
message = "encoded string too short for checksum"
+
}));
+
+
let cs_str = String.sub encoded (String.length encoded - 2) 2 in
+
let cs =
+
try Int64.of_string cs_str
+
with Failure _ ->
+
raise (Decode_error (Invalid_checksum {
+
checksum = cs_str;
+
message = "invalid checksum format"
+
}))
+
in
+
(String.sub encoded 0 (String.length encoded - 2), Some cs)
+
end else
+
(encoded, None)
+
in
+
+
(* Decode base32 *)
+
let number = ref 0L in
+
String.iter (fun c ->
+
number := Int64.mul !number 32L;
+
match String.index_opt encoding_chars c with
+
| Some pos -> number := Int64.add !number (Int64.of_int pos)
+
| None ->
+
raise (Decode_error (Invalid_character {
+
char = c;
+
message = Printf.sprintf "character '%c' not in base32 alphabet" c
+
}))
+
) encoded_part;
+
+
(* Validate checksum if present *)
+
(match checksum_value with
+
| Some cs ->
+
if not (validate !number ~checksum:cs) then
+
raise (Decode_error (Checksum_mismatch {
+
expected = generate_checksum !number;
+
got = cs;
+
identifier = str
+
}))
+
| None -> ());
+
+
!number
+
+
let generate ~length ?(split_every=0) ?(checksum=false) () =
+
if checksum && length < 3 then
+
raise (Decode_error (Invalid_length {
+
length;
+
message = "length must be >= 3 if checksum is enabled"
+
}));
+
+
let adjusted_length = if checksum then length - 2 else length in
+
+
(* Generate random number between 0 and 32^length *)
+
let max_val = 32.0 ** float_of_int adjusted_length in
+
let random_num = Int64.of_float (Random.float max_val) in
+
+
encode ~split_every ~min_length:adjusted_length ~checksum random_num
+89
lib/crockford.mli
···
+
(** Crockford Base32 encoding for OCaml *)
+
+
(** {1 Error Types} *)
+
+
type invalid_length = { length: int; message: string }
+
(** Error for invalid length parameters *)
+
+
type invalid_character = { char: char; message: string }
+
(** Error for invalid characters during decoding *)
+
+
type invalid_checksum = { checksum: string; message: string }
+
(** Error for invalid checksum format *)
+
+
type checksum_mismatch = { expected: int64; got: int64; identifier: string }
+
(** Error for checksum validation failures *)
+
+
type decode_error =
+
| Invalid_length of invalid_length
+
| Invalid_character of invalid_character
+
| Invalid_checksum of invalid_checksum
+
| Checksum_mismatch of checksum_mismatch
+
(** Union of all possible decode errors *)
+
+
exception Decode_error of decode_error
+
(** Main exception raised for all decoding errors *)
+
+
val pp_invalid_length : Format.formatter -> invalid_length -> unit
+
(** Pretty-print an invalid_length error *)
+
+
val pp_invalid_character : Format.formatter -> invalid_character -> unit
+
(** Pretty-print an invalid_character error *)
+
+
val pp_invalid_checksum : Format.formatter -> invalid_checksum -> unit
+
(** Pretty-print an invalid_checksum error *)
+
+
val pp_checksum_mismatch : Format.formatter -> checksum_mismatch -> unit
+
(** Pretty-print a checksum_mismatch error *)
+
+
val pp_decode_error : Format.formatter -> decode_error -> unit
+
(** Pretty-print a decode_error *)
+
+
(** {1 Constants} *)
+
+
val encoding_chars : string
+
(** The Crockford base32 encoding alphabet (excludes i, l, o, u) *)
+
+
(** {1 Encoding and Decoding} *)
+
+
val encode :
+
?split_every:int ->
+
?min_length:int ->
+
?checksum:bool ->
+
int64 -> string
+
(** [encode ?split_every ?min_length ?checksum n] encodes an int64 to a Crockford base32 string.
+
@param split_every Split the output with '-' every n characters (default: no splitting)
+
@param min_length Pad with zeros to this minimum length (default: no padding)
+
@param checksum Append ISO 7064 checksum as 2 digits (default: false) *)
+
+
val decode : ?checksum:bool -> string -> int64
+
(** [decode ?checksum str] decodes a Crockford base32 string to int64.
+
@param checksum Expect and validate ISO 7064 checksum (default: false)
+
@raise Decode_error if decoding fails (invalid characters, invalid checksum format, or checksum mismatch) *)
+
+
(** {1 ID Generation} *)
+
+
val generate :
+
length:int ->
+
?split_every:int ->
+
?checksum:bool ->
+
unit -> string
+
(** [generate ~length ?split_every ?checksum ()] generates a random Crockford base32 string.
+
@param length The length of the generated string (excluding checksum)
+
@param split_every Split the output with '-' every n characters (default: no splitting)
+
@param checksum Append ISO 7064 checksum as 2 digits (default: false)
+
@raise Decode_error if checksum is true and length < 3
+
+
Note: Caller must initialize Random module with {!Random.self_init} before use *)
+
+
(** {1 Utility Functions} *)
+
+
val normalize : string -> string
+
(** [normalize str] normalizes a string for decoding by converting to lowercase,
+
removing dashes, and mapping confusable characters (i→1, l→1, o→0) *)
+
+
val validate : int64 -> checksum:int64 -> bool
+
(** [validate n ~checksum] validates that a checksum matches the number *)
+
+
val generate_checksum : int64 -> int64
+
(** [generate_checksum n] generates an ISO 7064 (mod 97-10) checksum for a number *)
+3
lib/dune
···
+
(library
+
(name crockford)
+
(public_name crockford))
+3
test/dune
···
+
(test
+
(name test_crockford)
+
(libraries crockford alcotest))
+211
test/test_crockford.ml
···
+
(* Test suite using Alcotest - ported from crockford_test.go *)
+
+
(* Encode tests *)
+
let test_encode () =
+
let test_cases = [
+
(0L, 0, 0, false, "0");
+
(1234L, 0, 0, false, "16j");
+
(1234L, 2, 0, false, "16-j");
+
(1234L, 2, 4, false, "01-6j");
+
(538751765283013L, 5, 10, false, "f9zqn-sf065");
+
(736381604818L, 5, 10, true, "ndsw7-4yj20");
+
(258706475165200172L, 7, 14, true, "75rw5cg-n1bsc64");
+
(161006169L, 4, 8, true, "4shg-js75");
+
] in
+
List.iter (fun (input, split_every, length, checksum, expected) ->
+
let result = Crockford.encode ~split_every ~min_length:length ~checksum input in
+
let test_name = Printf.sprintf "encode %Ld (split=%d, len=%d, checksum=%b)" input split_every length checksum in
+
Alcotest.(check string) test_name expected result
+
) test_cases
+
+
(* Generate tests *)
+
let test_generate () =
+
Random.self_init ();
+
+
let test_cases = [
+
(4, 0, false);
+
(10, 5, false);
+
(10, 5, true);
+
] in
+
List.iter (fun (length, split_every, checksum) ->
+
let result = Crockford.generate ~length ~split_every ~checksum () in
+
let expected_length =
+
if split_every > 0 then
+
length + (length / split_every) - 1
+
else
+
length
+
in
+
let test_name = Printf.sprintf "generate length=%d split=%d checksum=%b" length split_every checksum in
+
Alcotest.(check int) test_name expected_length (String.length result)
+
) test_cases
+
+
(* Decode tests *)
+
let test_decode () =
+
let test_cases = [
+
("0", false, Some 0L);
+
("16j", false, Some 1234L);
+
("16-j", false, Some 1234L);
+
("01-6j", false, Some 1234L);
+
("f9zqn-sf065", false, Some 538751765283013L);
+
("twwjw-1ww98", true, Some 924377286556L);
+
("9ed5m-ytn", false, Some 324712168277L);
+
("9ed5m-ytn30", true, Some 324712168277L);
+
("elife.01567", false, None); (* Should fail - contains invalid character '.' *)
+
] in
+
List.iter (fun (input, checksum, expected) ->
+
let test_name = Printf.sprintf "decode '%s' (checksum=%b)" input checksum in
+
match expected with
+
| Some want ->
+
let got = Crockford.decode ~checksum input in
+
Alcotest.(check int64) test_name want got
+
| None ->
+
(* Expected to fail *)
+
Alcotest.check_raises
+
test_name
+
(Crockford.Decode_error (Crockford.Invalid_character {
+
char = '.';
+
message = "character '.' not in base32 alphabet"
+
}))
+
(fun () -> ignore (Crockford.decode ~checksum input))
+
) test_cases
+
+
(* Normalize tests *)
+
let test_normalize () =
+
let test_cases = [
+
("f9ZQNSF065", "f9zqnsf065");
+
("f9zqn-sf065", "f9zqnsf065");
+
("f9Llio", "f91110");
+
] in
+
List.iter (fun (input, expected) ->
+
let result = Crockford.normalize input in
+
let test_name = Printf.sprintf "normalize '%s'" input in
+
Alcotest.(check string) test_name expected result
+
) test_cases
+
+
(* GenerateChecksum tests *)
+
let test_generate_checksum () =
+
let test_cases = [
+
(450320459383L, 85L);
+
(123456789012L, 44L);
+
] in
+
List.iter (fun (input, expected) ->
+
let result = Crockford.generate_checksum input in
+
let test_name = Printf.sprintf "generate_checksum %Ld" input in
+
Alcotest.(check int64) test_name expected result
+
) test_cases
+
+
(* Validate tests *)
+
let test_validate () =
+
let test_cases = [
+
(375301249367L, 92L, true);
+
(930412369850L, 36L, true);
+
] in
+
List.iter (fun (input, checksum, expected) ->
+
let result = Crockford.validate input ~checksum in
+
let test_name = Printf.sprintf "validate %Ld checksum=%Ld" input checksum in
+
Alcotest.(check bool) test_name expected result
+
) test_cases
+
+
(* Additional roundtrip tests *)
+
let test_roundtrip () =
+
let test_numbers = [
+
0L;
+
1L;
+
32L;
+
1024L;
+
1234567890L;
+
Int64.max_int;
+
] in
+
+
List.iter (fun num ->
+
let encoded = Crockford.encode num in
+
let decoded = Crockford.decode encoded in
+
Alcotest.(check int64) (Printf.sprintf "roundtrip %Ld" num) num decoded
+
) test_numbers
+
+
let test_roundtrip_with_checksum () =
+
let test_numbers = [
+
0L;
+
1L;
+
32L;
+
1024L;
+
1234567890L;
+
] in
+
+
List.iter (fun num ->
+
let encoded = Crockford.encode ~checksum:true num in
+
let decoded = Crockford.decode ~checksum:true encoded in
+
Alcotest.(check int64) (Printf.sprintf "roundtrip with checksum %Ld" num) num decoded
+
) test_numbers
+
+
(* Error handling tests *)
+
let test_error_invalid_character () =
+
Alcotest.check_raises
+
"invalid character"
+
(Crockford.Decode_error (Crockford.Invalid_character {
+
char = '#';
+
message = "character '#' not in base32 alphabet"
+
}))
+
(fun () -> ignore (Crockford.decode "abc#def"))
+
+
let test_error_invalid_checksum () =
+
Alcotest.check_raises
+
"invalid checksum length"
+
(Crockford.Decode_error (Crockford.Invalid_checksum {
+
checksum = "ab";
+
message = "encoded string too short for checksum"
+
}))
+
(fun () -> ignore (Crockford.decode ~checksum:true "ab"))
+
+
let test_error_checksum_mismatch () =
+
let encoded = Crockford.encode ~checksum:true 1234L in
+
let corrupted = String.sub encoded 0 (String.length encoded - 2) ^ "00" in
+
+
Alcotest.check_raises
+
"checksum mismatch"
+
(Crockford.Decode_error (Crockford.Checksum_mismatch {
+
expected = Crockford.generate_checksum 1234L;
+
got = 0L;
+
identifier = corrupted
+
}))
+
(fun () -> ignore (Crockford.decode ~checksum:true corrupted))
+
+
let test_error_invalid_length () =
+
Alcotest.check_raises
+
"invalid length for generate"
+
(Crockford.Decode_error (Crockford.Invalid_length {
+
length = 2;
+
message = "length must be >= 3 if checksum is enabled"
+
}))
+
(fun () -> ignore (Crockford.generate ~length:2 ~checksum:true ()))
+
+
let () =
+
let open Alcotest in
+
run "Crockford" [
+
"encoding", [
+
test_case "encode" `Quick test_encode;
+
];
+
"decoding", [
+
test_case "decode" `Quick test_decode;
+
];
+
"normalization", [
+
test_case "normalize" `Quick test_normalize;
+
];
+
"checksum", [
+
test_case "generate_checksum" `Quick test_generate_checksum;
+
test_case "validate" `Quick test_validate;
+
];
+
"generation", [
+
test_case "generate" `Quick test_generate;
+
];
+
"roundtrip", [
+
test_case "roundtrip encoding/decoding" `Quick test_roundtrip;
+
test_case "roundtrip with checksum" `Quick test_roundtrip_with_checksum;
+
];
+
"errors", [
+
test_case "invalid character" `Quick test_error_invalid_character;
+
test_case "invalid checksum" `Quick test_error_invalid_checksum;
+
test_case "checksum mismatch" `Quick test_error_checksum_mismatch;
+
test_case "invalid length" `Quick test_error_invalid_length;
+
];
+
]