Source file domain.ml

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

let equal a b =
  match (a, b) with
  | IPv4 a, IPv4 b -> Ipaddr.(compare (V4 a) (V4 b)) = 0
  | IPv6 a, IPv6 b -> Ipaddr.(compare (V6 a) (V6 b)) = 0
  | IPv4 a, IPv6 b -> Ipaddr.(compare (V4 a) (V6 b)) = 0
  | IPv6 a, IPv4 b -> Ipaddr.(compare (V6 a) (V4 b)) = 0
  | Extension (ka, va), Extension (kb, vb) ->
      String.equal ka kb && String.equal va vb
  | Domain a, Domain b -> (
      try List.for_all2 String.equal a b with _ -> false)
  | _, _ -> false

let error_msgf fmt = Format.kasprintf (fun err -> Error (`Msg err)) fmt

let compare a b =
  let sup = 1 and inf = -1 in
  match (a, b) with
  | Domain a, Domain b ->
      let rec go a b =
        match (a, b) with
        | [], [] -> 0
        | a :: ar, b :: br ->
            let res =
              String.compare (String.lowercase_ascii a)
                (String.lowercase_ascii b) in
            if res = 0 then go ar br else res
        | [], _ :: _ -> inf
        | _ :: _, [] -> sup in
      go a b
  | IPv4 ipv4, IPv6 ipv6 | IPv6 ipv6, IPv4 ipv4 ->
      Ipaddr.(compare (V4 ipv4) (V6 ipv6))
  | IPv6 a, IPv6 b -> Ipaddr.V6.compare a b
  | IPv4 a, IPv4 b -> Ipaddr.V4.compare a b
  | Extension (ka, va), Extension (kb, vb) ->
      let ret = String.compare ka kb in
      if ret = 0 then String.compare va vb else ret
  | Domain _, _ -> sup
  | (IPv4 _ | IPv6 _), Domain _ -> inf
  | IPv6 _, _ -> sup
  | IPv4 _, _ -> sup
  | Extension _, (Domain _ | IPv4 _ | IPv6 _) -> inf

let pp ppf = function
  | IPv4 ipv4 -> Ipaddr.V4.pp ppf ipv4
  | IPv6 ipv6 -> Ipaddr.V6.pp ppf ipv6
  | Extension (k, v) -> Fmt.pf ppf "%s:%s" k v
  | Domain l -> Fmt.pf ppf "%a" Fmt.(list ~sep:(const string ".") string) l

module Decoder = struct
  open Angstrom

  let ( or ) a b x = a x || b x
  let is_alpha = function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false
  let is_digit = function '0' .. '9' -> true | _ -> false
  let is_dash = ( = ) '-'
  let let_dig = satisfy (is_alpha or is_digit)

  (* XXX(dinosaure): Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
   * and Let-dig = ALPHA / DIGIT
   *
   * This [ldh_str] does not strictly follow the definition but just
   * eats [*( ALPHA / DIGIT / "-" )] and check that the last character
   * **is not** a dash. *)
  let ldh_str =
    take_while1 (is_alpha or is_digit or is_dash) >>= fun res ->
    if String.get res (String.length res - 1) <> '-'
    then return res
    else fail "Invalid ldh-str token"

  let sub_domain =
    let_dig >>= fun pre ->
    option "" ldh_str >>| fun lst -> String.concat "" [ String.make 1 pre; lst ]

  let domain =
    sub_domain >>= fun x ->
    many (char '.' *> sub_domain) >>| fun r -> Domain (x :: r)

  (* XXX(dinosaure): from mrmime. *)

  let is_dcontent = function
    | '\033' .. '\090' | '\094' .. '\126' -> true
    | _ -> false

  let ipv4_address_literal =
    Unsafe.take_while1 is_dcontent (fun buf ~off ~len ->
        let raw = Bigstringaf.substring buf ~off ~len in
        let pos = ref 0 in
        try
          let res = Ipaddr.V4.of_string_raw raw pos in
          if !pos = len then Some res else None
        with Ipaddr.Parse_error _ -> None)
    >>= function
    | Some v -> return v
    | None -> fail "ipv4_address_literal"

  let ipv6_addr =
    Unsafe.take_while1 is_dcontent (fun buf ~off ~len ->
        let raw = Bigstringaf.substring buf ~off ~len in
        let pos = ref 0 in
        try
          let res = Ipaddr.V6.of_string_raw raw pos in
          if !pos = len then Some res else None
        with Ipaddr.Parse_error _ -> None)
    >>= function
    | Some v -> return v
    | None -> fail "ipv6_addr"

  let ipv6_address_literal = string "IPv6:" *> ipv6_addr
  let failf fmt = Fmt.kstr fail fmt

  let ldh_str =
    take_while1 (function
      | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '-' -> true
      | _ -> false)
    >>= fun ldh ->
    if String.unsafe_get ldh (String.length ldh - 1) = '-'
    then failf "ldh_str: %s is invalid" ldh
    else return ldh

  let general_address_literal =
    ldh_str <* char ':' >>= fun ldh ->
    take_while1 is_dcontent >>| fun value -> Extension (ldh, value)

  let address_literal =
    char '['
    *> (ipv4_address_literal
       >>| (fun v -> IPv4 v)
       <|> (ipv6_address_literal >>| fun v -> IPv6 v)
       <|> general_address_literal)
    <* char ']'

  let of_string_exn x =
    match parse_string ~consume:Consume.All (domain <|> address_literal) x with
    | Ok v -> v
    | Error _ -> Fmt.invalid_arg "Invalid domain: %s" x

  let of_string x =
    match parse_string ~consume:Consume.All (domain <|> address_literal) x with
    | Ok _ as v -> v
    | Error _ -> error_msgf "Invalid domain: %S" x
end

module Encoder = struct
  (* XXX(dinosaure): use [Fmt] was not the best idea when we will use
     [to_string] on the protocol. *)

  let to_string = function
    | IPv4 ipv4 -> Fmt.str "[%s]" (Ipaddr.V4.to_string ipv4)
    | IPv6 ipv6 -> Fmt.str "[IPv6:%s]" (Ipaddr.V6.to_string ipv6)
    | Extension (k, v) -> Fmt.str "[%s:%s]" k v
    | Domain l -> Fmt.str "%a" Fmt.(list ~sep:(const string ".") string) l
end

let of_string = Decoder.of_string
let of_string_exn = Decoder.of_string_exn
let to_string = Encoder.to_string

exception Break

let satisfy predicate x =
  let len = String.length x in
  try
    for i = 0 to len - 1 do
      if not (predicate x.[i]) then raise Break
    done ;
    true
  with Break -> false

let extension k v =
  let is_ldh = Decoder.(is_alpha or is_digit or is_dash) in
  let is_dcontent = Decoder.is_dcontent in
  if String.length k > 0
     && satisfy is_ldh k
     && k.[String.length k - 1] <> '-'
     && String.length v > 0
     && satisfy is_dcontent v
  then Ok (Extension (k, v))
  else error_msgf "Invalid key:%S or value:%S" k v

let is_atext_valid_string _ = true

type atom = string

let atom x =
  if is_atext_valid_string x
  then Ok x
  else error_msgf "atom %S does not respect standards" x

let atom_exn x =
  match atom x with Ok v -> v | Error (`Msg err) -> invalid_arg err

let a = atom_exn

module Peano = struct
  type z = Z
  type 'a s = S
end

let unsafe_domain_of_list_exn = function
  | [] -> Fmt.invalid_arg "A domain must contain at least one element"
  | domain -> Domain domain

type 'a domain =
  | ( :: ) : atom * 'a domain -> 'a Peano.s domain
  | [] : Peano.z domain

let rec coerce : type a. a Peano.s domain -> string list = function
  | [ x ] -> [ x ]
  | x :: y :: r -> List.cons x (coerce (y :: r))

let make_domain : type a. a domain -> (string list, [> `Msg of string ]) result
    = function
  | [] -> error_msgf "A domain must contain at least one element"
  | x :: r -> Ok (coerce (x :: r))

type 'a w =
  | WDomain : 'a domain w
  | WIPv4 : Ipaddr.V4.t w
  | WIPv6 : Ipaddr.V6.t w

let domain = WDomain
let ipv4 = WIPv4
let ipv6 = WIPv6

let make : type a. a w -> a -> (t, [> `Msg of string ]) result =
 fun witness v ->
  match witness with
  | WDomain ->
      let ( >>| ) x f =
        match x with Ok x -> Ok (f x) | Error err -> Error err in
      make_domain v >>| fun v -> Domain v
  | WIPv4 -> Ok (IPv4 v)
  | WIPv6 -> Ok (IPv6 v)

let v : type a. a w -> a -> t =
 fun witness v ->
  match make witness v with Ok v -> v | Error (`Msg err) -> invalid_arg err