Source file purl.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
let valid_key = function
  | 'a' .. 'z' | '0' .. '9' | '.' | '-' | '_' -> true
  | _ -> false

let alpha = function
  | 'A' .. 'Z' | 'a' .. 'z' -> true
  | _ -> false

let alphanum = function
  | 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' -> true
  | _ -> false

let punctation = function
  | '.' | '-' | '_' | '~' -> true
  | _ -> false

module Utils = struct
  let int_of_hex_char = function
    | '0' .. '9' as c -> Char.code c - 48
    | 'A' .. 'F' as c -> Char.code c - 55
    | 'a' .. 'f' as c -> Char.code c - 87
    | _ -> invalid_arg "not a hex char"

  let percent_decode str =
    let l = String.length str in
    let b = Buffer.create l in
    let rec scan s idx =
      if idx = l then begin
        Buffer.add_substring b str s (idx - s);
        Ok ()
      end else if str.[idx] = '%' then begin
        Buffer.add_substring b str s (idx - s);
        if idx + 2 > l then
          Error "bad percent encoding (bad length)"
        else
          let high = idx + 1
          and low = idx + 2
          in
          match int_of_hex_char str.[high], int_of_hex_char str.[low] with
          | exception _ ->
            Error "bad percent encoding (invalid hex encoding)"
          | highbits, lowbits ->
            let char = Char.chr (highbits lsl 4 + lowbits) in
            if alphanum char || punctation char then
              Error "bad percent encoding (alphanumeric or punctation)"
            else begin
              Buffer.add_char b char;
              scan (low + 1) (low + 1)
            end
      end else scan s (idx + 1)
    in
    match scan 0 0 with
    | Ok () -> Ok (Buffer.contents b)
    | Error _ as e -> e

  let percent_encode str =
    let l = String.length str in
    let b = Buffer.create l in
    let rec scan s idx =
      if idx = l then begin
        Buffer.add_substring b str s (idx - s)
      end else begin
        let char = str.[idx] in
        if alphanum char || punctation char || char = ':' then
          scan s (idx + 1)
        else begin
          if idx > s then Buffer.add_substring b str s (idx - s);
          Buffer.add_string b (Printf.sprintf "%%%02X" (Char.code char));
          scan (idx + 1) (idx + 1)
        end
      end
    in
    scan 0 0;
    Buffer.contents b


  let trim_char_really char s =
    let l = String.length s in
    let i = ref 0 in
    while !i < l && String.unsafe_get s !i = char do
      incr i
    done;
    let j = ref (l - 1) in
    while !j >= !i && String.unsafe_get s !j = char do
      decr j
    done;
    if !j >= !i then
      String.sub s !i (!j - !i + 1)
    else
      ""

  let trim_char char s =
    if s = "" then s
    else if String.unsafe_get s 0 = char ||
            String.unsafe_get s (String.length s - 1) = char
    then
      trim_char_really char s
    else
      s

  let split_from_right char s =
    match String.rindex_opt s char with
    | None -> s, None
    | Some idx ->
      String.sub s 0 idx,
      Some (String.sub s (idx + 1) (String.length s - idx - 1))

  let split_from_left char s =
    match String.index_opt s char with
    | None -> None, s
    | Some idx ->
      Some (String.sub s 0 idx),
      String.sub s (idx + 1) (String.length s - idx - 1)
end

let scheme = "pkg"

type t = {
  typ : string;
  namespace : string list;
  name : string;
  version : string option;
  qualifiers : (string * string) list;
  subpath : string list;
}

let to_string { typ ; namespace ; name ; version ; qualifiers ; subpath } =
  let buf = Buffer.create 100 in
  Buffer.add_string buf scheme;
  Buffer.add_char buf ':';
  Buffer.add_string buf typ;
  Buffer.add_char buf '/';
  (match namespace with
   | [] -> ()
   | segs ->
     List.iter (fun s ->
         let enc = Utils.percent_encode s in
         Buffer.add_string buf enc;
         Buffer.add_char buf '/')
       segs);
  Buffer.add_string buf name;
  (match version with
   | None -> ()
   | Some v ->
     Buffer.add_char buf '@';
     Buffer.add_string buf (Utils.percent_encode v));
  (match qualifiers with
   | [] -> ()
   | qs ->
     Buffer.add_char buf '?';
     let qs' =
       List.map (fun (k, v) -> k ^ "=" ^ Utils.percent_encode v)
         qs |> List.sort String.compare
     in
     Buffer.add_string buf (String.concat "&" qs'));
  (match subpath with
   | [] -> ()
   | s ->
     Buffer.add_char buf '#';
     Buffer.add_string buf
       (String.concat "/" (List.map Utils.percent_encode s)));
  Buffer.contents buf


let typ typ =
  let valid typ =
    String.for_all (fun c -> alphanum c || c = '.' || c = '-') typ &&
    String.length typ > 0 && alpha (String.get typ 0)
  in
  if valid typ then
    Ok (String.lowercase_ascii typ)
  else
    Error "invalid typ"

let namespace ns =
  let ( let* ) = Result.bind in
  let segment s =
    match Utils.percent_decode s with
    | Ok s ->
      if
        String.for_all (fun c -> c <> '/') s &&
        String.length s > 0
      then
        Ok s
      else
        Error "invalid segment in namespace"
    | Error _ as e -> e
  in
  let trimmed_ns = Utils.trim_char '/' ns in
  List.fold_left (fun acc s ->
      let* acc = acc in
      let* s' = segment s in
      Ok (s' :: acc))
    (Ok []) (String.split_on_char '/' trimmed_ns) |> Result.map List.rev

let name name =
  let ( let* ) = Result.bind in
  let trimmed_name = Utils.trim_char '/' name in
  let* r = Utils.percent_decode trimmed_name in
  if String.length r > 0 then Ok r else Error "empty name"

let version v = Utils.percent_decode v

let qualifiers q =
  let qs = String.split_on_char '&' q in
  let ( let* ) = Result.bind in
  let qualifier q =
    let* k, v =
      match String.split_on_char '=' q with
      | [] -> Error "invalid qualifier (missing =)"
      | k :: [] -> Ok (k, "")
      | k :: v -> Ok (k, String.concat "=" v)
    in
    if
      String.for_all valid_key k &&
      String.length k > 0 && alpha (String.unsafe_get k 0)
    then
      let* v = Utils.percent_decode v in
      Ok (k, v)
    else
      Error "invalid key for qualifier"
  in
  List.fold_left (fun acc q ->
      let* acc = acc in
      let* (k, v) = qualifier q in
      if v = "" then
        Ok acc
      else
        match List.assoc_opt k acc with
        | None -> Ok ((k, v) :: acc)
        | Some _ -> Error "key exists twice")
    (Ok []) qs |> Result.map List.rev

let subpath s =
  let trimmed = Utils.trim_char '/' s in
  let segment s =
    match Utils.percent_decode s with
    | Ok s ->
      if String.for_all (fun c -> not (c = '/')) s then
        Ok s
      else
        Error "bad segment in subpath"
    | Error _ as e -> e
  in
  let ( let* ) = Result.bind in
  List.fold_left (fun acc s ->
      let* acc = acc in
      let* segment = segment s in
      if segment = "" || segment = "." || segment = ".." then
        Ok acc
      else
        Ok (segment :: acc))
    (Ok []) (String.split_on_char '/' trimmed) |> Result.map List.rev

let of_string s =
  let ( let* ) = Result.bind in
  let remainder, sub_str = Utils.split_from_right '#' s in
  let* subpath = match sub_str with
    | None -> Ok []
    | Some s -> subpath s
  in
  let remainder, qs_str = Utils.split_from_right '?' remainder in
  let* qualifiers = match qs_str with
    | None -> Ok []
    | Some s -> qualifiers s
  in
  let scheme_str, remainder = Utils.split_from_left ':' remainder in
  let* scheme_str = Option.to_result ~none:"missing scheme" scheme_str in
  let scheme' = String.lowercase_ascii scheme_str in
  let trimmed_remainder = Utils.trim_char '/' remainder in
  let typ_str, remainder = Utils.split_from_left '/' trimmed_remainder in
  let* typ_str = Option.to_result ~none:"missing type" typ_str in
  let* typ = typ typ_str in
  let remainder, version_str = Utils.split_from_right '@' remainder in
  let* version = Option.fold ~none:(Ok None) ~some:(function Error _ as e -> e | Ok x -> Ok (Some x)) (Option.map version version_str) in
  let trimmed_remainder = Utils.trim_char '/' remainder in
  let remainder, name_str = Utils.split_from_right '/' trimmed_remainder in
  let name_str' = Option.value ~default:remainder name_str in
  let* name = name name_str' in
  let* namespace =
    if name_str = None then Ok [] else namespace remainder
  in
  if String.equal scheme scheme' then
    Ok { typ ; namespace ; name ; version ; qualifiers ; subpath }
  else
    Error "bad scheme"

let make typ' ?namespace:ns name' ?version:v ?qualifiers:qs ?subpath:sub () =
  let ( let* ) = Result.bind in
  let* typ = typ typ' in
  let* namespace = Option.value ~default:(Ok []) (Option.map namespace ns) in
  let* name = name name' in
  let* version =
    Option.fold
      ~none:(Ok None)
      ~some:(function Error _ as e -> e | Ok x -> Ok (Some x))
      (Option.map version v)
  in
  let* qualifiers = Option.value ~default:(Ok []) (Option.map qualifiers qs) in
  let* subpath = Option.value ~default:(Ok []) (Option.map subpath sub) in
  Ok { typ ; namespace ; name ; version ; qualifiers ; subpath }

let v_exn typ namespace name version qualifiers subpath =
  { typ ; namespace ; name ; version ; qualifiers ; subpath }

let equal a b = String.equal (to_string a) (to_string b)