├── .gitignore ├── .gitmodules ├── .ocamlformat ├── CHANGES.md ├── LICENSE ├── README.md ├── cid.opam ├── doc └── prelude.txt ├── dune ├── dune-project ├── src ├── cid.ml ├── cid.mli ├── cid_intf.ml ├── dune └── index.mld └── test ├── README.md ├── dune ├── irmin_cid.ml └── test.ml /.gitignore: -------------------------------------------------------------------------------- 1 | *.annot 2 | *.cmo 3 | *.cma 4 | *.cmi 5 | *.a 6 | *.o 7 | *.cmx 8 | *.cmxs 9 | *.cmxa 10 | 11 | # ocamlbuild working directory 12 | _build/ 13 | 14 | # ocamlbuild targets 15 | *.byte 16 | *.native 17 | 18 | # oasis generated files 19 | setup.data 20 | setup.log 21 | 22 | # Merlin configuring file for Vim and Emacs 23 | .merlin 24 | 25 | # Dune generated files 26 | *.install 27 | 28 | # Local OPAM switch 29 | _opam/ 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patricoferris/ocaml-cid/4f665cacb56e73462b690dcc793f1c6ec173c7b5/.gitmodules -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.25.1 -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # v0.1.0 (22/03/2023) Cambridge 2 | 3 | - Initial public release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Patrick Ferris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ocaml-cid 2 | --------- 3 | 4 | [Content-addressed identifiers](https://docs.ipfs.io/concepts/content-addressing/) in OCaml. 5 | 6 | 7 | ## Quick Example 8 | 9 | ```ocaml 10 | # let s = "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA";; 11 | val s : string = "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA" 12 | # let cid = Cid.of_string s |> Result.get_ok;; 13 | val cid : Cid.t = 14 | # Cid.pp_human Format.std_formatter cid;; 15 | cidv1 - base58btc - raw - ident(sha2-256) length(32) digest(6e 6f f7 95 0a 36 18 7a 80 16 13 42 6e 85 8d ce 16 | 68 6c d7 d7 e3 c0 fc 42 ee 03 30 07 2d 24 5c 95 17 | ) 18 | - : unit = () 19 | # Cid.to_string cid;; 20 | - : string = "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA" 21 | ``` 22 | 23 | See [test/irmin_cid.ml](https://github.com/patricoferris/ocaml-cid/blob/main/test/irmin_cid.ml) to see how they can be used for Irmin store hashing. 24 | -------------------------------------------------------------------------------- /cid.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Content-addressed Identifiers" 4 | maintainer: ["patrick@sirref.org"] 5 | authors: ["patrick@sirref.org"] 6 | homepage: "https://github.com/patricoferris/ocaml-cid" 7 | bug-reports: "https://github.com/patricoferris/ocaml-cid/issues" 8 | depends: [ 9 | "dune" {>= "3.2"} 10 | "multibase" 11 | "multicodec" 12 | "multihash-digestif" 13 | "alcotest" {with-test} 14 | "mdx" {with-test & >= "2.2.1"} 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ] 31 | dev-repo: "git+https://github.com/patricoferris/ocaml-cid.git" -------------------------------------------------------------------------------- /doc/prelude.txt: -------------------------------------------------------------------------------- 1 | #require "digestif.c";; 2 | #require "cid";; -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (mdx 2 | (libraries cid) 3 | (preludes doc/prelude.txt) 4 | (files README.md)) 5 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.2) 2 | (using mdx 0.3) 3 | (name cid) 4 | -------------------------------------------------------------------------------- /src/cid.ml: -------------------------------------------------------------------------------- 1 | include Cid_intf 2 | 3 | module Make (Md : Multihash.S) = struct 4 | type multihash = Cstruct.t Md.t 5 | 6 | (* Compatible with Multicodec.ipld *) 7 | 8 | let version_string = function 9 | | `Cidv0 -> "Cidv0" 10 | | (`Cidv1 | `Cidv2 | `Cidv3) as v -> Multicodec.cid_to_string v 11 | 12 | let version_of_ipld = function 13 | | (`Cidv1 | `Cidv2 | `Cidv3) as v -> v 14 | | _ -> invalid_arg "Expected a CID version" 15 | 16 | type t = { 17 | version : version; 18 | base : Multibase.Encoding.t; 19 | codec : Multicodec.t; 20 | hash : Cstruct.t Md.t; 21 | } 22 | 23 | let v ~version ~base ~codec ~hash = { version; base; codec; hash } 24 | let version t = t.version 25 | let base t = t.base 26 | let codec t = t.codec 27 | let hash t = t.hash 28 | 29 | let equal a b = 30 | a.version = b.version && a.base = b.base && a.codec = b.codec 31 | && Md.equal a.hash b.hash 32 | 33 | let cidv0_of_string ~base buf = 34 | match Md.read_buff buf with 35 | | Ok hash -> { version = `Cidv0; base; codec = `Dag_pb; hash } 36 | | Error (`Msg m) -> failwith ("Failed parsing Cidv0: " ^ m) 37 | 38 | let of_cstruct ~base buf = 39 | let l = Cstruct.length buf in 40 | if 41 | l = 34 && Cstruct.get_uint8 buf 0 = 0x12 && Cstruct.get_uint8 buf 1 = 0x20 42 | then Ok (cidv0_of_string ~base buf) 43 | else 44 | let v, off = Multihash.Uvarint.decode buf in 45 | let c, off' = Multihash.Uvarint.decode Cstruct.(sub buf off (l - off)) in 46 | match Md.read_buff Cstruct.(sub buf (off + off') (l - off - off')) with 47 | | Ok hash -> 48 | let version = 49 | version_of_ipld (Option.get @@ Multicodec.cid_of_code v) 50 | in 51 | let codec = Option.get @@ Multicodec.of_code c in 52 | let v = Ok { version; codec; base; hash } in 53 | (v 54 | :> ( t, 55 | [ `Msg of string | `Unsupported of Multibase.Encoding.t ] ) 56 | result) 57 | | Error _ as e -> 58 | (e 59 | :> ( t, 60 | [ `Msg of string | `Unsupported of Multibase.Encoding.t ] ) 61 | result) 62 | 63 | let ( <+> ) = Cstruct.append 64 | 65 | let to_cstruct { version; base; codec; hash } = 66 | match version with 67 | | `Cidv0 -> ( 68 | match base with 69 | | `Base58btc -> 70 | let hash = Md.write hash in 71 | let b = Multibase.Base58.encode (Cstruct.to_string hash) in 72 | Cstruct.of_string b 73 | | _ -> 74 | let hash = Md.write hash in 75 | let b = 76 | Multibase.encode base (Cstruct.to_string hash) |> Result.get_ok 77 | in 78 | Cstruct.of_string b) 79 | | (`Cidv1 | `Cidv2 | `Cidv3) as version -> 80 | let enc = 81 | (* TODO: when dropping support of older compilers, we can change to String.get_uint8 *) 82 | Multibase.Encoding.to_code base |> Bytes.of_string |> fun s -> 83 | Bytes.get_uint8 s 0 84 | in 85 | let ver = 86 | Multicodec.cid_to_code (version :> Multicodec.cid) 87 | |> Multihash.Uvarint.encode 88 | in 89 | let cod = Multicodec.to_code codec |> Multihash.Uvarint.encode in 90 | let has = Md.write hash in 91 | let buf = Cstruct.create 1 in 92 | Cstruct.set_uint8 buf 0 enc; 93 | buf <+> ver <+> cod <+> has 94 | 95 | let fail_encoding = function 96 | | Error (`Msg s) -> failwith s 97 | | Error (`Unsupported b) -> 98 | failwith ("Unsupported encoding: " ^ Multibase.Encoding.to_string b) 99 | | Ok v -> v 100 | 101 | let to_string t = 102 | let buf = to_cstruct t in 103 | if t.version = `Cidv0 then Cstruct.to_string buf 104 | else 105 | let data = Cstruct.(to_string (sub buf 1 (length buf - 1))) in 106 | Multibase.encode t.base data |> fail_encoding 107 | 108 | let of_string s = 109 | if String.length s = 46 && s.[0] = 'Q' && s.[1] = 'm' then 110 | let v = 111 | of_cstruct ~base:`Base58btc 112 | (Cstruct.of_string @@ Multibase.Base58.decode s) 113 | in 114 | (v 115 | :> (t, [ `Msg of string | `Unsupported of Multibase.Encoding.t ]) result) 116 | else 117 | match Multibase.decode s with 118 | | Ok (base, s) -> 119 | let v = of_cstruct ~base (Cstruct.of_string s) in 120 | (v 121 | :> ( t, 122 | [ `Msg of string | `Unsupported of Multibase.Encoding.t ] ) 123 | result) 124 | | Error _ as e -> e 125 | 126 | let pp_human ppf { version; base; codec; hash } = 127 | Format.fprintf ppf "%s - %s - %s - %a" (version_string version) 128 | (Multibase.Encoding.to_string base) 129 | (Multicodec.to_string codec) 130 | Md.pp hash 131 | end 132 | 133 | include Make (Multihash_digestif) 134 | -------------------------------------------------------------------------------- /src/cid.mli: -------------------------------------------------------------------------------- 1 | include Cid_intf.Intf 2 | (** @inline *) 3 | -------------------------------------------------------------------------------- /src/cid_intf.ml: -------------------------------------------------------------------------------- 1 | type version = [ `Cidv0 | `Cidv1 | `Cidv2 | `Cidv3 ] 2 | 3 | module type S = sig 4 | type multihash 5 | (** The type for multihashes *) 6 | 7 | type t 8 | (** A content-addressed identifier. *) 9 | 10 | val v : 11 | version:version -> 12 | base:Multibase.Encoding.t -> 13 | codec:Multicodec.t -> 14 | hash:multihash -> 15 | t 16 | (** Build a CID, this performs no checks on any of the inputs *) 17 | 18 | val version : t -> version 19 | (** The CID version. *) 20 | 21 | val base : t -> Multibase.Encoding.t 22 | (** The multibase encoding of the CID. *) 23 | 24 | val codec : t -> Multicodec.t 25 | (** The multicodec type of the data *) 26 | 27 | val hash : t -> multihash 28 | (** The multihash of the CID *) 29 | 30 | val equal : t -> t -> bool 31 | (** Tests the equality of two CIDs. *) 32 | 33 | val of_string : 34 | string -> 35 | (t, [ `Msg of string | `Unsupported of Multibase.Encoding.t ]) result 36 | (** [of_string s] takes an encoded string [s] that is the CID and 37 | pulls out each of the parts that make it up. *) 38 | 39 | val of_cstruct : 40 | base:Multibase.Encoding.t -> 41 | Cstruct.t -> 42 | (t, [ `Msg of string | `Unsupported of Multibase.Encoding.t ]) result 43 | (** [of_cstruct ~base buf] builds a value representing a CID. The buffer 44 | should not be encoded with the multibase encoding. *) 45 | 46 | val to_string : t -> string 47 | (** [to_string t] converts the CID to a multibase encoded string. Errors happen 48 | if the base encoding is not supported. This may raise an exception of the base 49 | encoding format is not supported. *) 50 | 51 | val to_cstruct : t -> Cstruct.t 52 | (** [to_cstruct t] returns a buffer with the bytes corresponding to the unencoded 53 | CID. *) 54 | 55 | val pp_human : Format.formatter -> t -> unit 56 | (** Pretty-prints a CID. *) 57 | end 58 | 59 | module type Intf = sig 60 | module type S = S 61 | 62 | module Make (H : Multihash.S) : S with type multihash = Cstruct.t H.t 63 | include S with type multihash = Cstruct.t Multihash_digestif.t 64 | end 65 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name cid) 3 | (public_name cid) 4 | (libraries multihash-digestif multibase multicodec)) 5 | 6 | (documentation 7 | (package cid)) 8 | -------------------------------------------------------------------------------- /src/index.mld: -------------------------------------------------------------------------------- 1 | {0 Content Identifiers} 2 | 3 | The {! Cid} library provides a pure OCaml interface to {e Content Identifiers}. 4 | These are like pointers to bits of data derived from the data itself. They are 5 | based on the cryptographic hash of the data along with extra information describing 6 | things like the encoding of the data, the hash function used to produce the digest etc. 7 | 8 | For a thorough handling of this information see {{: https://docs.ipfs.tech/concepts/content-addressing/} the IPFS documentation}. 9 | 10 | {1 Quick Example} 11 | 12 | A very small example using {! Cid.of_string} and the human-readable pretty printer {! Cid.pp_human}. 13 | 14 | {[ 15 | let s = "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA" 16 | let cid = Cid.of_string s |> Result.get_ok 17 | let () = Cid.pp_human Format.std_formatter cid 18 | ]} 19 | 20 | Which would print the following: 21 | 22 | {[ 23 | cidv1 - base58btc - raw - ident(sha2-256) length(32) 24 | digest(6e 6f f7 95 0a 36 18 7a 80 16 13 42 6e 85 8d ce 25 | 68 6c d7 d7 e3 c0 fc 42 ee 03 30 07 2d 24 5c 95) 26 | ]} 27 | 28 | If instead we used {! Cid.to_string} this will base encode this for us producing the original input. 29 | 30 | The repository also {{: https://github.com/patricoferris/ocaml-cid/blob/main/test/irmin_cid.ml} contains an example of 31 | using Irmin and CIDs together}. This is similar to the idea of using CIDs in IPFS. -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | [test.ml](./test.ml) contains some Alcotests. 4 | 5 | [irmin_cid.ml](./irmin_cid.ml) shows how you can replace Irmin's hash implementation with one using CIDs! 6 | 7 | ```sh 8 | $ ./irmin_cid.exe true 9 | Base encoded: bafkreicgjlwq6dwczrckohwxhp4axb4axtr33wnpehnw5qslofisgesrva 10 | Human: cidv1 - base32 - raw - ident(sha2-256) length(32) digest( 11 | 46 4a ed 0f 0e c2 cc 44 a7 1e d7 3b f8 0b 87 80 12 | bc e3 bd d9 af 21 db 6e c2 4b 71 51 23 12 51 a8 13 | ) 14 | ``` 15 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name test) 3 | (modules test) 4 | (libraries alcotest cid)) 5 | 6 | ; Enable once we're 4.10+ 7 | ; (test 8 | ; (name irmin_cid) 9 | ; (modules irmin_cid) 10 | ; (libraries lwt.unix irmin irmin-test irmin.mem cid)) 11 | 12 | ; (mdx 13 | ; (deps irmin_cid.exe) 14 | ; (package cid)) 15 | -------------------------------------------------------------------------------- /test/irmin_cid.ml: -------------------------------------------------------------------------------- 1 | let without_tests = 2 | try 3 | ignore Sys.argv.(1); 4 | true 5 | with _ -> false 6 | 7 | let () = if without_tests then Logs.set_reporter Logs.nop_reporter 8 | 9 | module Schema = struct 10 | include Irmin.Schema.KV (Irmin.Contents.String) 11 | module Md = Multihash_digestif 12 | 13 | module Hash = struct 14 | type t = Cid.t 15 | 16 | let hash = `Sha2_256 17 | let cid = Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Raw 18 | 19 | let get_64_little_endian str idx = 20 | if Sys.big_endian then Cstruct.BE.get_uint64 str idx 21 | else Cstruct.LE.get_uint64 str idx 22 | 23 | let short_hash c = 24 | Int64.to_int (get_64_little_endian (Md.write (Cid.hash c)) 0) 25 | 26 | let short_hash_substring bigstring ~off = 27 | Int64.to_int (Bigstringaf.get_int64_le bigstring off) 28 | 29 | let hash_size = 30 | Md.of_cstruct hash Cstruct.empty 31 | |> Result.get_ok |> Md.write |> Cstruct.length 32 | 33 | let of_string s = 34 | match Cid.of_string s with 35 | | Ok _ as v -> v 36 | | Error (`Msg _) as v -> v 37 | | Error (`Unsupported b) -> 38 | Error 39 | (`Msg ("Unsupported encoding " ^ Multibase.Encoding.to_string b)) 40 | 41 | let pp ppf cid = Fmt.string ppf (Cid.to_string cid) 42 | 43 | let read v = 44 | let hash = Result.get_ok (Md.read_buff (Cstruct.of_string v)) in 45 | cid ~hash 46 | 47 | let write v = Md.write (Cid.hash v) |> Cstruct.to_string 48 | 49 | let t = 50 | let open Irmin in 51 | Type.map ~pp ~of_string Type.(string_of (`Fixed hash_size)) read write 52 | 53 | let convert (f : (string -> unit) -> unit) : (Cstruct.t -> unit) -> unit = 54 | fun s -> f (fun buf -> s (Cstruct.of_string buf)) 55 | 56 | let hash (f : (string -> unit) -> unit) : t = 57 | let hash = Md.iter_cstruct hash (convert f) |> Result.get_ok in 58 | cid ~hash 59 | 60 | let to_raw_string = write 61 | let unsafe_of_raw_string = read 62 | end 63 | end 64 | 65 | module Store = Irmin_mem.Make (Schema) 66 | 67 | let main () = 68 | let open Lwt.Syntax in 69 | let config = Irmin_mem.config () in 70 | let* repo = Store.Repo.v config in 71 | let* main = Store.main repo in 72 | let content = "foo" in 73 | let* () = Store.set_exn ~info:Store.Info.none main [ "a" ] content in 74 | Fmt.pr "Base encoded: %a\nHuman: %a\n" 75 | Irmin.Type.(pp Store.hash_t) 76 | (Store.Contents.hash content) 77 | Cid.pp_human 78 | (Store.Contents.hash content); 79 | Lwt.return_unit 80 | 81 | let store = (module Store : Irmin_test.S) 82 | let suite config = Irmin_test.Suite.create ~name:"MEM.CID" ~store ~config () 83 | 84 | let () = 85 | if not without_tests then 86 | Lwt_main.run 87 | (Irmin_test.Store.run "irmin-mem-cid" ~slow:false ~misc:[] 88 | ~sleep:Lwt_unix.sleep 89 | [ (`Quick, suite (Irmin_mem.config ())) ]); 90 | Lwt_main.run (main ()) 91 | -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | module Md = Multihash_digestif 2 | 3 | let tests = 4 | [ 5 | ( "zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA", 6 | Cid.v ~version:`Cidv1 ~base:`Base58btc ~codec:`Raw 7 | ~hash: 8 | (Md.v `Sha2_256 (256 / 8) 9 | (Cstruct.of_hex 10 | "6E6FF7950A36187A801613426E858DCE686CD7D7E3C0FC42EE0330072D245C95")) 11 | ); 12 | ( "bafyreihdb57fdysx5h35urvxz64ros7zvywshber7id6t6c6fek37jgyfe", 13 | Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Dag_cbor 14 | ~hash: 15 | (Md.v `Sha2_256 (256 / 8) 16 | (Cstruct.of_hex 17 | "E30F7E51E257E9F7DA46B7CFB9174BF9AE2D238491FA07E9F85E2915BFA4D829")) 18 | ); 19 | ( "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy", 20 | Cid.v ~version:`Cidv1 ~base:`Base32 ~codec:`Raw 21 | ~hash: 22 | (Md.v `Sha2_256 (256 / 8) 23 | (Digestif.SHA256.digest_string "foo" 24 | |> Digestif.SHA256.to_hex |> Cstruct.of_hex)) ); 25 | ( "QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V", 26 | Cid.v ~version:`Cidv0 ~base:`Base58btc ~codec:`Dag_pb 27 | ~hash: 28 | (Md.v `Sha2_256 (256 / 8) 29 | (Cstruct.of_hex 30 | "8AB7A6C5E74737878AC73863CB76739D15D4666DE44E5756BF55A2F9E9AB5F44")) 31 | ); 32 | ( "mAVUSICwmtGto/8aP+ZtFPB0wQTQTQi1wZIO/oPmKXohiZueu", 33 | Cid.v ~version:`Cidv1 ~base:`Base64 ~codec:`Raw 34 | ~hash: 35 | (Md.v `Sha2_256 (256 / 8) 36 | (Digestif.SHA256.digest_string "foo" 37 | |> Digestif.SHA256.to_hex |> Cstruct.of_hex)) ); 38 | ] 39 | 40 | let cid = Alcotest.testable Cid.pp_human Cid.equal 41 | 42 | let pp_err ppf = function 43 | | `Msg m -> Fmt.string ppf m 44 | | `Unsupported _ -> Fmt.string ppf "Unsupported" 45 | 46 | let err = Alcotest.of_pp pp_err 47 | 48 | let tests = 49 | let test (expected_enc, expected) () = 50 | let actual = Cid.of_string expected_enc in 51 | Alcotest.(check (result cid err)) "same cid" actual (Ok expected); 52 | let actual_enc = Result.map Cid.to_string actual in 53 | Alcotest.(check (result string err)) 54 | "same encoding" (Ok expected_enc) actual_enc 55 | in 56 | List.mapi 57 | (fun i v -> Alcotest.test_case ("cid " ^ string_of_int i) `Quick (test v)) 58 | tests 59 | 60 | let () = Alcotest.run "CID" [ ("enc-dec", tests) ] 61 | --------------------------------------------------------------------------------