Use erlangs :inet instead of rolling our own address parsing.

This commit is contained in:
James Harton 2017-10-08 21:13:16 +13:00
parent 26d392cd49
commit c203db2c46
10 changed files with 179 additions and 133 deletions

View file

@ -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>

22
lib/inspect/ip/prefix.ex Normal file
View file

@ -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

View file

@ -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,30 @@ 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

View file

@ -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

32
lib/ip/prefix.ex Normal file
View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -1,4 +0,0 @@
defmodule IPAddressV6Test do
use ExUnit.Case
doctest IP.Address.V6
end

4
test/ip/prefix_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule IPPrefixTest do
use ExUnit.Case
doctest IP.Prefix
end

View file

@ -0,0 +1,4 @@
defmodule StringCharsIPPrefixTest do
use ExUnit.Case
doctest String.Chars.IP.Prefix
end