diff --git a/lib/inspect/ip/address.ex b/lib/inspect/ip/address.ex index 30a535b..f107af6 100644 --- a/lib/inspect/ip/address.ex +++ b/lib/inspect/ip/address.ex @@ -1,14 +1,15 @@ defimpl Inspect, for: IP.Address do + alias IP.Address + import Inspect.Algebra + @moduledoc """ Implement the `Inspect` protocol for `IP.Address` """ - alias IP.Address - import Inspect.Algebra @doc """ Inpect an `address`. - # Examples + ## Examples iex> IP.Address.from_string!("192.0.2.1", 4) #IP.Address<192.0.2.1> diff --git a/lib/inspect/ip/prefix.ex b/lib/inspect/ip/prefix.ex new file mode 100644 index 0000000..20be0e6 --- /dev/null +++ b/lib/inspect/ip/prefix.ex @@ -0,0 +1,22 @@ +defimpl Inspect, for: IP.Prefix do + alias IP.Prefix + import Inspect.Algebra + + @moduledoc """ + Implement the `Inspect` protocol for `IP.Prefix` + """ + + @doc """ + Inspect a `prefix`. + + ## Examples + + iex> IP.Address.from_string!("192.0.2.1", 4) + ...> |> IP.Address.to_prefix(32) + #IP.Prefix<192.0.2.1/32> + """ + @spec inspect(Prefix.t, list) :: binary + def inspect(%Prefix{address: address, length: length}, _opts) do + concat ["#IP.Prefix<#{address}/#{length}>"] + end +end diff --git a/lib/ip/address.ex b/lib/ip/address.ex index 1d8b968..de104ca 100644 --- a/lib/ip/address.ex +++ b/lib/ip/address.ex @@ -1,6 +1,6 @@ defmodule IP.Address do alias __MODULE__ - alias IP.Address.{InvalidAddress, V6, Helpers} + alias IP.Address.{InvalidAddress, Helpers, Prefix} defstruct ~w(address version)a import Helpers use Bitwise @@ -106,6 +106,56 @@ defmodule IP.Address do end end + @doc """ + Convert a string representation into an IP address of unknown version. + + Tries to parse the string as IPv6, then IPv4 before failing. Obviously if + you know the version then using `from_string/2` is faster. + + ## Examples + + iex> "192.0.2.1" + ...> |> IP.Address.from_string() + {:ok, %IP.Address{address: 3221225985, version: 4}} + + iex> "2001:db8::1" + ...> |> IP.Address.from_string() + {:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}} + """ + @spec from_string(binary) :: {:ok, t} | {:error, term} + def from_string(address) when is_binary(address) do + case from_string(address, 6) do + {:ok, address} -> {:ok, address} + {:error, _} -> + case from_string(address, 4) do + {:ok, address} -> {:ok, address} + {:error, _} -> {:error, "Unable to parse IP address"} + end + end + end + + @doc """ + Convert a string representation into an IP address or raise an + `IP.Address.InvalidAddress` exception. + + ## Examples + + iex> "192.0.2.1" + ...> |> IP.Address.from_string!() + %IP.Address{address: 3221225985, version: 4} + + iex> "2001:db8::1" + ...> |> IP.Address.from_string!() + %IP.Address{address: 42540766411282592856903984951653826561, version: 6} + """ + @spec from_string!(binary) :: t + def from_string!(address) when is_binary(address) do + case from_string(address) do + {:ok, addr} -> addr + {:error, msg} -> raise(InvalidAddress, message: msg) + end + end + @doc """ Convert a string representation into an IP address of specified version. @@ -121,18 +171,25 @@ defmodule IP.Address do """ @spec from_string(binary, ip_version) :: {:ok, t} | {:error, term} def from_string(address, 4) when is_binary(address) do - address = address - |> String.split(".") - |> Enum.map(&String.to_integer(&1)) - |> from_bytes() - - {:ok, %Address{version: 4, address: address}} + case :inet.parse_ipv4_address(String.to_charlist(address)) do + {:ok, addr} -> + addr = addr + |> Tuple.to_list() + |> from_bytes() + {:ok, %Address{version: 4, address: addr}} + {:error, _} -> {:error, "Cannot parse IPv4 address"} + end end def from_string(address, 6) when is_binary(address) do - address = address - |> V6.to_integer() - {:ok, %Address{version: 6, address: address}} + case :inet.parse_ipv6strict_address(String.to_charlist(address)) do + {:ok, addr} -> + addr = addr + |> Tuple.to_list() + |> from_bytes() + {:ok, %Address{version: 6, address: addr}} + {:error, _} -> {:error, "Cannot parse IPv6 address"} + end end def from_string(_address, 4), do: {:error, "Cannot parse IPv4 address"} @@ -140,7 +197,7 @@ defmodule IP.Address do def from_string(_address, version), do: {:error, "No such IP version #{inspect version}"} @doc """ - Convert a string representation into an IP address of specified versionor raise an + Convert a string representation into an IP address of specified version or raise an `IP.Address.InvalidAddress` exception. ## Examples @@ -180,7 +237,9 @@ defmodule IP.Address do b = addr >>> 0x10 &&& 0xff c = addr >>> 0x08 &&& 0xff d = addr &&& 0xff - "#{a}.#{b}.#{c}.#{d}" + {a, b, c, d} + |> :inet.ntoa() + |> List.to_string() end def to_string(%Address{version: 6, address: addr}) do @@ -192,13 +251,29 @@ defmodule IP.Address do f = addr >>> 0x20 &&& 0xffff g = addr >>> 0x10 &&& 0xffff h = addr &&& 0xffff - [a, b, c, d, e, f, g, h] - |> Enum.map(&Integer.to_string(&1, 16)) - |> Enum.join(":") - |> V6.compress() + {a, b, c, d, e, f, g, h} + |> :inet.ntoa() + |> List.to_string() end + @doc """ + Convert an `address` to an `IP.Prefix`. + + ## Examples + + iex> IP.Address.from_string!("192.0.2.1", 4) + ...> |> IP.Address.to_prefix(32) + #IP.Prefix<192.0.2.1/32> + """ + @spec to_prefix(t, Prefix.ipv4_prefix_length | Prefix.ipv6_prefix_length) :: Prefix.t + def to_prefix(%Address{} = address, length), do: IP.Prefix.new(address, length) + defp from_bytes([a, b, c, d]) do (a <<< 24) + (b <<< 16) + (c <<< 8) + d end + + defp from_bytes([a, b, c, d, e, f, g, h]) do + (a <<< 0x70) + (b <<< 0x60) + (c <<< 0x50) + (d <<< 0x40) + + (e <<< 0x30) + (f <<< 0x20) + (g <<< 0x10) + h + end end diff --git a/lib/ip/address/v6.ex b/lib/ip/address/v6.ex deleted file mode 100644 index 74c8f73..0000000 --- a/lib/ip/address/v6.ex +++ /dev/null @@ -1,109 +0,0 @@ -defmodule IP.Address.V6 do - use Bitwise - - @moduledoc """ - Helper module for working with IPv6 address strings. - """ - - @doc """ - Expand a compressed address. - - ## Examples - - iex> "2001:db8::1" - ...> |> IP.Address.V6.expand() - "2001:0db8:0000:0000:0000:0000:0000:0001" - - iex> "2001:0db8:0000:0000:0000:0000:0000:0001" - ...> |> IP.Address.V6.expand() - "2001:0db8:0000:0000:0000:0000:0000:0001" - """ - @spec expand(binary) :: {:ok, binary} | {:error, term} - def expand(address) do - address - |> expand_to_ints() - |> Enum.map(fn (i) -> - i - |> Integer.to_string(16) - |> String.pad_leading(4, "0") - end) - |> Enum.join(":") - |> String.downcase() - end - - @doc """ - Compress an IPv6 address - - ## Examples - - iex> "2001:0db8:0000:0000:0000:0000:0000:0001" - ...> |> IP.Address.V6.compress() - "2001:db8::1" - - iex> "2001:db8::1" - ...> |> IP.Address.V6.compress() - "2001:db8::1" - """ - @spec compress(binary) :: binary - def compress(address) do - address = address - |> expand_to_ints() - |> Enum.map(&Integer.to_string(&1, 16)) - |> Enum.join(":") - |> String.downcase() - Regex.replace(~r/\b(?:0+:){2,}/, address, ":") - end - - @doc """ - Convert an IPv6 into a 128 bit integer - - ## Examples - - iex> "2001:0db8:0000:0000:0000:0000:0000:0001" - ...> |> IP.Address.V6.to_integer() - 42540766411282592856903984951653826561 - - iex> "2001:db8::1" - ...> |> IP.Address.V6.to_integer() - 42540766411282592856903984951653826561 - """ - @spec to_integer(binary) :: non_neg_integer - def to_integer(address) do - address - |> expand_to_ints() - |> reduce_ints(0) - end - - defp reduce_ints([], addr), do: addr - defp reduce_ints([next | remaining] = all, addr) do - left_shift_size = (length(all) - 1) * 16 - addr = addr + (next <<< left_shift_size) - reduce_ints(remaining, addr) - end - - defp expand_to_ints(address) do - case String.split(address, "::") do - [head, tail] -> - head = decolonify(head) - tail = decolonify(tail) - pad(head, tail) - [head] -> decolonify(head) - end - end - - defp decolonify(chunk) do - chunk - |> String.split(":") - |> Enum.map(&String.to_integer(&1, 16)) - end - - defp pad(head, []) when length(head) == 8, do: head - defp pad([], tail) when length(tail) == 8, do: tail - defp pad(head, tail) when length(head) + length(tail) < 8 do - head_len = length(head) - tail_len = length(head) - pad_len = 8 - (head_len + tail_len) - pad = Enum.map(0..pad_len, fn (_) -> 0 end) - head ++ pad ++ tail - end -end diff --git a/lib/ip/prefix.ex b/lib/ip/prefix.ex new file mode 100644 index 0000000..a986f18 --- /dev/null +++ b/lib/ip/prefix.ex @@ -0,0 +1,32 @@ +defmodule IP.Prefix do + alias IP.{Prefix, Address} + defstruct ~w(address length)a + + @moduledoc """ + Defines an IP prefix, otherwise known as a subnet. + """ + + @type t :: %Prefix{} + @type ipv4_prefix_length :: 0..32 + @type ipv6_prefix_length :: 0..128 + + @doc """ + Create an IP prefix from an `IP.Address` and `length`. + + ## Examples + + iex> IP.Prefix.new(IP.Address.from_string!("192.0.2.1", 4), 32) + %IP.Prefix{address: %IP.Address{address: 3221225985, version: 4}, length: 32} + + iex> IP.Prefix.new(IP.Address.from_string!("2001:db8::1", 6), 128) + %IP.Prefix{address: %IP.Address{address: 42540766411282592856903984951653826561, version: 6}, length: 128} + """ + @spec new(Address.t, ipv4_prefix_length | ipv6_prefix_length) :: t + def new(%Address{version: 4} = address, length) when length > 0 and length <= 32 do + %Prefix{address: address, length: length} + end + + def new(%Address{version: 6} = address, length) when length > 0 and length <= 128 do + %Prefix{address: address, length: length} + end +end diff --git a/lib/string/chars/ip/address.ex b/lib/string/chars/ip/address.ex index 3536e1a..03bad24 100644 --- a/lib/string/chars/ip/address.ex +++ b/lib/string/chars/ip/address.ex @@ -6,7 +6,7 @@ defimpl String.Chars, for: IP.Address do @doc ~S""" Convert an `address` into a string representation. - # Examples + ## Examples iex> "#{IP.Address.from_string!("192.0.2.1", 4)}" "192.0.2.1" diff --git a/lib/string/chars/ip/prefix.ex b/lib/string/chars/ip/prefix.ex new file mode 100644 index 0000000..30e898b --- /dev/null +++ b/lib/string/chars/ip/prefix.ex @@ -0,0 +1,20 @@ +defimpl String.Chars, for: IP.Prefix do + alias IP.Prefix + @moduledoc """ + Implements `String.Chars` for `IP.Prefix`. + """ + + @doc ~S""" + Convert a `prefix` into a string representation. + + ## Examples + + iex> address = IP.Address.from_string!("192.0.2.1", 4) + ...> prefix = IP.Prefix.new(address, 32) + ...> "#{prefix}" + "192.0.2.1/32" + """ + def to_string(%Prefix{address: address, length: length}) do + "#{address}/#{length}" + end +end diff --git a/test/ip/address/v6_test.exs b/test/ip/address/v6_test.exs deleted file mode 100644 index d88f8c4..0000000 --- a/test/ip/address/v6_test.exs +++ /dev/null @@ -1,4 +0,0 @@ -defmodule IPAddressV6Test do - use ExUnit.Case - doctest IP.Address.V6 -end diff --git a/test/ip/prefix_test.exs b/test/ip/prefix_test.exs new file mode 100644 index 0000000..14f0fd0 --- /dev/null +++ b/test/ip/prefix_test.exs @@ -0,0 +1,4 @@ +defmodule IPPrefixTest do + use ExUnit.Case + doctest IP.Prefix +end diff --git a/test/string/chars/ip/prefix_test.exs b/test/string/chars/ip/prefix_test.exs new file mode 100644 index 0000000..e7508bc --- /dev/null +++ b/test/string/chars/ip/prefix_test.exs @@ -0,0 +1,4 @@ +defmodule StringCharsIPPrefixTest do + use ExUnit.Case + doctest String.Chars.IP.Prefix +end