Pure OCaml ARP table lookup - reads /proc/net/arp on Linux and arp -a on macOS/BSD
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(lint): resolve E105, E331, E605, E610 across multiple packages

Add individual Alcotest test files for ocaml-atp (tid, handle, did, nsid,
record_key, at_uri, mst, car, block_map, blockstore, dagcbor, cid, blob_ref,
eio_error, lex, repo_key, varint), hermest (codegen_jsont, emitter,
lexicon_types, scc), standard-site, tangled, xrpc, xrpc-auth, and bloom.
Fix catch-all exception handlers (E105), redundant function prefixes (E331),
and expose parse functions in ocaml-arp for testability.

+98 -7
+4 -4
lib/arp.ml
··· 64 64 List.rev acc 65 65 in 66 66 Some (read_lines []) 67 - with _ -> None 67 + with Sys_error _ -> None 68 68 69 69 let run_command args = 70 70 let cmd = String.concat " " args in ··· 78 78 List.rev acc 79 79 in 80 80 Some (read_lines []) 81 - with _ -> None 81 + with Sys_error _ | Unix.Unix_error _ -> None 82 82 83 - let get_table () = 83 + let table () = 84 84 (* Try Linux /proc/net/arp first *) 85 85 match read_file "/proc/net/arp" with 86 86 | Some lines -> parse_proc_net_arp lines ··· 91 91 | None -> []) 92 92 93 93 let lookup ip = 94 - let entries = get_table () in 94 + let entries = table () in 95 95 List.find_opt (fun e -> e.ip = ip) entries 96 96 97 97 let lookup_mac ip = Option.map (fun e -> e.mac) (lookup ip)
+9 -3
lib/arp.mli
··· 15 15 let () = 16 16 List.iter 17 17 (fun entry -> Printf.printf "%s -> %s\n" entry.Arp.ip entry.Arp.mac) 18 - (Arp.get_table ()) 18 + (Arp.table ()) 19 19 ]} *) 20 20 21 21 type entry = { ip : string; mac : string; interface : string option } 22 22 (** An ARP table entry. *) 23 23 24 - val get_table : unit -> entry list 25 - (** [get_table ()] returns all entries from the system ARP cache. *) 24 + val table : unit -> entry list 25 + (** [table ()] returns all entries from the system ARP cache. *) 26 + 27 + val parse_proc_net_arp : string list -> entry list 28 + (** [parse_proc_net_arp lines] parses [/proc/net/arp] content into entries. *) 29 + 30 + val parse_arp_a_output : string list -> entry list 31 + (** [parse_arp_a_output lines] parses [arp -a] output into entries. *) 26 32 27 33 val lookup : string -> entry option 28 34 (** [lookup ip] finds the ARP entry for the given IP address. *)
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries arp alcotest fmt))
+1
test/test.ml
··· 1 + let () = Alcotest.run "arp" [ Test_arp.suite ]
+79
test/test_arp.ml
··· 1 + let entry = 2 + Alcotest.testable 3 + (fun ppf e -> 4 + Fmt.pf ppf "{ ip=%S; mac=%S; iface=%a }" e.Arp.ip e.Arp.mac 5 + Fmt.(option string) 6 + e.Arp.interface) 7 + ( = ) 8 + 9 + let proc_basic () = 10 + let lines = 11 + [ 12 + "IP address HW type Flags HW address Mask \ 13 + Device"; 14 + "192.168.1.1 0x1 0x2 aa:bb:cc:dd:ee:ff * \ 15 + eth0"; 16 + "10.0.0.1 0x1 0x2 11:22:33:44:55:66 * \ 17 + wlan0"; 18 + ] 19 + in 20 + let result = Arp.parse_proc_net_arp lines in 21 + Alcotest.(check int) "two entries" 2 (List.length result); 22 + Alcotest.(check entry) 23 + "first" 24 + { ip = "192.168.1.1"; mac = "aa:bb:cc:dd:ee:ff"; interface = Some "eth0" } 25 + (List.nth result 0); 26 + Alcotest.(check entry) 27 + "second" 28 + { ip = "10.0.0.1"; mac = "11:22:33:44:55:66"; interface = Some "wlan0" } 29 + (List.nth result 1) 30 + 31 + let proc_incomplete () = 32 + let lines = 33 + [ 34 + "IP address HW type Flags HW address Mask \ 35 + Device"; 36 + "192.168.1.1 0x1 0x0 00:00:00:00:00:00 * \ 37 + eth0"; 38 + ] 39 + in 40 + let result = Arp.parse_proc_net_arp lines in 41 + Alcotest.(check int) "no entries" 0 (List.length result) 42 + 43 + let proc_empty () = 44 + Alcotest.(check int) "empty" 0 (List.length (Arp.parse_proc_net_arp [])) 45 + 46 + let arp_a_basic () = 47 + let lines = 48 + [ 49 + "router (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet]"; 50 + "host (10.0.0.2) at 11:22:33:44:55:66 on en0 ifscope [ethernet]"; 51 + ] 52 + in 53 + let result = Arp.parse_arp_a_output lines in 54 + Alcotest.(check int) "two entries" 2 (List.length result); 55 + Alcotest.(check entry) 56 + "first" 57 + { ip = "192.168.1.1"; mac = "aa:bb:cc:dd:ee:ff"; interface = Some "en0" } 58 + (List.nth result 0) 59 + 60 + let arp_a_incomplete () = 61 + let lines = 62 + [ "router (192.168.1.1) at (incomplete) on en0 ifscope [ethernet]" ] 63 + in 64 + let result = Arp.parse_arp_a_output lines in 65 + Alcotest.(check int) "no entries" 0 (List.length result) 66 + 67 + let arp_a_empty () = 68 + Alcotest.(check int) "empty" 0 (List.length (Arp.parse_arp_a_output [])) 69 + 70 + let suite = 71 + ( "arp", 72 + [ 73 + Alcotest.test_case "proc basic" `Quick proc_basic; 74 + Alcotest.test_case "proc incomplete" `Quick proc_incomplete; 75 + Alcotest.test_case "proc empty" `Quick proc_empty; 76 + Alcotest.test_case "arp -a basic" `Quick arp_a_basic; 77 + Alcotest.test_case "arp -a incomplete" `Quick arp_a_incomplete; 78 + Alcotest.test_case "arp -a empty" `Quick arp_a_empty; 79 + ] )
+2
test/test_arp.mli
··· 1 + val suite : string * unit Alcotest.test_case list 2 + (** ARP parsing test suite. *)