open Crowbar exception Encode_error of string exception Decode_error of string (** Pretty printers *) let register_printer () = Printexc.register_printer (function | Encode_error err -> Some (Fmt.str "(Encoding error: %s)" err) | Decode_error err -> Some (Fmt.str "(Decoding error: %s)" err) | _ -> None) let pp_chr = let escaped = function ' ' .. '~' as c -> String.make 1 c | _ -> "." in Fmt.using escaped Fmt.string let pp_scalar : type buffer. get:(buffer -> int -> char) -> length:(buffer -> int) -> buffer Fmt.t = fun ~get ~length ppf b -> let l = length b in for i = 0 to l / 16 do Fmt.pf ppf "%08x: " (i * 16) ; let j = ref 0 in while !j < 16 do if (i * 16) + !j < l then Fmt.pf ppf "%02x" (Char.code @@ get b ((i * 16) + !j)) else Fmt.pf ppf " " ; if !j mod 2 <> 0 then Fmt.pf ppf " " ; incr j done ; Fmt.pf ppf " " ; j := 0 ; while !j < 16 do if (i * 16) + !j < l then Fmt.pf ppf "%a" pp_chr (get b ((i * 16) + !j)) else Fmt.pf ppf " " ; incr j done ; Fmt.pf ppf "@\n" done let pp = pp_scalar ~get:String.get ~length:String.length (** Encoding and decoding *) let check_encode str = let subs = Astring.String.cuts ~sep:"\r\n" str in let check str = if String.length str > 78 then raise (Encode_error "too long string returned") in List.iter check subs ; str let encode input = let buf = Buffer.create 80 in let encoder = Base64_rfc2045.encoder (`Buffer buf) in String.iter (fun c -> let ret = Base64_rfc2045.encode encoder (`Char c) in match ret with `Ok -> () | _ -> assert false) (* XXX(dinosaure): [`Partial] can never occur. *) input ; let encode = Base64_rfc2045.encode encoder `End in match encode with | `Ok -> Buffer.contents buf |> check_encode | _ -> (* XXX(dinosaure): [`Partial] can never occur. *) assert false let decode input = let decoder = Base64_rfc2045.decoder (`String input) in let rec go acc = if Base64_rfc2045.decoder_dangerous decoder then raise (Decode_error "Dangerous input") ; match Base64_rfc2045.decode decoder with | `End -> List.rev acc | `Flush output -> go (output :: acc) | `Malformed _ -> raise (Decode_error "Malformed") | `Wrong_padding -> raise (Decode_error "Wrong padding") | _ -> (* XXX(dinosaure): [`Await] can never occur. *) assert false in String.concat "" (go []) (** String generators *) let bytes_fixed_range : string gen = dynamic_bind (range 78) bytes_fixed let char_from_alpha alpha : string gen = map [ range (String.length alpha) ] (fun i -> alpha.[i] |> String.make 1) let string_from_alpha n = let acc = const "" in let alpha = Base64_rfc2045.default_alphabet in let rec add_char_from_alpha alpha acc = function | 0 -> acc | n -> add_char_from_alpha alpha (concat_gen_list (const "") [ acc; char_from_alpha alpha ]) (n - 1) in add_char_from_alpha alpha acc n let random_string_from_alpha n = dynamic_bind (range n) string_from_alpha let bytes_fixed_range_from_alpha : string gen = dynamic_bind (range 78) bytes_fixed let set_canonic str = let l = String.length str in let to_drop = l * 6 mod 8 in if to_drop = 6 (* XXX(clecat): Case when we need to drop 6 bits which means a whole letter *) then String.sub str 0 (l - 1) else if to_drop <> 0 (* XXX(clecat): Case when we need to drop 2 or 4 bits: we apply a mask droping the bits *) then ( let buf = Bytes.of_string str in let value = String.index Base64_rfc2045.default_alphabet (Bytes.get buf (l - 1)) in let canonic = Base64_rfc2045.default_alphabet.[value land lnot ((1 lsl to_drop) - 1)] in Bytes.set buf (l - 1) canonic ; Bytes.unsafe_to_string buf) else str let add_padding str = let str = set_canonic str in let str = str ^ "===" in String.sub str 0 (String.length str / 4 * 4) (** Tests *) let e2d inputs = let input = String.concat "\r\n" inputs in let encode = encode input in let decode = decode encode in check_eq ~pp ~cmp:String.compare ~eq:String.equal input decode let d2e inputs end_input = let end_input = add_padding end_input in let inputs = inputs @ [ end_input ] in let input = List.fold_left (fun acc s -> if String.length s <> 0 then acc ^ "\r\n" ^ s else acc) (List.hd inputs) (List.tl inputs) in let decode = decode input in let encode = encode decode in check_eq ~pp ~cmp:String.compare ~eq:String.equal input encode let () = register_printer () ; add_test ~name:"rfc2045: encode -> decode" [ list bytes_fixed_range ] e2d ; add_test ~name:"rfc2045: decode -> encode" [ list (string_from_alpha 76); random_string_from_alpha 76 ] d2e