···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2024 The bytesrw programmers. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-open Bytesrw
77-88-(* Errors *)
99-1010-type Bytes.Stream.error += Error of Zstd.error
1111-1212-let error_message = Zstd.error_message
1313-1414-let format_error =
1515- let case e = Error e in
1616- let message = function Error e -> error_message e | _ -> assert false in
1717- Bytes.Stream.make_format_error ~format:"zstd" ~case ~message
1818-1919-let _error e = Bytes.Stream.error format_error e
2020-let reader_error r e = Bytes.Reader.error format_error r e
2121-let writer_error w e = Bytes.Writer.error format_error w e
2222-2323-(* Library parameters *)
2424-2525-let version = "1.0.0-pure-ocaml"
2626-let min_clevel = 1
2727-let max_clevel = 19
2828-let default_clevel = 3
2929-3030-(* Default slice length *)
3131-let default_slice_length = 65536
3232-3333-(* Buffer all slices from a reader into a single bytes *)
3434-let buffer_reader r =
3535- let buf = Buffer.create default_slice_length in
3636- let rec loop () =
3737- let slice = Bytes.Reader.read r in
3838- if Bytes.Slice.is_eod slice then
3939- Buffer.contents buf
4040- else begin
4141- Buffer.add_subbytes buf
4242- (Bytes.Slice.bytes slice)
4343- (Bytes.Slice.first slice)
4444- (Bytes.Slice.length slice);
4545- loop ()
4646- end
4747- in
4848- loop ()
4949-5050-(* Read a single zstd frame, returning leftover data *)
5151-let read_single_frame r =
5252- (* Buffer slices until we have enough to detect frame boundaries *)
5353- let buf = Buffer.create default_slice_length in
5454- let rec loop () =
5555- let slice = Bytes.Reader.read r in
5656- if Bytes.Slice.is_eod slice then begin
5757- (* End of input - return what we have *)
5858- let data = Buffer.contents buf in
5959- (data, "")
6060- end else begin
6161- Buffer.add_subbytes buf
6262- (Bytes.Slice.bytes slice)
6363- (Bytes.Slice.first slice)
6464- (Bytes.Slice.length slice);
6565- (* Check if we have a complete frame *)
6666- let data = Buffer.contents buf in
6767- if String.length data >= 4 && Zstd.is_zstd_frame data then
6868- (* Try to find frame boundary by checking decompressed size or
6969- attempting decompression. For now, buffer everything. *)
7070- loop ()
7171- else
7272- loop ()
7373- end
7474- in
7575- loop ()
7676-7777-(* Create a reader that yields slices from a string *)
7878-let reader_of_string ?(slice_length = default_slice_length) s =
7979- let len = String.length s in
8080- let pos = ref 0 in
8181- let bytes = Bytes.unsafe_of_string s in
8282- let read () =
8383- if !pos >= len then Bytes.Slice.eod
8484- else begin
8585- let chunk_len = min slice_length (len - !pos) in
8686- let slice = Bytes.Slice.make bytes ~first:!pos ~length:chunk_len in
8787- pos := !pos + chunk_len;
8888- slice
8989- end
9090- in
9191- Bytes.Reader.make ~slice_length read
9292-9393-(* Decompress *)
9494-9595-let decompress_reads ?(all_frames = true) () ?pos ?(slice_length = default_slice_length) r =
9696- let state = ref `Reading in
9797- let output_reader = ref None in
9898- let read () =
9999- match !state with
100100- | `Done -> Bytes.Slice.eod
101101- | `Outputting ->
102102- begin match !output_reader with
103103- | None -> Bytes.Slice.eod
104104- | Some or_ ->
105105- let slice = Bytes.Reader.read or_ in
106106- if Bytes.Slice.is_eod slice then begin
107107- state := `Done;
108108- output_reader := None;
109109- Bytes.Slice.eod
110110- end else
111111- slice
112112- end
113113- | `Reading ->
114114- (* Buffer all input *)
115115- let input =
116116- if all_frames then
117117- buffer_reader r
118118- else
119119- let (data, _leftover) = read_single_frame r in
120120- (* TODO: push back leftover to r *)
121121- data
122122- in
123123- if String.length input = 0 then begin
124124- state := `Done;
125125- Bytes.Slice.eod
126126- end else begin
127127- (* Decompress *)
128128- match Zstd.decompress input with
129129- | Error _msg ->
130130- state := `Done;
131131- reader_error r Zstd.Corruption
132132- | Ok decompressed ->
133133- let or_ = reader_of_string ~slice_length decompressed in
134134- output_reader := Some or_;
135135- state := `Outputting;
136136- let slice = Bytes.Reader.read or_ in
137137- if Bytes.Slice.is_eod slice then begin
138138- state := `Done;
139139- output_reader := None
140140- end;
141141- slice
142142- end
143143- in
144144- Bytes.Reader.make ?pos ~slice_length read
145145-146146-let decompress_writes () ?pos ?(slice_length = default_slice_length) ~eod w =
147147- let buf = Buffer.create default_slice_length in
148148- let write slice =
149149- if Bytes.Slice.is_eod slice then begin
150150- (* Decompress buffered data *)
151151- let input = Buffer.contents buf in
152152- if String.length input > 0 then begin
153153- match Zstd.decompress input with
154154- | Error _msg ->
155155- writer_error w Zstd.Corruption
156156- | Ok decompressed ->
157157- (* Write decompressed data in slices *)
158158- let len = String.length decompressed in
159159- let bytes = Bytes.unsafe_of_string decompressed in
160160- let rec write_chunks pos =
161161- if pos >= len then ()
162162- else begin
163163- let chunk_len = min (Bytes.Writer.slice_length w) (len - pos) in
164164- let slice = Bytes.Slice.make bytes ~first:pos ~length:chunk_len in
165165- Bytes.Writer.write w slice;
166166- write_chunks (pos + chunk_len)
167167- end
168168- in
169169- write_chunks 0
170170- end;
171171- if eod then Bytes.Writer.write_eod w
172172- end else begin
173173- Buffer.add_subbytes buf
174174- (Bytes.Slice.bytes slice)
175175- (Bytes.Slice.first slice)
176176- (Bytes.Slice.length slice)
177177- end
178178- in
179179- Bytes.Writer.make ?pos ~slice_length write
180180-181181-(* Compress *)
182182-183183-let compress_reads ?(level = default_clevel) () ?pos ?(slice_length = default_slice_length) r =
184184- let state = ref `Reading in
185185- let output_reader = ref None in
186186- let read () =
187187- match !state with
188188- | `Done -> Bytes.Slice.eod
189189- | `Outputting ->
190190- begin match !output_reader with
191191- | None -> Bytes.Slice.eod
192192- | Some or_ ->
193193- let slice = Bytes.Reader.read or_ in
194194- if Bytes.Slice.is_eod slice then begin
195195- state := `Done;
196196- output_reader := None;
197197- Bytes.Slice.eod
198198- end else
199199- slice
200200- end
201201- | `Reading ->
202202- (* Buffer all input *)
203203- let input = buffer_reader r in
204204- if String.length input = 0 then begin
205205- (* Compress empty input to get valid empty frame *)
206206- let compressed = Zstd.compress ~level "" in
207207- let or_ = reader_of_string ~slice_length compressed in
208208- output_reader := Some or_;
209209- state := `Outputting;
210210- Bytes.Reader.read or_
211211- end else begin
212212- (* Compress *)
213213- let compressed = Zstd.compress ~level input in
214214- let or_ = reader_of_string ~slice_length compressed in
215215- output_reader := Some or_;
216216- state := `Outputting;
217217- let slice = Bytes.Reader.read or_ in
218218- if Bytes.Slice.is_eod slice then begin
219219- state := `Done;
220220- output_reader := None
221221- end;
222222- slice
223223- end
224224- in
225225- Bytes.Reader.make ?pos ~slice_length read
226226-227227-let compress_writes ?(level = default_clevel) () ?pos ?(slice_length = default_slice_length) ~eod w =
228228- let buf = Buffer.create default_slice_length in
229229- let write slice =
230230- if Bytes.Slice.is_eod slice then begin
231231- (* Compress buffered data *)
232232- let input = Buffer.contents buf in
233233- let compressed = Zstd.compress ~level input in
234234- (* Write compressed data in slices *)
235235- let len = String.length compressed in
236236- let bytes = Bytes.unsafe_of_string compressed in
237237- let rec write_chunks pos =
238238- if pos >= len then ()
239239- else begin
240240- let chunk_len = min (Bytes.Writer.slice_length w) (len - pos) in
241241- let slice = Bytes.Slice.make bytes ~first:pos ~length:chunk_len in
242242- Bytes.Writer.write w slice;
243243- write_chunks (pos + chunk_len)
244244- end
245245- in
246246- write_chunks 0;
247247- if eod then Bytes.Writer.write_eod w
248248- end else begin
249249- Buffer.add_subbytes buf
250250- (Bytes.Slice.bytes slice)
251251- (Bytes.Slice.first slice)
252252- (Bytes.Slice.length slice)
253253- end
254254- in
255255- Bytes.Writer.make ?pos ~slice_length write
-103
ocaml-zstd/bytesrw/bytesrw_zstd.mli
···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2024 The bytesrw programmers. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-(** Zstd streams via pure OCaml implementation.
77-88- This module provides support for reading and writing
99- {{:https://www.rfc-editor.org/rfc/rfc8878.html}zstd} compressed
1010- streams using a pure OCaml zstd implementation.
1111-1212- Unlike the C-based [bytesrw-zstd] package, this implementation:
1313- - Has no C dependencies
1414- - Buffers entire frames before processing (not true streaming)
1515- - Works anywhere OCaml runs
1616-1717- {b Positions.} The positions of readers and writers created
1818- by filters of this module default to [0]. *)
1919-2020-open Bytesrw
2121-2222-(** {1:errors Errors} *)
2323-2424-type Bytes.Stream.error += Error of Zstd.error
2525-(** The type for zstd stream errors.
2626-2727- All functions of this module and resulting readers and writers may
2828- raise {!Bytesrw.Bytes.Stream.Error} with this error. *)
2929-3030-val error_message : Zstd.error -> string
3131-(** [error_message e] is a human-readable message for error [e]. *)
3232-3333-(** {1:decompress Decompress} *)
3434-3535-val decompress_reads : ?all_frames:bool -> unit -> Bytes.Reader.filter
3636-(** [decompress_reads () r] filters the reads of [r] by decompressing
3737- zstd frames.
3838- {ul
3939- {- [slice_length] defaults to [65536].}}
4040-4141- If [all_frames] is:
4242- {ul
4343- {- [true] (default), this decompresses all frames until [r] returns
4444- {!Bytesrw.Bytes.Slice.eod} and concatenates the result.}
4545- {- [false], this decompresses a single frame. Once the resulting reader
4646- returns {!Bytesrw.Bytes.Slice.eod}, [r] is positioned exactly after
4747- the end of frame and can be used again to perform other non-filtered
4848- reads (e.g. a new zstd frame or other unrelated data).}}
4949-5050- {b Note:} This implementation buffers the entire compressed input
5151- before decompressing. For large files, consider using the C-based
5252- [bytesrw-zstd] package instead. *)
5353-5454-val decompress_writes : unit -> Bytes.Writer.filter
5555-(** [decompress_writes () w ~eod] filters the writes on [w] by decompressing
5656- sequences of zstd frames until {!Bytesrw.Bytes.Slice.eod} is written.
5757- If [eod] is [false] the last {!Bytesrw.Bytes.Slice.eod} is not written
5858- on [w] and at this point [w] can be used again to perform other
5959- non-filtered writes.
6060- {ul
6161- {- [slice_length] defaults to [65536].}}
6262-6363- {b Note:} This implementation buffers the entire compressed input
6464- before decompressing. *)
6565-6666-(** {1:compress Compress} *)
6767-6868-val compress_reads : ?level:int -> unit -> Bytes.Reader.filter
6969-(** [compress_reads () r] filters the reads of [r] by compressing them
7070- to a single zstd frame.
7171- {ul
7272- {- [level] is the compression level (1-19, default 3).}
7373- {- [slice_length] defaults to [65536].}}
7474-7575- {b Note:} This implementation buffers the entire input before
7676- compressing. *)
7777-7878-val compress_writes : ?level:int -> unit -> Bytes.Writer.filter
7979-(** [compress_writes () w ~eod] filters the writes on [w] by compressing
8080- them to a single zstd frame until {!Bytesrw.Bytes.Slice.eod} is written.
8181- If [eod] is [false] the last {!Bytesrw.Bytes.Slice.eod} is not written
8282- on [w] and at this point [w] can be used again to perform non-filtered
8383- writes.
8484- {ul
8585- {- [level] is the compression level (1-19, default 3).}
8686- {- [slice_length] defaults to [65536].}}
8787-8888- {b Note:} This implementation buffers the entire input before
8989- compressing. *)
9090-9191-(** {1:params Library parameters} *)
9292-9393-val version : string
9494-(** [version] is the version of this pure OCaml zstd implementation. *)
9595-9696-val min_clevel : int
9797-(** [min_clevel] is the minimum compression level (1). *)
9898-9999-val max_clevel : int
100100-(** [max_clevel] is the maximum compression level (19). *)
101101-102102-val default_clevel : int
103103-(** [default_clevel] is the default compression level (3). *)
···11-(lang dune 3.21)
22-(name zstd)
33-(generate_opam_files true)
44-55-(license ISC)
66-(authors "Anil Madhavapeddy <anil@recoil.org>")
77-(maintainers "Anil Madhavapeddy <anil@recoil.org>")
88-(source (tangled anil.recoil.org/ocaml-zstd))
99-1010-(package
1111- (name zstd)
1212- (synopsis "Pure OCaml implementation of Zstandard compression")
1313- (description
1414- "A complete pure OCaml implementation of the Zstandard (zstd) compression
1515-algorithm (RFC 8878). Includes both compression and decompression with support
1616-for all compression levels and dictionaries. When the optional bytesrw
1717-dependency is installed, the zstd.bytesrw sublibrary provides streaming-style
1818-compression and decompression.")
1919- (depends
2020- (ocaml (>= 5.1))
2121- bitstream
2222- (alcotest (and :with-test (>= 1.7.0))))
2323- (depopts bytesrw))
-89
ocaml-zstd/src/bit_reader.ml
···11-(** Bitstream reader for Zstandard decompression.
22-33- This module wraps the Bitstream library, translating exceptions
44- to Zstd_error for consistent error handling. *)
55-66-(** Helper to wrap Bitstream operations and translate exceptions *)
77-let[@inline] wrap_truncated f =
88- try f ()
99- with Bitstream.End_of_stream ->
1010- raise (Constants.Zstd_error Constants.Truncated_input)
1111-1212-let[@inline] wrap_all f =
1313- try f ()
1414- with
1515- | Bitstream.End_of_stream ->
1616- raise (Constants.Zstd_error Constants.Truncated_input)
1717- | Bitstream.Invalid_state _ ->
1818- raise (Constants.Zstd_error Constants.Corruption)
1919- | Bitstream.Corrupted_stream _ ->
2020- raise (Constants.Zstd_error Constants.Corruption)
2121-2222-(** Forward bitstream reader - reads from start to end *)
2323-module Forward = struct
2424- type t = Bitstream.Forward_reader.t
2525-2626- let create src ~pos ~len =
2727- Bitstream.Forward_reader.create src ~pos ~len
2828-2929- let of_bytes src =
3030- Bitstream.Forward_reader.of_bytes src
3131-3232- let[@inline] remaining t =
3333- Bitstream.Forward_reader.remaining t
3434-3535- let[@inline] is_byte_aligned t =
3636- Bitstream.Forward_reader.is_byte_aligned t
3737-3838- let[@inline] read_bits t n =
3939- wrap_truncated (fun () -> Bitstream.Forward_reader.read_bits t n)
4040-4141- let[@inline] read_byte t =
4242- wrap_all (fun () -> Bitstream.Forward_reader.read_byte t)
4343-4444- let rewind_bits t n =
4545- wrap_truncated (fun () -> Bitstream.Forward_reader.rewind_bits t n)
4646-4747- let align t =
4848- Bitstream.Forward_reader.align t
4949-5050- let byte_position t =
5151- wrap_all (fun () -> Bitstream.Forward_reader.byte_position t)
5252-5353- let get_bytes t n =
5454- wrap_all (fun () -> Bitstream.Forward_reader.get_bytes t n)
5555-5656- let advance t n =
5757- wrap_all (fun () -> Bitstream.Forward_reader.advance t n)
5858-5959- let sub t n =
6060- wrap_all (fun () -> Bitstream.Forward_reader.sub t n)
6161-6262- let remaining_bytes t =
6363- wrap_all (fun () -> Bitstream.Forward_reader.remaining_bytes t)
6464-end
6565-6666-(** Backward bitstream reader - reads from end to start.
6767- Used for FSE and Huffman coded streams. *)
6868-module Backward = struct
6969- type t = Bitstream.Backward_reader.t
7070-7171- let create src ~pos ~len =
7272- wrap_all (fun () -> Bitstream.Backward_reader.of_bytes src ~pos ~len)
7373-7474- let of_bytes src ~pos ~len =
7575- create src ~pos ~len
7676-7777- let[@inline] remaining t =
7878- Bitstream.Backward_reader.remaining t
7979-8080- let[@inline] read_bits t n =
8181- Bitstream.Backward_reader.read_bits t n
8282-8383- let[@inline] is_empty t =
8484- Bitstream.Backward_reader.is_empty t
8585-end
8686-8787-(** Read little-endian integers from bytes *)
8888-let[@inline] get_u16_le src pos =
8989- Bytes.get_uint16_le src pos
-54
ocaml-zstd/src/bit_writer.ml
···11-(** Bitstream writer for Zstandard compression.
22-33- This module wraps the Bitstream library for consistent API
44- with the rest of the zstd implementation. *)
55-66-(** Forward bitstream writer - writes from start to end *)
77-module Forward = struct
88- type t = Bitstream.Forward_writer.t
99-1010- let create dst ~pos =
1111- Bitstream.Forward_writer.create dst ~pos
1212-1313- let of_bytes dst =
1414- Bitstream.Forward_writer.of_bytes dst
1515-1616- let flush t =
1717- Bitstream.Forward_writer.flush t
1818-1919- let write_bits t value n =
2020- Bitstream.Forward_writer.write_bits t value n
2121-2222- let write_byte t value =
2323- Bitstream.Forward_writer.write_byte t value
2424-2525- let write_bytes t src =
2626- Bitstream.Forward_writer.write_bytes t src
2727-2828- let byte_position t =
2929- Bitstream.Forward_writer.byte_position t
3030-3131- let finalize t =
3232- Bitstream.Forward_writer.finalize t
3333-end
3434-3535-(** Backward bitstream writer - accumulates bits to be read backwards.
3636- Used for FSE and Huffman encoding. *)
3737-module Backward = struct
3838- type t = Bitstream.Backward_writer.t
3939-4040- let create size =
4141- Bitstream.Backward_writer.create size
4242-4343- let[@inline] write_bits t value n =
4444- Bitstream.Backward_writer.write_bits t value n
4545-4646- let flush_bytes t =
4747- Bitstream.Backward_writer.flush_bytes t
4848-4949- let finalize t =
5050- Bitstream.Backward_writer.finalize t
5151-5252- let current_size t =
5353- Bitstream.Backward_writer.current_size t
5454-end
···11-(** Finite State Entropy (FSE) decoding for Zstandard.
22-33- FSE is an entropy coding method based on ANS (Asymmetric Numeral Systems).
44- FSE streams are read backwards (from end to beginning). *)
55-66-(** FSE decoding table entry *)
77-type entry = {
88- symbol : int;
99- num_bits : int;
1010- new_state_base : int;
1111-}
1212-1313-(** FSE decoding table *)
1414-type dtable = {
1515- entries : entry array;
1616- accuracy_log : int;
1717-}
1818-1919-(** Find the highest set bit (floor(log2(n))) *)
2020-let[@inline] highest_set_bit n =
2121- if n = 0 then -1
2222- else
2323- let rec loop i =
2424- if (1 lsl i) <= n then loop (i + 1)
2525- else i - 1
2626- in
2727- loop 0
2828-2929-(** Build FSE decoding table from normalized frequencies.
3030- Frequencies can be negative (-1 means probability < 1). *)
3131-let build_dtable frequencies accuracy_log =
3232- let table_size = 1 lsl accuracy_log in
3333- let num_symbols = Array.length frequencies in
3434-3535- (* Create entries array *)
3636- let entries = Array.init table_size (fun _ ->
3737- { symbol = 0; num_bits = 0; new_state_base = 0 }
3838- ) in
3939-4040- (* Track state descriptors for each symbol *)
4141- let state_desc = Array.make num_symbols 0 in
4242-4343- (* First pass: place symbols with prob < 1 at the end *)
4444- let high_threshold = ref table_size in
4545- for s = 0 to num_symbols - 1 do
4646- if frequencies.(s) = -1 then begin
4747- decr high_threshold;
4848- entries.(!high_threshold) <- { symbol = s; num_bits = 0; new_state_base = 0 };
4949- state_desc.(s) <- 1
5050- end
5151- done;
5252-5353- (* Second pass: distribute remaining symbols using the step formula *)
5454- let step = (table_size lsr 1) + (table_size lsr 3) + 3 in
5555- let mask = table_size - 1 in
5656- let pos = ref 0 in
5757-5858- for s = 0 to num_symbols - 1 do
5959- if frequencies.(s) > 0 then begin
6060- state_desc.(s) <- frequencies.(s);
6161- for _ = 0 to frequencies.(s) - 1 do
6262- entries.(!pos) <- { entries.(!pos) with symbol = s };
6363- (* Skip positions occupied by prob < 1 symbols *)
6464- pos := (!pos + step) land mask;
6565- while !pos >= !high_threshold do
6666- pos := (!pos + step) land mask
6767- done
6868- done
6969- end
7070- done;
7171-7272- if !pos <> 0 then
7373- raise (Constants.Zstd_error Constants.Invalid_fse_table);
7474-7575- (* Third pass: fill in num_bits and new_state_base *)
7676- for i = 0 to table_size - 1 do
7777- let s = entries.(i).symbol in
7878- let next_state_desc = state_desc.(s) in
7979- state_desc.(s) <- next_state_desc + 1;
8080-8181- (* Number of bits is accuracy_log - log2(next_state_desc) *)
8282- let num_bits = accuracy_log - highest_set_bit next_state_desc in
8383- (* new_state_base = (next_state_desc << num_bits) - table_size *)
8484- let new_state_base = (next_state_desc lsl num_bits) - table_size in
8585-8686- entries.(i) <- { entries.(i) with num_bits; new_state_base }
8787- done;
8888-8989- { entries; accuracy_log }
9090-9191-(** Build RLE table (single symbol repeated) *)
9292-let build_dtable_rle symbol =
9393- {
9494- entries = [| { symbol; num_bits = 0; new_state_base = 0 } |];
9595- accuracy_log = 0;
9696- }
9797-9898-(** Peek at the symbol for current state (doesn't update state) *)
9999-let[@inline] peek_symbol dtable state =
100100- dtable.entries.(state).symbol
101101-102102-(** Update state by reading bits from the stream *)
103103-let[@inline] update_state dtable state (stream : Bit_reader.Backward.t) =
104104- let entry = dtable.entries.(state) in
105105- let bits = Bit_reader.Backward.read_bits stream entry.num_bits in
106106- entry.new_state_base + bits
107107-108108-(** Decode symbol and update state *)
109109-let[@inline] decode_symbol dtable state stream =
110110- let symbol = peek_symbol dtable state in
111111- let new_state = update_state dtable state stream in
112112- (symbol, new_state)
113113-114114-(** Initialize state by reading accuracy_log bits *)
115115-let[@inline] init_state dtable (stream : Bit_reader.Backward.t) =
116116- Bit_reader.Backward.read_bits stream dtable.accuracy_log
117117-118118-(** Decode FSE header and build decoding table.
119119- Returns the table and advances the forward stream. *)
120120-let decode_header (stream : Bit_reader.Forward.t) max_accuracy_log =
121121- (* Accuracy log is first 4 bits + 5 *)
122122- let accuracy_log = (Bit_reader.Forward.read_bits stream 4) + 5 in
123123- if accuracy_log > max_accuracy_log then
124124- raise (Constants.Zstd_error Constants.Invalid_fse_table);
125125-126126- let table_size = 1 lsl accuracy_log in
127127- let frequencies = Array.make Constants.max_fse_symbols 0 in
128128-129129- let remaining = ref table_size in
130130- let symbol = ref 0 in
131131-132132- while !remaining > 0 && !symbol < Constants.max_fse_symbols do
133133- (* Determine how many bits we might need *)
134134- let bits_needed = highest_set_bit (!remaining + 1) + 1 in
135135- let value = Bit_reader.Forward.read_bits stream bits_needed in
136136-137137- (* Small value optimization: values < threshold use one less bit *)
138138- let threshold = (1 lsl bits_needed) - 1 - (!remaining + 1) in
139139- let lower_mask = (1 lsl (bits_needed - 1)) - 1 in
140140-141141- let (actual_value, bits_consumed) =
142142- if (value land lower_mask) < threshold then
143143- (value land lower_mask, bits_needed - 1)
144144- else if value > lower_mask then
145145- (value - threshold, bits_needed)
146146- else
147147- (value, bits_needed)
148148- in
149149-150150- (* Rewind if we read too many bits *)
151151- if bits_consumed < bits_needed then
152152- Bit_reader.Forward.rewind_bits stream 1;
153153-154154- (* Probability = value - 1 (so value 0 means prob = -1) *)
155155- let prob = actual_value - 1 in
156156- frequencies.(!symbol) <- prob;
157157- remaining := !remaining - abs prob;
158158- incr symbol;
159159-160160- (* Handle zero probability with repeat flags *)
161161- if prob = 0 then begin
162162- let rec read_zeroes () =
163163- let repeat = Bit_reader.Forward.read_bits stream 2 in
164164- for _ = 1 to repeat do
165165- if !symbol < Constants.max_fse_symbols then begin
166166- frequencies.(!symbol) <- 0;
167167- incr symbol
168168- end
169169- done;
170170- if repeat = 3 then read_zeroes ()
171171- in
172172- read_zeroes ()
173173- end
174174- done;
175175-176176- (* Align to byte boundary *)
177177- Bit_reader.Forward.align stream;
178178-179179- if !remaining <> 0 then
180180- raise (Constants.Zstd_error Constants.Invalid_fse_table);
181181-182182- (* Build the decoding table *)
183183- let freq_slice = Array.sub frequencies 0 !symbol in
184184- build_dtable freq_slice accuracy_log
185185-186186-(** Decompress interleaved 2-state FSE stream.
187187- Used for Huffman weight encoding. Returns number of symbols decoded. *)
188188-let decompress_interleaved2 dtable src ~pos ~len output =
189189- let stream = Bit_reader.Backward.of_bytes src ~pos ~len in
190190-191191- (* Initialize two states *)
192192- let state1 = ref (init_state dtable stream) in
193193- let state2 = ref (init_state dtable stream) in
194194-195195- let out_pos = ref 0 in
196196- let out_len = Bytes.length output in
197197-198198- (* Decode symbols alternating between states *)
199199- while Bit_reader.Backward.remaining stream >= 0 do
200200- if !out_pos >= out_len then
201201- raise (Constants.Zstd_error Constants.Output_too_small);
202202-203203- let (sym1, new_state1) = decode_symbol dtable !state1 stream in
204204- Bytes.set_uint8 output !out_pos sym1;
205205- incr out_pos;
206206- state1 := new_state1;
207207-208208- if Bit_reader.Backward.remaining stream < 0 then begin
209209- (* Stream exhausted, output final symbol from state2 *)
210210- if !out_pos < out_len then begin
211211- Bytes.set_uint8 output !out_pos (peek_symbol dtable !state2);
212212- incr out_pos
213213- end
214214- end else begin
215215- if !out_pos >= out_len then
216216- raise (Constants.Zstd_error Constants.Output_too_small);
217217-218218- let (sym2, new_state2) = decode_symbol dtable !state2 stream in
219219- Bytes.set_uint8 output !out_pos sym2;
220220- incr out_pos;
221221- state2 := new_state2;
222222-223223- if Bit_reader.Backward.remaining stream < 0 then begin
224224- (* Stream exhausted, output final symbol from state1 *)
225225- if !out_pos < out_len then begin
226226- Bytes.set_uint8 output !out_pos (peek_symbol dtable !state1);
227227- incr out_pos
228228- end
229229- end
230230- end
231231- done;
232232-233233- !out_pos
234234-235235-(** Build decoding table from predefined distribution *)
236236-let build_predefined_table distribution accuracy_log =
237237- build_dtable distribution accuracy_log
238238-239239-(* ========== ENCODING ========== *)
240240-241241-(** FSE compression table - matches C zstd's FSE_symbolCompressionTransform format.
242242- deltaNbBits is encoded as (maxBitsOut << 16) - minStatePlus
243243- This allows computing nbBitsOut = (state + deltaNbBits) >> 16 *)
244244-type symbol_transform = {
245245- delta_nb_bits : int; (* (maxBitsOut << 16) - minStatePlus *)
246246- delta_find_state : int; (* Cumulative offset to find next state *)
247247-}
248248-249249-(** FSE compression table *)
250250-type ctable = {
251251- symbol_tt : symbol_transform array; (* Symbol compression transforms *)
252252- state_table : int array; (* Next state lookup table *)
253253- accuracy_log : int;
254254- table_size : int;
255255-}
256256-257257-(** FSE compression state - matches C zstd's FSE_CState_t *)
258258-type cstate = {
259259- mutable value : int; (* Current state value *)
260260- ctable : ctable; (* Reference to compression table *)
261261-}
262262-263263-(** Count symbol frequencies *)
264264-let count_symbols src ~pos ~len max_symbol =
265265- let counts = Array.make (max_symbol + 1) 0 in
266266- for i = pos to pos + len - 1 do
267267- let s = Bytes.get_uint8 src i in
268268- if s <= max_symbol then
269269- counts.(s) <- counts.(s) + 1
270270- done;
271271- counts
272272-273273-(** Normalize counts to sum to table_size *)
274274-let normalize_counts counts total accuracy_log =
275275- let table_size = 1 lsl accuracy_log in
276276- let num_symbols = Array.length counts in
277277- let norm = Array.make num_symbols 0 in
278278-279279- if total = 0 then norm
280280- else begin
281281- let scale = table_size * 256 / total in
282282- let distributed = ref 0 in
283283-284284- for s = 0 to num_symbols - 1 do
285285- if counts.(s) > 0 then begin
286286- let proba = (counts.(s) * scale + 128) / 256 in
287287- let proba = max 1 proba in
288288- norm.(s) <- proba;
289289- distributed := !distributed + proba
290290- end
291291- done;
292292-293293- while !distributed > table_size do
294294- let max_val = ref 0 in
295295- let max_idx = ref 0 in
296296- for s = 0 to num_symbols - 1 do
297297- if norm.(s) > !max_val then begin
298298- max_val := norm.(s);
299299- max_idx := s
300300- end
301301- done;
302302- norm.(!max_idx) <- norm.(!max_idx) - 1;
303303- decr distributed
304304- done;
305305-306306- while !distributed < table_size do
307307- let min_val = ref max_int in
308308- let min_idx = ref 0 in
309309- for s = 0 to num_symbols - 1 do
310310- if norm.(s) > 0 && norm.(s) < !min_val then begin
311311- min_val := norm.(s);
312312- min_idx := s
313313- end
314314- done;
315315- norm.(!min_idx) <- norm.(!min_idx) + 1;
316316- incr distributed
317317- done;
318318-319319- norm
320320- end
321321-322322-(** Build FSE compression table from normalized counts.
323323- Matches C zstd's FSE_buildCTable_wksp algorithm exactly. *)
324324-let build_ctable norm_counts accuracy_log =
325325- let table_size = 1 lsl accuracy_log in
326326- let table_mask = table_size - 1 in
327327- let num_symbols = Array.length norm_counts in
328328- let step = (table_size lsr 1) + (table_size lsr 3) + 3 in
329329-330330- (* Symbol distribution table - which symbol at each state *)
331331- let table_symbol = Array.make table_size 0 in
332332-333333- (* Cumulative counts for state table indexing *)
334334- let cumul = Array.make (num_symbols + 1) 0 in
335335- cumul.(0) <- 0;
336336- for s = 0 to num_symbols - 1 do
337337- let count = if norm_counts.(s) = -1 then 1 else max 0 norm_counts.(s) in
338338- cumul.(s + 1) <- cumul.(s) + count
339339- done;
340340-341341- (* Place low probability symbols at the end *)
342342- let high_threshold = ref (table_size - 1) in
343343- for s = 0 to num_symbols - 1 do
344344- if norm_counts.(s) = -1 then begin
345345- table_symbol.(!high_threshold) <- s;
346346- decr high_threshold
347347- end
348348- done;
349349-350350- (* Spread remaining symbols using step formula *)
351351- let pos = ref 0 in
352352- for s = 0 to num_symbols - 1 do
353353- let count = norm_counts.(s) in
354354- if count > 0 then begin
355355- for _ = 0 to count - 1 do
356356- table_symbol.(!pos) <- s;
357357- pos := (!pos + step) land table_mask;
358358- while !pos > !high_threshold do
359359- pos := (!pos + step) land table_mask
360360- done
361361- done
362362- end
363363- done;
364364-365365- (* Build state table - for each position, compute next state *)
366366- let state_table = Array.make table_size 0 in
367367- let cumul_copy = Array.copy cumul in
368368- for u = 0 to table_size - 1 do
369369- let s = table_symbol.(u) in
370370- state_table.(cumul_copy.(s)) <- table_size + u;
371371- cumul_copy.(s) <- cumul_copy.(s) + 1
372372- done;
373373-374374- (* Build symbol compression transforms *)
375375- let symbol_tt = Array.init num_symbols (fun s ->
376376- let count = norm_counts.(s) in
377377- match count with
378378- | 0 ->
379379- (* Zero probability - use max bits (shouldn't be encoded) *)
380380- { delta_nb_bits = ((accuracy_log + 1) lsl 16) - (1 lsl accuracy_log);
381381- delta_find_state = 0 }
382382- | -1 | 1 ->
383383- (* Low probability symbol *)
384384- { delta_nb_bits = (accuracy_log lsl 16) - (1 lsl accuracy_log);
385385- delta_find_state = cumul.(s) - 1 }
386386- | _ ->
387387- (* Normal symbol *)
388388- let max_bits_out = accuracy_log - highest_set_bit (count - 1) in
389389- let min_state_plus = count lsl max_bits_out in
390390- { delta_nb_bits = (max_bits_out lsl 16) - min_state_plus;
391391- delta_find_state = cumul.(s) - count }
392392- ) in
393393-394394- { symbol_tt; state_table; accuracy_log; table_size }
395395-396396-(** Initialize compression state - matches C's FSE_initCState *)
397397-let init_cstate ctable =
398398- { value = 1 lsl ctable.accuracy_log; ctable }
399399-400400-(** Initialize compression state with first symbol - matches C's FSE_initCState2.
401401- This saves bits by using the smallest valid state for the first symbol. *)
402402-let init_cstate2 ctable symbol =
403403- let st = ctable.symbol_tt.(symbol) in
404404- let nb_bits_out = (st.delta_nb_bits + (1 lsl 15)) lsr 16 in
405405- let init_value = (nb_bits_out lsl 16) - st.delta_nb_bits in
406406- let state_idx = (init_value lsr nb_bits_out) + st.delta_find_state in
407407- { value = ctable.state_table.(state_idx); ctable }
408408-409409-(** Encode a single symbol - matches C's FSE_encodeSymbol exactly.
410410- Outputs bits representing state transition and updates state. *)
411411-let[@inline] encode_symbol (stream : Bit_writer.Backward.t) cstate symbol =
412412- let st = cstate.ctable.symbol_tt.(symbol) in
413413- let nb_bits_out = (cstate.value + st.delta_nb_bits) lsr 16 in
414414- Bit_writer.Backward.write_bits stream cstate.value nb_bits_out;
415415- let state_idx = (cstate.value lsr nb_bits_out) + st.delta_find_state in
416416- cstate.value <- cstate.ctable.state_table.(state_idx)
417417-418418-(** Flush compression state - matches C's FSE_flushCState.
419419- Outputs final state value to allow decoder to initialize. *)
420420-let[@inline] flush_cstate (stream : Bit_writer.Backward.t) cstate =
421421- Bit_writer.Backward.write_bits stream cstate.value cstate.ctable.accuracy_log
422422-423423-(** Write FSE header (normalized counts) *)
424424-let write_header (stream : Bit_writer.Forward.t) norm_counts accuracy_log =
425425- Bit_writer.Forward.write_bits stream (accuracy_log - 5) 4;
426426-427427- let table_size = 1 lsl accuracy_log in
428428- let num_symbols = Array.length norm_counts in
429429- let remaining = ref table_size in
430430- let symbol = ref 0 in
431431-432432- while !remaining > 0 && !symbol < num_symbols do
433433- let count = norm_counts.(!symbol) in
434434- let value = count + 1 in
435435-436436- let bits_needed = highest_set_bit (!remaining + 1) + 1 in
437437- let threshold = (1 lsl bits_needed) - 1 - (!remaining + 1) in
438438-439439- if value < threshold then
440440- Bit_writer.Forward.write_bits stream value (bits_needed - 1)
441441- else
442442- Bit_writer.Forward.write_bits stream (value + threshold) bits_needed;
443443-444444- remaining := !remaining - abs count;
445445- incr symbol;
446446-447447- if count = 0 then begin
448448- let rec count_zeroes acc =
449449- if !symbol < num_symbols && norm_counts.(!symbol) = 0 then begin
450450- incr symbol;
451451- count_zeroes (acc + 1)
452452- end else acc
453453- in
454454- let zeroes = count_zeroes 0 in
455455- let rec write_repeats n =
456456- if n >= 3 then begin
457457- Bit_writer.Forward.write_bits stream 3 2;
458458- write_repeats (n - 3)
459459- end else
460460- Bit_writer.Forward.write_bits stream n 2
461461- in
462462- write_repeats zeroes
463463- end
464464- done
465465-466466-(** Build encoding table from predefined distribution *)
467467-let build_predefined_ctable distribution accuracy_log =
468468- build_ctable distribution accuracy_log
-435
ocaml-zstd/src/huffman.ml
···11-(** Huffman coding for Zstandard literals decompression.
22-33- Zstd uses canonical Huffman codes for literal compression.
44- Huffman streams are read backwards like FSE streams. *)
55-66-(** Huffman decoding table entry *)
77-type entry = {
88- symbol : int;
99- num_bits : int;
1010-}
1111-1212-(** Huffman decoding table *)
1313-type dtable = {
1414- entries : entry array;
1515- max_bits : int;
1616-}
1717-1818-let highest_set_bit = Fse.highest_set_bit
1919-2020-(** Build Huffman table from bit lengths.
2121- Uses canonical Huffman coding. *)
2222-let build_dtable_from_bits bits num_symbols =
2323- if num_symbols > Constants.max_huffman_symbols then
2424- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
2525-2626- (* Find max bits and count symbols per bit length *)
2727- let max_bits = ref 0 in
2828- let rank_count = Array.make (Constants.max_huffman_bits + 1) 0 in
2929-3030- for i = 0 to num_symbols - 1 do
3131- let b = bits.(i) in
3232- if b > Constants.max_huffman_bits then
3333- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
3434- if b > !max_bits then max_bits := b;
3535- rank_count.(b) <- rank_count.(b) + 1
3636- done;
3737-3838- if !max_bits = 0 then
3939- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
4040-4141- let table_size = 1 lsl !max_bits in
4242- let entries = Array.init table_size (fun _ ->
4343- { symbol = 0; num_bits = 0 }
4444- ) in
4545-4646- (* Calculate starting indices for each rank *)
4747- let rank_idx = Array.make (Constants.max_huffman_bits + 1) 0 in
4848- rank_idx.(!max_bits) <- 0;
4949- for i = !max_bits downto 1 do
5050- rank_idx.(i - 1) <- rank_idx.(i) + rank_count.(i) * (1 lsl (!max_bits - i));
5151- (* Fill in num_bits for this range *)
5252- for j = rank_idx.(i) to rank_idx.(i - 1) - 1 do
5353- entries.(j) <- { entries.(j) with num_bits = i }
5454- done
5555- done;
5656-5757- if rank_idx.(0) <> table_size then
5858- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
5959-6060- (* Assign symbols to table entries *)
6161- for i = 0 to num_symbols - 1 do
6262- let b = bits.(i) in
6363- if b <> 0 then begin
6464- let code = rank_idx.(b) in
6565- let len = 1 lsl (!max_bits - b) in
6666- for j = code to code + len - 1 do
6767- entries.(j) <- { entries.(j) with symbol = i }
6868- done;
6969- rank_idx.(b) <- code + len
7070- end
7171- done;
7272-7373- { entries; max_bits = !max_bits }
7474-7575-(** Build table from weights (as decoded from zstd format) *)
7676-let build_dtable_from_weights weights num_symbols =
7777- if num_symbols + 1 > Constants.max_huffman_symbols then
7878- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
7979-8080- let bits = Array.make (num_symbols + 1) 0 in
8181-8282- (* Calculate weight sum to find max_bits and last weight *)
8383- let weight_sum = ref 0 in
8484- for i = 0 to num_symbols - 1 do
8585- let w = weights.(i) in
8686- if w > Constants.max_huffman_bits then
8787- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
8888- if w > 0 then
8989- weight_sum := !weight_sum + (1 lsl (w - 1))
9090- done;
9191-9292- (* Find max_bits (first power of 2 > weight_sum) *)
9393- let max_bits = highest_set_bit !weight_sum + 1 in
9494- let left_over = (1 lsl max_bits) - !weight_sum in
9595-9696- (* left_over must be a power of 2 *)
9797- if left_over land (left_over - 1) <> 0 then
9898- raise (Constants.Zstd_error Constants.Invalid_huffman_table);
9999-100100- let last_weight = highest_set_bit left_over + 1 in
101101-102102- (* Convert weights to bit lengths *)
103103- for i = 0 to num_symbols - 1 do
104104- let w = weights.(i) in
105105- bits.(i) <- if w > 0 then max_bits + 1 - w else 0
106106- done;
107107- bits.(num_symbols) <- max_bits + 1 - last_weight;
108108-109109- build_dtable_from_bits bits (num_symbols + 1)
110110-111111-(** Initialize Huffman state by reading max_bits *)
112112-let[@inline] init_state dtable (stream : Bit_reader.Backward.t) =
113113- Bit_reader.Backward.read_bits stream dtable.max_bits
114114-115115-(** Decode a symbol and update state *)
116116-let[@inline] decode_symbol dtable state (stream : Bit_reader.Backward.t) =
117117- let entry = dtable.entries.(state) in
118118- let symbol = entry.symbol in
119119- let bits_used = entry.num_bits in
120120- (* Shift out used bits and read new ones *)
121121- let mask = (1 lsl dtable.max_bits) - 1 in
122122- let rest = Bit_reader.Backward.read_bits stream bits_used in
123123- let new_state = ((state lsl bits_used) + rest) land mask in
124124- (symbol, new_state)
125125-126126-(** Decompress a single Huffman stream *)
127127-let decompress_1stream dtable src ~pos ~len output ~out_pos ~out_len =
128128- let stream = Bit_reader.Backward.of_bytes src ~pos ~len in
129129- let state = ref (init_state dtable stream) in
130130-131131- let written = ref 0 in
132132- while Bit_reader.Backward.remaining stream > -dtable.max_bits do
133133- if out_pos + !written >= out_pos + out_len then
134134- raise (Constants.Zstd_error Constants.Output_too_small);
135135-136136- let (symbol, new_state) = decode_symbol dtable !state stream in
137137- Bytes.set_uint8 output (out_pos + !written) symbol;
138138- incr written;
139139- state := new_state
140140- done;
141141-142142- (* Verify stream is exactly consumed *)
143143- if Bit_reader.Backward.remaining stream <> -dtable.max_bits then
144144- raise (Constants.Zstd_error Constants.Corruption);
145145-146146- !written
147147-148148-(** Decompress 4 interleaved Huffman streams *)
149149-let decompress_4stream dtable src ~pos ~len output ~out_pos ~regen_size =
150150- (* Read stream sizes from jump table (6 bytes) *)
151151- let size1 = Bit_reader.get_u16_le src pos in
152152- let size2 = Bit_reader.get_u16_le src (pos + 2) in
153153- let size3 = Bit_reader.get_u16_le src (pos + 4) in
154154- let size4 = len - 6 - size1 - size2 - size3 in
155155-156156- if size4 < 1 then
157157- raise (Constants.Zstd_error Constants.Corruption);
158158-159159- (* Calculate output sizes *)
160160- let out_size = (regen_size + 3) / 4 in
161161- let out_size4 = regen_size - 3 * out_size in
162162-163163- (* Decompress each stream *)
164164- let stream_pos = pos + 6 in
165165-166166- let written1 = decompress_1stream dtable src
167167- ~pos:stream_pos ~len:size1
168168- output ~out_pos ~out_len:out_size in
169169-170170- let written2 = decompress_1stream dtable src
171171- ~pos:(stream_pos + size1) ~len:size2
172172- output ~out_pos:(out_pos + out_size) ~out_len:out_size in
173173-174174- let written3 = decompress_1stream dtable src
175175- ~pos:(stream_pos + size1 + size2) ~len:size3
176176- output ~out_pos:(out_pos + 2 * out_size) ~out_len:out_size in
177177-178178- let written4 = decompress_1stream dtable src
179179- ~pos:(stream_pos + size1 + size2 + size3) ~len:size4
180180- output ~out_pos:(out_pos + 3 * out_size) ~out_len:out_size4 in
181181-182182- written1 + written2 + written3 + written4
183183-184184-(** Decode Huffman table from stream.
185185- Returns (dtable, bytes consumed) *)
186186-let decode_table (stream : Bit_reader.Forward.t) =
187187- let header = Bit_reader.Forward.read_byte stream in
188188-189189- let weights = Array.make Constants.max_huffman_symbols 0 in
190190- let num_symbols =
191191- if header >= 128 then begin
192192- (* Direct representation: 4 bits per weight *)
193193- let count = header - 127 in
194194- let bytes_needed = (count + 1) / 2 in
195195- let data = Bit_reader.Forward.get_bytes stream bytes_needed in
196196-197197- for i = 0 to count - 1 do
198198- let byte = Bytes.get_uint8 data (i / 2) in
199199- weights.(i) <- if i mod 2 = 0 then byte lsr 4 else byte land 0xf
200200- done;
201201- count
202202- end else begin
203203- (* FSE compressed weights *)
204204- let compressed_size = header in
205205- let fse_data = Bit_reader.Forward.get_bytes stream compressed_size in
206206-207207- (* Decode FSE table for weights (max accuracy 7) *)
208208- let fse_stream = Bit_reader.Forward.of_bytes fse_data in
209209- let fse_table = Fse.decode_header fse_stream 7 in
210210-211211- (* Remaining bytes are the compressed weights *)
212212- let weights_pos = Bit_reader.Forward.byte_position fse_stream in
213213- let weights_len = compressed_size - weights_pos in
214214-215215- let weight_bytes = Bytes.create Constants.max_huffman_symbols in
216216- let decoded = Fse.decompress_interleaved2 fse_table
217217- fse_data ~pos:weights_pos ~len:weights_len weight_bytes in
218218-219219- for i = 0 to decoded - 1 do
220220- weights.(i) <- Bytes.get_uint8 weight_bytes i
221221- done;
222222- decoded
223223- end
224224- in
225225-226226- build_dtable_from_weights weights num_symbols
227227-228228-(* ========== ENCODING ========== *)
229229-230230-(** Huffman encoding table *)
231231-type ctable = {
232232- codes : int array; (* Canonical code for each symbol *)
233233- num_bits : int array; (* Bit length for each symbol *)
234234- max_bits : int;
235235- num_symbols : int;
236236-}
237237-238238-(** Build Huffman code from frequencies using package-merge algorithm *)
239239-let build_ctable counts max_symbol max_bits_limit =
240240- let num_symbols = max_symbol + 1 in
241241- let freqs = Array.sub counts 0 num_symbols in
242242-243243- (* Count non-zero frequencies *)
244244- let non_zero = ref 0 in
245245- for i = 0 to num_symbols - 1 do
246246- if freqs.(i) > 0 then incr non_zero
247247- done;
248248-249249- if !non_zero = 0 then
250250- { codes = [||]; num_bits = [||]; max_bits = 0; num_symbols = 0 }
251251- else if !non_zero = 1 then begin
252252- (* Single symbol case *)
253253- let num_bits = Array.make num_symbols 0 in
254254- for i = 0 to num_symbols - 1 do
255255- if freqs.(i) > 0 then num_bits.(i) <- 1
256256- done;
257257- let codes = Array.make num_symbols 0 in
258258- { codes; num_bits; max_bits = 1; num_symbols }
259259- end else begin
260260- (* Sort symbols by frequency *)
261261- let sorted = Array.init num_symbols (fun i -> (freqs.(i), i)) in
262262- Array.sort (fun (f1, _) (f2, _) -> compare f1 f2) sorted;
263263-264264- (* Build Huffman tree using a simple greedy approach *)
265265- (* This produces a valid but not necessarily optimal tree *)
266266- let bit_lengths = Array.make num_symbols 0 in
267267-268268- (* Assign bit lengths based on frequency rank *)
269269- let active_count = ref 0 in
270270- for i = 0 to num_symbols - 1 do
271271- let (freq, _sym) = sorted.(num_symbols - 1 - i) in
272272- if freq > 0 then incr active_count
273273- done;
274274-275275- (* Use Kraft's inequality to assign optimal lengths *)
276276- (* Start with uniform distribution and adjust *)
277277- let target_bits = max 1 (highest_set_bit !active_count + 1) in
278278- let max_bits = min max_bits_limit (max target_bits 1) in
279279-280280- (* Simple heuristic: assign bits based on frequency ranking *)
281281- let rank = ref 0 in
282282- for i = num_symbols - 1 downto 0 do
283283- let (freq, sym) = sorted.(i) in
284284- if freq > 0 then begin
285285- (* More frequent symbols get shorter codes *)
286286- let bits =
287287- if !rank < (1 lsl (max_bits - 1)) then
288288- min max_bits (max 1 (max_bits - highest_set_bit (!rank + 1)))
289289- else
290290- max_bits
291291- in
292292- bit_lengths.(sym) <- bits;
293293- incr rank
294294- end
295295- done;
296296-297297- (* Validate and adjust bit lengths to satisfy Kraft inequality *)
298298- let rec adjust () =
299299- let kraft_sum = ref 0.0 in
300300- for i = 0 to num_symbols - 1 do
301301- if bit_lengths.(i) > 0 then
302302- kraft_sum := !kraft_sum +. (1.0 /. (float_of_int (1 lsl bit_lengths.(i))))
303303- done;
304304- if !kraft_sum > 1.0 then begin
305305- (* Increase some lengths *)
306306- for i = 0 to num_symbols - 1 do
307307- if bit_lengths.(i) > 0 && bit_lengths.(i) < max_bits then begin
308308- bit_lengths.(i) <- bit_lengths.(i) + 1
309309- end
310310- done;
311311- adjust ()
312312- end
313313- in
314314- adjust ();
315315-316316- (* Build canonical codes *)
317317- let codes = Array.make num_symbols 0 in
318318- let actual_max = ref 0 in
319319- for i = 0 to num_symbols - 1 do
320320- if bit_lengths.(i) > !actual_max then actual_max := bit_lengths.(i)
321321- done;
322322-323323- (* Count symbols at each bit length *)
324324- let bl_count = Array.make (!actual_max + 1) 0 in
325325- for i = 0 to num_symbols - 1 do
326326- if bit_lengths.(i) > 0 then
327327- bl_count.(bit_lengths.(i)) <- bl_count.(bit_lengths.(i)) + 1
328328- done;
329329-330330- (* Calculate starting code for each bit length *)
331331- let next_code = Array.make (!actual_max + 1) 0 in
332332- let code = ref 0 in
333333- for bits = 1 to !actual_max do
334334- code := (!code + bl_count.(bits - 1)) lsl 1;
335335- next_code.(bits) <- !code
336336- done;
337337-338338- (* Assign codes to symbols *)
339339- for i = 0 to num_symbols - 1 do
340340- let bits = bit_lengths.(i) in
341341- if bits > 0 then begin
342342- codes.(i) <- next_code.(bits);
343343- next_code.(bits) <- next_code.(bits) + 1
344344- end
345345- done;
346346-347347- { codes; num_bits = bit_lengths; max_bits = !actual_max; num_symbols }
348348- end
349349-350350-(** Convert bit lengths to weights (zstd format) *)
351351-let bits_to_weights num_bits num_symbols max_bits =
352352- let weights = Array.make num_symbols 0 in
353353- for i = 0 to num_symbols - 1 do
354354- if num_bits.(i) > 0 then
355355- weights.(i) <- max_bits + 1 - num_bits.(i)
356356- done;
357357- weights
358358-359359-(** Write Huffman table header using direct representation.
360360- Returns the number of actual symbols to encode.
361361- Note: For tables with >127 weights, FSE compression could be used
362362- for better ratios, but direct representation is always valid. *)
363363-let write_header (stream : Bit_writer.Forward.t) ctable =
364364- if ctable.num_symbols = 0 then 0
365365- else begin
366366- let weights = bits_to_weights ctable.num_bits ctable.num_symbols ctable.max_bits in
367367-368368- (* Find last non-zero weight (implicit last symbol) *)
369369- let last_nonzero = ref (ctable.num_symbols - 1) in
370370- while !last_nonzero > 0 && weights.(!last_nonzero) = 0 do
371371- decr last_nonzero
372372- done;
373373-374374- let num_weights = !last_nonzero in (* Last weight is implicit *)
375375-376376- (* Direct representation: header byte = 128 + num_weights, then 4 bits per weight *)
377377- let header = 128 + num_weights in
378378- Bit_writer.Forward.write_byte stream header;
379379-380380- (* Write weights packed as pairs (high nibble, low nibble) *)
381381- for i = 0 to (num_weights - 1) / 2 do
382382- let w1 = if 2 * i < num_weights then weights.(2 * i) else 0 in
383383- let w2 = if 2 * i + 1 < num_weights then weights.(2 * i + 1) else 0 in
384384- Bit_writer.Forward.write_byte stream ((w1 lsl 4) lor w2)
385385- done;
386386-387387- num_weights + 1
388388- end
389389-390390-(** Encode a single symbol (write to backward stream) *)
391391-let[@inline] encode_symbol ctable (stream : Bit_writer.Backward.t) symbol =
392392- let code = ctable.codes.(symbol) in
393393- let bits = ctable.num_bits.(symbol) in
394394- if bits > 0 then
395395- Bit_writer.Backward.write_bits stream code bits
396396-397397-(** Compress literals to a single Huffman stream *)
398398-let compress_1stream ctable literals ~pos ~len =
399399- let stream = Bit_writer.Backward.create (len * 2 + 16) in
400400-401401- (* Encode symbols in reverse order *)
402402- for i = pos + len - 1 downto pos do
403403- let sym = Bytes.get_uint8 literals i in
404404- encode_symbol ctable stream sym
405405- done;
406406-407407- Bit_writer.Backward.finalize stream
408408-409409-(** Compress literals to 4 interleaved Huffman streams *)
410410-let compress_4stream ctable literals ~pos ~len =
411411- let chunk_size = (len + 3) / 4 in
412412- let chunk4_size = len - 3 * chunk_size in
413413-414414- (* Compress each stream *)
415415- let stream1 = compress_1stream ctable literals ~pos ~len:chunk_size in
416416- let stream2 = compress_1stream ctable literals ~pos:(pos + chunk_size) ~len:chunk_size in
417417- let stream3 = compress_1stream ctable literals ~pos:(pos + 2 * chunk_size) ~len:chunk_size in
418418- let stream4 = compress_1stream ctable literals ~pos:(pos + 3 * chunk_size) ~len:chunk4_size in
419419-420420- (* Build output with jump table *)
421421- let size1 = Bytes.length stream1 in
422422- let size2 = Bytes.length stream2 in
423423- let size3 = Bytes.length stream3 in
424424- let total = 6 + size1 + size2 + size3 + Bytes.length stream4 in
425425-426426- let output = Bytes.create total in
427427- Bytes.set_uint16_le output 0 size1;
428428- Bytes.set_uint16_le output 2 size2;
429429- Bytes.set_uint16_le output 4 size3;
430430- Bytes.blit stream1 0 output 6 size1;
431431- Bytes.blit stream2 0 output (6 + size1) size2;
432432- Bytes.blit stream3 0 output (6 + size1 + size2) size3;
433433- Bytes.blit stream4 0 output (6 + size1 + size2 + size3) (Bytes.length stream4);
434434-435435- output
-183
ocaml-zstd/src/zstd.ml
···11-(** Pure OCaml implementation of Zstandard compression (RFC 8878).
22-33- {2 Decoder}
44-55- The decoder is fully compliant with the zstd format specification and can
66- decompress any valid zstd frame produced by any conforming encoder. It
77- supports all block types (raw, RLE, compressed), Huffman and FSE entropy
88- coding, and content checksums.
99-1010- {2 Encoder}
1111-1212- The encoder produces valid zstd frames that can be decompressed by any
1313- conforming decoder (including the reference C implementation). Current
1414- encoding strategy:
1515-1616- - {b RLE blocks}: Data consisting of a single repeated byte is encoded as
1717- RLE blocks (4 bytes total regardless of decompressed size)
1818- - {b Raw blocks}: All other data is stored as raw (uncompressed) blocks
1919-2020- This means the encoder always produces valid output, but compression ratios
2121- are not optimal for most data. The encoder is suitable for:
2222- - Applications where decompression speed matters more than compressed size
2323- - Data that is already compressed or has high entropy
2424- - Testing zstd decoders
2525-2626- Future improvements planned:
2727- - LZ77 match finding with sequence encoding
2828- - Huffman compression for literals
2929- - FSE-compressed blocks for better ratios
3030-3131- {2 Dictionary Support}
3232-3333- Dictionary decompression is supported. Dictionary compression is not yet
3434- implemented (falls back to regular compression). *)
3535-3636-type error = Constants.error =
3737- | Invalid_magic_number
3838- | Invalid_frame_header
3939- | Invalid_block_type
4040- | Invalid_block_size
4141- | Invalid_literals_header
4242- | Invalid_huffman_table
4343- | Invalid_fse_table
4444- | Invalid_sequence_header
4545- | Invalid_offset
4646- | Invalid_match_length
4747- | Truncated_input
4848- | Output_too_small
4949- | Checksum_mismatch
5050- | Dictionary_mismatch
5151- | Corruption
5252-5353-exception Zstd_error = Constants.Zstd_error
5454-5555-type dictionary = Zstd_decode.dictionary
5656-5757-let error_message = Constants.error_message
5858-5959-(** Check if data starts with zstd magic number *)
6060-let is_zstd_frame s =
6161- if String.length s < 4 then false
6262- else
6363- let b = Bytes.unsafe_of_string s in
6464- let magic = Bytes.get_int32_le b 0 in
6565- magic = Constants.zstd_magic_number
6666-6767-(** Get decompressed size from frame header *)
6868-let get_decompressed_size s =
6969- if String.length s < 5 then None
7070- else
7171- let b = Bytes.unsafe_of_string s in
7272- Zstd_decode.get_decompressed_size b ~pos:0 ~len:(String.length s)
7373-7474-(** Calculate maximum compressed size *)
7575-let compress_bound src_len =
7676- (* zstd guarantees compressed size <= src_len + (src_len >> 8) + constant *)
7777- src_len + (src_len lsr 8) + 64
7878-7979-(** Load dictionary *)
8080-let load_dictionary s =
8181- let b = Bytes.of_string s in
8282- Zstd_decode.parse_dictionary b ~pos:0 ~len:(String.length s)
8383-8484-(** Decompress bytes *)
8585-let decompress_bytes_exn src =
8686- Zstd_decode.decompress_frame src ~pos:0 ~len:(Bytes.length src)
8787-8888-let decompress_bytes src =
8989- try Ok (decompress_bytes_exn src)
9090- with Zstd_error e -> Error (error_message e)
9191-9292-(** Decompress string *)
9393-let decompress_exn s =
9494- let src = Bytes.unsafe_of_string s in
9595- let result = Zstd_decode.decompress_frame src ~pos:0 ~len:(String.length s) in
9696- Bytes.unsafe_to_string result
9797-9898-let decompress s =
9999- try Ok (decompress_exn s)
100100- with Zstd_error e -> Error (error_message e)
101101-102102-(** Decompress with dictionary *)
103103-let decompress_with_dict_exn dict s =
104104- let src = Bytes.unsafe_of_string s in
105105- let result = Zstd_decode.decompress_frame ~dict src ~pos:0 ~len:(String.length s) in
106106- Bytes.unsafe_to_string result
107107-108108-let decompress_with_dict dict s =
109109- try Ok (decompress_with_dict_exn dict s)
110110- with Zstd_error e -> Error (error_message e)
111111-112112-(** Decompress into pre-allocated buffer *)
113113-let decompress_into ~src ~src_pos ~src_len ~dst ~dst_pos =
114114- let result = Zstd_decode.decompress_frame src ~pos:src_pos ~len:src_len in
115115- let result_len = Bytes.length result in
116116- if dst_pos + result_len > Bytes.length dst then
117117- raise (Zstd_error Output_too_small);
118118- Bytes.blit result 0 dst dst_pos result_len;
119119- result_len
120120-121121-(** Compress string *)
122122-let compress ?(level=3) s =
123123- Zstd_encode.compress ~level ~checksum:true s
124124-125125-(** Compress bytes *)
126126-let compress_bytes ?(level=3) src =
127127- let s = Bytes.unsafe_to_string src in
128128- let result = Zstd_encode.compress ~level ~checksum:true s in
129129- Bytes.of_string result
130130-131131-let compress_with_dict ?level _dict s =
132132- (* Dictionary compression uses same encoder but with preloaded tables *)
133133- (* For now, just compress without dictionary *)
134134- compress ?level s
135135-136136-let compress_into ?(level=3) ~src ~src_pos ~src_len ~dst ~dst_pos () =
137137- let input = Bytes.sub_string src src_pos src_len in
138138- let result = Zstd_encode.compress ~level ~checksum:true input in
139139- let result_len = String.length result in
140140- if dst_pos + result_len > Bytes.length dst then
141141- raise (Zstd_error Output_too_small);
142142- Bytes.blit_string result 0 dst dst_pos result_len;
143143- result_len
144144-145145-(** Check if data starts with skippable frame magic *)
146146-let is_skippable_frame s =
147147- let b = Bytes.unsafe_of_string s in
148148- Zstd_decode.is_skippable_frame b ~pos:0 ~len:(String.length s)
149149-150150-(** Get skippable frame variant (0-15) *)
151151-let get_skippable_variant s =
152152- let b = Bytes.unsafe_of_string s in
153153- Zstd_decode.get_skippable_variant b ~pos:0 ~len:(String.length s)
154154-155155-(** Write a skippable frame *)
156156-let write_skippable_frame ?variant content =
157157- Zstd_encode.write_skippable_frame ?variant content
158158-159159-(** Read a skippable frame and return its content *)
160160-let read_skippable_frame s =
161161- let b = Bytes.unsafe_of_string s in
162162- let (content, _) = Zstd_decode.read_skippable_frame b ~pos:0 ~len:(String.length s) in
163163- content
164164-165165-(** Get total size of skippable frame *)
166166-let get_skippable_frame_size s =
167167- let b = Bytes.unsafe_of_string s in
168168- Zstd_decode.get_skippable_frame_size b ~pos:0 ~len:(String.length s)
169169-170170-(** Find compressed size of first frame *)
171171-let find_frame_compressed_size s =
172172- let b = Bytes.unsafe_of_string s in
173173- Zstd_decode.find_frame_compressed_size b ~pos:0 ~len:(String.length s)
174174-175175-(** Decompress all frames *)
176176-let decompress_all_exn s =
177177- let b = Bytes.unsafe_of_string s in
178178- let result = Zstd_decode.decompress_frames b ~pos:0 ~len:(String.length s) in
179179- Bytes.unsafe_to_string result
180180-181181-let decompress_all s =
182182- try Ok (decompress_all_exn s)
183183- with Zstd_error e -> Error (error_message e)
-201
ocaml-zstd/src/zstd.mli
···11-(** Pure OCaml implementation of Zstandard compression (RFC 8878).
22-33- Zstandard is a fast compression algorithm providing high compression
44- ratios. This library provides both compression and decompression
55- functionality in pure OCaml.
66-77- {1 Quick Start}
88-99- Decompress data:
1010- {[
1111- let compressed = ... in
1212- match Zstd.decompress compressed with
1313- | Ok data -> use data
1414- | Error msg -> handle_error msg
1515- ]}
1616-1717- Compress data:
1818- {[
1919- let data = ... in
2020- let compressed = Zstd.compress data in
2121- ...
2222- ]}
2323-2424- {1 Error Handling}
2525-2626- Two styles are provided:
2727- - Result-based: [decompress] returns [(string, string) result]
2828- - Exception-based: [decompress_exn] raises [Zstd_error]
2929-3030- {1 Compression Levels}
3131-3232- Compression levels range from 1 (fastest) to 19 (best compression).
3333- The default level is 3, which provides a good balance.
3434- Level 0 is a special level meaning "use default".
3535-*)
3636-3737-(** {1 Types} *)
3838-3939-(** Error codes for decompression failures *)
4040-type error =
4141- | Invalid_magic_number
4242- | Invalid_frame_header
4343- | Invalid_block_type
4444- | Invalid_block_size
4545- | Invalid_literals_header
4646- | Invalid_huffman_table
4747- | Invalid_fse_table
4848- | Invalid_sequence_header
4949- | Invalid_offset
5050- | Invalid_match_length
5151- | Truncated_input
5252- | Output_too_small
5353- | Checksum_mismatch
5454- | Dictionary_mismatch
5555- | Corruption
5656-5757-(** Exception raised by [*_exn] functions *)
5858-exception Zstd_error of error
5959-6060-(** Pre-loaded dictionary for compression/decompression *)
6161-type dictionary
6262-6363-(** {1 Simple API} *)
6464-6565-(** Decompress a zstd-compressed string.
6666- @return [Ok data] on success, [Error msg] on failure *)
6767-val decompress : string -> (string, string) result
6868-6969-(** Decompress a zstd-compressed string.
7070- @raise Zstd_error on failure *)
7171-val decompress_exn : string -> string
7272-7373-(** Compress a string using zstd.
7474- @param level Compression level 1-19 (default: 3)
7575- @return Compressed data *)
7676-val compress : ?level:int -> string -> string
7777-7878-(** {1 Bytes API} *)
7979-8080-(** Decompress from bytes.
8181- @return [Ok data] on success, [Error msg] on failure *)
8282-val decompress_bytes : bytes -> (bytes, string) result
8383-8484-(** Decompress from bytes.
8585- @raise Zstd_error on failure *)
8686-val decompress_bytes_exn : bytes -> bytes
8787-8888-(** Compress bytes.
8989- @param level Compression level 1-19 (default: 3) *)
9090-val compress_bytes : ?level:int -> bytes -> bytes
9191-9292-(** {1 Low-allocation API} *)
9393-9494-(** Decompress into a pre-allocated buffer.
9595- @param src Source buffer with compressed data
9696- @param src_pos Start position in source
9797- @param src_len Length of compressed data
9898- @param dst Destination buffer
9999- @param dst_pos Start position in destination
100100- @return Number of bytes written to destination
101101- @raise Zstd_error on failure or if destination is too small *)
102102-val decompress_into :
103103- src:bytes -> src_pos:int -> src_len:int ->
104104- dst:bytes -> dst_pos:int -> int
105105-106106-(** Compress into a pre-allocated buffer.
107107- @param level Compression level 1-19 (default: 3)
108108- @param src Source buffer
109109- @param src_pos Start position in source
110110- @param src_len Length of data to compress
111111- @param dst Destination buffer
112112- @param dst_pos Start position in destination
113113- @return Number of bytes written to destination
114114- @raise Zstd_error on failure or if destination is too small *)
115115-val compress_into :
116116- ?level:int ->
117117- src:bytes -> src_pos:int -> src_len:int ->
118118- dst:bytes -> dst_pos:int -> unit -> int
119119-120120-(** {1 Frame Information} *)
121121-122122-(** Get the decompressed size from a frame header, if available.
123123- Returns [None] if the frame doesn't include the content size. *)
124124-val get_decompressed_size : string -> int64 option
125125-126126-(** Check if data starts with a valid zstd magic number. *)
127127-val is_zstd_frame : string -> bool
128128-129129-(** Calculate the maximum compressed size for a given input size.
130130- This can be used to allocate a buffer for compression. *)
131131-val compress_bound : int -> int
132132-133133-(** {1 Dictionary Support} *)
134134-135135-(** Load a dictionary from data.
136136- The dictionary can be either a raw content dictionary or a
137137- formatted dictionary with pre-computed entropy tables. *)
138138-val load_dictionary : string -> dictionary
139139-140140-(** Decompress using a dictionary.
141141- @return [Ok data] on success, [Error msg] on failure *)
142142-val decompress_with_dict : dictionary -> string -> (string, string) result
143143-144144-(** Decompress using a dictionary.
145145- @raise Zstd_error on failure *)
146146-val decompress_with_dict_exn : dictionary -> string -> string
147147-148148-(** Compress using a dictionary.
149149- @param level Compression level 1-19 (default: 3) *)
150150-val compress_with_dict : ?level:int -> dictionary -> string -> string
151151-152152-(** {1 Error Utilities} *)
153153-154154-(** Convert an error code to a human-readable message. *)
155155-val error_message : error -> string
156156-157157-(** {1 Frame Type Detection} *)
158158-159159-(** Check if data starts with a valid skippable frame magic number.
160160- Skippable frames have magic numbers in the range 0x184D2A50 to 0x184D2A5F. *)
161161-val is_skippable_frame : string -> bool
162162-163163-(** Get the skippable frame variant (0-15) if present.
164164- Returns [None] if not a skippable frame. *)
165165-val get_skippable_variant : string -> int option
166166-167167-(** {1 Skippable Frame Support} *)
168168-169169-(** Write a skippable frame.
170170- Skippable frames can contain arbitrary data that will be ignored by decoders.
171171- @param variant Magic number variant 0-15 (default: 0)
172172- @param content The content to embed
173173- @return The complete skippable frame *)
174174-val write_skippable_frame : ?variant:int -> string -> string
175175-176176-(** Read a skippable frame and return its content.
177177- @return The content bytes
178178- @raise Zstd_error if not a valid skippable frame *)
179179-val read_skippable_frame : string -> bytes
180180-181181-(** Get the total size of a skippable frame (header + content).
182182- @return [Some size] if a valid skippable frame, [None] otherwise *)
183183-val get_skippable_frame_size : string -> int option
184184-185185-(** {1 Multi-Frame Support} *)
186186-187187-(** Find the compressed size of the first frame (zstd or skippable).
188188- This is useful for parsing concatenated frames.
189189- @return Size in bytes of the complete first frame
190190- @raise Zstd_error on invalid or truncated input *)
191191-val find_frame_compressed_size : string -> int
192192-193193-(** Decompress all frames (including skipping skippable frames).
194194- Concatenated zstd frames are decompressed and their output concatenated.
195195- Skippable frames are silently skipped.
196196- @return The concatenated decompressed output *)
197197-val decompress_all : string -> (string, string) result
198198-199199-(** Decompress all frames, raising on error.
200200- @raise Zstd_error on failure *)
201201-val decompress_all_exn : string -> string
-721
ocaml-zstd/src/zstd_decode.ml
···11-(** Zstandard decompression implementation (RFC 8878). *)
22-33-(** Frame header information *)
44-type frame_header = {
55- window_size : int;
66- frame_content_size : int64 option;
77- dictionary_id : int32 option;
88- content_checksum : bool;
99- single_segment : bool;
1010-}
1111-1212-(** Sequence command *)
1313-type sequence = {
1414- literal_length : int;
1515- match_length : int;
1616- offset : int;
1717-}
1818-1919-(** Dictionary *)
2020-type dictionary = {
2121- dict_id : int32;
2222- huf_table : Huffman.dtable option;
2323- ll_table : Fse.dtable;
2424- ml_table : Fse.dtable;
2525- of_table : Fse.dtable;
2626- content : bytes;
2727- repeat_offsets : int array;
2828-}
2929-3030-(** Frame context during decompression *)
3131-type frame_context = {
3232- mutable huf_table : Huffman.dtable option;
3333- mutable ll_table : Fse.dtable option;
3434- mutable ml_table : Fse.dtable option;
3535- mutable of_table : Fse.dtable option;
3636- mutable repeat_offsets : int array;
3737- mutable total_output : int;
3838- dict : dictionary option;
3939- dict_content : bytes option;
4040- window_size : int;
4141-}
4242-4343-(** Parse frame header *)
4444-let parse_frame_header stream =
4545- let descriptor = Bit_reader.Forward.read_byte stream in
4646-4747- let fcs_flag = descriptor lsr 6 in
4848- let single_segment = (descriptor lsr 5) land 1 = 1 in
4949- let (_ : int) = (descriptor lsr 4) land 1 in (* unused bit *)
5050- let reserved = (descriptor lsr 3) land 1 in
5151- let checksum_flag = (descriptor lsr 2) land 1 = 1 in
5252- let dict_id_flag = descriptor land 3 in
5353-5454- if reserved <> 0 then
5555- raise (Constants.Zstd_error Constants.Invalid_frame_header);
5656-5757- (* Window descriptor (if not single segment) *)
5858- let window_size =
5959- if not single_segment then begin
6060- let window_desc = Bit_reader.Forward.read_byte stream in
6161- let exponent = window_desc lsr 3 in
6262- let mantissa = window_desc land 7 in
6363- let window_base = 1 lsl (10 + exponent) in
6464- let window_add = (window_base / 8) * mantissa in
6565- window_base + window_add
6666- end else 0
6767- in
6868-6969- (* Dictionary ID *)
7070- let dictionary_id =
7171- if dict_id_flag <> 0 then begin
7272- let sizes = [| 0; 1; 2; 4 |] in
7373- let bytes = sizes.(dict_id_flag) in
7474- let id = ref 0l in
7575- for i = 0 to bytes - 1 do
7676- let b = Bit_reader.Forward.read_byte stream in
7777- id := Int32.logor !id (Int32.shift_left (Int32.of_int b) (i * 8))
7878- done;
7979- Some !id
8080- end else None
8181- in
8282-8383- (* Frame content size *)
8484- let frame_content_size =
8585- if single_segment || fcs_flag <> 0 then begin
8686- let sizes = [| 1; 2; 4; 8 |] in
8787- let bytes = sizes.(fcs_flag) in
8888- let size = ref 0L in
8989- for i = 0 to bytes - 1 do
9090- let b = Bit_reader.Forward.read_byte stream in
9191- size := Int64.logor !size (Int64.shift_left (Int64.of_int b) (i * 8))
9292- done;
9393- (* 2-byte sizes have 256 added *)
9494- if bytes = 2 then size := Int64.add !size 256L;
9595- Some !size
9696- end else None
9797- in
9898-9999- (* For single segment, window_size = frame_content_size *)
100100- let window_size =
101101- if single_segment then
102102- Option.fold ~none:0 ~some:Int64.to_int frame_content_size
103103- else window_size
104104- in
105105-106106- { window_size; frame_content_size; dictionary_id;
107107- content_checksum = checksum_flag; single_segment }
108108-109109-(** Decode literals section *)
110110-let decode_literals ctx stream output ~out_pos =
111111- (* Read first byte to get block type and size format *)
112112- let header_byte = Bit_reader.Forward.read_byte stream in
113113- let block_type = header_byte land 3 in
114114- let size_format = (header_byte lsr 2) land 3 in
115115-116116- match Constants.literals_block_type_of_int block_type with
117117- | Raw_literals | RLE_literals ->
118118- (* For Raw/RLE: Size_Format determines header size
119119- 00/10: 1 byte total (5 bit size in first byte)
120120- 01: 2 bytes total (12 bit size)
121121- 11: 3 bytes total (20 bit size) *)
122122- let regen_size =
123123- match size_format with
124124- | 0 | 2 ->
125125- (* 5-bit size is in upper 5 bits of first byte *)
126126- header_byte lsr 3
127127- | 1 ->
128128- (* 12-bit size: 4 bits from first byte + 8 bits from second *)
129129- let high = header_byte lsr 4 in
130130- let low = Bit_reader.Forward.read_byte stream in
131131- (low lsl 4) lor high
132132- | 3 | _ ->
133133- (* 20-bit size: 4 bits + 16 bits *)
134134- let high = header_byte lsr 4 in
135135- let b1 = Bit_reader.Forward.read_byte stream in
136136- let b2 = Bit_reader.Forward.read_byte stream in
137137- (b2 lsl 12) lor (b1 lsl 4) lor high
138138- in
139139-140140- if regen_size > Constants.max_literals_size then
141141- raise (Constants.Zstd_error Constants.Invalid_literals_header);
142142-143143- begin match Constants.literals_block_type_of_int block_type with
144144- | Raw_literals ->
145145- if regen_size > 0 then begin
146146- let data = Bit_reader.Forward.get_bytes stream regen_size in
147147- Bytes.blit data 0 output out_pos regen_size
148148- end
149149- | RLE_literals ->
150150- if regen_size > 0 then begin
151151- let byte = Bit_reader.Forward.read_byte stream in
152152- Bytes.fill output out_pos regen_size (Char.chr byte)
153153- end
154154- | _ -> ()
155155- end;
156156- regen_size
157157-158158- | Compressed_literals | Treeless_literals ->
159159- let num_streams = if size_format = 0 then 1 else 4 in
160160-161161- (* For compressed: Size_Format determines header size
162162- 0: 1 stream, 3 bytes (10-bit sizes)
163163- 1: 4 streams, 3 bytes (10-bit sizes)
164164- 2: 4 streams, 4 bytes (14-bit sizes)
165165- 3: 4 streams, 5 bytes (18-bit sizes) *)
166166- let (regen_size, compressed_size) =
167167- match size_format with
168168- | 0 | 1 ->
169169- (* 3 bytes: 4 bits type+format, 10 bits regen, 10 bits compressed *)
170170- let b1 = Bit_reader.Forward.read_byte stream in
171171- let b2 = Bit_reader.Forward.read_byte stream in
172172- let high = header_byte lsr 4 in
173173- let regen = ((b1 land 0x3f) lsl 4) lor high in
174174- let comp = (b2 lsl 2) lor (b1 lsr 6) in
175175- (regen, comp)
176176- | 2 ->
177177- (* 4 bytes: 4 bits, 14 bits, 14 bits *)
178178- let b1 = Bit_reader.Forward.read_byte stream in
179179- let b2 = Bit_reader.Forward.read_byte stream in
180180- let b3 = Bit_reader.Forward.read_byte stream in
181181- let high = header_byte lsr 4 in
182182- let regen = (((b2 land 3) lsl 12) lor (b1 lsl 4) lor high) in
183183- let comp = (b3 lsl 6) lor (b2 lsr 2) in
184184- (regen, comp)
185185- | 3 | _ ->
186186- (* 5 bytes: 4 bits, 18 bits, 18 bits *)
187187- let b1 = Bit_reader.Forward.read_byte stream in
188188- let b2 = Bit_reader.Forward.read_byte stream in
189189- let b3 = Bit_reader.Forward.read_byte stream in
190190- let b4 = Bit_reader.Forward.read_byte stream in
191191- let high = header_byte lsr 4 in
192192- let regen = ((b2 land 0x3f) lsl 12) lor (b1 lsl 4) lor high in
193193- let comp = (b4 lsl 10) lor (b3 lsl 2) lor (b2 lsr 6) in
194194- (regen, comp)
195195- in
196196-197197- if regen_size > Constants.max_literals_size then
198198- raise (Constants.Zstd_error Constants.Invalid_literals_header);
199199-200200- (* Get compressed data *)
201201- let huf_data = Bit_reader.Forward.get_bytes stream compressed_size in
202202- let huf_stream = Bit_reader.Forward.of_bytes huf_data in
203203-204204- (* Decode Huffman table if not treeless *)
205205- let dtable =
206206- if block_type = 2 then begin
207207- let table = Huffman.decode_table huf_stream in
208208- ctx.huf_table <- Some table;
209209- table
210210- end else begin
211211- match ctx.huf_table with
212212- | Some t -> t
213213- | None -> raise (Constants.Zstd_error Constants.Invalid_huffman_table)
214214- end
215215- in
216216-217217- (* Decode literals *)
218218- let huf_pos = Bit_reader.Forward.byte_position huf_stream in
219219- let huf_len = compressed_size - huf_pos in
220220-221221- let written =
222222- if num_streams = 1 then
223223- Huffman.decompress_1stream dtable huf_data
224224- ~pos:huf_pos ~len:huf_len
225225- output ~out_pos ~out_len:regen_size
226226- else
227227- Huffman.decompress_4stream dtable huf_data
228228- ~pos:huf_pos ~len:huf_len
229229- output ~out_pos ~regen_size
230230- in
231231-232232- if written <> regen_size then
233233- raise (Constants.Zstd_error Constants.Corruption);
234234-235235- regen_size
236236-237237-(** Decode sequence table based on mode *)
238238-let decode_seq_table stream mode default_dist default_acc max_acc get_table set_table =
239239- match mode with
240240- | Constants.Predefined_mode ->
241241- set_table (Some (Fse.build_predefined_table default_dist default_acc))
242242- | Constants.RLE_mode ->
243243- let symbol = Bit_reader.Forward.read_byte stream in
244244- set_table (Some (Fse.build_dtable_rle symbol))
245245- | Constants.FSE_mode ->
246246- set_table (Some (Fse.decode_header stream max_acc))
247247- | Constants.Repeat_mode ->
248248- match get_table () with
249249- | Some _ -> ()
250250- | None -> raise (Constants.Zstd_error Constants.Invalid_fse_table)
251251-252252-(** Decode sequences section *)
253253-let decode_sequences ctx stream =
254254- (* Number of sequences *)
255255- let header = Bit_reader.Forward.read_byte stream in
256256- let num_sequences =
257257- if header < 128 then header
258258- else if header < 255 then
259259- let second = Bit_reader.Forward.read_byte stream in
260260- ((header - 128) lsl 8) + second
261261- else begin
262262- let low = Bit_reader.Forward.read_byte stream in
263263- let high = Bit_reader.Forward.read_byte stream in
264264- low + (high lsl 8) + 0x7F00
265265- end
266266- in
267267-268268- if num_sequences = 0 then [||]
269269- else begin
270270- (* Compression modes byte (RFC 8878 section 3.1.1.3.2.1):
271271- bits 0-1: Literals_Lengths_Mode
272272- bits 2-3: Offsets_Mode
273273- bits 4-5: Match_Lengths_Mode
274274- bits 6-7: reserved (must be 0) *)
275275- let modes = Bit_reader.Forward.read_byte stream in
276276- if (modes lsr 6) land 3 <> 0 then
277277- raise (Constants.Zstd_error Constants.Invalid_sequence_header);
278278-279279- let ll_mode = Constants.seq_mode_of_int (modes land 3) in
280280- let of_mode = Constants.seq_mode_of_int ((modes lsr 2) land 3) in
281281- let ml_mode = Constants.seq_mode_of_int ((modes lsr 4) land 3) in
282282-283283- (* Decode tables *)
284284- decode_seq_table stream ll_mode
285285- Constants.ll_default_distribution Constants.ll_default_accuracy_log
286286- Constants.ll_max_accuracy_log
287287- (fun () -> ctx.ll_table) (fun t -> ctx.ll_table <- t);
288288-289289- decode_seq_table stream of_mode
290290- Constants.of_default_distribution Constants.of_default_accuracy_log
291291- Constants.of_max_accuracy_log
292292- (fun () -> ctx.of_table) (fun t -> ctx.of_table <- t);
293293-294294- decode_seq_table stream ml_mode
295295- Constants.ml_default_distribution Constants.ml_default_accuracy_log
296296- Constants.ml_max_accuracy_log
297297- (fun () -> ctx.ml_table) (fun t -> ctx.ml_table <- t);
298298-299299- let ll_table = Option.get ctx.ll_table in
300300- let of_table = Option.get ctx.of_table in
301301- let ml_table = Option.get ctx.ml_table in
302302-303303- (* Get remaining bytes for FSE decoding *)
304304- let remaining = Bit_reader.Forward.remaining_bytes stream in
305305- let seq_data = Bit_reader.Forward.get_bytes stream remaining in
306306-307307- (* Create backward stream *)
308308- let bstream = Bit_reader.Backward.of_bytes seq_data ~pos:0 ~len:remaining in
309309-310310- (* Initialize states *)
311311- let ll_state = ref (Fse.init_state ll_table bstream) in
312312- let of_state = ref (Fse.init_state of_table bstream) in
313313- let ml_state = ref (Fse.init_state ml_table bstream) in
314314-315315- (* Decode sequences *)
316316- let sequences = Array.init num_sequences (fun i ->
317317- let of_code = Fse.peek_symbol of_table !of_state in
318318- let ll_code = Fse.peek_symbol ll_table !ll_state in
319319- let ml_code = Fse.peek_symbol ml_table !ml_state in
320320-321321- if ll_code > Constants.ll_max_code ||
322322- ml_code > Constants.ml_max_code then
323323- raise (Constants.Zstd_error Constants.Corruption);
324324-325325- (* Read extra bits: offset, match_length, literal_length *)
326326- let offset = (1 lsl of_code) + Bit_reader.Backward.read_bits bstream of_code in
327327- let match_length =
328328- Constants.ml_baselines.(ml_code) +
329329- Bit_reader.Backward.read_bits bstream Constants.ml_extra_bits.(ml_code) in
330330- let literal_length =
331331- Constants.ll_baselines.(ll_code) +
332332- Bit_reader.Backward.read_bits bstream Constants.ll_extra_bits.(ll_code) in
333333-334334- (* Update states (except for last sequence) *)
335335- if i < num_sequences - 1 then begin
336336- ll_state := Fse.update_state ll_table !ll_state bstream;
337337- ml_state := Fse.update_state ml_table !ml_state bstream;
338338- of_state := Fse.update_state of_table !of_state bstream
339339- end;
340340-341341- { literal_length; match_length; offset }
342342- ) in
343343-344344- (* Verify stream is consumed *)
345345- if Bit_reader.Backward.remaining bstream <> 0 then
346346- raise (Constants.Zstd_error Constants.Corruption);
347347-348348- sequences
349349- end
350350-351351-(** Compute actual offset from sequence offset value *)
352352-let compute_offset seq repeat_offsets =
353353- let offset_value = seq.offset in
354354- if offset_value > 3 then begin
355355- (* Real offset: shift history and use value - 3 *)
356356- let actual_offset = offset_value - 3 in
357357- repeat_offsets.(2) <- repeat_offsets.(1);
358358- repeat_offsets.(1) <- repeat_offsets.(0);
359359- repeat_offsets.(0) <- actual_offset;
360360- actual_offset
361361- end else begin
362362- (* Repeat offset *)
363363- let idx = offset_value - 1 in
364364- let idx = if seq.literal_length = 0 then idx + 1 else idx in
365365-366366- let actual_offset =
367367- if idx = 3 then
368368- repeat_offsets.(0) - 1
369369- else
370370- repeat_offsets.(idx)
371371- in
372372-373373- (* Update history *)
374374- if idx > 0 then begin
375375- if idx > 1 then repeat_offsets.(2) <- repeat_offsets.(1);
376376- repeat_offsets.(1) <- repeat_offsets.(0);
377377- repeat_offsets.(0) <- actual_offset
378378- end;
379379-380380- actual_offset
381381- end
382382-383383-(** Execute sequences to produce output *)
384384-let execute_sequences ctx sequences literals ~lit_len output ~out_pos =
385385- let lit_pos = ref 0 in
386386- let out = ref out_pos in
387387-388388- for i = 0 to Array.length sequences - 1 do
389389- let seq = sequences.(i) in
390390-391391- (* Copy literals *)
392392- if seq.literal_length > 0 then begin
393393- if !lit_pos + seq.literal_length > lit_len then
394394- raise (Constants.Zstd_error Constants.Corruption);
395395- Bytes.blit literals !lit_pos output !out seq.literal_length;
396396- lit_pos := !lit_pos + seq.literal_length;
397397- out := !out + seq.literal_length
398398- end;
399399-400400- (* Compute actual offset *)
401401- let offset = compute_offset seq ctx.repeat_offsets in
402402-403403- (* Validate offset *)
404404- let total_available = ctx.total_output + (!out - out_pos) in
405405- let dict_len = Option.fold ~none:0 ~some:Bytes.length ctx.dict_content in
406406-407407- if offset > total_available + dict_len then
408408- raise (Constants.Zstd_error Constants.Invalid_offset);
409409-410410- (* Copy match *)
411411- let match_length = seq.match_length in
412412- if offset > total_available then begin
413413- (* Part of match is from dictionary *)
414414- let dict = Option.get ctx.dict_content in
415415- let dict_copy = min (offset - total_available) match_length in
416416- let dict_offset = dict_len - (offset - total_available) in
417417- Bytes.blit dict dict_offset output !out dict_copy;
418418- out := !out + dict_copy;
419419-420420- (* Rest from output buffer *)
421421- for _ = dict_copy to match_length - 1 do
422422- Bytes.set output !out (Bytes.get output (!out - offset));
423423- incr out
424424- done
425425- end else begin
426426- (* Match is entirely in output buffer *)
427427- (* Note: may overlap, so copy byte-by-byte for small offsets *)
428428- for _ = 0 to match_length - 1 do
429429- Bytes.set output !out (Bytes.get output (!out - offset));
430430- incr out
431431- done
432432- end
433433- done;
434434-435435- (* Copy remaining literals *)
436436- let remaining = lit_len - !lit_pos in
437437- if remaining > 0 then begin
438438- Bytes.blit literals !lit_pos output !out remaining;
439439- out := !out + remaining
440440- end;
441441-442442- !out - out_pos
443443-444444-(** Decompress a single block *)
445445-let decompress_block ctx stream output ~out_pos =
446446- (* Decode literals *)
447447- let literals = Bytes.create Constants.max_literals_size in
448448- let lit_len = decode_literals ctx stream literals ~out_pos:0 in
449449-450450- (* Decode and execute sequences *)
451451- let sequences = decode_sequences ctx stream in
452452-453453- let written = execute_sequences ctx sequences literals ~lit_len output ~out_pos in
454454- ctx.total_output <- ctx.total_output + written;
455455- written
456456-457457-(** Decompress frame data (all blocks) *)
458458-let decompress_data ctx stream output ~out_pos =
459459- let written = ref 0 in
460460- let last_block = ref false in
461461-462462- while not !last_block do
463463- let header = Bit_reader.Forward.read_bits stream 24 in
464464- last_block := (header land 1) = 1;
465465- let block_type = Constants.block_type_of_int ((header lsr 1) land 3) in
466466- let block_size = header lsr 3 in
467467-468468- if block_size > Constants.block_size_max then
469469- raise (Constants.Zstd_error Constants.Invalid_block_size);
470470-471471- match block_type with
472472- | Raw_block ->
473473- let data = Bit_reader.Forward.get_bytes stream block_size in
474474- Bytes.blit data 0 output (out_pos + !written) block_size;
475475- written := !written + block_size;
476476- ctx.total_output <- ctx.total_output + block_size
477477-478478- | RLE_block ->
479479- let byte = Bit_reader.Forward.read_byte stream in
480480- Bytes.fill output (out_pos + !written) block_size (Char.chr byte);
481481- written := !written + block_size;
482482- ctx.total_output <- ctx.total_output + block_size
483483-484484- | Compressed_block ->
485485- let block_data = Bit_reader.Forward.get_bytes stream block_size in
486486- let block_stream = Bit_reader.Forward.of_bytes block_data in
487487- let block_written = decompress_block ctx block_stream output
488488- ~out_pos:(out_pos + !written) in
489489- written := !written + block_written
490490-491491- | Reserved_block ->
492492- raise (Constants.Zstd_error Constants.Invalid_block_type)
493493- done;
494494-495495- !written
496496-497497-(** Create initial frame context *)
498498-let create_frame_context (header : frame_header) (dict_opt : dictionary option) : frame_context =
499499- let huf_table = Option.bind dict_opt (fun (d : dictionary) -> d.huf_table) in
500500- let ll_table = Option.map (fun (d : dictionary) -> d.ll_table) dict_opt in
501501- let ml_table = Option.map (fun (d : dictionary) -> d.ml_table) dict_opt in
502502- let of_table = Option.map (fun (d : dictionary) -> d.of_table) dict_opt in
503503- let repeat_offsets = Option.fold ~none:(Array.copy Constants.initial_repeat_offsets)
504504- ~some:(fun (d : dictionary) -> Array.copy d.repeat_offsets) dict_opt in
505505- let dict_content = Option.map (fun (d : dictionary) -> d.content) dict_opt in
506506- { huf_table; ll_table; ml_table; of_table; repeat_offsets;
507507- total_output = 0; dict = dict_opt; dict_content; window_size = header.window_size }
508508-509509-(** Decompress a single frame *)
510510-let decompress_frame ?dict src ~pos ~len =
511511- let stream = Bit_reader.Forward.create src ~pos ~len in
512512-513513- (* Check magic number *)
514514- let magic = Bit_reader.Forward.read_bits stream 32 in
515515- if Int32.of_int magic <> Constants.zstd_magic_number then
516516- raise (Constants.Zstd_error Constants.Invalid_magic_number);
517517-518518- (* Parse header *)
519519- let header = parse_frame_header stream in
520520-521521- (* Validate dictionary if required *)
522522- begin match header.dictionary_id, dict with
523523- | Some id, Some d when id <> d.dict_id ->
524524- raise (Constants.Zstd_error Constants.Dictionary_mismatch)
525525- | Some _, None ->
526526- raise (Constants.Zstd_error Constants.Dictionary_mismatch)
527527- | _ -> ()
528528- end;
529529-530530- (* Determine output size *)
531531- let output_size = match header.frame_content_size with
532532- | Some size -> Int64.to_int size
533533- | None -> header.window_size * 2 (* Estimate *)
534534- in
535535-536536- let output = Bytes.create output_size in
537537- let ctx = create_frame_context header dict in
538538-539539- (* Decompress all blocks *)
540540- let written = decompress_data ctx stream output ~out_pos:0 in
541541-542542- (* Verify checksum if present *)
543543- if header.content_checksum then begin
544544- let expected = Bit_reader.Forward.read_bits stream 32 in
545545- let actual = Xxhash.hash32 output ~pos:0 ~len:written in
546546- if Int32.of_int expected <> actual then
547547- raise (Constants.Zstd_error Constants.Checksum_mismatch)
548548- end;
549549-550550- Bytes.sub output 0 written
551551-552552-(** Get decompressed size from frame header (if available) *)
553553-let get_decompressed_size src ~pos ~len =
554554- let stream = Bit_reader.Forward.create src ~pos ~len in
555555-556556- let magic = Bit_reader.Forward.read_bits stream 32 in
557557- if Int32.of_int magic <> Constants.zstd_magic_number then
558558- None
559559- else begin
560560- let header = parse_frame_header stream in
561561- header.frame_content_size
562562- end
563563-564564-(** Check if a magic number is a skippable frame magic *)
565565-let[@inline] is_skippable_magic magic =
566566- Int32.equal (Int32.logand magic Constants.skippable_magic_mask) Constants.skippable_magic_start
567567-568568-(** Check if data starts with skippable frame magic *)
569569-let is_skippable_frame src ~pos ~len =
570570- len >= 4 && is_skippable_magic (Bytes.get_int32_le src pos)
571571-572572-(** Get skippable frame variant (0-15) *)
573573-let get_skippable_variant src ~pos ~len =
574574- if len < 4 then None
575575- else
576576- let magic = Bytes.get_int32_le src pos in
577577- if is_skippable_magic magic then
578578- Some (Int32.to_int (Int32.logand magic 0xFl))
579579- else
580580- None
581581-582582-(** Get skippable frame size (returns total frame size including header) *)
583583-let get_skippable_frame_size src ~pos ~len =
584584- if len < 8 then None
585585- else if not (is_skippable_frame src ~pos ~len) then None
586586- else
587587- let content_size = Int32.to_int (Bytes.get_int32_le src (pos + 4)) in
588588- Some (Constants.skippable_header_size + content_size)
589589-590590-(** Skip skippable frame and return content + next position *)
591591-let read_skippable_frame src ~pos ~len =
592592- if len < 8 then raise (Constants.Zstd_error Constants.Truncated_input);
593593- if not (is_skippable_frame src ~pos ~len) then
594594- raise (Constants.Zstd_error Constants.Invalid_magic_number);
595595- let content_size = Int32.to_int (Bytes.get_int32_le src (pos + 4)) in
596596- let total_size = Constants.skippable_header_size + content_size in
597597- if len < total_size then raise (Constants.Zstd_error Constants.Truncated_input);
598598- let content = Bytes.sub src (pos + 8) content_size in
599599- (content, pos + total_size)
600600-601601-(** Find compressed size of first frame (zstd or skippable) *)
602602-let find_frame_compressed_size src ~pos ~len =
603603- if len < 4 then raise (Constants.Zstd_error Constants.Truncated_input);
604604- let magic = Bytes.get_int32_le src pos in
605605- if is_skippable_magic magic then begin
606606- (* Skippable frame *)
607607- if len < 8 then raise (Constants.Zstd_error Constants.Truncated_input);
608608- let content_size = Int32.to_int (Bytes.get_int32_le src (pos + 4)) in
609609- Constants.skippable_header_size + content_size
610610- end else if Int32.equal magic Constants.zstd_magic_number then begin
611611- (* Regular zstd frame - need to scan through blocks *)
612612- let stream = Bit_reader.Forward.create src ~pos ~len in
613613- (* Skip magic *)
614614- let _ = Bit_reader.Forward.read_bits stream 32 in
615615- (* Parse header to get size *)
616616- let header = parse_frame_header stream in
617617- (* Now scan through blocks *)
618618- let last_block = ref false in
619619- while not !last_block do
620620- let block_header = Bit_reader.Forward.read_bits stream 24 in
621621- last_block := (block_header land 1) = 1;
622622- let block_type = (block_header lsr 1) land 3 in
623623- let block_size = block_header lsr 3 in
624624- (* Skip block content *)
625625- let bytes_to_skip = match block_type with
626626- | 0 -> block_size (* Raw *)
627627- | 1 -> 1 (* RLE: single byte *)
628628- | 2 -> block_size (* Compressed *)
629629- | _ -> raise (Constants.Zstd_error Constants.Invalid_block_type)
630630- in
631631- ignore (Bit_reader.Forward.get_bytes stream bytes_to_skip)
632632- done;
633633- (* Add checksum if present *)
634634- if header.content_checksum then
635635- ignore (Bit_reader.Forward.read_bits stream 32);
636636- Bit_reader.Forward.byte_position stream
637637- end else
638638- raise (Constants.Zstd_error Constants.Invalid_magic_number)
639639-640640-(** Decompress all frames (zstd and skippable) concatenated together *)
641641-let decompress_frames ?dict src ~pos ~len =
642642- let results = ref [] in
643643- let current_pos = ref pos in
644644- let remaining = ref len in
645645-646646- while !remaining > 0 do
647647- if !remaining < 4 then raise (Constants.Zstd_error Constants.Truncated_input);
648648- let magic = Bytes.get_int32_le src !current_pos in
649649-650650- if is_skippable_magic magic then begin
651651- (* Skippable frame - skip it *)
652652- match get_skippable_frame_size src ~pos:!current_pos ~len:!remaining with
653653- | Some frame_size ->
654654- current_pos := !current_pos + frame_size;
655655- remaining := !remaining - frame_size
656656- | None -> raise (Constants.Zstd_error Constants.Truncated_input)
657657- end else if Int32.equal magic Constants.zstd_magic_number then begin
658658- (* Regular zstd frame *)
659659- let frame_size = find_frame_compressed_size src ~pos:!current_pos ~len:!remaining in
660660- let result = decompress_frame ?dict src ~pos:!current_pos ~len:frame_size in
661661- results := result :: !results;
662662- current_pos := !current_pos + frame_size;
663663- remaining := !remaining - frame_size
664664- end else
665665- raise (Constants.Zstd_error Constants.Invalid_magic_number)
666666- done;
667667-668668- (* Concatenate results in order *)
669669- let results_rev = List.rev !results in
670670- let total_len = List.fold_left (fun acc b -> acc + Bytes.length b) 0 results_rev in
671671- let output = Bytes.create total_len in
672672- ignore (List.fold_left (fun pos b ->
673673- let len = Bytes.length b in
674674- Bytes.blit b 0 output pos len;
675675- pos + len
676676- ) 0 results_rev);
677677- output
678678-679679-(** Parse dictionary *)
680680-let parse_dictionary src ~pos ~len =
681681- let stream = Bit_reader.Forward.create src ~pos ~len in
682682-683683- let magic = Bit_reader.Forward.read_bits stream 32 in
684684- if Int32.of_int magic <> Constants.dict_magic_number then begin
685685- (* Raw content dictionary (no magic) *)
686686- {
687687- dict_id = 0l;
688688- huf_table = None;
689689- ll_table = Fse.build_predefined_table
690690- Constants.ll_default_distribution Constants.ll_default_accuracy_log;
691691- ml_table = Fse.build_predefined_table
692692- Constants.ml_default_distribution Constants.ml_default_accuracy_log;
693693- of_table = Fse.build_predefined_table
694694- Constants.of_default_distribution Constants.of_default_accuracy_log;
695695- content = Bytes.sub src pos len;
696696- repeat_offsets = Array.copy Constants.initial_repeat_offsets;
697697- }
698698- end else begin
699699- (* Formatted dictionary *)
700700- let dict_id = Int32.of_int (Bit_reader.Forward.read_bits stream 32) in
701701-702702- (* Decode entropy tables *)
703703- let huf_table = Some (Huffman.decode_table stream) in
704704-705705- (* Decode FSE tables (always FSE mode for dictionaries) *)
706706- let of_table = Fse.decode_header stream Constants.of_max_accuracy_log in
707707- let ml_table = Fse.decode_header stream Constants.ml_max_accuracy_log in
708708- let ll_table = Fse.decode_header stream Constants.ll_max_accuracy_log in
709709-710710- (* Read repeat offsets *)
711711- let repeat_offsets = Array.init 3 (fun _ ->
712712- Bit_reader.Forward.read_bits stream 32
713713- ) in
714714-715715- (* Remaining is content *)
716716- let content_pos = Bit_reader.Forward.byte_position stream in
717717- let content_len = len - content_pos in
718718- let content = Bytes.sub src (pos + content_pos) content_len in
719719-720720- { dict_id; huf_table; ll_table; ml_table; of_table; content; repeat_offsets }
721721- end
-752
ocaml-zstd/src/zstd_encode.ml
···11-(** Zstandard compression implementation.
22-33- Implements LZ77 matching, block compression, and frame encoding. *)
44-55-(** Compression level affects speed vs ratio tradeoff *)
66-type compression_level = {
77- window_log : int; (* Log2 of window size *)
88- chain_log : int; (* Log2 of hash chain length *)
99- hash_log : int; (* Log2 of hash table size *)
1010- search_log : int; (* Number of searches per position *)
1111- min_match : int; (* Minimum match length *)
1212- target_len : int; (* Target match length *)
1313- strategy : int; (* 0=fast, 1=greedy, 2=lazy *)
1414-}
1515-1616-(** Default levels 1-19 *)
1717-let level_params = [|
1818- (* Level 0/1: Fast *)
1919- { window_log = 17; chain_log = 12; hash_log = 11; search_log = 1; min_match = 4; target_len = 0; strategy = 0 };
2020- { window_log = 17; chain_log = 12; hash_log = 11; search_log = 1; min_match = 4; target_len = 0; strategy = 0 };
2121- (* Level 2 *)
2222- { window_log = 18; chain_log = 13; hash_log = 12; search_log = 1; min_match = 5; target_len = 4; strategy = 0 };
2323- (* Level 3 *)
2424- { window_log = 18; chain_log = 14; hash_log = 13; search_log = 1; min_match = 5; target_len = 8; strategy = 1 };
2525- (* Level 4 *)
2626- { window_log = 18; chain_log = 14; hash_log = 14; search_log = 2; min_match = 4; target_len = 8; strategy = 1 };
2727- (* Level 5 *)
2828- { window_log = 18; chain_log = 15; hash_log = 14; search_log = 3; min_match = 4; target_len = 16; strategy = 1 };
2929- (* Level 6 *)
3030- { window_log = 19; chain_log = 16; hash_log = 15; search_log = 3; min_match = 4; target_len = 32; strategy = 1 };
3131- (* Level 7 *)
3232- { window_log = 19; chain_log = 16; hash_log = 15; search_log = 4; min_match = 4; target_len = 32; strategy = 2 };
3333- (* Level 8 *)
3434- { window_log = 19; chain_log = 17; hash_log = 16; search_log = 4; min_match = 4; target_len = 64; strategy = 2 };
3535- (* Level 9 *)
3636- { window_log = 20; chain_log = 17; hash_log = 16; search_log = 5; min_match = 4; target_len = 64; strategy = 2 };
3737- (* Level 10 *)
3838- { window_log = 20; chain_log = 17; hash_log = 16; search_log = 6; min_match = 4; target_len = 128; strategy = 2 };
3939- (* Level 11 *)
4040- { window_log = 20; chain_log = 18; hash_log = 17; search_log = 6; min_match = 4; target_len = 128; strategy = 2 };
4141- (* Level 12 *)
4242- { window_log = 21; chain_log = 18; hash_log = 17; search_log = 7; min_match = 4; target_len = 256; strategy = 2 };
4343- (* Level 13 *)
4444- { window_log = 21; chain_log = 19; hash_log = 18; search_log = 7; min_match = 4; target_len = 256; strategy = 2 };
4545- (* Level 14 *)
4646- { window_log = 22; chain_log = 19; hash_log = 18; search_log = 8; min_match = 4; target_len = 256; strategy = 2 };
4747- (* Level 15 *)
4848- { window_log = 22; chain_log = 20; hash_log = 18; search_log = 9; min_match = 4; target_len = 256; strategy = 2 };
4949- (* Level 16 *)
5050- { window_log = 22; chain_log = 20; hash_log = 19; search_log = 10; min_match = 4; target_len = 512; strategy = 2 };
5151- (* Level 17 *)
5252- { window_log = 22; chain_log = 21; hash_log = 19; search_log = 11; min_match = 4; target_len = 512; strategy = 2 };
5353- (* Level 18 *)
5454- { window_log = 22; chain_log = 21; hash_log = 20; search_log = 12; min_match = 4; target_len = 512; strategy = 2 };
5555- (* Level 19 *)
5656- { window_log = 23; chain_log = 22; hash_log = 20; search_log = 12; min_match = 4; target_len = 1024; strategy = 2 };
5757-|]
5858-5959-let get_level_params level =
6060- let level = max 1 (min level 19) in
6161- level_params.(level)
6262-6363-(** A sequence represents a literal run + match *)
6464-type sequence = {
6565- lit_length : int;
6666- match_offset : int;
6767- match_length : int;
6868-}
6969-7070-(** Hash table for fast match finding *)
7171-type hash_table = {
7272- table : int array; (* Position indexed by hash *)
7373- chain : int array; (* Chain of previous matches at same hash *)
7474- mask : int;
7575-}
7676-7777-let create_hash_table log_size =
7878- let size = 1 lsl log_size in
7979- {
8080- table = Array.make size (-1);
8181- chain = Array.make (1 lsl 20) (-1); (* Max input size *)
8282- mask = size - 1;
8383- }
8484-8585-(** Compute hash of 4 bytes *)
8686-let[@inline] hash4 src pos =
8787- let v = Bytes.get_int32_le src pos in
8888- (* MurmurHash3-like mixing *)
8989- let h = Int32.to_int (Int32.mul v 0xcc9e2d51l) in
9090- (h lxor (h lsr 15))
9191-9292-(** Check if positions match and return length *)
9393-let match_length src pos1 pos2 limit =
9494- let len = ref 0 in
9595- let max_len = min (limit - pos1) (pos1 - pos2) in
9696- while !len < max_len &&
9797- Bytes.get_uint8 src (pos1 + !len) = Bytes.get_uint8 src (pos2 + !len) do
9898- incr len
9999- done;
100100- !len
101101-102102-(** Find best match at current position *)
103103-let find_best_match ht src pos limit params =
104104- if pos + 4 > limit then
105105- (0, 0)
106106- else begin
107107- let h = hash4 src pos land ht.mask in
108108- let prev_pos = ht.table.(h) in
109109-110110- (* Update hash table *)
111111- ht.chain.(pos) <- prev_pos;
112112- ht.table.(h) <- pos;
113113-114114- if prev_pos < 0 || pos - prev_pos > (1 lsl params.window_log) then
115115- (0, 0)
116116- else begin
117117- (* Search chain for best match *)
118118- let best_offset = ref 0 in
119119- let best_length = ref 0 in
120120- let chain_pos = ref prev_pos in
121121- let searches = ref 0 in
122122- let max_searches = 1 lsl params.search_log in
123123-124124- while !chain_pos >= 0 && !searches < max_searches do
125125- let offset = pos - !chain_pos in
126126- if offset > (1 lsl params.window_log) then
127127- chain_pos := -1
128128- else begin
129129- let len = match_length src pos !chain_pos limit in
130130- if len >= params.min_match && len > !best_length then begin
131131- best_length := len;
132132- best_offset := offset
133133- end;
134134- chain_pos := ht.chain.(!chain_pos);
135135- incr searches
136136- end
137137- done;
138138-139139- (!best_offset, !best_length)
140140- end
141141- end
142142-143143-(** Parse input into sequences using greedy/lazy matching *)
144144-let parse_sequences src ~pos ~len params =
145145- let sequences = ref [] in
146146- let cur_pos = ref pos in
147147- let limit = pos + len in
148148- let lit_start = ref pos in
149149-150150- let ht = create_hash_table params.hash_log in
151151-152152- while !cur_pos + 4 <= limit do
153153- let (offset, length) = find_best_match ht src !cur_pos limit params in
154154-155155- if length >= params.min_match then begin
156156- (* Emit sequence *)
157157- let lit_len = !cur_pos - !lit_start in
158158- sequences := { lit_length = lit_len; match_offset = offset; match_length = length } :: !sequences;
159159-160160- (* Update hash table for matched positions *)
161161- for i = !cur_pos + 1 to !cur_pos + length - 1 do
162162- if i + 4 <= limit then begin
163163- let h = hash4 src i land ht.mask in
164164- ht.chain.(i) <- ht.table.(h);
165165- ht.table.(h) <- i
166166- end
167167- done;
168168-169169- cur_pos := !cur_pos + length;
170170- lit_start := !cur_pos
171171- end else begin
172172- incr cur_pos
173173- end
174174- done;
175175-176176- (* Handle remaining literals *)
177177- let remaining = limit - !lit_start in
178178- if remaining > 0 || !sequences = [] then
179179- sequences := { lit_length = remaining; match_offset = 0; match_length = 0 } :: !sequences;
180180-181181- List.rev !sequences
182182-183183-(** Encode literal length code *)
184184-let encode_lit_length_code lit_len =
185185- if lit_len < 16 then
186186- (lit_len, 0, 0)
187187- else if lit_len < 64 then
188188- (16 + (lit_len - 16) / 4, (lit_len - 16) mod 4, 2)
189189- else if lit_len < 128 then
190190- (28 + (lit_len - 64) / 8, (lit_len - 64) mod 8, 3)
191191- else begin
192192- (* Use baseline tables for larger values *)
193193- let rec find_code code =
194194- if code >= 35 then (35, lit_len - Constants.ll_baselines.(35), Constants.ll_extra_bits.(35))
195195- else if lit_len < Constants.ll_baselines.(code + 1) then
196196- (code, lit_len - Constants.ll_baselines.(code), Constants.ll_extra_bits.(code))
197197- else find_code (code + 1)
198198- in
199199- find_code 16
200200- end
201201-202202-(** Minimum match length for zstd *)
203203-let min_match = 3
204204-205205-(** Encode match length code *)
206206-let encode_match_length_code match_len =
207207- let ml = match_len - min_match in
208208- if ml < 32 then
209209- (ml, 0, 0)
210210- else if ml < 64 then
211211- (32 + (ml - 32) / 2, (ml - 32) mod 2, 1)
212212- else begin
213213- let rec find_code code =
214214- if code >= 52 then (52, ml - Constants.ml_baselines.(52) + 3, Constants.ml_extra_bits.(52))
215215- else if ml < Constants.ml_baselines.(code + 1) - 3 then
216216- (code, ml - Constants.ml_baselines.(code) + 3, Constants.ml_extra_bits.(code))
217217- else find_code (code + 1)
218218- in
219219- find_code 32
220220- end
221221-222222-(** Encode offset code.
223223- Returns (of_code, extra_value, extra_bits).
224224-225225- Repeat offsets use offBase 1,2,3:
226226- - offBase=1: ofCode=0, no extra bits
227227- - offBase=2: ofCode=1, extra=0 (1 bit)
228228- - offBase=3: ofCode=1, extra=1 (1 bit)
229229-230230- Real offsets use offBase = offset + 3:
231231- - ofCode = highbit(offBase)
232232- - extra = lower ofCode bits of offBase *)
233233-let encode_offset_code offset offset_history =
234234- let off_base =
235235- if offset = offset_history.(0) then 1
236236- else if offset = offset_history.(1) then 2
237237- else if offset = offset_history.(2) then 3
238238- else offset + 3
239239- in
240240- let of_code = Fse.highest_set_bit off_base in
241241- let extra = off_base land ((1 lsl of_code) - 1) in
242242- (of_code, extra, of_code)
243243-244244-(** Write raw literals section *)
245245-let write_raw_literals literals ~pos ~len output ~out_pos =
246246- if len = 0 then begin
247247- (* Empty literals: single-byte header with type=0, size=0 *)
248248- Bytes.set_uint8 output out_pos 0;
249249- 1
250250- end else if len < 32 then begin
251251- (* Raw literals, single stream, 1-byte header *)
252252- (* Header: type=0 (raw), size_format=0 (5-bit), regen_size in bits 3-7 *)
253253- let header = 0b00 lor ((len land 0x1f) lsl 3) in
254254- Bytes.set_uint8 output out_pos header;
255255- Bytes.blit literals pos output (out_pos + 1) len;
256256- 1 + len
257257- end else if len < 4096 then begin
258258- (* Raw literals, 2-byte header *)
259259- (* type=0 (bits 0-1), size_format=1 (bits 2-3), size in bits 4-15 *)
260260- let header = 0b0100 lor ((len land 0x0fff) lsl 4) in
261261- Bytes.set_uint16_le output out_pos header;
262262- Bytes.blit literals pos output (out_pos + 2) len;
263263- 2 + len
264264- end else begin
265265- (* Raw literals, 3-byte header *)
266266- (* type=0 (bits 0-1), size_format=2 (bits 2-3), size in bits 4-17 (14 bits) *)
267267- let header = 0b1000 lor ((len land 0x3fff) lsl 4) in
268268- Bytes.set_uint8 output out_pos (header land 0xff);
269269- Bytes.set_uint8 output (out_pos + 1) ((header lsr 8) land 0xff);
270270- Bytes.set_uint8 output (out_pos + 2) ((header lsr 16) land 0xff);
271271- Bytes.blit literals pos output (out_pos + 3) len;
272272- 3 + len
273273- end
274274-275275-(** Write compressed literals with Huffman encoding *)
276276-let write_compressed_literals literals ~pos ~len output ~out_pos =
277277- if len < 32 then
278278- (* Too small for Huffman, use raw *)
279279- write_raw_literals literals ~pos ~len output ~out_pos
280280- else begin
281281- (* Count symbol frequencies *)
282282- let counts = Array.make 256 0 in
283283- for i = pos to pos + len - 1 do
284284- let c = Bytes.get_uint8 literals i in
285285- counts.(c) <- counts.(c) + 1
286286- done;
287287-288288- (* Find max symbol used *)
289289- let max_symbol = ref 0 in
290290- for i = 0 to 255 do
291291- if counts.(i) > 0 then max_symbol := i
292292- done;
293293-294294- (* Build Huffman table *)
295295- let ctable = Huffman.build_ctable counts !max_symbol Constants.max_huffman_bits in
296296-297297- if ctable.num_symbols = 0 then
298298- write_raw_literals literals ~pos ~len output ~out_pos
299299- else begin
300300- (* Decide single vs 4-stream based on size *)
301301- let use_4streams = len >= 256 in
302302-303303- (* Write Huffman table header to temp buffer *)
304304- let header_buf = Bytes.create 256 in
305305- let header_stream = Bit_writer.Forward.of_bytes header_buf in
306306- let _num_written = Huffman.write_header header_stream ctable in
307307- let header_size = Bit_writer.Forward.byte_position header_stream in
308308-309309- (* Compress literals *)
310310- let compressed =
311311- if use_4streams then
312312- Huffman.compress_4stream ctable literals ~pos ~len
313313- else
314314- Huffman.compress_1stream ctable literals ~pos ~len
315315- in
316316- let compressed_size = Bytes.length compressed in
317317-318318- (* Check if compression is worthwhile (should save at least 10%) *)
319319- let total_compressed_size = header_size + compressed_size in
320320- if total_compressed_size >= len - len / 10 then
321321- write_raw_literals literals ~pos ~len output ~out_pos
322322- else begin
323323- (* Write compressed literals header *)
324324- (* Type: 2 = compressed, size_format based on sizes *)
325325- let regen_size = len in
326326- let lit_type = 2 in (* Compressed_literals *)
327327-328328- let header_pos = ref out_pos in
329329- if regen_size < 1024 && total_compressed_size < 1024 then begin
330330- (* 3-byte header: type(2) + size_format(2) + regen(10) + compressed(10) + streams(2) *)
331331- let size_format = 0 in
332332- let streams_flag = if use_4streams then 3 else 0 in
333333- let h0 = lit_type lor (size_format lsl 2) lor (streams_flag lsl 4) lor ((regen_size land 0x3f) lsl 6) in
334334- let h1 = ((regen_size lsr 6) land 0xf) lor ((total_compressed_size land 0xf) lsl 4) in
335335- let h2 = (total_compressed_size lsr 4) land 0xff in
336336- Bytes.set_uint8 output !header_pos h0;
337337- Bytes.set_uint8 output (!header_pos + 1) h1;
338338- Bytes.set_uint8 output (!header_pos + 2) h2;
339339- header_pos := !header_pos + 3
340340- end else begin
341341- (* 5-byte header for larger sizes *)
342342- let size_format = 1 in
343343- let streams_flag = if use_4streams then 3 else 0 in
344344- let h0 = lit_type lor (size_format lsl 2) lor (streams_flag lsl 4) lor ((regen_size land 0x3f) lsl 6) in
345345- Bytes.set_uint8 output !header_pos h0;
346346- Bytes.set_uint16_le output (!header_pos + 1) (((regen_size lsr 6) land 0x3fff) lor ((total_compressed_size land 0x3) lsl 14));
347347- Bytes.set_uint16_le output (!header_pos + 3) ((total_compressed_size lsr 2) land 0xffff);
348348- header_pos := !header_pos + 5
349349- end;
350350-351351- (* Write Huffman table *)
352352- Bytes.blit header_buf 0 output !header_pos header_size;
353353- header_pos := !header_pos + header_size;
354354-355355- (* Write compressed streams *)
356356- Bytes.blit compressed 0 output !header_pos compressed_size;
357357-358358- !header_pos + compressed_size - out_pos
359359- end
360360- end
361361- end
362362-363363-(** Compress literals - try Huffman, fall back to raw *)
364364-let compress_literals literals ~pos ~len output ~out_pos =
365365- write_compressed_literals literals ~pos ~len output ~out_pos
366366-367367-(** Build predefined FSE compression tables *)
368368-let ll_ctable = lazy (Fse.build_predefined_ctable Constants.ll_default_distribution Constants.ll_default_accuracy_log)
369369-let ml_ctable = lazy (Fse.build_predefined_ctable Constants.ml_default_distribution Constants.ml_default_accuracy_log)
370370-let of_ctable = lazy (Fse.build_predefined_ctable Constants.of_default_distribution Constants.of_default_accuracy_log)
371371-372372-(** Compress sequences section using predefined FSE tables.
373373- This implements proper zstd sequence encoding following RFC 8878.
374374-375375- Matches C zstd's ZSTD_encodeSequences_body exactly:
376376- 1. Initialize states with FSE_initCState2 using LAST sequence's codes
377377- 2. Write LAST sequence's extra bits (LL, ML, OF order)
378378- 3. For sequences n-2 down to 0:
379379- - FSE_encodeSymbol for OF, ML, LL
380380- - Extra bits for LL, ML, OF
381381- 4. FSE_flushCState for ML, OF, LL
382382-*)
383383-let compress_sequences sequences output ~out_pos offset_history =
384384- if sequences = [] then begin
385385- (* Zero sequences *)
386386- Bytes.set_uint8 output out_pos 0;
387387- 1
388388- end else begin
389389- let num_seq = List.length sequences in
390390- let header_size = ref 0 in
391391-392392- (* Write sequence count (1-3 bytes) *)
393393- if num_seq < 128 then begin
394394- Bytes.set_uint8 output out_pos num_seq;
395395- header_size := 1
396396- end else if num_seq < 0x7f00 then begin
397397- Bytes.set_uint8 output out_pos ((num_seq lsr 8) + 128);
398398- Bytes.set_uint8 output (out_pos + 1) (num_seq land 0xff);
399399- header_size := 2
400400- end else begin
401401- Bytes.set_uint8 output out_pos 0xff;
402402- Bytes.set_uint16_le output (out_pos + 1) (num_seq - 0x7f00);
403403- header_size := 3
404404- end;
405405-406406- (* Symbol compression modes byte:
407407- bits 0-1: Literals_Lengths_Mode (0 = predefined)
408408- bits 2-3: Offsets_Mode (0 = predefined)
409409- bits 4-5: Match_Lengths_Mode (0 = predefined)
410410- bits 6-7: reserved *)
411411- Bytes.set_uint8 output (out_pos + !header_size) 0b00;
412412- incr header_size;
413413-414414- (* Get predefined FSE tables *)
415415- let ll_ct = Lazy.force ll_ctable in
416416- let ml_ct = Lazy.force ml_ctable in
417417- let of_ct = Lazy.force of_ctable in
418418-419419- let offset_hist = Array.copy offset_history in
420420- let seq_array = Array.of_list sequences in
421421-422422- (* Encode all sequences in forward order to track offset history *)
423423- let encoded = Array.map (fun seq ->
424424- let (ll_code, ll_extra, ll_extra_bits) = encode_lit_length_code seq.lit_length in
425425- let (ml_code, ml_extra, ml_extra_bits) = encode_match_length_code seq.match_length in
426426- let (of_code, of_extra, of_extra_bits) = encode_offset_code seq.match_offset offset_hist in
427427-428428- (* Update offset history for real offsets (of_code > 1 means offBase > 2) *)
429429- if seq.match_offset > 0 && of_code > 1 then begin
430430- offset_hist.(2) <- offset_hist.(1);
431431- offset_hist.(1) <- offset_hist.(0);
432432- offset_hist.(0) <- seq.match_offset
433433- end;
434434-435435- (ll_code, ll_extra, ll_extra_bits, ml_code, ml_extra, ml_extra_bits, of_code, of_extra, of_extra_bits)
436436- ) seq_array in
437437-438438- (* Use a backward bit writer *)
439439- let stream = Bit_writer.Backward.create (num_seq * 20 + 32) in
440440-441441- (* Get last sequence's codes for state initialization *)
442442- let last_idx = num_seq - 1 in
443443- let (ll_code_last, ll_extra_last, ll_extra_bits_last,
444444- ml_code_last, ml_extra_last, ml_extra_bits_last,
445445- of_code_last, of_extra_last, of_extra_bits_last) = encoded.(last_idx) in
446446-447447- (* Initialize FSE states with LAST sequence's codes *)
448448- let ll_state = Fse.init_cstate2 ll_ct ll_code_last in
449449- let ml_state = Fse.init_cstate2 ml_ct ml_code_last in
450450- let of_state = Fse.init_cstate2 of_ct of_code_last in
451451-452452- (* Write LAST sequence's extra bits first (LL, ML, OF order) *)
453453- if ll_extra_bits_last > 0 then
454454- Bit_writer.Backward.write_bits stream ll_extra_last ll_extra_bits_last;
455455- if ml_extra_bits_last > 0 then
456456- Bit_writer.Backward.write_bits stream ml_extra_last ml_extra_bits_last;
457457- if of_extra_bits_last > 0 then
458458- Bit_writer.Backward.write_bits stream of_extra_last of_extra_bits_last;
459459-460460- (* Process sequences from n-2 down to 0 *)
461461- for i = last_idx - 1 downto 0 do
462462- let (ll_code, ll_extra, ll_extra_bits,
463463- ml_code, ml_extra, ml_extra_bits,
464464- of_code, of_extra, of_extra_bits) = encoded.(i) in
465465-466466- (* FSE encode: OF, ML, LL order *)
467467- Fse.encode_symbol stream of_state of_code;
468468- Fse.encode_symbol stream ml_state ml_code;
469469- Fse.encode_symbol stream ll_state ll_code;
470470-471471- (* Extra bits: LL, ML, OF order *)
472472- if ll_extra_bits > 0 then
473473- Bit_writer.Backward.write_bits stream ll_extra ll_extra_bits;
474474- if ml_extra_bits > 0 then
475475- Bit_writer.Backward.write_bits stream ml_extra ml_extra_bits;
476476- if of_extra_bits > 0 then
477477- Bit_writer.Backward.write_bits stream of_extra of_extra_bits
478478- done;
479479-480480- (* Flush states: ML, OF, LL order *)
481481- Fse.flush_cstate stream ml_state;
482482- Fse.flush_cstate stream of_state;
483483- Fse.flush_cstate stream ll_state;
484484-485485- (* Finalize and copy to output *)
486486- let seq_data = Bit_writer.Backward.finalize stream in
487487- let seq_len = Bytes.length seq_data in
488488- Bytes.blit seq_data 0 output (out_pos + !header_size) seq_len;
489489-490490- !header_size + seq_len
491491- end
492492-493493-(** Write raw block (no compression) *)
494494-let write_raw_block src ~pos ~len output ~out_pos =
495495- (* Raw block: header (3 bytes) + raw data
496496- Header format: bit 0 = last_block, bits 1-2 = block_type, bits 3-23 = block_size
497497- For raw: block_type = 0, block_size = number of bytes *)
498498- let header = (Constants.block_raw lsl 1) lor ((len land 0x1fffff) lsl 3) in
499499- Bytes.set_uint8 output out_pos (header land 0xff);
500500- Bytes.set_uint8 output (out_pos + 1) ((header lsr 8) land 0xff);
501501- Bytes.set_uint8 output (out_pos + 2) ((header lsr 16) land 0xff);
502502- Bytes.blit src pos output (out_pos + 3) len;
503503- 3 + len
504504-505505-(** Write compressed block with sequences *)
506506-let write_compressed_block src ~pos ~len sequences output ~out_pos offset_history =
507507- (* Collect all literals *)
508508- let total_lit_len = List.fold_left (fun acc seq -> acc + seq.lit_length) 0 sequences in
509509- let literals = Bytes.create total_lit_len in
510510- let lit_pos = ref 0 in
511511- let src_pos = ref pos in
512512- List.iter (fun seq ->
513513- if seq.lit_length > 0 then begin
514514- Bytes.blit src !src_pos literals !lit_pos seq.lit_length;
515515- lit_pos := !lit_pos + seq.lit_length;
516516- src_pos := !src_pos + seq.lit_length
517517- end;
518518- src_pos := !src_pos + seq.match_length
519519- ) sequences;
520520-521521- (* Build block content in temp buffer *)
522522- let block_buf = Bytes.create (len * 2 + 256) in
523523- let block_pos = ref 0 in
524524-525525- (* Write literals section *)
526526- let lit_size = compress_literals literals ~pos:0 ~len:total_lit_len block_buf ~out_pos:!block_pos in
527527- block_pos := !block_pos + lit_size;
528528-529529- (* Filter out sequences with only literals (match_length = 0 and match_offset = 0)
530530- at the end - the last sequence can be literal-only *)
531531- let real_sequences = List.filter (fun seq ->
532532- seq.match_length > 0 || seq.match_offset > 0
533533- ) sequences in
534534-535535- (* Write sequences section *)
536536- let seq_size = compress_sequences real_sequences block_buf ~out_pos:!block_pos offset_history in
537537- block_pos := !block_pos + seq_size;
538538-539539- let block_size = !block_pos in
540540-541541- (* Check if compressed block is actually smaller *)
542542- if block_size >= len then begin
543543- (* Fall back to raw block *)
544544- write_raw_block src ~pos ~len output ~out_pos
545545- end else begin
546546- (* Write compressed block header *)
547547- let header = (Constants.block_compressed lsl 1) lor ((block_size land 0x1fffff) lsl 3) in
548548- Bytes.set_uint8 output out_pos (header land 0xff);
549549- Bytes.set_uint8 output (out_pos + 1) ((header lsr 8) land 0xff);
550550- Bytes.set_uint8 output (out_pos + 2) ((header lsr 16) land 0xff);
551551- Bytes.blit block_buf 0 output (out_pos + 3) block_size;
552552- 3 + block_size
553553- end
554554-555555-(** Write RLE block (single byte repeated) *)
556556-let write_rle_block byte len output ~out_pos =
557557- (* RLE block: header (3 bytes) + single byte
558558- Header format: bit 0 = last_block, bits 1-2 = block_type, bits 3-23 = regen_size
559559- For RLE: block_type = 1, regen_size = number of bytes when expanded *)
560560- let header = (Constants.block_rle lsl 1) lor ((len land 0x1fffff) lsl 3) in
561561- Bytes.set_uint8 output out_pos (header land 0xff);
562562- Bytes.set_uint8 output (out_pos + 1) ((header lsr 8) land 0xff);
563563- Bytes.set_uint8 output (out_pos + 2) ((header lsr 16) land 0xff);
564564- Bytes.set_uint8 output (out_pos + 3) byte;
565565- 4
566566-567567-(** Check if block is all same byte *)
568568-let is_rle_block src ~pos ~len =
569569- if len = 0 then None
570570- else begin
571571- let first = Bytes.get_uint8 src pos in
572572- let all_same = ref true in
573573- for i = pos + 1 to pos + len - 1 do
574574- if Bytes.get_uint8 src i <> first then all_same := false
575575- done;
576576- if !all_same then Some first else None
577577- end
578578-579579-(** Compress a single block using LZ77 + FSE + Huffman.
580580- Falls back to RLE for repetitive data, or raw blocks if compression doesn't help. *)
581581-let compress_block src ~pos ~len output ~out_pos params offset_history =
582582- if len = 0 then
583583- 0
584584- else
585585- (* Check for RLE opportunity (all same byte) *)
586586- match is_rle_block src ~pos ~len with
587587- | Some byte when len > 4 ->
588588- (* RLE is worthwhile: 4 bytes instead of len+3 *)
589589- write_rle_block byte len output ~out_pos
590590- | _ ->
591591- (* Try LZ77 + FSE compression for compressible data *)
592592- let sequences = parse_sequences src ~pos ~len params in
593593- let match_count = List.fold_left (fun acc s ->
594594- if s.match_length > 0 then acc + 1 else acc) 0 sequences in
595595- (* Use compressed blocks for compressible data. The backward bitstream
596596- writer now uses periodic flushing like C zstd, supporting any size. *)
597597- if match_count >= 2 && len >= 64 then
598598- write_compressed_block src ~pos ~len sequences output ~out_pos offset_history
599599- else
600600- write_raw_block src ~pos ~len output ~out_pos
601601-602602-(** Write frame header *)
603603-let write_frame_header output ~pos content_size window_log checksum_flag =
604604- (* Magic number *)
605605- Bytes.set_int32_le output pos Constants.zstd_magic_number;
606606- let out_pos = ref (pos + 4) in
607607-608608- (* Use single segment mode for smaller content (no window descriptor needed).
609609- FCS field sizes when single_segment is set:
610610- - fcs_flag=0: 1 byte (content size 0-255)
611611- - fcs_flag=1: 2 bytes (content size 256-65791, stored with -256)
612612- - fcs_flag=2: 4 bytes
613613- - fcs_flag=3: 8 bytes *)
614614- let single_segment = content_size <= 131072L in
615615-616616- let (fcs_flag, fcs_bytes) =
617617- if single_segment then begin
618618- if content_size <= 255L then (0, 1)
619619- else if content_size <= 65791L then (1, 2) (* 2-byte has +256 offset *)
620620- else if content_size <= 0xFFFFFFFFL then (2, 4)
621621- else (3, 8)
622622- end else begin
623623- (* For non-single-segment, fcs_flag=0 means no FCS field *)
624624- if content_size = 0L then (0, 0)
625625- else if content_size <= 65535L then (1, 2)
626626- else if content_size <= 0xFFFFFFFFL then (2, 4)
627627- else (3, 8)
628628- end
629629- in
630630-631631- (* Frame header descriptor:
632632- bit 0-1: dict ID flag (0 = no dict)
633633- bit 2: content checksum flag
634634- bit 3: reserved
635635- bit 4: unused
636636- bit 5: single segment (no window descriptor)
637637- bit 6-7: FCS field size flag *)
638638- let descriptor =
639639- (if checksum_flag then 0b00000100 else 0)
640640- lor (if single_segment then 0b00100000 else 0)
641641- lor (fcs_flag lsl 6)
642642- in
643643- Bytes.set_uint8 output !out_pos descriptor;
644644- incr out_pos;
645645-646646- (* Window descriptor (only if not single segment) *)
647647- if not single_segment then begin
648648- let window_desc = ((window_log - 10) lsl 3) in
649649- Bytes.set_uint8 output !out_pos window_desc;
650650- incr out_pos
651651- end;
652652-653653- (* Frame content size *)
654654- begin match fcs_bytes with
655655- | 1 ->
656656- Bytes.set_uint8 output !out_pos (Int64.to_int content_size);
657657- out_pos := !out_pos + 1
658658- | 2 ->
659659- (* 2-byte FCS stores value - 256 *)
660660- let adjusted = Int64.sub content_size 256L in
661661- Bytes.set_uint16_le output !out_pos (Int64.to_int adjusted);
662662- out_pos := !out_pos + 2
663663- | 4 ->
664664- Bytes.set_int32_le output !out_pos (Int64.to_int32 content_size);
665665- out_pos := !out_pos + 4
666666- | 8 ->
667667- Bytes.set_int64_le output !out_pos content_size;
668668- out_pos := !out_pos + 8
669669- | _ -> ()
670670- end;
671671-672672- !out_pos - pos
673673-674674-(** Compress data to zstd frame *)
675675-let compress ?(level = 3) ?(checksum = true) src =
676676- let src = Bytes.of_string src in
677677- let len = Bytes.length src in
678678- let params = get_level_params level in
679679-680680- (* Allocate output buffer - worst case is slightly larger than input *)
681681- let max_output = len + len / 128 + 256 in
682682- let output = Bytes.create max_output in
683683-684684- (* Initialize offset history *)
685685- let offset_history = Array.copy Constants.initial_repeat_offsets in
686686-687687- (* Write frame header *)
688688- let header_size = write_frame_header output ~pos:0 (Int64.of_int len) params.window_log checksum in
689689- let out_pos = ref header_size in
690690-691691- (* Compress blocks *)
692692- if len = 0 then begin
693693- (* Empty content: write an empty raw block with last_block flag *)
694694- (* Block header: last_block=1, block_type=raw(0), block_size=0 *)
695695- (* Header = 1 | (0 << 1) | (0 << 3) = 0x01 *)
696696- Bytes.set_uint8 output !out_pos 0x01;
697697- Bytes.set_uint8 output (!out_pos + 1) 0x00;
698698- Bytes.set_uint8 output (!out_pos + 2) 0x00;
699699- out_pos := !out_pos + 3
700700- end else begin
701701- let block_size = min len Constants.block_size_max in
702702- let pos = ref 0 in
703703-704704- while !pos < len do
705705- let this_block = min block_size (len - !pos) in
706706- let is_last = !pos + this_block >= len in
707707-708708- let block_len = compress_block src ~pos:!pos ~len:this_block output ~out_pos:!out_pos params offset_history in
709709-710710- (* Set last block flag *)
711711- if is_last then begin
712712- let current = Bytes.get_uint8 output !out_pos in
713713- Bytes.set_uint8 output !out_pos (current lor 0x01)
714714- end;
715715-716716- out_pos := !out_pos + block_len;
717717- pos := !pos + this_block
718718- done
719719- end;
720720-721721- (* Write checksum if requested *)
722722- if checksum then begin
723723- let hash = Xxhash.hash64 src ~pos:0 ~len in
724724- (* Write only lower 32 bits *)
725725- Bytes.set_int32_le output !out_pos (Int64.to_int32 hash);
726726- out_pos := !out_pos + 4
727727- end;
728728-729729- Bytes.sub_string output 0 !out_pos
730730-731731-(** Calculate maximum compressed size *)
732732-let compress_bound len =
733733- len + len / 128 + 256
734734-735735-(** Write a skippable frame.
736736- @param variant Magic number variant 0-15
737737- @param content The content to embed in the skippable frame
738738- @return The complete skippable frame as a string *)
739739-let write_skippable_frame ?(variant = 0) content =
740740- let variant = max 0 (min 15 variant) in
741741- let len = String.length content in
742742- if len > 0xFFFFFFFF then
743743- invalid_arg "Skippable frame content too large (max 4GB)";
744744- let output = Bytes.create (Constants.skippable_header_size + len) in
745745- (* Magic number: 0x184D2A50 + variant *)
746746- let magic = Int32.add Constants.skippable_magic_start (Int32.of_int variant) in
747747- Bytes.set_int32_le output 0 magic;
748748- (* Content size (4 bytes little-endian) *)
749749- Bytes.set_int32_le output 4 (Int32.of_int len);
750750- (* Content *)
751751- Bytes.blit_string content 0 output 8 len;
752752- Bytes.unsafe_to_string output
-5
ocaml-zstd/test-interop/dune
···11-; Test: Verify pure OCaml can decompress C-compressed data
22-; and C zstd can decompress pure OCaml compressed data
33-(test
44- (name test_interop)
55- (libraries zstd alcotest))
-364
ocaml-zstd/test-interop/test_interop.ml
···11-(** Interop tests between pure OCaml zstd and C libzstd.
22-33- Tests:
44- 1. Pure OCaml can decompress data compressed by C libzstd
55- 2. C libzstd can decompress data compressed by pure OCaml zstd *)
66-77-(* Test vectors compressed by C libzstd (from bytesrw's test_zstd.ml) *)
88-99-(* 30 'a' characters compressed by C zstd with checksum *)
1010-let a30_c_compressed =
1111- "\x28\xb5\x2f\xfd\x04\x58\x45\x00\x00\x10\x61\x61\x01\x00\x0c\xc0\x02\x61\
1212- \x36\xf8\xbb"
1313-let a30_expected = String.make 30 'a'
1414-1515-(* 30 'b' characters compressed by C zstd with checksum *)
1616-let b30_c_compressed =
1717- "\x28\xb5\x2f\xfd\x04\x58\x45\x00\x00\x10\x62\x62\x01\x00\x0c\xc0\x02\xb3\
1818- \x56\x1f\x2e"
1919-let b30_expected = String.make 30 'b'
2020-2121-(* Helper to run a shell command and capture output *)
2222-let run_command cmd =
2323- let ic = Unix.open_process_in cmd in
2424- let buf = Buffer.create 256 in
2525- (try
2626- while true do
2727- Buffer.add_channel buf ic 1
2828- done
2929- with End_of_file -> ());
3030- let status = Unix.close_process_in ic in
3131- (Buffer.contents buf, status)
3232-3333-(* Test: Pure OCaml decompresses C-compressed data *)
3434-let test_ocaml_decompress_c_data () =
3535- (* Decompress a30 *)
3636- let result = Zstd.decompress a30_c_compressed in
3737- Alcotest.(check (result string string)) "a30 decompressed" (Ok a30_expected) result;
3838- (* Decompress b30 *)
3939- let result = Zstd.decompress b30_c_compressed in
4040- Alcotest.(check (result string string)) "b30 decompressed" (Ok b30_expected) result
4141-4242-(* Test: Pure OCaml decompresses each C frame separately *)
4343-let test_ocaml_decompress_each_frame () =
4444- (* Our decompressor handles one frame at a time (standard behavior) *)
4545- (* Decompress first frame *)
4646- let result1 = Zstd.decompress a30_c_compressed in
4747- Alcotest.(check (result string string)) "frame 1" (Ok a30_expected) result1;
4848- (* Decompress second frame *)
4949- let result2 = Zstd.decompress b30_c_compressed in
5050- Alcotest.(check (result string string)) "frame 2" (Ok b30_expected) result2
5151-5252-(* Test: C libzstd decompresses pure OCaml-compressed data *)
5353-let test_c_decompress_ocaml_data () =
5454- let test_data = "Hello from pure OCaml zstd! This is a test of interoperability." in
5555- let compressed = Zstd.compress test_data in
5656-5757- (* Verify it has valid zstd magic *)
5858- Alcotest.(check bool) "has zstd magic" true (Zstd.is_zstd_frame compressed);
5959-6060- (* Write compressed data to temp file *)
6161- let tmp_compressed = Filename.temp_file "zstd_test" ".zst" in
6262- let tmp_output = Filename.temp_file "zstd_test" ".txt" in
6363- let oc = open_out_bin tmp_compressed in
6464- output_string oc compressed;
6565- close_out oc;
6666-6767- (* Use C zstd CLI to decompress *)
6868- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_compressed in
6969- let (output, status) = run_command cmd in
7070- (match status with
7171- | Unix.WEXITED 0 -> ()
7272- | _ -> Alcotest.fail (Printf.sprintf "zstd -d failed: %s" output));
7373-7474- (* Read and verify decompressed content *)
7575- let ic = open_in_bin tmp_output in
7676- let decompressed = really_input_string ic (in_channel_length ic) in
7777- close_in ic;
7878-7979- (* Cleanup *)
8080- Sys.remove tmp_compressed;
8181- Sys.remove tmp_output;
8282-8383- Alcotest.(check string) "C decompressed matches" test_data decompressed
8484-8585-(* Test: C libzstd decompresses larger pure OCaml-compressed data *)
8686-let test_c_decompress_large () =
8787- (* 10KB of varied data *)
8888- let size = 10000 in
8989- let test_data = String.init size (fun i -> Char.chr (i mod 256)) in
9090- let compressed = Zstd.compress test_data in
9191-9292- (* Write to temp file *)
9393- let tmp_compressed = Filename.temp_file "zstd_large" ".zst" in
9494- let tmp_output = Filename.temp_file "zstd_large" ".bin" in
9595- let oc = open_out_bin tmp_compressed in
9696- output_string oc compressed;
9797- close_out oc;
9898-9999- (* Use C zstd to decompress *)
100100- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_compressed in
101101- let (output, status) = run_command cmd in
102102- (match status with
103103- | Unix.WEXITED 0 -> ()
104104- | _ -> Alcotest.fail (Printf.sprintf "zstd -d failed: %s" output));
105105-106106- (* Read and verify *)
107107- let ic = open_in_bin tmp_output in
108108- let decompressed = really_input_string ic (in_channel_length ic) in
109109- close_in ic;
110110-111111- Sys.remove tmp_compressed;
112112- Sys.remove tmp_output;
113113-114114- Alcotest.(check int) "size matches" size (String.length decompressed);
115115- Alcotest.(check string) "content matches" test_data decompressed
116116-117117-(* Test: C compression -> OCaml decompression using CLI *)
118118-let test_c_compress_ocaml_decompress () =
119119- let test_data = "Testing C compression with OCaml decompression roundtrip!" in
120120-121121- (* Write original to temp file *)
122122- let tmp_input = Filename.temp_file "zstd_input" ".txt" in
123123- let tmp_compressed = Filename.temp_file "zstd_compressed" ".zst" in
124124- let oc = open_out_bin tmp_input in
125125- output_string oc test_data;
126126- close_out oc;
127127-128128- (* Compress with C zstd *)
129129- let cmd = Printf.sprintf "zstd -f -o %s %s 2>&1" tmp_compressed tmp_input in
130130- let (output, status) = run_command cmd in
131131- (match status with
132132- | Unix.WEXITED 0 -> ()
133133- | _ -> Alcotest.fail (Printf.sprintf "zstd compress failed: %s" output));
134134-135135- (* Read compressed data *)
136136- let ic = open_in_bin tmp_compressed in
137137- let compressed = really_input_string ic (in_channel_length ic) in
138138- close_in ic;
139139-140140- (* Cleanup temp files *)
141141- Sys.remove tmp_input;
142142- Sys.remove tmp_compressed;
143143-144144- (* Verify our OCaml can decompress it *)
145145- Alcotest.(check bool) "C output has magic" true (Zstd.is_zstd_frame compressed);
146146- let result = Zstd.decompress compressed in
147147- Alcotest.(check (result string string)) "OCaml decompressed C output" (Ok test_data) result
148148-149149-(* Test: Empty data roundtrip *)
150150-let test_empty_interop () =
151151- let compressed = Zstd.compress "" in
152152-153153- (* Write to temp file *)
154154- let tmp_compressed = Filename.temp_file "zstd_empty" ".zst" in
155155- let tmp_output = Filename.temp_file "zstd_empty" ".bin" in
156156- let oc = open_out_bin tmp_compressed in
157157- output_string oc compressed;
158158- close_out oc;
159159-160160- (* C zstd decompress *)
161161- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_compressed in
162162- let (output, status) = run_command cmd in
163163- (match status with
164164- | Unix.WEXITED 0 -> ()
165165- | _ -> Alcotest.fail (Printf.sprintf "zstd -d empty failed: %s" output));
166166-167167- (* Verify empty output *)
168168- let ic = open_in_bin tmp_output in
169169- let decompressed = really_input_string ic (in_channel_length ic) in
170170- close_in ic;
171171-172172- Sys.remove tmp_compressed;
173173- Sys.remove tmp_output;
174174-175175- Alcotest.(check string) "empty roundtrip" "" decompressed
176176-177177-(* Test: Various compression levels *)
178178-let test_compression_levels_interop () =
179179- let test_data = String.make 1000 'x' in
180180-181181- List.iter (fun level ->
182182- let compressed = Zstd.compress ~level test_data in
183183-184184- let tmp_compressed = Filename.temp_file "zstd_level" ".zst" in
185185- let tmp_output = Filename.temp_file "zstd_level" ".bin" in
186186- let oc = open_out_bin tmp_compressed in
187187- output_string oc compressed;
188188- close_out oc;
189189-190190- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_compressed in
191191- let (output, status) = run_command cmd in
192192- (match status with
193193- | Unix.WEXITED 0 -> ()
194194- | _ -> Alcotest.fail (Printf.sprintf "level %d: zstd -d failed: %s" level output));
195195-196196- let ic = open_in_bin tmp_output in
197197- let decompressed = really_input_string ic (in_channel_length ic) in
198198- close_in ic;
199199-200200- Sys.remove tmp_compressed;
201201- Sys.remove tmp_output;
202202-203203- Alcotest.(check string) (Printf.sprintf "level %d roundtrip" level) test_data decompressed
204204- ) [1; 3; 5; 10; 15; 19]
205205-206206-(* Test: OCaml skippable frame + C zstd handling *)
207207-let test_skippable_interop () =
208208- (* Create OCaml skippable frame *)
209209- let metadata = "OCaml metadata content" in
210210- let skippable = Zstd.write_skippable_frame metadata in
211211-212212- (* Write to temp file *)
213213- let tmp_skip = Filename.temp_file "zstd_skip" ".zst" in
214214- let oc = open_out_bin tmp_skip in
215215- output_string oc skippable;
216216- close_out oc;
217217-218218- (* C zstd should recognize it as a valid skippable frame *)
219219- let cmd = Printf.sprintf "zstd -l %s 2>&1" tmp_skip in
220220- let (output, status) = run_command cmd in
221221- (match status with
222222- | Unix.WEXITED 0 ->
223223- (* Should report it as a skippable frame *)
224224- Alcotest.(check bool) "C recognizes skip"
225225- true (String.length output > 0)
226226- | _ ->
227227- (* Some versions of zstd may error - that's ok if it reads the format *)
228228- ());
229229-230230- Sys.remove tmp_skip;
231231-232232- (* Also test mixed: skippable + zstd frame *)
233233- let data = "Hello, mixed frames!" in
234234- let compressed = Zstd.compress data in
235235- let mixed = skippable ^ compressed in
236236-237237- let tmp_mixed = Filename.temp_file "zstd_mixed" ".zst" in
238238- let tmp_output = Filename.temp_file "zstd_mixed" ".txt" in
239239- let oc = open_out_bin tmp_mixed in
240240- output_string oc mixed;
241241- close_out oc;
242242-243243- (* C zstd should decompress, skipping the skippable frame *)
244244- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_mixed in
245245- let (output, status) = run_command cmd in
246246- (match status with
247247- | Unix.WEXITED 0 -> ()
248248- | _ -> Alcotest.fail (Printf.sprintf "C zstd mixed failed: %s" output));
249249-250250- let ic = open_in_bin tmp_output in
251251- let decompressed = really_input_string ic (in_channel_length ic) in
252252- close_in ic;
253253-254254- Sys.remove tmp_mixed;
255255- Sys.remove tmp_output;
256256-257257- Alcotest.(check string) "mixed decompressed" data decompressed
258258-259259-(* Test: C skippable frame + OCaml handling *)
260260-let test_c_skippable_to_ocaml () =
261261- (* Create skippable frame using zstd CLI *)
262262- (* zstd doesn't have a direct CLI for skippable frames, so we create one manually *)
263263- (* and verify OCaml can read it *)
264264-265265- (* Instead, test that OCaml can handle C-compressed multi-frame *)
266266- let data1 = "First frame data" in
267267- let data2 = "Second frame data" in
268268-269269- let tmp1 = Filename.temp_file "zstd_m1" ".txt" in
270270- let tmp1z = Filename.temp_file "zstd_m1" ".zst" in
271271- let tmp2 = Filename.temp_file "zstd_m2" ".txt" in
272272- let tmp2z = Filename.temp_file "zstd_m2" ".zst" in
273273- let tmp_combined = Filename.temp_file "zstd_combined" ".zst" in
274274-275275- (* Write and compress each *)
276276- let oc = open_out_bin tmp1 in output_string oc data1; close_out oc;
277277- let oc = open_out_bin tmp2 in output_string oc data2; close_out oc;
278278-279279- let cmd1 = Printf.sprintf "zstd -f -o %s %s 2>&1" tmp1z tmp1 in
280280- let cmd2 = Printf.sprintf "zstd -f -o %s %s 2>&1" tmp2z tmp2 in
281281- ignore (run_command cmd1);
282282- ignore (run_command cmd2);
283283-284284- (* Concatenate *)
285285- let ic1 = open_in_bin tmp1z in
286286- let ic2 = open_in_bin tmp2z in
287287- let z1 = really_input_string ic1 (in_channel_length ic1) in
288288- let z2 = really_input_string ic2 (in_channel_length ic2) in
289289- close_in ic1;
290290- close_in ic2;
291291-292292- let combined = z1 ^ z2 in
293293- let oc = open_out_bin tmp_combined in
294294- output_string oc combined;
295295- close_out oc;
296296-297297- (* OCaml should decompress all frames *)
298298- let result = Zstd.decompress_all combined in
299299- Alcotest.(check (result string string)) "C multi-frame"
300300- (Ok (data1 ^ data2)) result;
301301-302302- (* Cleanup *)
303303- Sys.remove tmp1;
304304- Sys.remove tmp1z;
305305- Sys.remove tmp2;
306306- Sys.remove tmp2z;
307307- Sys.remove tmp_combined
308308-309309-(* Test: Compression ratio on compressible data *)
310310-let test_compression_ratio () =
311311- (* Create highly compressible data: all same byte (triggers RLE) *)
312312- let size = 1000 in
313313- let test_data = String.make size 'x' in
314314-315315- let compressed = Zstd.compress test_data in
316316- let ratio = float_of_int (String.length compressed) /. float_of_int size in
317317-318318- (* RLE should achieve excellent compression *)
319319- Alcotest.(check bool) "RLE compression achieved"
320320- true (ratio < 0.1); (* RLE for 1000 bytes should be ~15 bytes *)
321321-322322- (* Also test that our decoder can handle it *)
323323- let decompressed = Zstd.decompress compressed in
324324- Alcotest.(check (result string string)) "roundtrip" (Ok test_data) decompressed;
325325-326326- (* Write to temp file and verify C zstd can decompress *)
327327- let tmp_compressed = Filename.temp_file "zstd_ratio" ".zst" in
328328- let tmp_output = Filename.temp_file "zstd_ratio" ".txt" in
329329- let oc = open_out_bin tmp_compressed in
330330- output_string oc compressed;
331331- close_out oc;
332332-333333- let cmd = Printf.sprintf "zstd -d -f -o %s %s 2>&1" tmp_output tmp_compressed in
334334- let (output, status) = run_command cmd in
335335- (match status with
336336- | Unix.WEXITED 0 -> ()
337337- | _ -> Alcotest.fail (Printf.sprintf "zstd -d failed: %s" output));
338338-339339- let ic = open_in_bin tmp_output in
340340- let decompressed_c = really_input_string ic (in_channel_length ic) in
341341- close_in ic;
342342-343343- Sys.remove tmp_compressed;
344344- Sys.remove tmp_output;
345345-346346- Alcotest.(check string) "C decompressed matches" test_data decompressed_c
347347-348348-let tests = [
349349- "OCaml decompresses C data", `Quick, test_ocaml_decompress_c_data;
350350- "OCaml decompresses each C frame", `Quick, test_ocaml_decompress_each_frame;
351351- "C decompresses OCaml data", `Quick, test_c_decompress_ocaml_data;
352352- "C decompresses large OCaml data", `Quick, test_c_decompress_large;
353353- "C compress -> OCaml decompress", `Quick, test_c_compress_ocaml_decompress;
354354- "Empty interop", `Quick, test_empty_interop;
355355- "Compression levels interop", `Quick, test_compression_levels_interop;
356356- "Skippable frame interop", `Quick, test_skippable_interop;
357357- "C multi-frame to OCaml", `Quick, test_c_skippable_to_ocaml;
358358- "Compression ratio", `Quick, test_compression_ratio;
359359-]
360360-361361-let () =
362362- Alcotest.run "zstd interop" [
363363- "C <-> OCaml interop", tests;
364364- ]