(** dag-cbor encode / decode — pure OCaml, WASM-safe. RFC 8949 deterministic subset as constrained by IPLD dag-cbor (RFC 8742): unsigned/negative ints, text strings, arrays, maps with keys sorted by **length-then-bytewise**, bool, null, and tag 42 (CID link, decode-side passthrough). Floats are not supported (no fed-sx shape needs them yet) — encoding a [Number] or decoding a float head raises. Reference: RFC 8949 §3, §4.2. *) open Sx_types exception Cbor_error of string (* ---- Encoder ---- *) let write_head buf major v = let m = major lsl 5 in if v < 24 then Buffer.add_char buf (Char.chr (m lor v)) else if v < 0x100 then begin Buffer.add_char buf (Char.chr (m lor 24)); Buffer.add_char buf (Char.chr v) end else if v < 0x10000 then begin Buffer.add_char buf (Char.chr (m lor 25)); Buffer.add_char buf (Char.chr ((v lsr 8) land 0xFF)); Buffer.add_char buf (Char.chr (v land 0xFF)) end else if v < 0x100000000 then begin Buffer.add_char buf (Char.chr (m lor 26)); for i = 3 downto 0 do Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) done end else begin Buffer.add_char buf (Char.chr (m lor 27)); for i = 7 downto 0 do Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) done end (* dag-cbor map key order: shorter key first, then bytewise. *) let key_order a b = let la = String.length a and lb = String.length b in if la <> lb then compare la lb else compare a b let rec encode_into buf (v : value) : unit = match v with | Integer n -> if n >= 0 then write_head buf 0 n else write_head buf 1 (-1 - n) | String s -> write_head buf 3 (String.length s); Buffer.add_string buf s | Symbol s | Keyword s -> write_head buf 3 (String.length s); Buffer.add_string buf s | Bool false -> Buffer.add_char buf '\xf4' | Bool true -> Buffer.add_char buf '\xf5' | Nil -> Buffer.add_char buf '\xf6' | List items -> write_head buf 4 (List.length items); List.iter (encode_into buf) items | Dict d -> let keys = Hashtbl.fold (fun k _ acc -> k :: acc) d [] in let keys = List.sort_uniq key_order keys in write_head buf 5 (List.length keys); List.iter (fun k -> write_head buf 3 (String.length k); Buffer.add_string buf k; encode_into buf (Hashtbl.find d k)) keys | Number _ -> raise (Cbor_error "cbor-encode: floats unsupported (dag-cbor subset)") | _ -> raise (Cbor_error ("cbor-encode: unencodable value " ^ type_of v)) let encode (v : value) : string = let buf = Buffer.create 64 in encode_into buf v; Buffer.contents buf (* ---- Decoder ---- *) let decode (s : string) : value = let pos = ref 0 in let len = String.length s in let byte () = if !pos >= len then raise (Cbor_error "cbor-decode: truncated"); let c = Char.code s.[!pos] in incr pos; c in let read_uint ai = if ai < 24 then ai else if ai = 24 then byte () else if ai = 25 then let a = byte () in let b = byte () in (a lsl 8) lor b else if ai = 26 then begin let v = ref 0 in for _ = 0 to 3 do v := (!v lsl 8) lor byte () done; !v end else if ai = 27 then begin let v = ref 0 in for _ = 0 to 7 do v := (!v lsl 8) lor byte () done; !v end else raise (Cbor_error "cbor-decode: bad additional info") in let read_bytes n = if !pos + n > len then raise (Cbor_error "cbor-decode: truncated"); let r = String.sub s !pos n in pos := !pos + n; r in let rec item () = let b = byte () in let major = b lsr 5 and ai = b land 0x1f in match major with | 0 -> Integer (read_uint ai) | 1 -> Integer (-1 - read_uint ai) | 2 -> String (read_bytes (read_uint ai)) | 3 -> String (read_bytes (read_uint ai)) | 4 -> let n = read_uint ai in List (List.init n (fun _ -> item ())) | 5 -> let n = read_uint ai in let d = make_dict () in for _ = 1 to n do let k = match item () with | String k -> k | _ -> raise (Cbor_error "cbor-decode: non-string map key") in Hashtbl.replace d k (item ()) done; Dict d | 6 -> (* Tag: tag-42 CID link → pass the inner item through. *) ignore (read_uint ai); item () | 7 -> (match ai with | 20 -> Bool false | 21 -> Bool true | 22 -> Nil | 23 -> Nil | _ -> raise (Cbor_error "cbor-decode: floats/simple unsupported (dag-cbor subset)")) | _ -> raise (Cbor_error "cbor-decode: bad major type") in let v = item () in v