package corvus-json-schema

  1. Overview
  2. Docs
A fast JSON Schema evaluator (draft 4 to 2020-12) with no dependencies outside the OCaml distribution

Install

dune-project
 Dependency

Authors

Maintainers

Sources

v0.1.1.tar.gz
sha256=d0a084719770883384e488d42ff1a2c0f84a53e2b2898016183533b8393f2cab
sha512=d5875f234b316cd50a888e1a3acc055133d705eb6622e3856c22f6719fa3eae196f1f08cd08cc073cc0924d27e330ae2ea5388142dd5e91b173bdfd91ad997f0

doc/src/corvus-json-schema.ecmaregex/corvus_json_schema_ecmaregex.ml.html

Source file corvus_json_schema_ecmaregex.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
(* The port of ecmaregex.go of the Go module, with the choice of back end of the Object Pascal port
   (Corvus.JsonSchema.EcmaRegex.pas). The interface describes the engine as a whole.

   charset.ml is charset.go and fold.go, parse.ml is parse.go, compile.ml is compile.go, vm.ml is vm.go and dfa.ml is
   dfa.go. utf8.ml holds what the Go source takes from the unicode/utf8 package. *)

type t = {
  pattern : string;
  unicode : bool;  (** Says the pattern was read with the u flag grammar. *)
  regular : bool;
  dfa : Dfa.t option;  (** The automaton over ASCII text, when the pattern has one. *)
  prog : Compile.program;  (** The pattern compiled for the backtracking matcher. *)
  slot : Vm.state;
      (** Working memory of the backtracking matcher for the entry points that are given none. Only the match that holds
          [busy] touches it. *)
  busy : bool Atomic.t;
}

type engine = Dfa | Backtrack

let engine_name = function Dfa -> "dfa" | Backtrack -> "backtrack"

(* The largest count the regexp package of Go accepts in a counted quantifier, and the largest product of the counts
   of nested ones. *)
let max_regexp_repeat = 1000

(* Reports whether a quantifier is one the Go package writes with braces for the regexp package: every one but *, +
   and ?. *)
let is_counted (n : Parse.node) =
  not ((n.min = 0 && n.max = Parse.unbounded) || (n.min = 1 && n.max = Parse.unbounded) || (n.min = 0 && n.max = 1))

(* Reports whether the counted quantifiers at and below n nest to a product of at most limit. It is the rule of the
   same name in the regexp/syntax package of Go. *)
let rec repeat_is_valid (n : Parse.node) limit =
  let counted = n.kind = Parse.Repeat && is_counted n in
  let m = if counted then if n.max < 0 then n.min else n.max else 1 in
  if counted && n.max = 0 then true
  else if counted && m > limit then false
  else
    let limit = if counted && m > 0 then limit / m else limit in
    Array.for_all (fun sub -> repeat_is_valid sub limit) n.subs

(* Reports whether every counted quantifier at and below n is one the regexp package of Go accepts. The Go package
   runs a pattern on the backtracking matcher alone when the regexp package refuses its translation, which it does
   for these quantifiers. The rule is kept here so that the same patterns are tried for an automaton, and because it
   bounds the work of building one (see Dfa.build). The other limits of the regexp package, on the size and the depth
   of a pattern, are not kept: a pattern beyond them is too large for an automaton in any case. *)
let rec within_regexp_limits (n : Parse.node) =
  (* The quantifier itself first, so that a nest of quantifiers that is too large is found at its outermost. *)
  (not
     (n.kind = Parse.Repeat && is_counted n
     && (n.min > max_regexp_repeat || n.max > max_regexp_repeat
        || ((n.min >= 2 || n.max >= 2) && not (repeat_is_valid n max_regexp_repeat)))))
  && Array.for_all within_regexp_limits n.subs

(* compile with a way to leave out the automaton, which the tests use to compare the back ends. *)
type error = Parse.error = Invalid of string | Too_large of string * int

let error_message = Parse.error_message
let max_pattern_length = Parse.max_pattern_length
let max_nesting = Parse.max_nesting
let max_size = Parse.max_size

(* Reads the pattern with the u flag grammar and, failing that, with the Annex B grammar. A pattern over a limit of
   size under either is too large, whether or not it is valid. A pattern neither accepts has the error of the u flag
   grammar. *)
let parse_either pattern =
  match Parse.parse pattern true with
  | Ok (root, ps) -> Ok (root, ps, true)
  | Error (Too_large _ as large) -> Error large
  | Error invalid -> (
      match Parse.parse pattern false with
      | Ok (root, ps) -> Ok (root, ps, false)
      | Error (Too_large _ as large) -> Error large
      | Error _ -> Error invalid)

let compile_with pattern force_backtrack =
  match parse_either pattern with
  | Error error -> Error error
  | Ok (root, ps, unicode) ->
      let regular = (not ps.needs_backtracking) && within_regexp_limits root in
      let dfa = if regular && not force_backtrack then Dfa.build root else None in
      let prog = Compile.compile_program root (max ps.groups ps.group_count) unicode in
      Ok { pattern; unicode; regular; dfa; prog; slot = Vm.new_state (); busy = Atomic.make false }

let compile pattern = compile_with pattern false
let compile_backtrack pattern = compile_with pattern true
let valid pattern = match Parse.parse pattern true with Ok _ -> true | Error _ -> false
let check pattern = match parse_either pattern with Ok (_, _, unicode) -> Ok unicode | Error error -> Error error
let check_range name length off len = if off < 0 || len < 0 || off > length - len then invalid_arg name

type scratch = Vm.state

let create_scratch = Vm.new_state

exception Match_time = Vm.Match_time
exception Match_memory = Vm.Match_memory

let set_limit = Vm.set_limit
let set_clock = Vm.set_clock
let default_clock = Vm.default_clock
let limit = Vm.limit
let check_every = Vm.check_every
let max_stack = Vm.max_stack

let is_match_with scratch re text off len =
  check_range "Corvus_json_schema_ecmaregex.is_match_with" (Bytes.length text) off len;
  let answer = match re.dfa with Some d -> Dfa.match_bytes d text off (off + len) | None -> Dfa.undecided in
  if answer <> Dfa.undecided then answer = Dfa.matched else Vm.match_bytes re.prog scratch text off (off + len)

let is_match_sub_with scratch re text off len =
  check_range "Corvus_json_schema_ecmaregex.is_match_sub_with" (String.length text) off len;
  let answer = match re.dfa with Some d -> Dfa.match_string d text off (off + len) | None -> Dfa.undecided in
  if answer <> Dfa.undecided then answer = Dfa.matched else Vm.match_string re.prog scratch text off (off + len)

let is_match re text off len =
  check_range "Corvus_json_schema_ecmaregex.is_match" (Bytes.length text) off len;
  let answer = match re.dfa with Some d -> Dfa.match_bytes d text off (off + len) | None -> Dfa.undecided in
  if answer <> Dfa.undecided then answer = Dfa.matched
  else if Atomic.compare_and_set re.busy false true then (
    match Vm.match_bytes re.prog re.slot text off (off + len) with
    | matched ->
        Atomic.set re.busy false;
        matched
    | exception e ->
        Atomic.set re.busy false;
        raise e)
  else
    (* Another thread or domain is matching with the memory of the pattern. This match makes its own. *)
    Vm.match_bytes re.prog (Vm.new_state ()) text off (off + len)

let is_match_sub re text off len =
  check_range "Corvus_json_schema_ecmaregex.is_match_sub" (String.length text) off len;
  let answer = match re.dfa with Some d -> Dfa.match_string d text off (off + len) | None -> Dfa.undecided in
  if answer <> Dfa.undecided then answer = Dfa.matched
  else if Atomic.compare_and_set re.busy false true then (
    match Vm.match_string re.prog re.slot text off (off + len) with
    | matched ->
        Atomic.set re.busy false;
        matched
    | exception e ->
        Atomic.set re.busy false;
        raise e)
  else Vm.match_string re.prog (Vm.new_state ()) text off (off + len)

let is_match_string re text = is_match_sub re text 0 (String.length text)
let engine re = match re.dfa with Some _ -> Dfa | None -> Backtrack
let unicode re = re.unicode
let pattern re = re.pattern
let regular re = re.regular

module Internal = struct
  let automaton re text =
    match re.dfa with
    | None -> None
    | Some d ->
        let answer = Dfa.match_string d text 0 (String.length text) in
        if answer = Dfa.undecided then None else Some (answer = Dfa.matched)

  let canonicalize = Charset.canonicalize

  let fold_classes unicode_mode =
    let table = Charset.fold_classes unicode_mode in
    (table.points, table.classes)

  let property_resolves expr = Charset.property_set expr <> None
  let property_pairs = Charset.property_pairs
  let binary_names = Array.map fst Charset.binary_names
  let category_names = Array.map fst Charset.category_names

  let program_shape re =
    ( re.prog.loops,
      Array.fold_left (fun n (i : Compile.inst) -> if i.op = Compile.Op_backref then n + 1 else n) 0 re.prog.insts )

  let needs re =
    ( re.prog.need,
      Array.of_list
        (List.filter_map
           (fun (i : Compile.inst) -> if i.op = Compile.Op_loop then Some i.need else None)
           (Array.to_list re.prog.insts)) )

  let iterations re =
    Array.of_list
      (List.filter_map
         (fun (i : Compile.inst) -> if i.op = Compile.Op_loop then Some i.iter else None)
         (Array.to_list re.prog.insts))

  let no_match = Compile.no_match
  let memo_from_the_start = Vm.memo_from_the_start

  let memo_points re =
    List.filter_map
      (fun (i : Compile.inst) ->
        if i.m = 0 then None
        else
          let name =
            match i.op with
            | Compile.Op_split -> "split"
            | Compile.Op_loop -> "loop"
            | Compile.Op_rep -> "rep"
            | Compile.Op_rep_lazy -> "lazy"
            | _ -> "?"
          in
          let loop l = if l >= 0 then " " ^ string_of_int l else "" in
          Some (name ^ loop i.ml0 ^ loop i.ml1))
      (Array.to_list re.prog.insts)

  let point_count re = re.prog.points
  let shown_quick (q : Compile.quick) = (Bytes.to_string q.ascii, q.other, q.mid, q.at_end)
  let quick re = shown_quick re.prog.insts.(0).q

  let quick_of_wild re =
    List.filter_map
      (fun (i : Compile.inst) ->
        if i.op = Compile.Op_backref || i.op = Compile.Op_char_back then Some (shown_quick i.q) else None)
      (Array.to_list re.prog.insts)

  let memory_use points width checks =
    let st = Vm.new_state () in
    st.points <- points;
    st.width <- width;
    st.memo_off <- points = 0;
    let k = ref 0 in
    while !k < checks && (not st.memo_on) && not st.memo_off do
      st.checks <- st.checks + 1;
      Vm.remember st;
      incr k
    done;
    (st.memo_on, st.memo_off, Bytes.length st.memo / 8)

  let max_memo_words = Vm.max_memo_words
  let need_passes re = re.prog.need_passes
  let quick_passes re = re.prog.quick_passes
  let prefix re = re.prog.prefix
  let size pattern unicode = match Parse.parse pattern unicode with Ok (_, ps) -> ps.size | Error _ -> -1
  let unpack packed = (Utf8.rune packed, Utf8.width packed)
  let unit_at text at = unpack (Utf8.unit_at { b = Bytes.empty; s = text; is_string = true } 0 at (String.length text))

  let unit_before text at =
    unpack (Utf8.unit_before { b = Bytes.empty; s = text; is_string = true } 0 at (String.length text))
end