212 lines
5.5 KiB
OCaml
212 lines
5.5 KiB
OCaml
open Types
|
|
|
|
type prefix =
|
|
| No_prefix
|
|
| Server_prefix of string
|
|
| User_prefix of name * userinfo option
|
|
|
|
let prefix_string = function
|
|
| No_prefix -> ""
|
|
| Server_prefix servername -> servername
|
|
| User_prefix (nick, None) -> nick
|
|
| User_prefix (nick, Some uinfo) -> Format.asprintf "%s%a" nick pp_userinfo uinfo
|
|
|
|
let pp_prefix ppf p =
|
|
Format.fprintf ppf "%S" (prefix_string p)
|
|
|
|
type t = {
|
|
prefix : prefix;
|
|
command : string;
|
|
params : string list;
|
|
trailing : bool;
|
|
} [@@deriving show { with_path = false }]
|
|
|
|
let rec is_trailing arg i =
|
|
if i >= String.length arg then
|
|
arg = ""
|
|
else match arg.[i] with
|
|
| ' ' | '\t' -> true
|
|
| ':' when i = 0 -> true
|
|
| _ -> is_trailing arg (i + 1)
|
|
|
|
let rec ends_with_trailing = function
|
|
| [] -> false
|
|
| [p] -> is_trailing p 0
|
|
| _ :: tl -> ends_with_trailing tl
|
|
|
|
let make ?(prefix = No_prefix) ?(always_trailing = false) command params =
|
|
let trailing = always_trailing || ends_with_trailing params in
|
|
{ prefix; command; params; trailing }
|
|
|
|
let write buf t =
|
|
if t.prefix <> No_prefix then begin
|
|
Buffer.add_char buf ':';
|
|
Buffer.add_string buf (prefix_string t.prefix);
|
|
Buffer.add_char buf ' ';
|
|
end;
|
|
Buffer.add_string buf t.command;
|
|
let rec add_params = function
|
|
| [] -> ()
|
|
| [p] when t.trailing ->
|
|
Buffer.add_string buf " :";
|
|
Buffer.add_string buf p
|
|
| p::tl ->
|
|
Buffer.add_char buf ' ';
|
|
Buffer.add_string buf p;
|
|
add_params tl
|
|
in
|
|
add_params t.params;
|
|
Buffer.add_string buf "\r\n"
|
|
|
|
exception Empty_message of int
|
|
exception Incomplete
|
|
|
|
let crlf = function '\r' | '\n' | '\x00' -> true | _ -> false
|
|
let cl = function ':' -> true | _ -> false
|
|
let sp = function ' ' | '\t' -> true | _ -> false
|
|
let nocrlf c = not (crlf c)
|
|
let nospcrlf c = not (sp c || crlf c)
|
|
|
|
let startswith str f i =
|
|
if i >= String.length str then
|
|
raise_notrace Incomplete;
|
|
f str.[i]
|
|
|
|
let rec skip str f i =
|
|
if i < String.length str && f str.[i] then
|
|
skip str f (i + 1)
|
|
else
|
|
i
|
|
|
|
let span str f i =
|
|
let e = skip str f i in
|
|
e, String.sub str i (e - i)
|
|
|
|
let parse str =
|
|
let parse_prefix i =
|
|
if startswith str cl i then
|
|
let i, pfx = span str nospcrlf (i + 1) in
|
|
(* TODO: parse [User_prefix] *)
|
|
(* not really needed because prefixes from clients are always ignored *)
|
|
i, Server_prefix pfx
|
|
else
|
|
i, No_prefix
|
|
in
|
|
|
|
let parse_command i =
|
|
let i = skip str sp i in
|
|
if startswith str crlf i then
|
|
raise_notrace (Empty_message (skip str crlf i));
|
|
let i, cmd = span str nospcrlf i in
|
|
i, String.uppercase_ascii cmd
|
|
in
|
|
|
|
let rec parse_params acc i =
|
|
let i = skip str sp i in
|
|
if startswith str crlf i then
|
|
let i = skip str crlf i in
|
|
i, List.rev acc, false
|
|
else if startswith str cl i then
|
|
let i, par = span str nocrlf (i + 1) in
|
|
i, List.rev (par :: acc), true
|
|
else
|
|
let i, par = span str nospcrlf i in
|
|
parse_params (par :: acc) i
|
|
in
|
|
|
|
let parse_msg i =
|
|
let i, prefix = parse_prefix i in
|
|
let i, command = parse_command i in
|
|
let i, params, always_trailing = parse_params [] i in
|
|
let msg = make command params ~prefix ~always_trailing in
|
|
i, msg
|
|
in
|
|
|
|
let rec parse_all acc i =
|
|
match parse_msg i with
|
|
| i, msg -> parse_all (msg :: acc) i
|
|
| exception Empty_message i -> parse_all acc i
|
|
| exception Incomplete -> i, List.rev acc
|
|
in
|
|
|
|
let i, msgs = parse_all [] 0 in
|
|
let rest = if i = 0 then str else String.sub str i (String.length str - i) in
|
|
msgs, rest
|
|
|
|
;;
|
|
|
|
let%expect_test _ =
|
|
let print_msg_nl m =
|
|
let buf = Buffer.create 64 in
|
|
write buf m;
|
|
Printf.printf "%S\n" (Buffer.contents buf)
|
|
in
|
|
let print_parsed_msgs (msgs, rest) =
|
|
List.iter
|
|
(fun m -> Format.kasprintf print_string "%a\n" pp m)
|
|
msgs;
|
|
Format.printf "%S\n" rest
|
|
in
|
|
|
|
Format.printf "%a" pp_prefix No_prefix;
|
|
[%expect {| "" |}];
|
|
|
|
Format.printf "%a" pp_prefix (Server_prefix "localhost");
|
|
[%expect {| "localhost" |}];
|
|
|
|
Format.printf "%a" pp_prefix (User_prefix ("tali", None));
|
|
[%expect {| "tali" |}];
|
|
|
|
let milo = {
|
|
username = "milo";
|
|
realname = "Milo";
|
|
hostname = "elsewhere";
|
|
} in
|
|
Format.printf "%a" pp_prefix (User_prefix ("tali", Some milo));
|
|
[%expect {| "tali!milo@elsewhere" |}];
|
|
|
|
make "NICK" ["tali"] |> print_msg_nl;
|
|
[%expect {| "NICK tali\r\n" |}];
|
|
|
|
make "NICK" [":)"] |> print_msg_nl;
|
|
[%expect {| "NICK ::)\r\n" |}];
|
|
|
|
make "NICK" ["(:"] |> print_msg_nl;
|
|
[%expect {| "NICK (:\r\n" |}];
|
|
|
|
make "NICK" [""] |> print_msg_nl;
|
|
[%expect {| "NICK :\r\n" |}];
|
|
|
|
make "USER" ["milo"; "0"; "*"; "milo"] ~always_trailing:true |> print_msg_nl;
|
|
[%expect {| "USER milo 0 * :milo\r\n" |}];
|
|
|
|
let lo = Server_prefix "localhost" in
|
|
make "001" ["tali"; "Welcome to the IRC Network"] ~prefix:lo |> print_msg_nl;
|
|
[%expect {| ":localhost 001 tali :Welcome to the IRC Network\r\n" |}];
|
|
|
|
parse ":server.com usEr tali 0 * iitalics\r\n" |> print_parsed_msgs;
|
|
[%expect {|
|
|
{ prefix = "server.com"; command = "USER";
|
|
params = ["tali"; "0"; "*"; "iitalics"]; trailing = false }
|
|
""
|
|
|}];
|
|
|
|
parse "PRIVMSG #lol :Hello world\r\nPRIVMSG #lol :meow\r\nPRIVM" |> print_parsed_msgs;
|
|
[%expect {|
|
|
{ prefix = ""; command = "PRIVMSG"; params = ["#lol"; "Hello world"];
|
|
trailing = true }
|
|
{ prefix = ""; command = "PRIVMSG"; params = ["#lol"; "meow"];
|
|
trailing = true }
|
|
"PRIVM"
|
|
|}];
|
|
|
|
parse " \r\n" |> print_parsed_msgs;
|
|
[%expect {| "" |}];
|
|
|
|
parse ":ignore-me \r\nFOO\r\n" |> print_parsed_msgs;
|
|
[%expect {|
|
|
{ prefix = ""; command = "FOO"; params = []; trailing = false }
|
|
""
|
|
|}];
|
|
|