diff --git a/lib/enumerable/ip/prefix.ex b/lib/enumerable/ip/prefix.ex new file mode 100644 index 0000000..ecc10aa --- /dev/null +++ b/lib/enumerable/ip/prefix.ex @@ -0,0 +1,75 @@ +defimpl Enumerable, for: IP.Prefix do + alias IP.{Prefix, Address} + use Bitwise + + @moduledoc """ + Implements `Enumerable` for `IP.Prefix`, allowing consumers to iterate + through all addresses in a prefix. + """ + + @doc """ + Returns the number of addresses within the `prefix`. + + ## Examples + + iex> "192.0.2.128/25" + ...> |> IP.Prefix.from_string!() + ...> |> Enum.count() + 128 + + iex> "2001:db8::/121" + ...> |> IP.Prefix.from_string!() + ...> |> Enum.count() + 128 + """ + @spec count(Prefix.t) :: {:ok, non_neg_integer} | {:error, module} + def count(prefix), do: {:ok, Prefix.space(prefix)} + + @doc """ + Returns whether an `address` is contained by the `prefix`. + + ## Examples + + iex> IP.Prefix.from_string!("192.0.2.128/25") + ...> |> Enum.member?(IP.Address.from_string!("192.0.2.250")) + true + """ + @spec member?(Prefix.t, Address.t) :: {:ok, boolean} | {:error, module} + def member?(prefix, %Address{} = address), do: {:ok, Prefix.contains?(prefix, address)} + + @doc """ + Allows the reduction of `prefix` into a colection of addresses. + + ## Examples + + iex> IP.Prefix.from_string!("192.0.2.128/29") + ...> |> Stream.filter(fn a -> rem(IP.Address.to_integer(a), 2) == 0 end) + ...> |> Enum.map(fn a -> IP.Address.to_string(a) end) + ["192.0.2.130", "192.0.2.132", "192.0.2.134"] + """ + @spec reduce(Prefix.t, Enumerable.acc, Enumerable.reducer) :: Enumerable.result + def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} + def reduce({prefix, pos, last}, {:suspend, acc}, fun), do: {:suspended, acc, &reduce({prefix, pos, last}, &1, fun)} + + def reduce(%Prefix{} = prefix, {:cont, acc}, fun) do + first = prefix + |> Prefix.first() + |> Address.to_integer() + + last = prefix + |> Prefix.last() + |> Address.to_integer() + + reduce({prefix, first, last}, {:cont, acc}, fun) + end + + def reduce({%Prefix{address: %Address{version: version}} = prefix, pos, last}, {:cont, acc}, fun) do + case pos do + ^last -> {:done, acc} + pos -> + pos = pos + 1 + next = Address.from_integer!(pos, version) + reduce({prefix, pos, last}, fun.(next, acc), fun) + end + end +end diff --git a/lib/ip/prefix.ex b/lib/ip/prefix.ex index cefe2f8..bddc9eb 100644 --- a/lib/ip/prefix.ex +++ b/lib/ip/prefix.ex @@ -28,12 +28,12 @@ defmodule IP.Prefix do #IP.Prefix<2001:db8::/64> """ @spec new(Address.t, ipv4_prefix_length | ipv6_prefix_length) :: t - def new(%Address{address: address, version: 4}, length) when length > 0 and length <= 32 do + def new(%Address{address: address, version: 4}, length) when length >= 0 and length <= 32 do mask = calculate_mask_from_length(length, 32) %Prefix{address: Address.from_integer!(address, 4), mask: mask} end - def new(%Address{address: address, version: 6}, length) when length > 0 and length <= 128 do + def new(%Address{address: address, version: 6}, length) when length >= 0 and length <= 128 do mask = calculate_mask_from_length(length, 128) %Prefix{address: Address.from_integer!(address, 6), mask: mask} end @@ -188,13 +188,12 @@ defmodule IP.Prefix do iex> IP.Prefix.from_string!("192.0.2.0/24") ...> |> IP.Prefix.subnet_mask() - "255.255.255.0" + #IP.Address<255.255.255.0> """ @spec subnet_mask(t) :: binary def subnet_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do mask |> Address.from_integer!(4) - |> Address.to_string() end @doc """ @@ -204,7 +203,7 @@ defmodule IP.Prefix do iex> IP.Prefix.from_string!("192.0.2.0/24") ...> |> IP.Prefix.wildcard_mask() - "0.0.0.255" + #IP.Address<0.0.0.255> """ @spec wildcard_mask(t) :: binary def wildcard_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do @@ -212,7 +211,6 @@ defmodule IP.Prefix do |> bnot() |> band(@ipv4_mask) |> Address.from_integer!(4) - |> Address.to_string() end @doc """ @@ -358,13 +356,15 @@ defmodule IP.Prefix do 18446744073709551616 """ @spec space(t) :: non_neg_integer - def space(%Prefix{} = prefix) do - first = prefix - |> Prefix.first() - |> Address.to_integer() - last = prefix - |> Prefix.last() - |> Address.to_integer() + def space(%Prefix{address: %Address{address: address, version: 4}, mask: mask}) do + first = address &&& mask + last = first + (~~~mask &&& @ipv4_mask) + last - first + 1 + end + + def space(%Prefix{address: %Address{address: address, version: 6}, mask: mask}) do + first = address &&& mask + last = first + (~~~mask &&& @ipv6_mask) last - first + 1 end diff --git a/lib/ip/prefix/helpers.ex b/lib/ip/prefix/helpers.ex index 3d55afb..ffef3e4 100644 --- a/lib/ip/prefix/helpers.ex +++ b/lib/ip/prefix/helpers.ex @@ -5,9 +5,9 @@ defmodule IP.Prefix.Helpers do @doc false def calculate_mask_from_length(length, mask_length) do - pad = mask_length - length - 0..(length - 1) - |> Enum.reduce(0, fn (i, mask) -> mask + (1 <<< i + pad) end) + pad = mask_length - length - 1 + mask = n_times_reduce(length, 0, fn (i, mask) -> mask + (1 <<< i) end) + mask <<< pad end @doc false @@ -20,4 +20,10 @@ defmodule IP.Prefix.Helpers do end) |> Enum.count() end + + defp n_times_reduce(0, acc, _fun), do: acc + defp n_times_reduce(n, acc, fun) do + acc = fun.(n, acc) + n_times_reduce(n - 1, acc, fun) + end end diff --git a/lib/ip/prefix/parser.ex b/lib/ip/prefix/parser.ex index e5ab4bc..7a25f0a 100644 --- a/lib/ip/prefix/parser.ex +++ b/lib/ip/prefix/parser.ex @@ -85,8 +85,8 @@ defmodule IP.Prefix.Parser do case Address.from_string(mask, 4) do {:ok, address} -> mask = address - |> Address.to_integer() - |> calculate_length_from_mask() + |> Address.to_integer() + |> calculate_length_from_mask() {:ok, mask} _ -> {:ok, String.to_integer(mask)} diff --git a/lib/ip/scope.ex b/lib/ip/scope.ex new file mode 100644 index 0000000..ac78b2e --- /dev/null +++ b/lib/ip/scope.ex @@ -0,0 +1,81 @@ +defmodule IP.Scope do + alias IP.{Prefix, Address} + use Bitwise + require IP.Prefix + + @moduledoc """ + Implements scope lookup for all (currently) known scopes. + + Please open a pull-request if this needs changing. + """ + + @v4_scopes [ + {"0.0.0.0/8", "CURRENT NETWORK"}, + {"10.0.0.0/8", "RFC1918 PRIVATE"}, + {"127.0.0.0/8", "LOOPBACK"}, + {"168.254.0.0/16", "AUTOCONF PRIVATE"}, + {"172.16.0.0/12", "RFC1918 PRIVATE"}, + {"192.0.0.0/24", "RESERVED (IANA)"}, + {"192.0.2.0/24", "DOCUMENTATION"}, + {"192.88.99.0/24", "6to4 ANYCAST"}, + {"192.168.0.0/16", "RFC1918 PRIVATE"}, + {"198.18.0.0/15", "NETWORK BENCHMARK TESTS"}, + {"198.51.100.0/24", "DOCUMENTATION"}, + {"203.0.113.0/24", "DOCUMENTATION"}, + {"239.0.0.0/8", "LOCAL MULTICAST"}, + {"224.0.0.0/4", "GLOBAL MULTICAST"}, + {"240.0.0.0/4", "RESERVED"}, + {"255.255.255.255/32", "GLOBAL BROADCAST"}, + {"0.0.0.0/0", "GLOBAL UNICAST"} + ] + + @v6_scopes [ + {"2001:10::/28", "ORCHID"}, + {"2001:db8::/32", "DOCUMENTATION"}, + {"2000::/3", "GLOBAL UNICAST"}, + {"::/128", "UNSPECIFIED ADDRESS"}, + {"::1/128", "LINK LOCAL LOOPBACK"}, + {"::ffff:0:0/96", "IPv4 MAPPED"}, + {"::/96", "IPv4 TRANSITION (deprecated)"}, + {"fc00::/7", "UNIQUE LOCAL UNICAST"}, + {"fec0::/10", "SITE LOCAL (deprecated)"}, + {"fe80::/10", "LINK LOCAL UNICAST"}, + {"ff00::/8", "MULTICAST"}, + {"::/0", "RESERVED"} + ] + + @doc """ + Return the scope of `address`` + + ## Examples + + iex> IP.Address.from_string!("192.0.2.0") + ...> |> IP.Scope.address_scope() + "DOCUMENTATION" + + iex> IP.Address.from_string!("2001:db8::") + ...> |> IP.Scope.address_scope() + "DOCUMENTATION" + """ + @spec address_scope(Address.t) :: binary + + Enum.each(@v4_scopes, fn {prefix, description} -> + %Prefix{address: %Address{address: addr0}, mask: mask} = Prefix.from_string!(prefix) + def address_scope(%Address{address: addr1}) + when (unquote(addr0) &&& unquote(mask)) <= addr1 + and (unquote(addr0) &&& unquote(mask)) + (~~~unquote(mask) &&& 0xffffffff) >= addr1 + do + unquote(description) + end + end) + + Enum.each(@v6_scopes, fn {prefix, description} -> + %Prefix{address: %Address{address: addr0}, mask: mask} = Prefix.from_string!(prefix) + def address_scope(%Address{address: addr1}) + when (unquote(addr0) &&& unquote(mask)) <= addr1 + and (unquote(addr0) &&& unquote(mask)) + (~~~unquote(mask) &&& 0xffffffffffffffffffffffffffffffff) >= addr1 + do + unquote(description) + end + end) +end diff --git a/test/enumerable/ip/prefix_test.exs b/test/enumerable/ip/prefix_test.exs new file mode 100644 index 0000000..245b04b --- /dev/null +++ b/test/enumerable/ip/prefix_test.exs @@ -0,0 +1,4 @@ +defmodule EnumerableIPPrefixTest do + use ExUnit.Case + doctest Enumerable.IP.Prefix +end diff --git a/test/ip/scope_test.exs b/test/ip/scope_test.exs new file mode 100644 index 0000000..bf1626f --- /dev/null +++ b/test/ip/scope_test.exs @@ -0,0 +1,4 @@ +defmodule IPScopeTest do + use ExUnit.Case + doctest IP.Scope +end