File: fuzz_buf_read.ml

package info (click to toggle)
ocaml-eio 1.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,548 kB
  • sloc: ml: 14,608; ansic: 1,237; makefile: 25
file content (268 lines) | stat: -rw-r--r-- 8,684 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
(* This file contains a simple model of `Buf_read`, using a single string.
   It runs random operations on both the model and the real buffer and
   checks they always give the same result. *)

module String = struct
  include String

  let rec find ?(start=0) p t =
    if start = String.length t then None
    else if p t.[start] then Some start
    else find ~start:(succ start) p t

  let drop t n = String.sub t n (String.length t - n)

  let cut ~sep t =
    match String.index_opt t sep with
    | None -> None
    | Some i -> Some (String.sub t 0 i, drop t (i + 1))
end

let debug = false

module Buf_read = Eio.Buf_read
exception Buffer_limit_exceeded = Buf_read.Buffer_limit_exceeded

let initial_size = 10
let max_size = 100

module Mock_flow = struct
  type t = string list ref

  let rec single_read t buf =
    match !t with
    | [] ->
      raise End_of_file
    | "" :: xs ->
      t := xs;
      single_read t buf
    | x :: xs ->
      let len = min (Cstruct.length buf) (String.length x) in
      Cstruct.blit_from_string x 0 buf 0 len;
      let x' = String.drop x len in
      t := (if x' = "" then xs else x' :: xs);
      len

  let read_methods = []
end

let mock_flow =
  let ops = Eio.Flow.Pi.source (module Mock_flow) in
  fun chunks -> Eio.Resource.T (ref chunks, ops)

module Model = struct
  type t = string ref

  let of_chunks chunks = ref (String.concat "" chunks)

  let take_all t =
    let old = !t in
    if String.length old >= max_size then raise Buffer_limit_exceeded;
    t := "";
    old

  let line t =
    match String.cut ~sep:'\n' !t with
    | Some (line, rest) ->
      if String.length line >= max_size then raise Buffer_limit_exceeded;
      t := rest;
      if String.ends_with ~suffix:"\r" line then String.sub line 0 (String.length line - 1)
      else line
    | None when !t = "" -> raise End_of_file
    | None when String.length !t >= max_size -> raise Buffer_limit_exceeded
    | None -> take_all t

  let any_char t =
    match !t with
    | "" -> raise End_of_file
    | s ->
      t := String.drop s 1;
      s.[0]

  let peek_char t =
    match !t with
    | "" -> None
    | s -> Some (s.[0])

  let consume t n =
    t := String.drop !t n

  let char c t =
    match peek_char t with
    | Some c2 when c = c2 -> consume t 1
    | Some _ -> failwith "char"
    | None -> raise End_of_file

  let string s t =
    if debug then Fmt.pr "string %S@." s;
    let len_t = String.length !t in
    let prefix = String.sub s 0 (min len_t (String.length s)) in
    if not (String.starts_with ~prefix !t) then failwith "string";
    if String.length s > max_size then raise Buffer_limit_exceeded;
    if String.starts_with ~prefix:s !t then consume t (String.length s)
    else raise End_of_file

  let take n t =
    if n < 0 then invalid_arg "neg";
    if n > max_size then raise Buffer_limit_exceeded
    else if String.length !t >= n then (
      let data = String.sub !t 0 n in
      t := String.drop !t n;
      data
    ) else raise End_of_file

  let take_while p t =
    match String.find (Fun.negate p) !t with
    | Some i when i >= max_size -> raise Buffer_limit_exceeded
    | Some i ->
      let data = String.sub !t 0 i in
      consume t i;
      data
    | None -> take_all t

  let skip_while p t =
    match String.find (Fun.negate p) !t with
    | Some i -> consume t i
    | None -> t := ""

  let skip n t =
    if n < 0 then invalid_arg "skip";
    if n > String.length !t then (
      t := "";
      raise End_of_file;
    );
    consume t n

  let end_of_input t =
    if !t <> "" then failwith "not eof"

  let rec lines t =
    match line t with
    | line -> line :: lines t
    | exception End_of_file -> []

  module BE = struct
    let uint16 t = String.get_uint16_be (take 2 t) 0

    let uint32 t = String.get_int32_be (take 4 t) 0

    let uint48 t =
      let s = take 6 t in
      let upper_16 = String.get_uint16_be s 0 |> Int64.of_int in
      let middle_16 = String.get_uint16_be s 2 |> Int64.of_int in
      let lower_16 = String.get_uint16_be s 4 |> Int64.of_int in
      Int64.(
        add 
          (shift_left upper_16 32)
        (add
          (shift_left middle_16 16)
          (lower_16))
      )

    let uint64 t = String.get_int64_be (take 8 t) 0

    let float t =
      Int32.float_of_bits (
        String.get_int32_be (take 4 t) 0)

    let double t =
      Int64.float_of_bits (
        String.get_int64_be (take 8 t) 0)
  end

  module LE = struct
    let uint16 t = String.get_uint16_le (take 2 t) 0

    let uint32 t = String.get_int32_le (take 4 t) 0

    let uint48 t =
      let s = take 6 t in
      let lower_16 = String.get_uint16_le s 0 |> Int64.of_int in
      let middle_16 = String.get_uint16_le s 2 |> Int64.of_int in
      let upper_16 = String.get_uint16_le s 4 |> Int64.of_int in
      Int64.(
        add 
          (shift_left upper_16 32)
        (add
          (shift_left middle_16 16)
          (lower_16))
      )

    let uint64 t = String.get_int64_le (take 8 t) 0

    let float t =
      Int32.float_of_bits (
        String.get_int32_le (take 4 t) 0)

    let double t =
      Int64.float_of_bits (
        String.get_int64_le (take 8 t) 0)
  end
end

type op = Op : 'a Crowbar.printer * 'a Buf_read.parser * (Model.t -> 'a) -> op

let unit = Fmt.(const string) "()"
let dump_char f c = Fmt.pf f "%C" c

let digit = function
  | '0'..'9' -> true
  | _ -> false

let op =
  let label (name, gen) = Crowbar.with_printer Fmt.(const string name) gen in
  Crowbar.choose @@ List.map label [
    "line", Crowbar.const @@ Op (Fmt.Dump.string, Buf_read.line, Model.line);
    "char 'x'", Crowbar.const @@ Op (unit, Buf_read.char 'x', Model.char 'x');
    "any_char", Crowbar.const @@ Op (dump_char, Buf_read.any_char, Model.any_char);
    "peek_char", Crowbar.const @@ Op (Fmt.Dump.option dump_char, Buf_read.peek_char, Model.peek_char);
    "string", Crowbar.(map [bytes]) (fun s -> Op (unit, Buf_read.string s, Model.string s));
    "take", Crowbar.(map [int]) (fun n -> Op (Fmt.Dump.string, Buf_read.take n, Model.take n));
    "take_all", Crowbar.const @@ Op (Fmt.Dump.string, Buf_read.take_all, Model.take_all);
    "take_while digit", Crowbar.const @@ Op (Fmt.Dump.string, Buf_read.take_while digit, Model.take_while digit);
    "skip_while digit", Crowbar.const @@ Op (unit, Buf_read.skip_while digit, Model.skip_while digit);
    "skip", Crowbar.(map [int]) (fun n -> Op (unit, Buf_read.skip n, Model.skip n));
    "end_of_input", Crowbar.const @@ Op (unit, Buf_read.end_of_input, Model.end_of_input);
    "lines", Crowbar.const @@ Op (Fmt.Dump.(list string), (Buf_read.(map List.of_seq lines)), Model.lines);
    "be_uint16", Crowbar.const @@ Op (Fmt.int, (Buf_read.BE.uint16), Model.BE.uint16);
    "be_uint32", Crowbar.const @@ Op (Fmt.int32, (Buf_read.BE.uint32), Model.BE.uint32);
    "be_uint48", Crowbar.const @@ Op (Fmt.int64, (Buf_read.BE.uint48), Model.BE.uint48);
    "be_uint64", Crowbar.const @@ Op (Fmt.int64, (Buf_read.BE.uint64), Model.BE.uint64);
    "be_float", Crowbar.const @@ Op (Fmt.float, (Buf_read.BE.float), Model.BE.float);
    "be_double", Crowbar.const @@ Op (Fmt.float, (Buf_read.BE.double), Model.BE.double);
    "le_uint16", Crowbar.const @@ Op (Fmt.int, (Buf_read.LE.uint16), Model.LE.uint16);
    "le_uint32", Crowbar.const @@ Op (Fmt.int32, (Buf_read.LE.uint32), Model.LE.uint32);
    "le_uint48", Crowbar.const @@ Op (Fmt.int64, (Buf_read.LE.uint48), Model.LE.uint48);
    "le_uint64", Crowbar.const @@ Op (Fmt.int64, (Buf_read.LE.uint64), Model.LE.uint64);
    "le_float", Crowbar.const @@ Op (Fmt.float, (Buf_read.LE.float), Model.LE.float);
    "le_double", Crowbar.const @@ Op (Fmt.float, (Buf_read.LE.double), Model.LE.double);
  ]

let catch f x =
  match f x with
  | y -> Ok y
  | exception End_of_file -> Error "EOF"
  | exception Invalid_argument _ -> Error "Invalid"
  | exception Failure _ -> Error "Failure"
  | exception Buffer_limit_exceeded -> Error "TooBig"

let random chunks ops =
  let model = Model.of_chunks chunks in
  let chunks_len = String.length !model in
  let r = Buf_read.of_flow (mock_flow chunks) ~initial_size ~max_size in
  if debug then print_endline "*** start ***";
  let check_eq (Op (pp, a, b)) =
    if debug then (
      Fmt.pr "---@.";
      Fmt.pr "real :%S@." (Cstruct.to_string (Buf_read.peek r));
      Fmt.pr "model:%S@." !model;
    );
    let x = catch a r in
    let y = catch b model in
    Crowbar.check_eq ~pp:Fmt.(result ~ok:pp ~error:string) x y
  in
  List.iter check_eq ops;
  Crowbar.check_eq ~pp:Fmt.int (Buf_read.consumed_bytes r) (chunks_len - String.length !model)

let () =
  Crowbar.(add_test ~name:"random ops" [list bytes; list op] random)