package bytesrw

  1. Overview
  2. Docs

Source file bytesrw_zstd.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
(*---------------------------------------------------------------------------
   Copyright (c) 2024 The bytesrw programmers. All rights reserved.
   SPDX-License-Identifier: ISC
  ---------------------------------------------------------------------------*)

open Bytesrw

(* ZSTD_dParameter values,  keep order in sync with ocaml_zstd_d_parameter
   table in C stub *)

type d_parameter =
| D_windowlogmax | D_unsafe of int

(* ZSTD_cParameter values, keep order in sync with ocaml_zstd_c_parameter
   table in C stub *)

type c_parameter =
| C_compressionlevel | C_windowlog | C_checksumflag | C_unsafe of int

(* ZSTD_EndDirective values, keep order in sync with ocaml_zstd_end_directive
   table in C stub *)

type end_directive = E_continue | E_flush | E_end

(* [Zbuf.t] values are used to communicate buffers with C. It is a counter
   part to ZSTD_inBuffer and ZSTD_outBuffer on the OCaml side. *)

module Zbuf = struct
  type t = (* keep in sync with ocaml_zbuf_fields enum in C *)
    { mutable bytes : Bytes.t;
      mutable size : int; (* last read or write position + 1 *)
      mutable pos : int; (* next read or write position *) }

  let make_empty () = { bytes = Bytes.empty; size = 0; pos = 0 }
  let make size =
    { bytes = Bytes.create (Bytes.Slice.check_length size); size; pos = 0 }

  let src_is_consumed buf = buf.pos >= buf.size
  let src_rem buf = buf.size - buf.pos
  let src_set_slice buf s =
    buf.bytes <- Bytes.Slice.bytes s;
    buf.size <- Bytes.Slice.first s + Bytes.Slice.length s;
    buf.pos <- Bytes.Slice.first s

  let src_to_slice_or_eod buf =
    let src_rem = src_rem buf in
    if src_rem = 0 then Bytes.Slice.eod else
    Bytes.Slice.make buf.bytes ~first:buf.pos ~length:src_rem

  let dst_clear buf = buf.pos <- 0
  let dst_is_empty buf = buf.pos = 0
  let dst_is_full buf = buf.pos = buf.size
  let dst_to_slice buf = Bytes.Slice.make buf.bytes ~first:0 ~length:buf.pos
end

(* Errors. Stubs raise [Failure] in case of error which we turn into
   Bytes.Stream.Error with the following error. *)

type Bytes.Stream.error += Error of string

let format_error =
  let case msg = Error msg in
  let message = function Error msg -> msg | _ -> assert false in
  Bytes.Stream.make_format_error ~format:"zstd" ~case ~message

let error e = Bytes.Stream.error format_error e
let reader_error r e = Bytes.Reader.error format_error r e
let writer_error w e = Bytes.Writer.error format_error w e

(* Library parameters *)

external version : unit -> string = "ocaml_bytesrw_ZSTD_versionString"
external min_clevel : unit -> int = "ocaml_bytesrw_ZSTD_minCLevel"
external max_clevel : unit -> int = "ocaml_bytesrw_ZSTD_maxCLevel"
external default_clevel : unit -> int = "ocaml_bytesrw_ZSTD_defaultCLevel"
external cstream_in_size : unit -> int = "ocaml_bytesrw_ZSTD_CStreamInSize"
external cstream_out_size : unit -> int = "ocaml_bytesrw_ZSTD_CStreamOutSize"
external dstream_in_size : unit -> int = "ocaml_bytesrw_ZSTD_DStreamInSize"
external dstream_out_size : unit -> int = "ocaml_bytesrw_ZSTD_DStreamOutSize"

(* Decompression *)

type dctx (* Custom value holding a ZSTD_DCtx. We manually deallocate
  them when we know but they have a finalizer which accepts NULL pointers. *)

external create_dctx : unit -> dctx = "ocaml_bytesrw_ZSTD_createDCtx"
external free_dctx : dctx -> unit = "ocaml_bytesrw_ZSTD_freeDCtx"

external dctx_set_parameter : dctx -> d_parameter -> int -> unit =
  "ocaml_bytesrw_ZSTD_DCtx_setParameter"

external dctx_load_dictionary : dctx -> string -> unit =
  "ocaml_bytesrw_ZSTD_DCtx_loadDictionary"

external decompress_stream : dctx -> src:Zbuf.t -> dst:Zbuf.t -> bool =
  (* returns [true] at end of frames *)
  "ocaml_bytesrw_ZSTD_decompressStream"

module Dctx_params = struct
  type t =
    { window_log_max : int;
      unsafe_params : (int * int) list }

  let default =
    { window_log_max = 0;
      unsafe_params = [] }

  let make ?(init = default) ?window_log_max:wl () =
    let window_log_max = Option.value ~default:init.window_log_max wl in
    { window_log_max; unsafe_params = [] }

  let set ctx p = (* raises Failure *)
    let set_unsafe (n, v) = dctx_set_parameter ctx (D_unsafe n) v in
    dctx_set_parameter ctx D_windowlogmax p.window_log_max;
    List.iter set_unsafe p.unsafe_params

  let window_log_max p = p.window_log_max
  let unsafe_param n v p = { p with unsafe_params = (n, v) :: p.unsafe_params }
end

module Ddict = struct (* Could be expanded to full ZSTD_DDict support. *)
  type t = string
  let of_binary_string = Fun.id
end

let make_dctx ?dict ?(params = Dctx_params.default) () =
  match create_dctx () with
  | exception Failure e -> error e
  | ctx ->
      try
        Dctx_params.set ctx params;
        Option.iter (dctx_load_dictionary ctx) dict;
        ctx
      with Failure e -> free_dctx ctx; error e

type decompress_eof_action = Stop | Next
type decompress_state =
| Await | Eod | Eof of { leftover : Bytes.Slice.t } | Flush

let err_unexp_eod = "Unexpected end of compressed data"

let decompress_reads
    ?(all_frames = true) ?dict ?params ()
    ?pos ?(slice_length = dstream_out_size ()) r
  =
  let eof_action = if all_frames then Next else Stop in
  let ctx = make_dctx ?dict ?params () in
  let src = Zbuf.make_empty () and dst = Zbuf.make slice_length in
  let error = reader_error r in
  let state = ref Await in
  (* The following invariant must hold. [free_dctx ctx] is only ever
     called after [state] becomes Eod. This state is sticky and any
     read in this state returns [Bytes.Slice.eod]. *)
  let rec decompress ~error ctx ~src ~dst =
    match decompress_stream ctx ~src ~dst with
    | exception Failure e -> state := Eod; free_dctx ctx; error e
    | eof ->
        state := begin
          if eof then Eof { leftover = Zbuf.src_to_slice_or_eod src } else
          if not (Zbuf.src_is_consumed src) || (Zbuf.dst_is_full dst) then Flush
          else Await
        end;
        if Zbuf.dst_is_empty dst
        then read ()
        else let slice = Zbuf.dst_to_slice dst in (Zbuf.dst_clear dst; slice)
  and await r ~error ctx ~src ~dst =
    let slice = Bytes.Reader.read r in
    if Bytes.Slice.is_eod slice
    then (state := Eod; free_dctx ctx; error err_unexp_eod)
    else (Zbuf.src_set_slice src slice; decompress ~error ctx ~src ~dst)
  and eof_stop ~leftover r ctx =
    state := Eod; free_dctx ctx;
    Bytes.Reader.push_back r leftover;
    Bytes.Slice.eod;
  and eof_next ~leftover r ~error ctx ~src ~dst =
    let slice = match Bytes.Slice.is_eod leftover with
    | true -> Bytes.Reader.read r | false -> leftover
    in
    if Bytes.Slice.is_eod slice
    then (state := Eod; free_dctx ctx; slice) else
    (Zbuf.src_set_slice src slice; decompress ~error ctx ~src ~dst)
  and read () = match !state with
  | Await -> await r ~error ctx ~src ~dst
  | Flush -> decompress ~error ctx ~src ~dst
  | Eof { leftover } ->
      begin match eof_action with
      | Stop -> eof_stop ~leftover r ctx
      | Next -> eof_next ~leftover r ~error ctx ~src ~dst
      end
  | Eod -> Bytes.Slice.eod
  in
  Bytes.Reader.make ?pos ~slice_length read

let decompress_writes
    ?dict ?params () ?pos ?(slice_length = dstream_in_size ()) ~eod w
  =
  let ctx = make_dctx ?dict ?params () in
  let src = Zbuf.make_empty () in
  let dst = Zbuf.make (Bytes.Writer.slice_length w) in
  let error w = writer_error w in
  let eof = ref false (* true on end of frames *) in
  let rec decompress wf ctx ~src ~dst =
    match decompress_stream ctx ~src ~dst with
    | exception Failure e -> (* cannot free ctx here *) error wf e
    | is_eof ->
        eof := is_eof;
        let flush_dst = Zbuf.dst_is_full dst && not is_eof in
        if not (Zbuf.dst_is_empty dst) then begin
          let slice = Zbuf.dst_to_slice dst in
          Zbuf.dst_clear dst; Bytes.Writer.write w slice;
        end;
        if not (Zbuf.src_is_consumed src) || flush_dst
        then decompress wf ctx ~src ~dst else () (* await *)
  in
  let write wf = function
  | slice when Bytes.Slice.is_eod slice ->
      free_dctx ctx; (* Note: [write] is never called again *)
      if !eof then (if eod then Bytes.Writer.write_eod w) else
      error wf err_unexp_eod
  | slice ->
      Zbuf.src_set_slice src slice; decompress wf ctx ~src ~dst
  in
  Bytes.Writer.make' ?pos ~slice_length write

(* Compression *)

type cctx (* Custom value holding a ZSTD_CCtx. We manually deallocate
  them when we know but they have a finalizer which accepts NULL pointers. *)

external create_cctx : unit -> cctx = "ocaml_bytesrw_ZSTD_createCCtx"
external free_cctx : cctx -> unit = "ocaml_bytesrw_ZSTD_freeCCtx"

external cctx_set_parameter : cctx -> c_parameter -> int -> unit =
  "ocaml_bytesrw_ZSTD_CCtx_setParameter"

external cctx_load_dictionary : cctx -> string -> unit =
  "ocaml_bytesrw_ZSTD_CCtx_loadDictionary"

external compress_stream2 :
  cctx -> src:Zbuf.t -> dst:Zbuf.t -> end_dir:end_directive -> bool =
  (* returns [true] when end_dir is complete *)
  "ocaml_bytesrw_ZSTD_compressStream2"

module Cctx_params = struct
  type clevel = int
  type t =
    { checksum : bool;
      clevel : int;
      window_log : int;
      unsafe_params : (int * int) list }

  let default =
    { clevel = default_clevel (); checksum = false; window_log = 0;
      unsafe_params = [] }

  let make ?(init = default) ?checksum ?clevel ?window_log () =
    let checksum = Option.value ~default:init.checksum checksum in
    let clevel = Option.value ~default:init.clevel clevel in
    let window_log = Option.value ~default:init.window_log window_log in
    { clevel; checksum; window_log; unsafe_params = [] }

  let set ctx p = (* raises Failure *)
    let set_unsafe (n, v) = cctx_set_parameter ctx (C_unsafe n) v in
    cctx_set_parameter ctx C_checksumflag(if p.checksum then 1 else 0);
    cctx_set_parameter ctx C_compressionlevel p.clevel;
    cctx_set_parameter ctx C_windowlog p.window_log;
    List.iter set_unsafe p.unsafe_params

  let checksum p = p.checksum
  let clevel p = p.clevel
  let window_log p = p.window_log
  let unsafe_param n v p = { p with unsafe_params = (n, v) :: p.unsafe_params }
end

module Cdict = struct (* Could be expanded to full ZSTD_CDict support. *)
  type t = string
  let of_binary_string = Fun.id
end

let make_cctx ?dict ?(params = Cctx_params.default) () =
  match create_cctx () with
  | exception Failure e -> error e
  | ctx ->
      try
        Cctx_params.set ctx params;
        Option.iter (cctx_load_dictionary ctx) dict;
        ctx
      with Failure e -> free_cctx ctx; error e

type compress_state = Await | Flush | Flush_eod | Eod

let compress_reads
    ?dict ?params () ?pos ?(slice_length = cstream_out_size ()) r
  =
  let ctx = make_cctx ?dict ?params () in
  let src = Zbuf.make_empty () and dst = Zbuf.make slice_length in
  let error = reader_error r in
  let state = ref Await in
  (* The following invariant must hold. [free_cctx ctx] is only ever
     called after [state] becomes Eod. This state is sticky and any
     read in this state returns [Bytes.Slice.eod]. *)
  let rec compress_eod ~error ctx ~src ~dst =
    match compress_stream2 ctx ~src ~dst ~end_dir:E_end with
    | exception Failure e -> state := Eod; free_cctx ctx; error e
    | is_eos ->
        if not is_eos && Zbuf.dst_is_empty dst
        then compress_eod ~error ctx ~src ~dst else
        let slice = match Zbuf.dst_is_empty dst with
        | true -> Bytes.Slice.eod | false -> Zbuf.dst_to_slice dst
        in
        if is_eos
        then (state := Eod; free_cctx ctx; slice)
        else (Zbuf.dst_clear dst; slice)
  in
  let rec compress ~error ctx ~src ~dst =
    match compress_stream2 ctx ~src ~dst ~end_dir:E_continue with
    | exception Failure e -> state := Eod; free_cctx ctx; error e
    | _is_eos ->
        if Zbuf.dst_is_empty dst then (state := Await; read ()) else
        let slice = Zbuf.dst_to_slice dst in
        let flush = Zbuf.dst_is_full dst || not (Zbuf.src_is_consumed src)in
        state := if flush then Flush else Await;
        Zbuf.dst_clear dst;
        slice
  and read () = match !state with
  | Await ->
      begin match Bytes.Reader.read r with
      | slice when Bytes.Slice.is_eod slice ->
          state := Flush_eod; compress_eod ~error ctx ~src ~dst
      | slice ->
          Zbuf.src_set_slice src slice; compress ~error ctx ~src ~dst
      end
  | Flush -> compress ~error ctx ~src ~dst
  | Flush_eod -> compress_eod ~error ctx ~src ~dst
  | Eod -> Bytes.Slice.eod
  in
  Bytes.Reader.make ?pos ~slice_length read

let compress error ctx ~src ~dst ~end_dir =
  match compress_stream2 ctx ~src ~dst ~end_dir with
  | is_eodir -> is_eodir | exception Failure e -> free_cctx ctx; error e

let compress_writes
    ?dict ?params () ?pos ?(slice_length = cstream_in_size ()) ~eod w
  =
  let ctx = make_cctx ?dict ?params () in
  let src = Zbuf.make_empty () in
  let dst = Zbuf.make (Bytes.Writer.slice_length w) in
  let error = writer_error w in
  let write_dst w dst =
    if Zbuf.dst_is_empty dst then () else
    (Bytes.Writer.write w (Zbuf.dst_to_slice dst); Zbuf.dst_clear dst)
  in
  let rec compress_eod ~error w ctx ~src ~dst = (* N.B. only gets call once *)
    match compress_stream2 ctx ~src ~dst ~end_dir:E_end with
    | exception Failure e -> free_cctx ctx; error e
    | is_eos ->
        write_dst w dst;
        if is_eos
        then (free_cctx ctx; if eod then Bytes.Writer.write_eod w)
        else compress_eod ~error w ctx ~src ~dst
  in
  let rec compress ~error w ctx ~src ~dst =
    match compress_stream2 ctx ~src ~dst ~end_dir:E_continue with
    | exception Failure e -> (* cannot free ctx here *) error e
    | _is_eos ->
        let flush = Zbuf.dst_is_full dst || not (Zbuf.src_is_consumed src) in
        write_dst w dst;
        if flush then compress ~error w ctx ~src ~dst else () (* await *)
  in
  let write = function
  | slice when Bytes.Slice.is_eod slice -> compress_eod ~error w ctx ~src ~dst
  | slice -> Zbuf.src_set_slice src slice; compress ~error w ctx ~src ~dst
  in
  Bytes.Writer.make ?pos ~slice_length write