diff --git a/bin/enum_gen b/bin/enum_gen index d58e812..4fe9e79 100755 Binary files a/bin/enum_gen and b/bin/enum_gen differ diff --git a/lib/tres/ipv4_address.ex b/lib/tres/ipv4_address.ex new file mode 100644 index 0000000..2b44652 --- /dev/null +++ b/lib/tres/ipv4_address.ex @@ -0,0 +1,111 @@ +defmodule Tres.IPv4Address do + @moduledoc """ + IP Address Utils + """ + + use Bitwise + + @typep in_addr :: {byte, byte, byte, byte} + @typep in_addr_str :: String.t() + + @doc """ + iex> {{192, 168, 0, 1}, {255, 255, 255, 255}} = IPv4Address.parse("192.168.0.1") + iex> {{192, 168, 0, 1}, {255, 255, 255, 0}} = IPv4Address.parse("192.168.0.1/24") + """ + @spec parse(in_addr_str) :: {in_addr, in_addr} + def parse(addr) do + case String.split(addr, ~r/\//) do + [^addr] -> + {:ok, ipaddr} = addr |> to_charlist |> :inet.parse_address() + {ipaddr, {255, 255, 255, 255}} + [netaddr, cidr_str] -> + cidr = String.to_integer(cidr_str) + mask = (0xFFFFFFFF >>> (32 - cidr)) <<< (32 - cidr) + + net_mask = { + (0xFF000000 &&& mask) >>> 24, + (0x00FF0000 &&& mask) >>> 16, + (0x0000FF00 &&& mask) >>> 8, + (0x000000FF &&& mask) + } + + {:ok, ipaddr} = netaddr |> to_charlist |> :inet.parse_address() + {ipaddr, net_mask} + end + end + + @doc """ + iex> "192.168.0.1" = IPv4Address.to_str({192, 168, 0, 1}) + """ + @spec to_str(in_addr | in_addr_str) :: in_addr_str + def to_str(addr) when is_binary(addr) do + addr + end + + def to_str(addr) when is_tuple(addr) do + "#{:inet.ntoa(addr)}" + end + + @doc """ + iex> 0xc0a80001 = IPv4Address.to_int({192, 168, 0, 1}) + """ + @spec to_int(in_addr) :: 0..0xFFFFFFFF + def to_int({a, b, c, d}) do + <> |> :binary.decode_unsigned(:big) + end + + @doc """ + iex> true = IPv4Address.is_private?({192, 168, 0, 1}) + """ + @spec is_private?(in_addr) :: boolean + def is_private?(addr) do + ipaddr_int = to_int(addr) + priv_class_a?(ipaddr_int) or priv_class_b?(ipaddr_int) or priv_class_c?(ipaddr_int) + end + + @doc """ + iex> false = IPv4Address.is_loopback?({192, 168, 0, 1}) + iex> true = IPv4Address.is_loopback?({127, 0, 0, 1}) + """ + @spec is_loopback?(in_addr) :: boolean + def is_loopback?({127, _, _, _}), do: true + def is_loopback?({_, _, _, _}), do: false + + @doc """ + iex> false = IPv4Address.is_multicast?({192, 168, 0, 1}) + iex> true = IPv4Address.is_multicast?({224, 0, 0, 1}) + """ + @spec is_multicast?(in_addr) :: boolean + def is_multicast?(in_addr) do + in_addr + |> to_int + |> mcast_class_d? + end + + @doc """ + iex> false = IPv4Address.is_broadcast?({192, 168, 0, 1}) + iex> true = IPv4Address.is_broadcast?({255, 255, 255, 255}) + """ + @spec is_broadcast?(in_addr) :: boolean + def is_broadcast?({255, _, _, _}), do: true + def is_broadcast?({_, _, _, _}), do: false + + # private functions + + @class_a_subnet 0xFF000000 + @class_b_subnet 0xFFF00000 + @class_c_subnet 0xFFFF0000 + @class_d_subnet 0xE0000000 + + @class_a_start_addr 167_772_160 + @class_b_start_addr 2_886_729_728 + @class_c_start_addr 3_232_235_520 + + defp priv_class_a?(ipaddr_int), do: (ipaddr_int &&& @class_a_subnet) == @class_a_start_addr + + defp priv_class_b?(ipaddr_int), do: (ipaddr_int &&& @class_b_subnet) == @class_b_start_addr + + defp priv_class_c?(ipaddr_int), do: (ipaddr_int &&& @class_c_subnet) == @class_c_start_addr + + defp mcast_class_d?(ipaddr_int), do: (ipaddr_int &&& @class_d_subnet) == @class_d_subnet +end diff --git a/lib/tres/mac_address.ex b/lib/tres/mac_address.ex new file mode 100644 index 0000000..06844bb --- /dev/null +++ b/lib/tres/mac_address.ex @@ -0,0 +1,86 @@ +defmodule Tres.MacAddress do + @moduledoc """ + MAC Address Utils + """ + + use Bitwise + + @mac_addr_pattern ~r/^(?:[0-9a-fA-F][0-9a-fA-F]){6}$/ + + @spec bin_to_str(<<_::48>>) :: String.t() + def bin_to_str(<<_::6-bytes>> = mac) do + mac + |> :erlang.binary_to_list() + |> Enum.reduce("", &to_hex/2) + |> String.downcase() + end + + @spec to_a(String.t()) :: [non_neg_integer()] + def to_a(mac) do + mac + |> check_format() + |> to_a([]) + rescue + _e in ArgumentError -> + {:error, :invalid_format} + end + + @spec to_i(String.t()) :: non_neg_integer() + def to_i(mac) do + mac + |> check_format() + |> String.to_integer(16) + rescue + _e in ArgumentError -> + {:error, :invalid_format} + end + + @spec is_broadcast?(String.t()) :: boolean() + def is_broadcast?(mac) do + mac + |> check_format() + |> to_a() + |> Enum.at(0) + |> Kernel.==(0xff) + rescue + _e in ArgumentError -> + {:error, :invalid_format} + end + + @spec is_multicast?(String.t()) :: boolean() + def is_multicast?(mac) do + mac + |> check_format() + |> to_a() + |> Enum.at(0) + |> Bitwise.&&&(1) + |> Kernel.==(1) + rescue + _e in ArgumentError -> + {:error, :invalid_format} + end + + # private functions + + defp to_a(<<>>, acc), do: Enum.reverse(acc) + defp to_a(<>, acc) do + to_a(rest, [String.to_integer(octet, 16) | acc]) + end + + @spec to_hex(0..0xFF, String.t()) :: String.t() + defp to_hex(int, acc), do: "#{acc}#{to_hex(int)}" + + @spec to_hex(0..0xFF) :: String.t() + defp to_hex(int) do + int + |> Integer.to_string(16) + |> String.pad_leading(2, "0") + end + + @spec check_format(String.t()) :: String.t() | none() + defp check_format(mac) when is_binary(mac) do + if String.match?(mac, @mac_addr_pattern), + do: mac, + else: raise ArgumentError, message: "MAC address should be 12 letters hex string format" + end +end diff --git a/test/ipv4_address_test.exs b/test/ipv4_address_test.exs new file mode 100644 index 0000000..ebe6818 --- /dev/null +++ b/test/ipv4_address_test.exs @@ -0,0 +1,17 @@ +defmodule IPv4AddressTest do + use ExUnit.Case, async: false + + describe "IPv4Address.parse/1" do + test "with host(/32) address string" do + expect = {{192, 168, 10, 1}, {255, 255, 255, 255}} + ipaddr = "192.168.10.1" + assert Tres.IPv4Address.parse(ipaddr) == expect + end + + test "with network(/24) address string" do + expect = {{192, 168, 10, 0}, {255, 255, 255, 0}} + ipaddr = "192.168.10.0/24" + assert Tres.IPv4Address.parse(ipaddr) == expect + end + end +end diff --git a/test/mac_address_test.exs b/test/mac_address_test.exs new file mode 100644 index 0000000..d24142f --- /dev/null +++ b/test/mac_address_test.exs @@ -0,0 +1,53 @@ +defmodule MacAddressTest do + use ExUnit.Case, async: false + + describe "MacAddr.bin_to_str/1" do + test "with 48 bit binary" do + mac_bin = <<0x11, 0x22, 0x33, 0x44, 0x55, 0x66>> + assert Tres.MacAddress.bin_to_str(mac_bin) == "112233445566" + end + end + + describe "MacAddr.to_a/1" do + test "with String" do + expect = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66] + assert Tres.MacAddress.to_a("112233445566") == expect + end + end + + describe "MacAddr.to_i/1" do + test "with String" do + expect = 0x112233445566 + assert Tres.MacAddress.to_i("112233445566") == expect + end + end + + describe "MacAddr.broadcast?/1" do + test "with unicast" do + mac = "0e2ecad87537" + refute Tres.MacAddress.is_broadcast?(mac) + end + + test "with bcast" do + mac = "ffffffffffff" + assert Tres.MacAddress.is_broadcast?(mac) + end + end + + describe "MacAddr.multicast?/1" do + test "with unicast" do + mac = "0e2ecad87537" + refute Tres.MacAddress.is_multicast?(mac) + end + + test "with bcast" do + mac = "ffffffffffff" + assert Tres.MacAddress.is_multicast?(mac) + end + + test "with mcast" do + mac = "ffffffffffff" + assert Tres.MacAddress.is_multicast?(mac) + end + end +end