Repository: mirage/decompress (ocaml)

Issue:

Pull request:
A possible static and exportable reverse binding of decompress
According ocamllabs/ocaml-ctypes#660, this is a way to produce a reverse binding of `decompress` to be usable by C sources.

Patch:
diff --git a/bench/dune b/bench/dune
index e9b55ce..3a9feac 100644
--- a/bench/dune
+++ b/bench/dune
@@ -6,12 +6,20 @@
   (names mclock bs))
  (preprocess
   (pps ppx_deriving_yojson))
- (libraries unix fmt decompress.zl cmdliner yojson ppx_deriving_yojson))
+ (libraries
+  bigarray-compat
+  unix
+  fmt
+  decompress.de
+  decompress.zl
+  cmdliner
+  yojson
+  ppx_deriving_yojson))
 
 (executable
  (name run)
  (modules run)
- (libraries cmdliner rresult bos))
+ (libraries cmdliner fpath rresult bos))
 
 (rule
  (targets zpipe)
@@ -35,7 +43,7 @@
  (name lz_landmarks)
  (optional)
  (modules lz_landmarks)
- (libraries landmarks de)
+ (libraries bigarray-compat checkseum optint landmarks de)
  (preprocess
   (pps landmarks.ppx --auto)))
 
@@ -48,4 +56,4 @@
 (executable
  (name lzld)
  (modules lzld)
- (libraries lz_landmarks))
+ (libraries decompress.de lz_landmarks))
diff --git a/bin/dune b/bin/dune
index 1fb0878..eb1f744 100644
--- a/bin/dune
+++ b/bin/dune
@@ -1,10 +1,12 @@
 (executable
  (name pipe)
  (modules pipe)
- (libraries checkseum.c bigstringaf fmt rresult mtime mtime.clock.os
-   decompress.de decompress.zl decompress.gz cmdliner))
-
-(executable
- (name lz)
- (modules lz)
- (libraries checkseum.c decompress.lz))
+ (package decompress)
+ (public_name decompress.pipe)
+ (libraries
+  checkseum.c
+  unix
+  decompress.de
+  decompress.zl
+  decompress.gz
+  cmdliner))
diff --git a/bin/lz.ml b/bin/lz.ml
deleted file mode 100644
index 2507f9a..0000000
--- a/bin/lz.ml
+++ /dev/null
@@ -1,15 +0,0 @@
-let () = Printexc.record_backtrace true
-
-open Lz
-
-let q = De.Queue.create 0x4000
-let w = make_window ~bits:15
-
-let () =
-  let state = state ~q ~w (`Channel stdin) in
-  let rec go state =
-    match compress state with
-    | `Flush -> De.Queue.reset q ; go state
-    | `End -> ()
-    | `Await -> assert false in
-  go state
diff --git a/bin/pipe.ml b/bin/pipe.ml
index df54c46..2d2456f 100644
--- a/bin/pipe.ml
+++ b/bin/pipe.ml
@@ -3,16 +3,23 @@ let l = De.Lz77.make_window ~bits:15
 let o = De.bigstring_create De.io_buffer_size
 let i = De.bigstring_create De.io_buffer_size
 let q = De.Queue.create 4096
+let str fmt = Format.asprintf fmt
+let error_msgf fmt = Format.kasprintf (fun err -> Error (`Msg err)) fmt
 
 let bigstring_input ic buf off len =
   let tmp = Bytes.create len in
   let len = input ic tmp 0 len in
-  Bigstringaf.blit_from_bytes tmp ~src_off:0 buf ~dst_off:off ~len
+  for i = 0 to len - 1 do
+    buf.{off + i} <- Bytes.get tmp i
+  done
   ; len
 
 let bigstring_output oc buf off len =
-  let tmp = Bigstringaf.substring buf ~off ~len in
-  output_string oc tmp
+  let res = Bytes.create len in
+  for i = 0 to len - 1 do
+    Bytes.set res i buf.{off + i}
+  done
+  ; output_string oc (Bytes.unsafe_to_string res)
 
 let run_inflate () =
   let open De in
@@ -27,11 +34,11 @@ let run_inflate () =
       bigstring_output stdout o 0 len
       ; Inf.flush decoder
       ; go ()
-    | `Malformed err -> Fmt.epr "%s\n%!" err ; `Error err
+    | `Malformed err -> `Error (false, str "%s." err)
     | `End ->
       let len = io_buffer_size - Inf.dst_rem decoder in
       if len > 0 then bigstring_output stdout o 0 len
-      ; `Ok () in
+      ; `Ok 0 in
   go ()
 
 let run_deflate () =
@@ -62,7 +69,7 @@ let run_deflate () =
     | `Ok | `Block -> compress () in
   Def.dst encoder o 0 io_buffer_size
   ; compress ()
-  ; `Ok ()
+  ; `Ok 0
 
 let run_zlib_inflate () =
   let open Zl in
@@ -78,13 +85,11 @@ let run_zlib_inflate () =
       let len = De.io_buffer_size - Inf.dst_rem decoder in
       bigstring_output stdout o 0 len
       ; Inf.flush decoder |> go
-    | `Malformed err ->
-      Fmt.epr "%si (remaining byte(s): %d)\n%!" err (Inf.dst_rem decoder)
-      ; `Error err
+    | `Malformed err -> `Error (false, str "%s." err)
     | `End decoder ->
       let len = De.io_buffer_size - Inf.dst_rem decoder in
       if len > 0 then bigstring_output stdout o 0 len
-      ; `Ok () in
+      ; `Ok 0 in
   go decoder
 
 let run_zlib_deflate () =
@@ -103,7 +108,7 @@ let run_zlib_deflate () =
     | `End encoder ->
       let len = De.io_buffer_size - Def.dst_rem encoder in
       if len > 0 then bigstring_output stdout o 0 len
-      ; `Ok () in
+      ; `Ok 0 in
   Def.dst encoder o 0 De.io_buffer_size |> go
 
 let run_gzip_inflate () =
@@ -119,19 +124,14 @@ let run_gzip_inflate () =
       let len = io_buffer_size - Inf.dst_rem decoder in
       bigstring_output stdout o 0 len
       ; Inf.flush decoder |> go
-    | `Malformed err ->
-      Fmt.epr "%s (remaining byte(s): %d)\n%!" err (Inf.dst_rem decoder)
-      ; `Error err
+    | `Malformed err -> `Error (false, str "%s." err)
     | `End decoder ->
       let len = io_buffer_size - Inf.dst_rem decoder in
       if len > 0 then bigstring_output stdout o 0 len
-      ; `Ok () in
+      ; `Ok 0 in
   go decoder
 
-(* XXX(dinosaure): UNSAFE! *)
-let now () =
-  let res = Mtime_clock.now () in
-  Int64.to_int32 (Mtime.to_uint64_ns res)
+let now () = Int32.of_float (Unix.gettimeofday ())
 
 let run_gzip_deflate () =
   let open Gz in
@@ -150,14 +150,17 @@ let run_gzip_deflate () =
     | `End encoder ->
       let len = io_buffer_size - Def.dst_rem encoder in
       if len > 0 then bigstring_output stdout o 0 len
-      ; `Ok () in
+      ; `Ok 0 in
   Def.dst encoder o 0 io_buffer_size |> go
 
 let run deflate format =
-  match format with
-  | `Deflate -> if deflate then run_deflate () else run_inflate ()
-  | `Zlib -> if deflate then run_zlib_deflate () else run_zlib_inflate ()
-  | `Gzip -> if deflate then run_gzip_deflate () else run_gzip_inflate ()
+  match deflate, format with
+  | true, `Deflate -> run_deflate ()
+  | false, `Deflate -> run_inflate ()
+  | true, `Zlib -> run_zlib_deflate ()
+  | false, `Zlib -> run_zlib_inflate ()
+  | true, `Gzip -> run_gzip_deflate ()
+  | false, `Gzip -> run_gzip_inflate ()
 
 open Cmdliner
 
@@ -171,11 +174,11 @@ let format =
     | "zlib" -> Ok `Zlib
     | "gzip" -> Ok `Gzip
     | "deflate" -> Ok `Deflate
-    | x -> Rresult.R.error_msgf "Invalid format: %S" x in
+    | x -> error_msgf "Invalid format: %S" x in
   let pp ppf = function
-    | `Zlib -> Fmt.pf ppf "zlib"
-    | `Gzip -> Fmt.pf ppf "gzip"
-    | `Deflate -> Fmt.pf ppf "deflate" in
+    | `Zlib -> Format.pp_print_string ppf "zlib"
+    | `Gzip -> Format.pp_print_string ppf "gzip"
+    | `Deflate -> Format.pp_print_string ppf "deflate" in
   let format = Arg.conv (parser, pp) in
   Arg.(value & opt format `Deflate & info ["f"; "format"])
 
@@ -184,11 +187,11 @@ let command =
   let exits = Term.default_exits in
   let man =
     [
-      `S "Description"
+      `S Manpage.s_description
     ; `P
         "$(tname) reads from standard input and writes the \
          compressed/decompressed data to standard output."
     ] in
-  Term.(pure run $ deflate $ format), Term.info "pipe" ~exits ~doc ~man
+  Term.(ret (const run $ deflate $ format)), Term.info "pipe" ~exits ~doc ~man
 
-let () = Term.(exit @@ eval command)
+let () = Term.(exit_status @@ eval command)
diff --git a/bindings/generator/dune b/bindings/generator/dune
new file mode 100644
index 0000000..f85a998
--- /dev/null
+++ b/bindings/generator/dune
@@ -0,0 +1,3 @@
+(executable
+ (name generate)
+ (libraries ctypes.stubs gen_decompress_bindings))
diff --git a/bindings/generator/generate.ml b/bindings/generator/generate.ml
new file mode 100644
index 0000000..1c7f853
--- /dev/null
+++ b/bindings/generator/generate.ml
@@ -0,0 +1,33 @@
+let generate dirname =
+  let prefix = "decompress" in
+  let path basename = Filename.concat dirname basename in
+  let ml_fd = open_out (path "decompress_bindings.ml") in
+  let c_fd = open_out (path "gen_decompress.c") in
+  let h_fd = open_out (path "decompress.h") in
+  let stubs =
+    (module Gen_decompress_bindings.Stubs : Cstubs_inverted.BINDINGS) in
+
+  Cstubs_inverted.write_ml (Format.formatter_of_out_channel ml_fd) ~prefix stubs
+  ; Format.fprintf
+      (Format.formatter_of_out_channel c_fd)
+      "#include \"decompress.h\"@\n %a%!"
+      (Cstubs_inverted.write_c ~prefix)
+      stubs
+  ; Format.fprintf
+      (Format.formatter_of_out_channel h_fd)
+      "#if defined(__cplusplus)@\n\
+      \ extern \"C\" {@\n\
+      \ #endif@\n\
+      \ %a@\n\
+      \ #if defined(__cplusplus)@\n\
+      \ }@\n\
+      \ #endif@\n\
+       %!"
+      (Cstubs_inverted.write_c_header ~prefix)
+      stubs
+
+  ; close_out h_fd
+  ; close_out c_fd
+  ; close_out ml_fd
+
+let () = generate Sys.argv.(1)
diff --git a/bindings/stubs/apply_bindings.ml b/bindings/stubs/apply_bindings.ml
new file mode 100644
index 0000000..9d62e18
--- /dev/null
+++ b/bindings/stubs/apply_bindings.ml
@@ -0,0 +1,1 @@
+include Gen_decompress_bindings.Stubs (Decompress_bindings)
diff --git a/bindings/stubs/decompress.ml b/bindings/stubs/decompress.ml
new file mode 100644
index 0000000..f1c4226
--- /dev/null
+++ b/bindings/stubs/decompress.ml
@@ -0,0 +1,1 @@
+module Apply_bindings = Apply_bindings
diff --git a/bindings/stubs/dune b/bindings/stubs/dune
new file mode 100644
index 0000000..d61a69c
--- /dev/null
+++ b/bindings/stubs/dune
@@ -0,0 +1,31 @@
+(rule
+ (targets decompress_bindings.ml gen_decompress.c decompress.h)
+ (action
+  (run ../generator/generate.exe .)))
+
+(library
+ (name gen_decompress_bindings)
+ (modules gen_decompress_bindings)
+ (wrapped false)
+ (libraries bigarray-compat decompress.de decompress.zl ctypes.stubs))
+
+(executable
+ (name decompress)
+ (modules decompress apply_bindings decompress_bindings)
+ (forbidden_libraries unix)
+ (flags
+  (:standard -w -27))
+ (foreign_stubs
+  (language c)
+  (names init gen_decompress))
+ (ocamlopt_flags -ccopt -static)
+ (modes
+  (native object))
+ (libraries gen_decompress_bindings checkseum.c ctypes.stubs))
+
+(rule
+ (targets libdecompress.a)
+ (package decompress)
+ (deps %{exe:decompress.exe.o})
+ (action
+  (run ar r %{targets} decompress.exe.o)))
diff --git a/bindings/stubs/gen_decompress_bindings.ml b/bindings/stubs/gen_decompress_bindings.ml
new file mode 100644
index 0000000..cb74791
--- /dev/null
+++ b/bindings/stubs/gen_decompress_bindings.ml
@@ -0,0 +1,61 @@
+open Ctypes
+open Zl
+
+let inflate i i_len o o_len =
+  let i = bigarray_of_ptr array1 i_len Bigarray.char i in
+  let o = bigarray_of_ptr array1 o_len Bigarray.char o in
+  let rec trail decoder res =
+    match Zl.Inf.decode decoder with
+    | `End decoder ->
+      if Zl.Inf.dst_rem decoder = o_len then res
+      else invalid_arg "Too small output buffer"
+    | `Flush _ -> invalid_arg "Too small output buffer"
+    | `Await _ -> assert false
+    | `Malformed err -> invalid_arg err
+  and go decoder =
+    match Zl.Inf.decode decoder with
+    | `Await _ -> assert false
+    | `Flush decoder ->
+      trail (Zl.Inf.flush decoder) (o_len - Zl.Inf.dst_rem decoder)
+    | `Malformed err -> invalid_arg err
+    | `End decoder -> o_len - Zl.Inf.dst_rem decoder in
+  let decoder =
+    Zl.Inf.decoder `Manual ~o ~allocate:(fun bits -> De.make_window ~bits) in
+  let decoder = Zl.Inf.src decoder i 0 i_len in
+  go decoder
+
+let deflate i i_len o o_len level =
+  let i = bigarray_of_ptr array1 i_len Bigarray.char i in
+  let o = bigarray_of_ptr array1 o_len Bigarray.char o in
+  let q = De.Queue.create 0x10000 in
+  let w = De.Lz77.make_window ~bits:15 in
+  let i_pos = ref 0 in
+  let o_pos = ref 0 in
+  let rec go encoder =
+    match Zl.Def.encode encoder with
+    | `Await encoder ->
+      let len = i_len - !i_pos and p = !i_pos in
+      i_pos := !i_pos + len
+      ; go (Zl.Def.src encoder i p len)
+    | `Flush encoder ->
+      let len = o_len - !o_pos - Zl.Def.dst_rem encoder in
+      o_pos := !o_pos + len
+      ; go (Zl.Def.dst encoder o !o_pos (o_len - !o_pos))
+    | `End encoder ->
+      let len = o_len - !o_pos - Zl.Def.dst_rem encoder in
+      !o_pos + len in
+  let encoder = Zl.Def.encoder `Manual `Manual ~q ~w ~level in
+  let encoder = Zl.Def.dst encoder o 0 o_len in
+  go encoder
+
+module Stubs (I : Cstubs_inverted.INTERNAL) = struct
+  let () =
+    I.internal "decompress_inflate"
+      (ptr char @-> int @-> ptr char @-> int @-> returning int)
+      inflate
+
+  let () =
+    I.internal "decompress_deflate"
+      (ptr char @-> int @-> ptr char @-> int @-> int @-> returning int)
+      deflate
+end
diff --git a/bindings/stubs/init.c b/bindings/stubs/init.c
new file mode 100644
index 0000000..bacddc8
--- /dev/null
+++ b/bindings/stubs/init.c
@@ -0,0 +1,9 @@
+#include <caml/callback.h>
+
+__attribute__ ((__constructor__))
+void
+init(void)
+{
+	char *caml_argv[1] = { NULL };
+	caml_startup(caml_argv);
+}
diff --git a/decompress.opam b/decompress.opam
index 559f5b6..13adb9b 100644
--- a/decompress.opam
+++ b/decompress.opam
@@ -18,14 +18,16 @@ run-test: [ "dune" "runtest" "-p" name "-j" jobs ]
 
 depends: [
   "ocaml"       {>= "4.07.0"}
-  "dune"        {>= "1.0"}
+  "dune"        {>= "2.8.0"}
   "base-bytes"
   "bigarray-compat"
+  "cmdliner"
   "optint"      {>= "0.0.4"}
   "checkseum"   {>= "0.2.0"}
   "bigstringaf" {with-test}
   "alcotest"    {with-test}
-  "hxd"         {with-test}
+  "ctypes"      {with-test & >= "0.18.0"}
+  "fmt"         {with-test}
   "camlzip"     {>= "1.10" & with-test}
   "base64"      {>= "3.0.0" & with-test}
 ]
diff --git a/dune-project b/dune-project
index 38de8da..1201bbf 100644
--- a/dune-project
+++ b/dune-project
@@ -1,4 +1,4 @@
-(lang dune 2.0)
+(lang dune 2.8)
 (name decompress)
 (version dev)
-(allow_approximate_merlin)
+(cram enable)
diff --git a/fuzz/dune b/fuzz/dune
index 2185f1e..2277781 100644
--- a/fuzz/dune
+++ b/fuzz/dune
@@ -1,3 +1,3 @@
 (executable
  (name fuzz)
- (libraries fmt bigstringaf camlzip hxd.string checkseum.c de crowbar))
+ (libraries fmt bigstringaf camlzip checkseum.c de crowbar))
diff --git a/lib/dune b/lib/dune
index fdc89de..e5f4cb7 100644
--- a/lib/dune
+++ b/lib/dune
@@ -3,20 +3,20 @@
  (public_name decompress.de)
  (modules de)
  (ocamlopt_flags -O3 -unbox-closures -unbox-closures-factor 20)
- (libraries checkseum bigarray-compat))
+ (libraries optint checkseum bigarray-compat))
 
 (library
  (name lz)
  (public_name decompress.lz)
  (modules lz)
  (ocamlopt_flags -O3 -unbox-closures -unbox-closures-factor 20)
- (libraries de))
+ (libraries optint checkseum bigarray-compat de))
 
 (library
  (name rfc1951)
  (public_name rfc1951)
  (modules rfc1951)
- (libraries checkseum bigarray-compat))
+ (libraries optint checkseum bigarray-compat))
 
 (rule
  (deps de.ml)
@@ -28,13 +28,13 @@
  (name zl)
  (public_name decompress.zl)
  (modules zl)
- (libraries de))
+ (libraries bigarray-compat optint checkseum de))
 
 (library
  (name gz)
  (public_name decompress.gz)
  (modules gz)
- (libraries de))
+ (libraries bigarray-compat optint checkseum de))
 
 (library
  (name lzo)
diff --git a/rfc1951.opam b/rfc1951.opam
index 5cedfcf..bff7bd5 100644
--- a/rfc1951.opam
+++ b/rfc1951.opam
@@ -22,4 +22,6 @@ depends: [
   "decompress" {= version}
   "bigarray-compat"
   "checkseum"
+  "optint"
+  "ctypes"     {with-test & >= "0.18.0"}
 ]


Test patch:
diff --git a/test/bin/bindings.t b/test/bin/bindings.t
new file mode 100644
index 0000000..5d8d633
--- /dev/null
+++ b/test/bin/bindings.t
@@ -0,0 +1,28 @@
+Test reverse bindings
+  $ cat >main.c <<EOF
+  > #include <stdlib.h>
+  > #include <string.h>
+  > #include <stdio.h>
+  > #include <caml/callback.h>
+  > #include "decompress.h"
+  > 
+  > int main() {
+  >   char i[] = "Hello World!" ;
+  >   char *o = malloc(0x1000) ;
+  >   char *r = malloc(strlen(i)) ;
+  > 
+  >   memset(o, 0, 0x1000);
+  >   memset(r, 0, strlen(i));
+  > 
+  >   int res0 = decompress_deflate(i, strlen(i), o, 0x1000, 6);
+  >   int res1 = decompress_inflate(o, res0, r, strlen(i));
+  > 
+  >   printf("%s\n", r);
+  >   fflush(stdout);
+  > 
+  >   return (0);
+  > }
+  > EOF
+  $ cc -o a.out main.c -I$(ocamlopt -where) -L../../bindings/stubs -I../../bindings/stubs -ldecompress -lm -ldl
+  $ ./a.out
+  Hello World!
diff --git a/test/bin/dune b/test/bin/dune
new file mode 100644
index 0000000..6f08c89
--- /dev/null
+++ b/test/bin/dune
@@ -0,0 +1,7 @@
+(cram
+ (deps
+  ../corpus/news
+  ../../bindings/stubs/libdecompress.a
+  ../../bindings/stubs/decompress.h
+  zpipe.c
+  %{bin:decompress.pipe}))
diff --git a/test/bin/simple.t b/test/bin/simple.t
new file mode 100644
index 0000000..cb61f5b
--- /dev/null
+++ b/test/bin/simple.t
@@ -0,0 +1,19 @@
+Simple tests
+  $ echo "Hello World!" | decompress.pipe -d > simple.z
+  $ decompress.pipe < simple.z
+  Hello World!
+  $ echo "Hello World!" | decompress.pipe -d -fzlib > simple.z
+  $ decompress.pipe -fzlib < simple.z
+  Hello World!
+  $ cc -o zpipe zpipe.c -lz
+  $ ./zpipe -d < simple.z
+  Hello World!
+  $ ./zpipe < ../corpus/news > news.z
+  $ decompress.pipe -fzlib < news.z > news
+  $ diff news ../corpus/news
+  $ decompress.pipe -fzlib -d < ../corpus/news > news.z
+  $ ./zpipe -d < news.z > news
+  $ diff news ../corpus/news
+  $ decompress.pipe -fgzip -d < ../corpus/news > news.gz
+  $ decompress.pipe -fgzip < news.gz > news
+  $ diff news ../corpus/news
diff --git a/test/bin/zpipe.c b/test/bin/zpipe.c
new file mode 100644
index 0000000..17cb053
--- /dev/null
+++ b/test/bin/zpipe.c
@@ -0,0 +1,205 @@
+/* zpipe.c: example of proper use of zlib's inflate() and deflate()
+   Not copyrighted -- provided to the public domain
+   Version 1.4  11 December 2005  Mark Adler */
+
+/* Version history:
+   1.0  30 Oct 2004  First version
+   1.1   8 Nov 2004  Add void casting for unused return values
+                     Use switch statement for inflate() return values
+   1.2   9 Nov 2004  Add assertions to document zlib guarantees
+   1.3   6 Apr 2005  Remove incorrect assertion in inf()
+   1.4  11 Dec 2005  Add hack to avoid MSDOS end-of-line conversions
+                     Avoid some compiler warnings for input and output buffers
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include "zlib.h"
+
+#if defined(MSDOS) || defined(OS2) || defined(WIN32) || defined(__CYGWIN__)
+#  include <fcntl.h>
+#  include <io.h>
+#  define SET_BINARY_MODE(file) setmode(fileno(file), O_BINARY)
+#else
+#  define SET_BINARY_MODE(file)
+#endif
+
+#define CHUNK 16384
+
+/* Compress from file source to file dest until EOF on source.
+   def() returns Z_OK on success, Z_MEM_ERROR if memory could not be
+   allocated for processing, Z_STREAM_ERROR if an invalid compression
+   level is supplied, Z_VERSION_ERROR if the version of zlib.h and the
+   version of the library linked do not match, or Z_ERRNO if there is
+   an error reading or writing the files. */
+int def(FILE *source, FILE *dest, int level)
+{
+    int ret, flush;
+    unsigned have;
+    z_stream strm;
+    unsigned char in[CHUNK];
+    unsigned char out[CHUNK];
+
+    /* allocate deflate state */
+    strm.zalloc = Z_NULL;
+    strm.zfree = Z_NULL;
+    strm.opaque = Z_NULL;
+    ret = deflateInit(&strm, level);
+    if (ret != Z_OK)
+        return ret;
+
+    /* compress until end of file */
+    do {
+        strm.avail_in = fread(in, 1, CHUNK, source);
+        if (ferror(source)) {
+            (void)deflateEnd(&strm);
+            return Z_ERRNO;
+        }
+        flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
+        strm.next_in = in;
+
+        /* run deflate() on input until output buffer not full, finish
+           compression if all of source has been read in */
+        do {
+            strm.avail_out = CHUNK;
+            strm.next_out = out;
+            ret = deflate(&strm, flush);    /* no bad return value */
+            assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
+            have = CHUNK - strm.avail_out;
+            if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
+                (void)deflateEnd(&strm);
+                return Z_ERRNO;
+            }
+        } while (strm.avail_out == 0);
+        assert(strm.avail_in == 0);     /* all input will be used */
+
+        /* done when last data in file processed */
+    } while (flush != Z_FINISH);
+    assert(ret == Z_STREAM_END);        /* stream will be complete */
+
+    /* clean up and return */
+    (void)deflateEnd(&strm);
+    return Z_OK;
+}
+
+/* Decompress from file source to file dest until stream ends or EOF.
+   inf() returns Z_OK on success, Z_MEM_ERROR if memory could not be
+   allocated for processing, Z_DATA_ERROR if the deflate data is
+   invalid or incomplete, Z_VERSION_ERROR if the version of zlib.h and
+   the version of the library linked do not match, or Z_ERRNO if there
+   is an error reading or writing the files. */
+int inf(FILE *source, FILE *dest)
+{
+    int ret;
+    unsigned have;
+    z_stream strm;
+    unsigned char in[CHUNK];
+    unsigned char out[CHUNK];
+
+    /* allocate inflate state */
+    strm.zalloc = Z_NULL;
+    strm.zfree = Z_NULL;
+    strm.opaque = Z_NULL;
+    strm.avail_in = 0;
+    strm.next_in = Z_NULL;
+    ret = inflateInit(&strm);
+    if (ret != Z_OK)
+        return ret;
+
+    /* decompress until deflate stream ends or end of file */
+    do {
+        strm.avail_in = fread(in, 1, CHUNK, source);
+        if (ferror(source)) {
+            (void)inflateEnd(&strm);
+            return Z_ERRNO;
+        }
+        if (strm.avail_in == 0)
+            break;
+        strm.next_in = in;
+
+        /* run inflate() on input until output buffer not full */
+        do {
+            strm.avail_out = CHUNK;
+            strm.next_out = out;
+            ret = inflate(&strm, Z_NO_FLUSH);
+            assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
+            switch (ret) {
+            case Z_NEED_DICT:
+                ret = Z_DATA_ERROR;     /* and fall through */
+            case Z_DATA_ERROR:
+            case Z_MEM_ERROR:
+                (void)inflateEnd(&strm);
+                return ret;
+            }
+            have = CHUNK - strm.avail_out;
+            if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
+                (void)inflateEnd(&strm);
+                return Z_ERRNO;
+            }
+        } while (strm.avail_out == 0);
+
+        /* done when inflate() says it's done */
+    } while (ret != Z_STREAM_END);
+
+    /* clean up and return */
+    (void)inflateEnd(&strm);
+    return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;
+}
+
+/* report a zlib or i/o error */
+void zerr(int ret)
+{
+    fputs("zpipe: ", stderr);
+    switch (ret) {
+    case Z_ERRNO:
+        if (ferror(stdin))
+            fputs("error reading stdin\n", stderr);
+        if (ferror(stdout))
+            fputs("error writing stdout\n", stderr);
+        break;
+    case Z_STREAM_ERROR:
+        fputs("invalid compression level\n", stderr);
+        break;
+    case Z_DATA_ERROR:
+        fputs("invalid or incomplete deflate data\n", stderr);
+        break;
+    case Z_MEM_ERROR:
+        fputs("out of memory\n", stderr);
+        break;
+    case Z_VERSION_ERROR:
+        fputs("zlib version mismatch!\n", stderr);
+    }
+}
+
+/* compress or decompress from stdin to stdout */
+int main(int argc, char **argv)
+{
+    int ret;
+
+    /* avoid end-of-line conversions */
+    SET_BINARY_MODE(stdin);
+    SET_BINARY_MODE(stdout);
+
+    /* do compression if no arguments */
+    if (argc == 1) {
+        ret = def(stdin, stdout, 4);
+        if (ret != Z_OK)
+            zerr(ret);
+        return ret;
+    }
+
+    /* do decompression if -d specified */
+    else if (argc == 2 && strcmp(argv[1], "-d") == 0) {
+        ret = inf(stdin, stdout);
+        if (ret != Z_OK)
+            zerr(ret);
+        return ret;
+    }
+
+    /* otherwise, report usage */
+    else {
+        fputs("zpipe usage: zpipe [-d] < source > dest\n", stderr);
+        return 1;
+    }
+}
diff --git a/test/dune b/test/dune
index bcac898..c0ce802 100644
--- a/test/dune
+++ b/test/dune
@@ -1,13 +1,23 @@
 (executable
  (name test)
  (modules test)
- (libraries base64 camlzip hxd.string bigstringaf checkseum.c de zl gz lzo
-   alcotest))
+ (libraries
+  fmt
+  base64
+  camlzip
+  bigarray-compat
+  bigstringaf
+  checkseum.c
+  de
+  zl
+  gz
+  lzo
+  alcotest))
 
 (executable
  (name test_deflate)
  (modules test_deflate)
- (libraries de bigstringaf alcotest))
+ (libraries fmt de bigstringaf alcotest))
 
 (rule
  (alias runtest)
diff --git a/test/test.ml b/test/test.ml
index 5f598ea..0379bc0 100644
--- a/test/test.ml
+++ b/test/test.ml
@@ -1501,19 +1501,18 @@ let test_empty_with_zlib () =
         Alcotest.failf "Unexpected `Flush or `Await signal"
       | `End _ -> Buffer.contents buf in
     let zl = go encoder in
-    Fmt.epr ">>> @[<hov>%a@]\n%!" (Hxd_string.pp Hxd.O.default) zl
-    ; let decoder =
-        Zl.Inf.decoder (`String zl) ~o ~allocate:(fun bits ->
-            De.make_window ~bits) in
-      match Zl.Inf.decode decoder with
-      | `Await _ -> Alcotest.failf "Unexpected `Await signal"
-      | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
-      | `End decoder ->
-        Alcotest.(check int)
-          "empty"
-          (bigstring_length o - Zl.Inf.dst_rem decoder)
-          0
-      | `Malformed err -> Alcotest.failf "Malformed Zlib: %s" err
+    let decoder =
+      Zl.Inf.decoder (`String zl) ~o ~allocate:(fun bits ->
+          De.make_window ~bits) in
+    match Zl.Inf.decode decoder with
+    | `Await _ -> Alcotest.failf "Unexpected `Await signal"
+    | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
+    | `End decoder ->
+      Alcotest.(check int)
+        "empty"
+        (bigstring_length o - Zl.Inf.dst_rem decoder)
+        0
+    | `Malformed err -> Alcotest.failf "Malformed Zlib: %s" err
 
 let test_empty_with_zlib_and_small_output () =
   Alcotest.test_case "empty zlib & small output" `Quick @@ fun () ->
@@ -1537,19 +1536,18 @@ let test_empty_with_zlib_and_small_output () =
           0
         ; Buffer.contents buf in
     let zl = go encoder in
-    Fmt.epr ">>> @[<hov>%a@].\n%!" (Hxd_string.pp Hxd.O.default) zl
-    ; let decoder =
-        Zl.Inf.decoder (`String zl) ~o ~allocate:(fun bits ->
-            De.make_window ~bits) in
-      match Zl.Inf.decode decoder with
-      | `Await _ -> Alcotest.failf "Unexpected `Await signal"
-      | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
-      | `End decoder ->
-        Alcotest.(check int)
-          "empty"
-          (bigstring_length o - Zl.Inf.dst_rem decoder)
-          0
-      | `Malformed err -> Alcotest.failf "Malformed Zlib: %s" err
+    let decoder =
+      Zl.Inf.decoder (`String zl) ~o ~allocate:(fun bits ->
+          De.make_window ~bits) in
+    match Zl.Inf.decode decoder with
+    | `Await _ -> Alcotest.failf "Unexpected `Await signal"
+    | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
+    | `End decoder ->
+      Alcotest.(check int)
+        "empty"
+        (bigstring_length o - Zl.Inf.dst_rem decoder)
+        0
+    | `Malformed err -> Alcotest.failf "Malformed Zlib: %s" err
 
 let test_empty_with_zlib_byte_per_byte () =
   Alcotest.test_case "empty zlib (byte per byte)" `Quick @@ fun () ->
@@ -1695,17 +1693,16 @@ let test_generate_empty_gzip () =
       | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
       | `End _ -> Buffer.contents buf in
     let gz = go encoder in
-    Fmt.epr ">>> @[<hov>%a@].\n%!" (Hxd_string.pp Hxd.O.default) gz
-    ; let decoder = Gz.Inf.decoder (`String gz) ~o in
-      match Gz.Inf.decode decoder with
-      | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
-      | `Await _ -> Alcotest.failf "Unexpected `Await signal"
-      | `End decoder ->
-        Alcotest.(check int)
-          "empty GZip"
-          (bigstring_length o - Gz.Inf.dst_rem decoder)
-          0
-      | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
+    let decoder = Gz.Inf.decoder (`String gz) ~o in
+    match Gz.Inf.decode decoder with
+    | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
+    | `Await _ -> Alcotest.failf "Unexpected `Await signal"
+    | `End decoder ->
+      Alcotest.(check int)
+        "empty GZip"
+        (bigstring_length o - Gz.Inf.dst_rem decoder)
+        0
+    | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
 
 let test_generate_empty_gzip_with_name () =
   Alcotest.test_case "generate empty GZip with name" `Quick @@ fun () ->
@@ -1720,19 +1717,18 @@ let test_generate_empty_gzip_with_name () =
       | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
       | `End _ -> Buffer.contents buf in
     let gz = go encoder in
-    Fmt.epr ">>> @[<hov>%a@].\n%!" (Hxd_string.pp Hxd.O.default) gz
-    ; let decoder = Gz.Inf.decoder (`String gz) ~o in
-      match Gz.Inf.decode decoder with
-      | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
-      | `Await _ -> Alcotest.failf "Unexpected `Await signal"
-      | `End decoder ->
-        Alcotest.(check int)
-          "emtpy GZip"
-          (bigstring_length o - Gz.Inf.dst_rem decoder)
-          0
-        ; Alcotest.(check (option string))
-            "foo" (Gz.Inf.filename decoder) (Some "foo")
-      | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
+    let decoder = Gz.Inf.decoder (`String gz) ~o in
+    match Gz.Inf.decode decoder with
+    | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
+    | `Await _ -> Alcotest.failf "Unexpected `Await signal"
+    | `End decoder ->
+      Alcotest.(check int)
+        "emtpy GZip"
+        (bigstring_length o - Gz.Inf.dst_rem decoder)
+        0
+      ; Alcotest.(check (option string))
+          "foo" (Gz.Inf.filename decoder) (Some "foo")
+    | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
 
 let test_generate_foo_gzip () =
   Alcotest.test_case "generate foo GZip" `Quick @@ fun () ->
@@ -1747,22 +1743,21 @@ let test_generate_foo_gzip () =
       | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
       | `End _ -> Buffer.contents buf in
     let gz = go encoder in
-    Fmt.epr ">>> @[<hov>%a@].\n%!" (Hxd_string.pp Hxd.O.default) gz
-    ; let decoder = Gz.Inf.decoder (`String gz) ~o in
-      let decoder =
-        match Gz.Inf.decode decoder with
-        | `Await _ | `End _ -> Alcotest.failf "Unexpected `Await or `End signal"
-        | `Flush decoder ->
-          let len = bigstring_length o - Gz.Inf.dst_rem decoder in
-          let raw = Bigstringaf.substring o ~off:0 ~len in
-          Alcotest.(check string) "contents" raw "foo"
-          ; Gz.Inf.flush decoder
-        | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err in
+    let decoder = Gz.Inf.decoder (`String gz) ~o in
+    let decoder =
       match Gz.Inf.decode decoder with
-      | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
-      | `Await _ -> Alcotest.failf "Unexpected `Await signal"
-      | `End _ -> Alcotest.(check pass) "GZip terminate" () ()
-      | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
+      | `Await _ | `End _ -> Alcotest.failf "Unexpected `Await or `End signal"
+      | `Flush decoder ->
+        let len = bigstring_length o - Gz.Inf.dst_rem decoder in
+        let raw = Bigstringaf.substring o ~off:0 ~len in
+        Alcotest.(check string) "contents" raw "foo"
+        ; Gz.Inf.flush decoder
+      | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err in
+    match Gz.Inf.decode decoder with
+    | `Flush _ -> Alcotest.failf "Unexpected `Flush signal"
+    | `Await _ -> Alcotest.failf "Unexpected `Await signal"
+    | `End _ -> Alcotest.(check pass) "GZip terminate" () ()
+    | `Malformed err -> Alcotest.failf "Malformed GZip: %s" err
 
 let test_with_camlzip () =
   Alcotest.test_case "compare with camlzip" `Quick @@ fun () ->