type credentials = | Credentials of string * string| Username of string
Client authentication credentials.
MQTT supports two authentication methods: username with password, or username only.
The credentials will be sent in plain text, unless TLS is used. See connection options for more information.
Client error & exceptions
Defines the exceptions raised by the client
exception Connection_errortype qos = | Atmost_once| Atleast_once| Exactly_once
Quality of Service level.
Defines the guarantee of delivery for messages.
val connect :
3 | ?id:string ->
4 | ?tls_ca:string ->
5 | ?credentials:credentials ->
6 | ?will:(string * string) ->
7 | ?clean_session:bool ->
8 | ?keep_alive:int ->
9 | ?on_message:(topic:string -> string -> unit Lwt.t) ->
10 | ?on_disconnect:(t -> unit Lwt.t) ->
11 | ?on_error:(t -> exn -> unit Lwt.t) ->
12 | ?port:int ->
13 | string list ->
14 | t Lwt.tConnects to the MQTT broker.
Multiple hosts can be provided in case the broker supports failover. The client will attempt to connect to each one of hosts sequentially until one of them is successful.
on_error can be provided to handle errors during client's execution. By default all internal exceptions will be raised with Lwt.fail.
Note: Reconnection logic is not implemented currently.
let broker_hosts = [ ("host-1", "host-2") ] in
15 |
16 | let on_message ~topic payload =
17 | Lwt.printlf "topic=%S payload=%S" topic payload
18 | in
19 |
20 | Mqtt_client.connect ~id:"my-client" ~port:1883 ~on_message broker_hosts
21 | |> Lwt_main.run
val disconnect : t -> unit Lwt.tDisconnects the client from the MQTT broker.
let%lwt () = Mqtt_client.disconnect client
val publish :
22 | ?dup:bool ->
23 | ?qos:qos ->
24 | ?retain:bool ->
25 | topic:string ->
26 | string ->
27 | t ->
28 | unit Lwt.tPublish a message with payload to a given topic.
let payload = "Hello world";
29 | let%lwt () = Mqtt_client.publish(~topic="news", payload, client);
val subscribe : (string * qos) list -> t -> unit Lwt.tSubscribes the client to a non-empty list of topics.
let topics =
30 | [
31 | ("news/fashion", Mqtt_client.Atmost_once);
32 | ("news/science", Mqtt_client.Atleast_once);
33 | ]
34 | in
35 | Mqtt_client.subscribe topics client
36 |
--------------------------------------------------------------------------------
/lib/mqtt_client/Mqtt_client.ml:
--------------------------------------------------------------------------------
1 | let fmt = Format.asprintf
2 |
3 | type connection = Lwt_io.input_channel * Lwt_io.output_channel
4 |
5 | let decode_length inch =
6 | let rec loop value mult =
7 | let%lwt ch = Lwt_io.read_char inch in
8 | let ch = Char.code ch in
9 | let digit = ch land 127 in
10 | let value = value + (digit * mult) in
11 | let mult = mult * 128 in
12 | if ch land 128 = 0 then Lwt.return value else loop value mult
13 | in
14 | loop 0 1
15 |
16 | let read_packet inch =
17 | let%lwt header_byte = Lwt_io.read_char inch in
18 | let msgid, opts =
19 | Mqtt_packet.Decoder.decode_fixed_header (Char.code header_byte)
20 | in
21 | let%lwt count = decode_length inch in
22 |
23 | let data = Bytes.create count in
24 | let%lwt () =
25 | try Lwt_io.read_into_exactly inch data 0 count
26 | with End_of_file -> Lwt.fail (Failure "could not read bytes")
27 | in
28 | let pkt =
29 | Read_buffer.make (data |> Bytes.to_string)
30 | |> Mqtt_packet.Decoder.decode_packet opts msgid
31 | in
32 | Lwt.return (opts, pkt)
33 |
34 | module Log = (val Logs_lwt.src_log (Logs.Src.create "mqtt.client"))
35 |
36 | type t = {
37 | cxn : connection;
38 | id : string;
39 | inflight : (int, unit Lwt_condition.t * Mqtt_packet.t) Hashtbl.t;
40 | mutable reader : unit Lwt.t;
41 | on_message : topic:string -> string -> unit Lwt.t;
42 | on_disconnect : t -> unit Lwt.t;
43 | on_error : t -> exn -> unit Lwt.t;
44 | should_stop_reader : unit Lwt_condition.t;
45 | }
46 |
47 | let wrap_catch client f = Lwt.catch f (client.on_error client)
48 |
49 | let default_on_error client exn =
50 | let%lwt () =
51 | Log.err (fun log ->
52 | log "[%s]: Unhandled exception: %a" client.id Fmt.exn exn)
53 | in
54 | Lwt.fail exn
55 |
56 | let default_on_message ~topic:_ _ = Lwt.return_unit
57 | let default_on_disconnect _ = Lwt.return_unit
58 |
59 | let read_packets client =
60 | let in_chan, out_chan = client.cxn in
61 |
62 | let ack_inflight id pkt =
63 | try
64 | let cond, expected_ack_pkt = Hashtbl.find client.inflight id in
65 | if pkt = expected_ack_pkt then (
66 | Hashtbl.remove client.inflight id;
67 | Lwt_condition.signal cond ();
68 | Lwt.return_unit)
69 | else Lwt.fail (Failure "unexpected packet in ack")
70 | with Not_found -> Lwt.fail (Failure (fmt "ack for id=%d not found" id))
71 | in
72 |
73 | let rec loop () =
74 | let%lwt (_dup, qos, _retain), packet = read_packet in_chan in
75 | let%lwt () =
76 | match packet with
77 | (* Publish with QoS 0: push *)
78 | | Publish (None, topic, payload) when qos = Atmost_once ->
79 | client.on_message ~topic payload
80 | (* Publish with QoS 0 and packet identifier: error *)
81 | | Publish (Some _id, _topic, _payload) when qos = Atmost_once ->
82 | Lwt.fail
83 | (Failure
84 | "protocol violation: publish packet with qos 0 must not have id")
85 | (* Publish with QoS 1 *)
86 | | Publish (Some id, topic, payload) when qos = Atleast_once ->
87 | (* - Push the message to the consumer queue.
88 | - Send back the PUBACK packet. *)
89 | let%lwt () = client.on_message ~topic payload in
90 | let puback = Mqtt_packet.Encoder.puback id in
91 | Lwt_io.write out_chan puback
92 | | Publish (None, _topic, _payload) when qos = Atleast_once ->
93 | Lwt.fail
94 | (Failure
95 | "protocol violation: publish packet with qos > 0 must have id")
96 | | Publish _ ->
97 | Lwt.fail (Failure "not supported publish packet (probably qos 2)")
98 | | Suback (id, _)
99 | | Unsuback id
100 | | Puback id
101 | | Pubrec id
102 | | Pubrel id
103 | | Pubcomp id ->
104 | ack_inflight id packet
105 | | Pingresp -> Lwt.return_unit
106 | | _ -> Lwt.fail (Failure "unknown packet from server")
107 | in
108 | loop ()
109 | in
110 |
111 | let%lwt () =
112 | Log.debug (fun log -> log "[%s] Starting reader loop..." client.id)
113 | in
114 | Lwt.pick
115 | [
116 | (let%lwt () = Lwt_condition.wait client.should_stop_reader in
117 | Log.info (fun log -> log "[%s] Stopping reader loop..." client.id));
118 | loop ();
119 | ]
120 |
121 | let disconnect client =
122 | let%lwt () =
123 | Log.info (fun log -> log "[%s] Disconnecting client..." client.id)
124 | in
125 | let _, oc = client.cxn in
126 | Lwt_condition.signal client.should_stop_reader ();
127 | let%lwt () = Lwt_io.write oc (Mqtt_packet.Encoder.disconnect ()) in
128 | let%lwt () = client.on_disconnect client in
129 | Log.info (fun log -> log "[%s] Client disconnected." client.id)
130 |
131 | let shutdown client =
132 | let%lwt () =
133 | Log.debug (fun log -> log "[%s] Shutting down the connection..." client.id)
134 | in
135 | let ic, oc = client.cxn in
136 | let%lwt () = Lwt_io.flush oc in
137 | let%lwt () = Lwt_io.close ic in
138 | let%lwt () = Lwt_io.close oc in
139 | Log.debug (fun log -> log "[%s] Client connection shut down." client.id)
140 |
141 | let open_tls_connection ~client_id ~ca_file host port =
142 | try%lwt
143 | let%lwt authenticator = X509_lwt.authenticator (`Ca_file ca_file) in
144 | Tls_lwt.connect authenticator (host, port)
145 | with exn ->
146 | let%lwt () =
147 | Log.err (fun log ->
148 | log "[%s] could not get address info for %S" client_id host)
149 | in
150 | Lwt.fail exn
151 |
152 | let run_pinger ~keep_alive client =
153 | let%lwt () = Log.debug (fun log -> log "Starting ping timer...") in
154 | let _, output = client.cxn in
155 | (* 25% leeway *)
156 | let keep_alive = 0.75 *. float_of_int keep_alive in
157 | let rec loop () =
158 | let%lwt () = Lwt_unix.sleep keep_alive in
159 | let pingreq_packet = Mqtt_packet.Encoder.pingreq () in
160 | let%lwt () = Lwt_io.write output pingreq_packet in
161 | loop ()
162 | in
163 | loop ()
164 |
165 | exception Connection_error
166 |
167 | let open_tcp_connection ~client_id host port =
168 | let%lwt addresses = Lwt_unix.getaddrinfo host (string_of_int port) [] in
169 | match addresses with
170 | | address :: _ ->
171 | let sockaddr = Lwt_unix.(address.ai_addr) in
172 | Lwt_io.open_connection sockaddr
173 | | _ ->
174 | let%lwt () =
175 | Log.err (fun log ->
176 | log "[%s] could not get address info for %S" client_id host)
177 | in
178 | Lwt.fail Connection_error
179 |
180 | let rec create_connection ?tls_ca ~port ~client_id hosts =
181 | match hosts with
182 | | [] ->
183 | let%lwt () =
184 | Log.err (fun log ->
185 | log "[%s] Could not connect to any of the hosts (on port %d): %a"
186 | client_id port
187 | Fmt.Dump.(list string)
188 | hosts)
189 | in
190 | Lwt.fail Connection_error
191 | | host :: hosts -> (
192 | try%lwt
193 | let%lwt () =
194 | Log.debug (fun log ->
195 | log "[%s] Connecting to `%s:%d`..." client_id host port)
196 | in
197 | let%lwt connection =
198 | match tls_ca with
199 | | Some ca_file -> open_tls_connection ~client_id ~ca_file host port
200 | | None -> open_tcp_connection ~client_id host port
201 | in
202 | let%lwt () =
203 | Log.info (fun log ->
204 | log "[%s] Connection opened on `%s:%d`." client_id host port)
205 | in
206 | Lwt.return connection
207 | with _ ->
208 | let%lwt () =
209 | Log.debug (fun log ->
210 | log "[%s] Could not connect, trying next host..." client_id)
211 | in
212 | create_connection ?tls_ca ~port ~client_id hosts)
213 |
214 | let connect ?(id = "ocaml-mqtt") ?tls_ca ?credentials ?will
215 | ?(clean_session = true) ?(keep_alive = 30)
216 | ?(on_message = default_on_message) ?(on_disconnect = default_on_disconnect)
217 | ?(on_error = default_on_error) ?(port = 1883) hosts =
218 | let flags =
219 | if clean_session || id = "" then [ Mqtt_packet.Clean_session ] else []
220 | in
221 | let cxn_data =
222 | { Mqtt_packet.clientid = id; credentials; will; flags; keep_alive }
223 | in
224 |
225 | let%lwt ((ic, oc) as connection) =
226 | create_connection ?tls_ca ~port ~client_id:id hosts
227 | in
228 |
229 | let connect_packet =
230 | Mqtt_packet.Encoder.connect ?credentials:cxn_data.credentials
231 | ?will:cxn_data.will ~flags:cxn_data.flags ~keep_alive:cxn_data.keep_alive
232 | cxn_data.clientid
233 | in
234 | let%lwt () = Lwt_io.write oc connect_packet in
235 | let inflight = Hashtbl.create 16 in
236 |
237 | match%lwt read_packet ic with
238 | | _, Connack { connection_status = Accepted; session_present } ->
239 | let%lwt () =
240 | Log.debug (fun log ->
241 | log "[%s] Connection acknowledged (session_present=%b)" id
242 | session_present)
243 | in
244 |
245 | let client =
246 | {
247 | cxn = connection;
248 | id;
249 | inflight;
250 | reader = Lwt.return_unit;
251 | should_stop_reader = Lwt_condition.create ();
252 | on_message;
253 | on_disconnect;
254 | on_error;
255 | }
256 | in
257 |
258 | Lwt.async (fun () ->
259 | client.reader <- wrap_catch client (fun () -> read_packets client);
260 | let%lwt () =
261 | Log.debug (fun log -> log "[%s] Packet reader started." client.id)
262 | in
263 | let%lwt () =
264 | Lwt.pick [ client.reader; run_pinger ~keep_alive client ]
265 | in
266 | let%lwt () =
267 | Log.debug (fun log ->
268 | log "[%s] Packet reader stopped, shutting down..." client.id)
269 | in
270 | shutdown client);
271 |
272 | Lwt.return client
273 | | _, Connack pkt ->
274 | let conn_status =
275 | Mqtt_packet.connection_status_to_string pkt.connection_status
276 | in
277 | let%lwt () =
278 | Log.err (fun log -> log "[%s] Connection failed: %s" id conn_status)
279 | in
280 | Lwt.fail Connection_error
281 | | _ ->
282 | let%lwt () =
283 | Log.err (fun log ->
284 | log "[%s] Invalid response from broker on connection" id)
285 | in
286 | Lwt.fail Connection_error
287 |
288 | let publish ?(dup = false) ?(qos = Mqtt_core.Atleast_once) ?(retain = false)
289 | ~topic payload client =
290 | let _, oc = client.cxn in
291 | match qos with
292 | | Atmost_once ->
293 | let pkt_data =
294 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id:0 ~topic payload
295 | in
296 | Lwt_io.write oc pkt_data
297 | | Atleast_once ->
298 | let id = Mqtt_packet.gen_id () in
299 | let cond = Lwt_condition.create () in
300 | let expected_ack_pkt = Mqtt_packet.puback id in
301 | Hashtbl.add client.inflight id (cond, expected_ack_pkt);
302 | let pkt_data =
303 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id ~topic payload
304 | in
305 | let%lwt () = Lwt_io.write oc pkt_data in
306 | Lwt_condition.wait cond
307 | | Exactly_once ->
308 | let id = Mqtt_packet.gen_id () in
309 | let cond = Lwt_condition.create () in
310 | let expected_ack_pkt = Mqtt_packet.pubrec id in
311 | Hashtbl.add client.inflight id (cond, expected_ack_pkt);
312 | let pkt_data =
313 | Mqtt_packet.Encoder.publish ~dup ~qos ~retain ~id ~topic payload
314 | in
315 | let%lwt () = Lwt_io.write oc pkt_data in
316 | let%lwt () = Lwt_condition.wait cond in
317 | let expected_ack_pkt = Mqtt_packet.pubcomp id in
318 | Hashtbl.add client.inflight id (cond, expected_ack_pkt);
319 | let pkt_data = Mqtt_packet.Encoder.pubrel id in
320 | let%lwt () = Lwt_io.write oc pkt_data in
321 | Lwt_condition.wait cond
322 |
323 | let subscribe topics client =
324 | if topics = [] then raise (Invalid_argument "empty topics");
325 | let _, oc = client.cxn in
326 | let pkt_id = Mqtt_packet.gen_id () in
327 | let subscribe_packet = Mqtt_packet.Encoder.subscribe ~id:pkt_id topics in
328 | let qos_list = List.map (fun (_, q) -> Ok q) topics in
329 | let cond = Lwt_condition.create () in
330 | Hashtbl.add client.inflight pkt_id (cond, Suback (pkt_id, qos_list));
331 | wrap_catch client (fun () ->
332 | let%lwt () = Lwt_io.write oc subscribe_packet in
333 | let%lwt () = Lwt_condition.wait cond in
334 | let topics = List.map fst topics in
335 | Log.info (fun log ->
336 | log "[%s] Subscribed to %a." client.id Fmt.Dump.(list string) topics))
337 |
338 | include Mqtt_core
339 |
--------------------------------------------------------------------------------
/tests/subscriptions.ml:
--------------------------------------------------------------------------------
1 | open OUnit
2 |
3 | module Subscriptions : sig
4 | type 'a t
5 |
6 | val empty : 'a t
7 | val add_node : string -> 'a -> 'a t -> 'a t
8 | val remove_node : string -> 'a -> 'a t -> 'a t
9 | val query : string -> 'a t -> 'a list
10 | val length : 'a t -> int
11 | val tests : OUnit.test list
12 | end = struct
13 | type 'a t =
14 | | E (* empty *)
15 | | NV of string * (string, 'a t) Hashtbl.t (* no value *)
16 | | V of string * (string, 'a t) Hashtbl.t * 'a list (* value *)
17 | | Pound of 'a list
18 | (* wildcard # *)
19 |
20 | let split str =
21 | let strs = ref [] in
22 | let prev = ref 0 in
23 | let split_segment i c =
24 | if c = '/' then (
25 | let newstr = String.sub str !prev (i - !prev) in
26 | prev := i + 1;
27 | (* skip slash *)
28 | strs := newstr :: !strs)
29 | in
30 | String.iteri split_segment str;
31 | (* fixup the last element *)
32 | strs := String.sub str !prev (String.length str - !prev) :: !strs;
33 | List.rev !strs
34 |
35 | let empty = E
36 | let _tbl_keys tbl = Hashtbl.fold (fun k _ acc -> k :: acc) tbl []
37 | let tbl_vals tbl = Hashtbl.fold (fun _ v acc -> v :: acc) tbl []
38 |
39 | let rec length = function
40 | | E -> 0
41 | | Pound _ -> 1
42 | | NV (_, t) | V (_, t, _) ->
43 | let children = tbl_vals t in
44 | 1 + List.fold_left (fun acc x -> acc + length x) 0 children
45 |
46 | let _get_vals = function E | NV _ -> [] | V (_, _, v) | Pound v -> v
47 |
48 | let find_branches tbl k : 'a t list =
49 | let f x = try [ Hashtbl.find tbl x ] with Not_found -> [] in
50 | List.map f [ k; "+"; "#" ] |> List.concat
51 |
52 | let pound_lookahead t v =
53 | try
54 | match Hashtbl.find t "#" with
55 | | Pound z -> v @ z
56 | | _ -> failwith "should never happen"
57 | with Not_found -> v
58 |
59 | let query key tree =
60 | let k_parts = split key in
61 | let rec inner tree v p : 'a list =
62 | match p with
63 | | [] -> (
64 | match tree with
65 | | Pound z -> v @ z
66 | | V (_, t, z) -> pound_lookahead t (v @ z)
67 | | NV (_, t) -> pound_lookahead t v
68 | | E -> v)
69 | | h :: m -> (
70 | match tree with
71 | | E -> v
72 | | Pound z -> v @ z
73 | | NV (_, t) | V (_, t, _) ->
74 | let branches = find_branches t h in
75 | let r = List.map (fun x -> inner x v m) branches in
76 | v @ List.concat r)
77 | in
78 | inner tree [] k_parts
79 |
80 | let add_node keys v t =
81 | let new_node k v i n =
82 | let h = Hashtbl.create 10 in
83 | let j = k.(i) in
84 | if j = "#" then Pound [] else if n <> 0 then NV (j, h) else V (j, h, [ v ])
85 | in
86 |
87 | let rec add k v i n = function
88 | | E -> new_node [| "*root*" |] v i n |> add k v i n
89 | | NV (key, h) as e ->
90 | if n = 0 then V (key, h, [ v ])
91 | else
92 | let child =
93 | try Hashtbl.find h k.(i) with Not_found -> new_node k v i n
94 | in
95 | let child = add k v (i + 1) (n - 1) child in
96 | Hashtbl.replace h k.(i) child;
97 | e
98 | | V (key, h, v1) as e ->
99 | if n = 0 then V (key, h, v :: v1)
100 | else
101 | let child =
102 | try Hashtbl.find h k.(i) with Not_found -> new_node k v i n
103 | in
104 | let child = add k v (i + 1) (n - 1) child in
105 | Hashtbl.replace h k.(i) child;
106 | e
107 | | Pound v1 -> Pound (v :: v1)
108 | in
109 |
110 | let k = split keys |> Array.of_list in
111 | add k v 0 (Array.length k) t
112 |
113 | let get_branches = function
114 | | E | Pound _ -> []
115 | | V (_, t, _) | NV (_, t) -> tbl_vals t
116 |
117 | let rec get_value = function
118 | | E -> []
119 | | NV (_, t) -> List.concat (List.map get_value (tbl_vals t))
120 | | Pound v -> v
121 | | V (_, t, v) -> v @ List.concat (List.map get_value (tbl_vals t))
122 |
123 | let find_branch k = function
124 | | E | Pound _ -> None
125 | | V (_, t, _) | NV (_, t) -> (
126 | try Some (Hashtbl.find t k) with Not_found -> None)
127 |
128 | let remove_branch k = function
129 | | (E as e) | (Pound _ as e) -> e
130 | | (NV (_, t) as e) | (V (_, t, _) as e) ->
131 | Hashtbl.remove t k;
132 | if 0 = Hashtbl.length t then E else e
133 |
134 | let replace_branch k g = function
135 | | (E as e) | (Pound _ as e) -> e
136 | | (NV (_, t) as e) | (V (_, t, _) as e) ->
137 | Hashtbl.replace t k g;
138 | e
139 |
140 | let remove_node key value tree =
141 | let parts = split key in
142 | let remove values v = List.filter (fun x -> x <> v) values in
143 | let rec inner tree = function
144 | | [ h ] -> (
145 | match find_branch h tree with
146 | | None -> tree
147 | | Some b -> (
148 | match b with
149 | | E | NV _ -> tree
150 | | Pound v ->
151 | if "#" = h then
152 | let v = remove v value in
153 | if 0 = List.length v then remove_branch h tree
154 | else replace_branch h (Pound v) tree
155 | else failwith "not pound; should never happen"
156 | | V (k, t, v) ->
157 | let v = remove v value in
158 | if 0 = List.length v then
159 | if 0 = Hashtbl.length t then remove_branch k tree
160 | else replace_branch k (NV (k, t)) tree
161 | else replace_branch k (V (k, t, v)) tree))
162 | | h :: t -> (
163 | match find_branch h tree with
164 | | Some b ->
165 | let e = inner b t in
166 | if E = e then remove_branch h tree else replace_branch h e tree
167 | | None -> tree)
168 | | [] -> tree
169 | in
170 | inner tree parts
171 |
172 | let rec _tree_of_string tree level =
173 | let vals = match tree with E | NV _ -> [] | V (_, _, v) | Pound v -> v in
174 | let key =
175 | match tree with
176 | | E -> "*empty*"
177 | | Pound _ -> "#"
178 | | NV (k, _) -> "nv: " ^ k
179 | | V (k, _, _) -> "v: " ^ k
180 | in
181 | let branches =
182 | match tree with
183 | | E | Pound _ -> []
184 | | NV (_, t) | V (_, t, _) -> tbl_vals t
185 | in
186 | let vals = String.concat "," vals in
187 | let vals = if "" <> vals then ": " ^ vals else "" in
188 | let _ = Printf.printf "%*d %s %s\n" level level key vals in
189 | let func x = _tree_of_string x (level + 4) in
190 | List.iter func branches
191 |
192 | let split_test _ =
193 | let printer = String.concat "," in
194 | let ae = assert_equal ~printer in
195 | let res = split "a/b/c/d" in
196 | ae [ "a"; "b"; "c"; "d" ] res;
197 | let res = split "/abc//def/ghi/" in
198 | ae [ ""; "abc"; ""; "def"; "ghi"; "" ] res
199 |
200 | let plus_test _ =
201 | let tree =
202 | add_node "helo/happy/world!" "helosadworld" empty
203 | |> add_node "helo/pretty/wurld" "helothere"
204 | |> add_node "omg/wtf" "omgwtf"
205 | |> add_node "omg/wtf/bbq" "omgwtfbbq"
206 | |> add_node "omg/srsly" "omgsrsly"
207 | |> add_node "omg/+/bbqz" "omgwildbbq"
208 | |> add_node "omg/+" "omgwildcard"
209 | |> add_node "a/+/c/+/e/+" "abcdef"
210 | |> add_node "asdfies" "QWERTIES"
211 | in
212 | let r =
213 | [
214 | "helo/pretty/wurld";
215 | "helo/heh/qwert/blah";
216 | "omg/lol/bbqz";
217 | "helo/pretty";
218 | "omg/wtf";
219 | "omg/wtf!";
220 | "a/lol/c/def/e/orly";
221 | "alol/c/def/e";
222 | ]
223 | in
224 | let expected =
225 | [
226 | [ "helothere" ];
227 | [];
228 | [ "omgwildbbq" ];
229 | [];
230 | [ "omgwildcard"; "omgwtf" ];
231 | [ "omgwildcard" ];
232 | [ "abcdef" ];
233 | [];
234 | ]
235 | in
236 | let printer = String.concat "," in
237 | let cmp a b =
238 | let s = List.sort (fun x y -> String.compare x y) in
239 | s a = s b
240 | in
241 | let ae = assert_equal ~printer ~cmp in
242 | let res = List.map (fun x -> query x tree) r in
243 | List.iter2 (fun x y -> ae x y) expected res
244 |
245 | let pound_test _ =
246 | let root =
247 | add_node "a/#" "a" empty
248 | |> add_node "a/b/#" "ab"
249 | |> add_node "a/b" "plainab"
250 | |> add_node "a/b/c/#" "abc"
251 | |> add_node "a/b/c/d/#" "abcd"
252 | |> add_node "a/b/c/d/e/#" "abcde"
253 | in
254 | let r = [ "a"; "a/b"; "a/b/c"; "a/c"; "d/e" ] in
255 | let expected =
256 | [ [ "a" ]; [ "plainab"; "ab"; "a" ]; [ "abc"; "ab"; "a" ]; [ "a" ]; [] ]
257 | in
258 | let printer = String.concat "," in
259 | let res = List.map (fun x -> query x root) r in
260 | List.iter2 (fun x y -> assert_equal ~printer x y) expected res
261 |
262 | let remove_test _ =
263 | let root =
264 | add_node "a" "a" empty
265 | |> add_node "a/+" "a+"
266 | |> add_node "a/#" "a#"
267 | |> add_node "a/#" "tst"
268 | |> add_node "a" "tst" (* to double check wilds *)
269 | |> add_node "a/b" "ab"
270 | |> add_node "a/b/#" "ab#"
271 | |> add_node "a/b" "ab2"
272 | |> add_node "a/+/c" "a+c"
273 | |> add_node "a/b/c" "abc"
274 | |> add_node "a/+/c/#" "a+c#"
275 | |> add_node "a/b/c/d" "abcd"
276 | |> add_node "a/b/c/d" "abcd2"
277 | in
278 | let printer = String.concat ", " in
279 | let cmp a b =
280 | let s = List.sort (fun x y -> String.compare x y) in
281 | s a = s b
282 | in
283 | let ae = assert_equal ~printer ~cmp in
284 |
285 | (* really should modify to return the fully qualified key
286 | of the empty node
287 | *)
288 | let rec has_empty_leaves tree =
289 | let check acc x = if acc then acc else has_empty_leaves x in
290 | if 0 = List.length (get_branches tree) && 0 = List.length (get_value tree)
291 | then true
292 | else List.fold_left check false (get_branches tree)
293 | in
294 |
295 | (* sanity check the first element *)
296 | let res = query "a" root in
297 | ae [ "tst"; "a"; "tst"; "a#" ] res;
298 | let root = remove_node "a/#" "tst" root in
299 | let res = query "a" root in
300 | ae [ "tst"; "a"; "a#" ] res;
301 | let root = remove_node "a" "tst" root in
302 | let res = query "a" root in
303 | ae [ "a"; "a#" ] res;
304 |
305 | (* check that there are no empty leaves *)
306 | let res = query "a/b" root in
307 | ae [ "ab"; "ab#"; "ab2"; "a#"; "a+" ] res;
308 | (* should produce an empty leaf; the "#" dangles *)
309 | let root = remove_node "a/b/#" "ab#" root in
310 | let res = query "a/b" root in
311 | ae [ "ab"; "ab2"; "a#"; "a+" ] res;
312 | assert_equal ~msg:"empty leaves a/b/#" false (has_empty_leaves root);
313 |
314 | (* misc things, such as removing all values from a parent *)
315 | let res = query "a/b/c" root in
316 | ae [ "a+c"; "abc"; "a+c#"; "a#" ] res;
317 | let root = remove_node "a/b/c" "abc" root in
318 | let res = query "a/b/c" root in
319 | ae [ "a+c"; "a+c#"; "a#" ] res;
320 | let root = remove_node "a/+/c" "a+c" root in
321 | let res = query "a/b/c" root in
322 | ae [ "a+c#"; "a#" ] res;
323 | let root = remove_node "a/+/c/#" "a+c#" root in
324 | let res = query "a/b/c" root in
325 | ae [ "a#" ] res;
326 | assert_equal ~msg:"empty leaves; none" false (has_empty_leaves root);
327 |
328 | (* remove a sibling value from same key *)
329 | let res = query "a/b/c/d" root in
330 | ae [ "a#"; "abcd"; "abcd2" ] res;
331 | let root = remove_node "a/b/c/d" "abcd" root in
332 | let res = query "a/b/c/d" root in
333 | ae [ "a#"; "abcd2" ] res;
334 |
335 | (* remove nonexistent value from valid key *)
336 | let root = remove_node "a/b/c/d" "nothere" root in
337 | let res = query "a/b/c/d" root in
338 | ae [ "a#"; "abcd2" ] res;
339 |
340 | (* remove nonexistent value from invalid key *)
341 | let root = remove_node "a/c/d/" "rlynothere" root in
342 | let res = query "a/c/d" root in
343 | ae [ "a#" ] res;
344 | assert_equal ~msg:"empty leaves; final" false (has_empty_leaves root);
345 | ()
346 |
347 | let tests =
348 | [
349 | "split_test" >:: split_test;
350 | "plus test" >:: plus_test;
351 | "pound_test" >:: pound_test;
352 | "remove_test" >:: remove_test;
353 | ]
354 | end
355 |
--------------------------------------------------------------------------------
/lib/mqtt_client/Mqtt_packet.ml:
--------------------------------------------------------------------------------
1 | module BE = EndianBytes.BigEndian
2 | open Mqtt_core
3 |
4 | let _msgid = ref 0
5 |
6 | let gen_id () =
7 | let () = incr _msgid in
8 | if !_msgid >= 0xFFFF then _msgid := 1;
9 | !_msgid
10 |
11 | let int16be n =
12 | let s = Bytes.create 2 in
13 | BE.set_int16 s 0 n;
14 | s
15 |
16 | let int8be n =
17 | let s = Bytes.create 1 in
18 | BE.set_int8 s 0 n;
19 | s
20 |
21 | type messages =
22 | | Connect_pkt
23 | | Connack_pkt
24 | | Publish_pkt
25 | | Puback_pkt
26 | | Pubrec_pkt
27 | | Pubrel_pkt
28 | | Pubcomp_pkt
29 | | Subscribe_pkt
30 | | Suback_pkt
31 | | Unsubscribe_pkt
32 | | Unsuback_pkt
33 | | Pingreq_pkt
34 | | Pingresp_pkt
35 | | Disconnect_pkt
36 |
37 | type cxn_flags = Will_retain | Will_qos of qos | Clean_session
38 |
39 | type cxn_data = {
40 | clientid : string;
41 | credentials : credentials option;
42 | will : (string * string) option;
43 | flags : cxn_flags list;
44 | keep_alive : int;
45 | }
46 |
47 | type client_options = { ping_timeout : float; cxn_data : cxn_data }
48 |
49 | type connection_status =
50 | | Accepted
51 | | Unacceptable_protocol_version
52 | | Identifier_rejected
53 | | Server_unavailable
54 | | Bad_username_or_password
55 | | Not_authorized
56 |
57 | let connection_status_to_string = function
58 | | Accepted -> "Accepted"
59 | | Unacceptable_protocol_version -> "Unacceptable_protocol_version"
60 | | Identifier_rejected -> "Identifier_rejected"
61 | | Server_unavailable -> "Server_unavailable"
62 | | Bad_username_or_password -> "Bad_username_or_password"
63 | | Not_authorized -> "Not_authorized"
64 |
65 | let connection_status_to_int = function
66 | | Accepted -> 0
67 | | Unacceptable_protocol_version -> 1
68 | | Identifier_rejected -> 2
69 | | Server_unavailable -> 3
70 | | Bad_username_or_password -> 4
71 | | Not_authorized -> 5
72 |
73 | let connection_status_of_int = function
74 | | 0 -> Accepted
75 | | 1 -> Unacceptable_protocol_version
76 | | 2 -> Identifier_rejected
77 | | 3 -> Server_unavailable
78 | | 4 -> Bad_username_or_password
79 | | 5 -> Not_authorized
80 | | _ -> raise (Invalid_argument "Invalid connection status code")
81 |
82 | type t =
83 | | Connect of cxn_data
84 | | Connack of { session_present : bool; connection_status : connection_status }
85 | | Subscribe of (int * (string * qos) list)
86 | | Suback of (int * (qos, unit) result list)
87 | | Unsubscribe of (int * string list)
88 | | Unsuback of int
89 | | Publish of (int option * string * string)
90 | | Puback of int
91 | | Pubrec of int
92 | | Pubrel of int
93 | | Pubcomp of int
94 | | Pingreq
95 | | Pingresp
96 | | Disconnect
97 |
98 | type options = bool * qos * bool
99 |
100 | let bits_of_message = function
101 | | Connect_pkt -> 1
102 | | Connack_pkt -> 2
103 | | Publish_pkt -> 3
104 | | Puback_pkt -> 4
105 | | Pubrec_pkt -> 5
106 | | Pubrel_pkt -> 6
107 | | Pubcomp_pkt -> 7
108 | | Subscribe_pkt -> 8
109 | | Suback_pkt -> 9
110 | | Unsubscribe_pkt -> 10
111 | | Unsuback_pkt -> 11
112 | | Pingreq_pkt -> 12
113 | | Pingresp_pkt -> 13
114 | | Disconnect_pkt -> 14
115 |
116 | let message_of_bits = function
117 | | 1 -> Connect_pkt
118 | | 2 -> Connack_pkt
119 | | 3 -> Publish_pkt
120 | | 4 -> Puback_pkt
121 | | 5 -> Pubrec_pkt
122 | | 6 -> Pubrel_pkt
123 | | 7 -> Pubcomp_pkt
124 | | 8 -> Subscribe_pkt
125 | | 9 -> Suback_pkt
126 | | 10 -> Unsubscribe_pkt
127 | | 11 -> Unsuback_pkt
128 | | 12 -> Pingreq_pkt
129 | | 13 -> Pingresp_pkt
130 | | 14 -> Disconnect_pkt
131 | | _ -> raise (Invalid_argument "invalid bits in message")
132 |
133 | let bits_of_qos = function
134 | | Atmost_once -> 0
135 | | Atleast_once -> 1
136 | | Exactly_once -> 2
137 |
138 | let qos_of_bits = function
139 | | 0 -> Atmost_once
140 | | 1 -> Atleast_once
141 | | 2 -> Exactly_once
142 | | b -> raise (Invalid_argument ("invalid qos number: " ^ string_of_int b))
143 |
144 | let suback_qos_of_bits = function 0x80 -> Error () | b -> Ok (qos_of_bits b)
145 | let bit_of_bool = function true -> 1 | false -> 0
146 |
147 | let bool_of_bit = function
148 | | 1 -> true
149 | | 0 -> false
150 | | n ->
151 | raise
152 | (Invalid_argument ("expected zero or one, but got " ^ string_of_int n))
153 |
154 | let trunc str =
155 | (* truncate leading zeroes *)
156 | let len = String.length str in
157 | let rec loop count =
158 | if count >= len || str.[count] <> '\000' then count else loop (count + 1)
159 | in
160 | let leading = loop 0 in
161 | if leading = len then "\000" else String.sub str leading (len - leading)
162 |
163 | let addlen s =
164 | let len = String.length s in
165 | if len > 0xFFFF then raise (Invalid_argument "string too long");
166 | Bytes.to_string (int16be len) ^ s
167 |
168 | let opt_with s n = function Some a -> s a | None -> n
169 | let puback id = Puback id
170 | let pubrec id = Pubrec id
171 | let pubcomp id = Pubcomp id
172 |
173 | module Encoder = struct
174 | let encode_length len =
175 | let rec loop ll digits =
176 | if ll <= 0 then digits
177 | else
178 | let incr = Int32.logor (Int32.of_int 0x80) in
179 | let shft = Int32.logor (Int32.shift_left digits 8) in
180 | let getdig x dig = if x > 0 then incr dig else dig in
181 | let quotient = ll / 128 in
182 | let digit = getdig quotient (Int32.of_int (ll mod 128)) in
183 | let digits = shft digit in
184 | loop quotient digits
185 | in
186 | loop len 0l
187 |
188 | let fixed_header typ ?(flags = 0) body_len =
189 | let msgid = bits_of_message typ lsl 4 in
190 | let hdr = Bytes.create 1 in
191 | let len = Bytes.create 4 in
192 | BE.set_int8 hdr 0 (msgid + flags);
193 | BE.set_int32 len 0 (encode_length body_len);
194 | let len = trunc (Bytes.to_string len) in
195 | Bytes.to_string hdr ^ len
196 |
197 | let unsubscribe ~id topics =
198 | let accum acc i = acc + 2 + String.length i in
199 | let tl = List.fold_left accum 2 topics in
200 | (* +2 for msgid *)
201 | let buf = Buffer.create (tl + 5) in
202 | (* ~5 for fixed header *)
203 | let addtopic t = addlen t |> Buffer.add_string buf in
204 | let msgid = int16be id |> Bytes.to_string in
205 | let hdr = fixed_header Unsubscribe_pkt ~flags:2 tl in
206 | Buffer.add_string buf hdr;
207 | Buffer.add_string buf msgid;
208 | List.iter addtopic topics;
209 | Buffer.contents buf
210 |
211 | let unsuback id =
212 | let msgid = int16be id |> Bytes.to_string in
213 | let hdr = fixed_header Unsuback_pkt 2 in
214 | hdr ^ msgid
215 |
216 | let simple_pkt typ = fixed_header typ 0
217 | let pingreq () = simple_pkt Pingreq_pkt
218 | let pingresp () = simple_pkt Pingresp_pkt
219 |
220 | let pubpkt ?flags typ id =
221 | let hdr = fixed_header ?flags typ 2 in
222 | let msgid = int16be id |> Bytes.to_string in
223 | let buf = Buffer.create 4 in
224 | Buffer.add_string buf hdr;
225 | Buffer.add_string buf msgid;
226 | Buffer.contents buf
227 |
228 | let pubrec = pubpkt Pubrec_pkt
229 | let pubrel = pubpkt ~flags:2 Pubrel_pkt
230 | let pubcomp = pubpkt Pubcomp_pkt
231 |
232 | let suback id qoses =
233 | let paylen = List.length qoses + 2 in
234 | let buf = Buffer.create (paylen + 5) in
235 | let msgid = int16be id |> Bytes.to_string in
236 | let q2i q = bits_of_qos q |> int8be |> Bytes.to_string in
237 | let blit q = Buffer.add_string buf (q2i q) in
238 | let hdr = fixed_header Suback_pkt paylen in
239 | Buffer.add_string buf hdr;
240 | Buffer.add_string buf msgid;
241 | List.iter blit qoses;
242 | Buffer.contents buf
243 |
244 | let puback = pubpkt Puback_pkt
245 | let disconnect () = simple_pkt Disconnect_pkt
246 |
247 | let subscribe ~id topics =
248 | let accum acc (i, _) = acc + 3 + String.length i in
249 | let tl = List.fold_left accum 0 topics in
250 | let tl = tl + 2 in
251 | (* add msgid to total len *)
252 | let buf = Buffer.create (tl + 5) in
253 | (* ~5 for fixed header *)
254 | let addtopic (t, q) =
255 | Buffer.add_string buf (addlen t);
256 | Buffer.add_string buf (Bytes.to_string @@ int8be (bits_of_qos q))
257 | in
258 | let msgid = int16be id |> Bytes.to_string in
259 | let hdr = fixed_header Subscribe_pkt ~flags:2 tl in
260 | Buffer.add_string buf hdr;
261 | Buffer.add_string buf msgid;
262 | List.iter addtopic topics;
263 | Buffer.contents buf
264 |
265 | let publish ~dup ~qos ~retain ~id ~topic payload =
266 | let id_data =
267 | if qos = Atleast_once || qos = Exactly_once then
268 | Bytes.to_string (int16be id)
269 | else ""
270 | in
271 | let dup = if qos = Atmost_once then false else dup in
272 | let topic = addlen topic in
273 | let sl = String.length in
274 | let tl = sl topic + sl payload + sl id_data in
275 | let buf = Buffer.create (tl + 5) in
276 | let flags =
277 | let dup = bit_of_bool dup lsl 3 in
278 | let qos = bits_of_qos qos lsl 1 in
279 | let retain = bit_of_bool retain in
280 | dup + qos + retain
281 | in
282 | let hdr = fixed_header Publish_pkt ~flags tl in
283 | Buffer.add_string buf hdr;
284 | Buffer.add_string buf topic;
285 | Buffer.add_string buf id_data;
286 | Buffer.add_string buf payload;
287 | Buffer.contents buf
288 |
289 | let connect_payload ?credentials ?will ?(flags = []) ?(keep_alive = 10) id =
290 | let name = addlen "MQTT" in
291 | let version = "\004" in
292 | if keep_alive > 0xFFFF then raise (Invalid_argument "keep_alive too large");
293 | let addhdr2 flag term (flags, hdr) =
294 | match term with
295 | | None -> (flags, hdr)
296 | | Some (a, b) -> (flags lor flag, hdr ^ addlen a ^ addlen b)
297 | in
298 | let adduserpass term (flags, hdr) =
299 | match term with
300 | | None -> (flags, hdr)
301 | | Some (Username s) -> (flags lor 0x80, hdr ^ addlen s)
302 | | Some (Credentials (u, p)) -> addhdr2 0xC0 (Some (u, p)) (flags, hdr)
303 | in
304 | let flag_nbr = function
305 | | Clean_session -> 0x02
306 | | Will_qos qos -> bits_of_qos qos lsl 3
307 | | Will_retain -> 0x20
308 | in
309 | let accum a acc = acc lor flag_nbr a in
310 | let flags, pay =
311 | (List.fold_right accum flags 0, addlen id)
312 | |> addhdr2 0x04 will
313 | |> adduserpass credentials
314 | in
315 | let tbuf = int16be keep_alive in
316 | let fbuf = Bytes.create 1 in
317 | BE.set_int8 fbuf 0 flags;
318 | let accum acc a = acc + String.length a in
319 | let fields =
320 | [ name; version; Bytes.to_string fbuf; Bytes.to_string tbuf; pay ]
321 | in
322 | let lens = List.fold_left accum 0 fields in
323 | let buf = Buffer.create lens in
324 | List.iter (Buffer.add_string buf) fields;
325 | Buffer.contents buf
326 |
327 | let connect ?credentials ?will ?flags ?keep_alive id =
328 | let cxn_pay = connect_payload ?credentials ?will ?flags ?keep_alive id in
329 | let hdr = fixed_header Connect_pkt (String.length cxn_pay) in
330 | hdr ^ cxn_pay
331 |
332 | let connect_data d =
333 | let clientid = d.clientid in
334 | let credentials = d.credentials in
335 | let will = d.will in
336 | let flags = d.flags in
337 | let keep_alive = d.keep_alive in
338 | connect_payload ?credentials ?will ~flags ~keep_alive clientid
339 |
340 | let connack ~session_present status =
341 | let fixed_header = fixed_header Connack_pkt 2 in
342 | let flags = Bytes.to_string (int8be (bit_of_bool session_present)) in
343 | let connection_status =
344 | Bytes.to_string (int8be (connection_status_to_int status))
345 | in
346 | let variable_header = flags ^ connection_status in
347 | fixed_header ^ variable_header
348 | end
349 |
350 | module Decoder = struct
351 | let decode_connect rb =
352 | let lead = Read_buffer.read rb 9 in
353 | if "\000\004MQTT\004" <> lead then
354 | raise (Invalid_argument "invalid MQTT or version");
355 | let hdr = Read_buffer.read_uint8 rb in
356 | let keep_alive = Read_buffer.read_uint16 rb in
357 | let has_username = 0 <> hdr land 0x80 in
358 | let has_password = 0 <> hdr land 0xC0 in
359 | let will_flag = bool_of_bit ((hdr land 0x04) lsr 2) in
360 | let will_retain = will_flag && 0 <> hdr land 0x20 in
361 | let will_qos =
362 | if will_flag then Some (qos_of_bits ((hdr land 0x18) lsr 3)) else None
363 | in
364 | let clean_session = bool_of_bit ((hdr land 0x02) lsr 1) in
365 | let rs = Read_buffer.read_string in
366 | let clientid = rs rb in
367 | let will =
368 | if will_flag then
369 | let t = rs rb in
370 | let m = rs rb in
371 | Some (t, m)
372 | else None
373 | in
374 | let credentials =
375 | if has_password then
376 | let u = rs rb in
377 | let p = rs rb in
378 | Some (Credentials (u, p))
379 | else if has_username then Some (Username (rs rb))
380 | else None
381 | in
382 | let flags = if clean_session then [ Clean_session ] else [] in
383 | let flags = opt_with (fun qos -> Will_qos qos :: flags) flags will_qos in
384 | let flags = if will_retain then Will_retain :: flags else flags in
385 | Connect { clientid; credentials; will; flags; keep_alive }
386 |
387 | let decode_connack rb =
388 | let flags = Read_buffer.read_uint8 rb in
389 | let session_present = bool_of_bit flags in
390 | let connection_status =
391 | connection_status_of_int (Read_buffer.read_uint8 rb)
392 | in
393 | Connack { session_present; connection_status }
394 |
395 | let decode_publish (_, qos, _) rb =
396 | let topic = Read_buffer.read_string rb in
397 | let msgid =
398 | if qos = Atleast_once || qos = Exactly_once then
399 | Some (Read_buffer.read_uint16 rb)
400 | else None
401 | in
402 | let payload = Read_buffer.len rb |> Read_buffer.read rb in
403 | Publish (msgid, topic, payload)
404 |
405 | let decode_puback rb = Puback (Read_buffer.read_uint16 rb)
406 | let decode_pubrec rb = Pubrec (Read_buffer.read_uint16 rb)
407 | let decode_pubrel rb = Pubrel (Read_buffer.read_uint16 rb)
408 | let decode_pubcomp rb = Pubcomp (Read_buffer.read_uint16 rb)
409 |
410 | let decode_subscribe rb =
411 | let id = Read_buffer.read_uint16 rb in
412 | let get_topic rb =
413 | let topic = Read_buffer.read_string rb in
414 | let qos = Read_buffer.read_uint8 rb |> qos_of_bits in
415 | (topic, qos)
416 | in
417 | let topics = Read_buffer.read_all rb get_topic in
418 | Subscribe (id, topics)
419 |
420 | let decode_suback rb =
421 | let id = Read_buffer.read_uint16 rb in
422 | let get_qos rb = Read_buffer.read_uint8 rb |> suback_qos_of_bits in
423 | let qoses = Read_buffer.read_all rb get_qos in
424 | Suback (id, List.rev qoses)
425 |
426 | let decode_unsub rb =
427 | let id = Read_buffer.read_uint16 rb in
428 | let topics = Read_buffer.read_all rb Read_buffer.read_string in
429 | Unsubscribe (id, topics)
430 |
431 | let decode_unsuback rb = Unsuback (Read_buffer.read_uint16 rb)
432 | let decode_pingreq _rb = Pingreq
433 | let decode_pingresp _rb = Pingresp
434 | let decode_disconnect _rb = Disconnect
435 |
436 | let decode_packet opts = function
437 | | Connect_pkt -> decode_connect
438 | | Connack_pkt -> decode_connack
439 | | Publish_pkt -> decode_publish opts
440 | | Puback_pkt -> decode_puback
441 | | Pubrec_pkt -> decode_pubrec
442 | | Pubrel_pkt -> decode_pubrel
443 | | Pubcomp_pkt -> decode_pubcomp
444 | | Subscribe_pkt -> decode_subscribe
445 | | Suback_pkt -> decode_suback
446 | | Unsubscribe_pkt -> decode_unsub
447 | | Unsuback_pkt -> decode_unsuback
448 | | Pingreq_pkt -> decode_pingreq
449 | | Pingresp_pkt -> decode_pingresp
450 | | Disconnect_pkt -> decode_disconnect
451 |
452 | let decode_fixed_header byte : messages * options =
453 | let typ = (byte land 0xF0) lsr 4 in
454 | let dup = (byte land 0x08) lsr 3 in
455 | let qos = (byte land 0x06) lsr 1 in
456 | let retain = byte land 0x01 in
457 | let typ = message_of_bits typ in
458 | let dup = bool_of_bit dup in
459 | let qos = qos_of_bits qos in
460 | let retain = bool_of_bit retain in
461 | (typ, (dup, qos, retain))
462 | end
463 |
--------------------------------------------------------------------------------
/etc/odoc.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | /* Copyright (c) 2016 The odoc contributors. All rights reserved.
3 | Distributed under the ISC license, see terms at the end of the file.
4 | %%NAME%% %%VERSION%% */
5 |
6 | /* Fonts */
7 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,500');
8 | @import url('https://fonts.googleapis.com/css?family=Noticia+Text:400,400i,700');
9 | @import url('https://fonts.googleapis.com/css?family=Fira+Sans:400,400i,500,500i,600,600i,700,700i');
10 |
11 |
12 | /* Reset a few things. */
13 |
14 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
15 | margin: 0;
16 | padding: 0;
17 | border: 0;
18 | font-size: inherit;
19 | font: inherit;
20 | line-height: inherit;
21 | vertical-align: baseline;
22 | text-align: inherit;
23 | color: inherit;
24 | background: transparent;
25 | }
26 |
27 | table {
28 | border-collapse: collapse;
29 | border-spacing: 0;
30 | }
31 |
32 | *, *:before, *:after {
33 | box-sizing: border-box;
34 | }
35 |
36 | html {
37 | font-size: 15px;
38 | }
39 |
40 | body {
41 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
42 | text-align: left;
43 | color: #333;
44 | }
45 |
46 | .content {
47 | max-width: 90ex;
48 | margin-left: calc(10vw + 20ex);
49 | margin-right: 4ex;
50 | margin-top: 20px;
51 | margin-bottom: 50px;
52 | line-height: 1.5;
53 | }
54 |
55 | .content>header {
56 | margin-bottom: 30px;
57 | }
58 |
59 | /* Basic markup elements */
60 |
61 | b, strong {
62 | font-weight: 500;
63 | }
64 |
65 | i, em {
66 | font-style: italic;
67 | }
68 |
69 | sup {
70 | vertical-align: super;
71 | }
72 |
73 | sub {
74 | vertical-align: sub;
75 | }
76 |
77 | sup, sub {
78 | font-size: 12px;
79 | line-height: 0;
80 | margin-left: 0.2ex;
81 | }
82 |
83 | pre {
84 | margin-top: 0.8em;
85 | margin-bottom: 1.2em;
86 | }
87 |
88 | p, ul, ol {
89 | margin-top: 0.5em;
90 | margin-bottom: 1em;
91 | }
92 | ul, ol {
93 | list-style-position: outside
94 | }
95 |
96 | ul>li {
97 | margin-left: 22px;
98 | }
99 |
100 | ol>li {
101 | margin-left: 27.2px;
102 | }
103 |
104 | li>*:first-child {
105 | margin-top: 0
106 | }
107 |
108 | /* Text alignements, this should be forbidden. */
109 |
110 | .left {
111 | text-align: left;
112 | }
113 |
114 | .right {
115 | text-align: right;
116 | }
117 |
118 | .center {
119 | text-align: center;
120 | }
121 |
122 | /* Links and anchors */
123 |
124 | a {
125 | text-decoration: none;
126 | color: #2C5CBD;
127 | }
128 |
129 | a:hover {
130 | text-decoration: underline;
131 | }
132 |
133 | /* Linked highlight */
134 | *:target {
135 | background-color: rgba(187,239,253,0.3) !important;
136 | box-shadow: 0 0px 0 1px rgba(187,239,253,0.8) !important;
137 | border-radius: 1px;
138 | }
139 |
140 | *:hover>a.anchor {
141 | visibility: visible;
142 | }
143 |
144 | a.anchor:before {
145 | content: "#"
146 | }
147 |
148 | a.anchor:hover {
149 | box-shadow: none;
150 | text-decoration: none;
151 | color: #555;
152 | }
153 |
154 | a.anchor {
155 | visibility: hidden;
156 | position: absolute;
157 | /* top: 0px; */
158 | /* margin-left: -3ex; */
159 | margin-left: -1.3em;
160 | font-weight: normal;
161 | font-style: normal;
162 | padding-right: 0.4em;
163 | padding-left: 0.4em;
164 | /* To remain selectable */
165 | color: #d5d5d5;
166 | }
167 |
168 | .spec > a.anchor {
169 | margin-left: -2.3em;
170 | padding-right: 0.9em;
171 | }
172 |
173 | .xref-unresolved {
174 | color: #2C5CBD;
175 | }
176 | .xref-unresolved:hover {
177 | box-shadow: 0 1px 0 0 #CC6666;
178 | }
179 |
180 | /* Section and document divisions.
181 | Until at least 4.03 many of the modules of the stdlib start at .h7,
182 | we restart the sequence there like h2 */
183 |
184 | h1, h2, h3, h4, h5, h6, .h7, .h8, .h9, .h10 {
185 | font-family: "Fira Sans", Helvetica, Arial, sans-serif;
186 | font-weight: 400;
187 | margin: 0.5em 0 0.5em 0;
188 | padding-top: 0.1em;
189 | line-height: 1.2;
190 | overflow-wrap: break-word;
191 | }
192 |
193 | h1 {
194 | font-weight: 500;
195 | font-size: 2.441em;
196 | margin-top: 1.214em;
197 | }
198 |
199 | h1 {
200 | font-weight: 500;
201 | font-size: 1.953em;
202 | box-shadow: 0 1px 0 0 #ddd;
203 | }
204 |
205 | h2 {
206 | font-size: 1.563em;
207 | }
208 |
209 | h3 {
210 | font-size: 1.25em;
211 | }
212 |
213 | small, .font_small {
214 | font-size: 0.8em;
215 | }
216 |
217 | h1 code, h1 tt {
218 | font-size: inherit;
219 | font-weight: inherit;
220 | }
221 |
222 | h2 code, h2 tt {
223 | font-size: inherit;
224 | font-weight: inherit;
225 | }
226 |
227 | h3 code, h3 tt {
228 | font-size: inherit;
229 | font-weight: inherit;
230 | }
231 |
232 | h3 code, h3 tt {
233 | font-size: inherit;
234 | font-weight: inherit;
235 | }
236 |
237 | h4 {
238 | font-size: 1.12em;
239 | }
240 |
241 |
242 | /* Preformatted and code */
243 |
244 | tt, code, pre {
245 | font-family: "Fira Mono", courier;
246 | font-weight: 400;
247 | }
248 |
249 | pre {
250 | padding: 0.1em;
251 | border: 1px solid #eee;
252 | border-radius: 5px;
253 | overflow-x: auto;
254 | }
255 |
256 | p code, li code {
257 | background-color: #f6f8fa;
258 | color: #0d2b3e;
259 | border-radius: 3px;
260 | padding: 0 0.3ex;
261 | }
262 |
263 | p a > code {
264 | color: #2C5CBD;
265 | }
266 |
267 | /* Code blocks (e.g. Examples) */
268 |
269 | pre code {
270 | font-size: 0.893rem;
271 | }
272 |
273 | /* Code lexemes */
274 |
275 | .keyword {
276 | font-weight: 500;
277 | }
278 |
279 | /* Module member specification */
280 |
281 | .spec:not(.include), .spec.include details summary {
282 | background-color: #f6f8fa;
283 | border-radius: 3px;
284 | border-left: 4px solid #5c9cf5;
285 | border-right: 5px solid transparent;
286 | padding: 0.35em 0.5em;
287 | }
288 |
289 | .spec.include details summary:hover {
290 | background-color: #ebeff2;
291 | }
292 |
293 | dl, div.spec, .doc, aside {
294 | margin-bottom: 20px;
295 | }
296 |
297 | dl > dd {
298 | padding: 0.5em;
299 | }
300 |
301 | dd> :first-child {
302 | margin-top: 0;
303 | }
304 |
305 | dl:last-child, dd> :last-child, aside:last-child, article:last-child {
306 | margin-bottom: 0;
307 | }
308 |
309 | dt+dt {
310 | margin-top: 15px;
311 | }
312 |
313 | section+section, section > header + dl {
314 | margin-top: 25px;
315 | }
316 |
317 | .spec.type .variant {
318 | margin-left: 2ch;
319 | }
320 | .spec.type .variant p {
321 | margin: 0;
322 | font-style: italic;
323 | }
324 | .spec.type .record {
325 | margin-left: 2ch;
326 | }
327 | .spec.type .record p {
328 | margin: 0;
329 | font-style: italic;
330 | }
331 | .spec.value code {
332 | display: inline-block;
333 | }
334 | .spec.value code .label {
335 | font-style: italic;
336 | font-weight: 500;
337 | }
338 |
339 | .arg:nth-child(2):nth-last-child(n + 5),
340 | .arg:nth-child(2):nth-last-child(n + 5) ~ .arg,
341 | .arg:nth-child(2):nth-last-child(n + 5) ~ span:last-child {
342 | display: block;
343 | margin-left: 1.2em;
344 | }
345 |
346 | div.def {
347 | margin-top: 0;
348 | text-indent: -2ex;
349 | padding-left: 2ex;
350 | }
351 |
352 | div.def+div.doc {
353 | margin-left: 1ex;
354 | margin-top: 2.5px
355 | }
356 |
357 | div.doc>*:first-child {
358 | margin-top: 0;
359 | }
360 |
361 | /* The elements other than heading should be wrapped in