RosettaCodeData/Task/Parse-an-IP-Address/OCaml/parse-an-ip-address.ml
2025-08-11 18:05:26 -07:00

192 lines
6.1 KiB
OCaml

open Printf
(* Custom exception for invalid addresses *)
exception Invalid_address of string
(* Helper functions for string parsing *)
let is_digit c = c >= '0' && c <= '9'
let is_hex_digit c = is_digit c || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
let split_string s delimiter =
let len = String.length s in
let rec aux acc start i =
if i >= len then
if start < len then (String.sub s start (len - start)) :: acc
else acc
else if s.[i] = delimiter then
let part = String.sub s start (i - start) in
aux (part :: acc) (i + 1) (i + 1)
else
aux acc start (i + 1)
in
List.rev (aux [] 0 0)
let string_contains s substr =
let s_len = String.length s in
let substr_len = String.length substr in
let rec aux i =
if i > s_len - substr_len then false
else if String.sub s i substr_len = substr then true
else aux (i + 1)
in
if substr_len = 0 then true else aux 0
(* Check if string contains only digits *)
let is_numeric s =
let len = String.length s in
len > 0 &&
let rec aux i =
if i >= len then true
else if is_digit s.[i] then aux (i + 1)
else false
in
aux 0
(* Check if string contains only hex digits *)
let is_hex s =
let len = String.length s in
len > 0 &&
let rec aux i =
if i >= len then true
else if is_hex_digit (Char.lowercase_ascii s.[i]) then aux (i + 1)
else false
in
aux 0
(* Convert decimal string to 2-digit hex for IPv4 *)
let to_hex4 s =
if not (is_numeric s) then
raise (Invalid_address ("ERROR 101: Invalid value : " ^ s));
let val_int = int_of_string s in
if val_int < 0 || val_int > 255 then
raise (Invalid_address ("ERROR 101: Invalid value : " ^ s))
else
Printf.sprintf "%02x" val_int
(* Validate and return hex string for IPv6 *)
let to_hex6 s =
if not (is_hex s) then
raise (Invalid_address ("ERROR 102: Invalid hex value : " ^ s));
let val_int = int_of_string ("0x" ^ s) in
if val_int < 0 || val_int > 65535 then
raise (Invalid_address ("ERROR 102: Invalid hex value : " ^ s))
else
s
(* Parse IPv4 address *)
let parse_ipv4 ip =
let parts = split_string ip '.' in
match parts with
| [p1; p2; p3; p4_port] ->
(* Check if last part has port *)
if string_contains p4_port ":" then
let port_parts = split_string p4_port ':' in
(match port_parts with
| [p4; port] when is_numeric p4 && is_numeric port ->
let hex = (to_hex4 p1) ^ (to_hex4 p2) ^ (to_hex4 p3) ^ (to_hex4 p4) in
(hex, port)
| _ -> raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip)))
else if is_numeric p4_port then
let hex = (to_hex4 p1) ^ (to_hex4 p2) ^ (to_hex4 p3) ^ (to_hex4 p4_port) in
(hex, "")
else
raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
| _ -> raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
(* Parse IPv6 address *)
let parse_ipv6 ip =
let (clean_ip, port) =
if String.length ip > 0 && ip.[0] = '[' then
(* Bracketed IPv6 with possible port *)
try
let bracket_end = String.index ip ']' in
let addr_part = String.sub ip 1 (bracket_end - 1) in
if bracket_end + 1 < String.length ip && ip.[bracket_end + 1] = ':' then
let port_part = String.sub ip (bracket_end + 2) (String.length ip - bracket_end - 2) in
(addr_part, port_part)
else
(addr_part, "")
with Not_found ->
raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
else
(ip, "") in
(* Handle double colon expansion *)
let expanded_ip =
if string_contains clean_ip "::" then
(* Split on :: to get before and after parts *)
let double_colon_pos =
let rec find_pos i =
if i >= String.length clean_ip - 1 then -1
else if clean_ip.[i] = ':' && clean_ip.[i+1] = ':' then i
else find_pos (i + 1)
in
find_pos 0 in
if double_colon_pos = -1 then
raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
else
let before_part =
if double_colon_pos = 0 then ""
else String.sub clean_ip 0 double_colon_pos in
let after_part =
let start = double_colon_pos + 2 in
if start >= String.length clean_ip then ""
else String.sub clean_ip start (String.length clean_ip - start) in
let before_groups = if before_part = "" then [] else split_string before_part ':' in
let after_groups = if after_part = "" then [] else split_string after_part ':' in
let total_existing = List.length before_groups + List.length after_groups in
let zeros_needed = 8 - total_existing in
let zero_groups = List.init zeros_needed (fun _ -> "0") in
String.concat ":" (before_groups @ zero_groups @ after_groups)
else
clean_ip in
(* Parse the expanded IPv6 *)
let hex_parts = split_string expanded_ip ':' in
if List.length hex_parts = 8 then
let hex = String.concat "" (List.map (fun part ->
Printf.sprintf "%04s" (to_hex6 part) |>
String.map (function ' ' -> '0' | c -> c)) hex_parts) in
(hex, port)
else
raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
(* Main parsing function *)
let parse_ip ip =
(* Check if it looks like IPv4 (contains dots but no colons except maybe one for port) *)
if string_contains ip "." &&
(not (string_contains ip ":") ||
(let colon_count = List.length (split_string ip ':') - 1 in colon_count = 1)) then
parse_ipv4 ip
else if string_contains ip ":" then
parse_ipv6 ip
else
raise (Invalid_address ("ERROR 103: Unknown address: " ^ ip))
(* Test cases *)
let tests = [|
"192.168.0.1";
"127.0.0.1";
"256.0.0.1";
"127.0.0.1:80";
"::1";
"[::1]:80";
"[32e::12f]:80";
"2605:2700:0:3::4713:93e3";
"[2605:2700:0:3::4713:93e3]:80";
"2001:db8:85a3:0:0:8a2e:370:7334"
|]
(* Main function *)
let () =
printf "%-40s %-32s %s\n" "Test Case" "Hex Address" "Port";
Array.iter (fun ip ->
try
let (hex_addr, port) = parse_ip ip in
printf "%-40s %-32s %s\n" ip hex_addr port
with Invalid_address msg ->
printf "%-40s Invalid address: %s\n" ip msg
) tests