Use erlangs :inet
instead of rolling our own address parsing.
This commit is contained in:
parent
26d392cd49
commit
fb6a664635
10 changed files with 178 additions and 133 deletions
|
@ -1,14 +1,15 @@
|
||||||
defimpl Inspect, for: IP.Address do
|
defimpl Inspect, for: IP.Address do
|
||||||
|
alias IP.Address
|
||||||
|
import Inspect.Algebra
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Implement the `Inspect` protocol for `IP.Address`
|
Implement the `Inspect` protocol for `IP.Address`
|
||||||
"""
|
"""
|
||||||
alias IP.Address
|
|
||||||
import Inspect.Algebra
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Inpect an `address`.
|
Inpect an `address`.
|
||||||
|
|
||||||
# Examples
|
## Examples
|
||||||
|
|
||||||
iex> IP.Address.from_string!("192.0.2.1", 4)
|
iex> IP.Address.from_string!("192.0.2.1", 4)
|
||||||
#IP.Address<192.0.2.1>
|
#IP.Address<192.0.2.1>
|
||||||
|
|
22
lib/inspect/ip/prefix.ex
Normal file
22
lib/inspect/ip/prefix.ex
Normal 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
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule IP.Address do
|
defmodule IP.Address do
|
||||||
alias __MODULE__
|
alias __MODULE__
|
||||||
alias IP.Address.{InvalidAddress, V6, Helpers}
|
alias IP.Address.{InvalidAddress, Helpers, Prefix}
|
||||||
defstruct ~w(address version)a
|
defstruct ~w(address version)a
|
||||||
import Helpers
|
import Helpers
|
||||||
use Bitwise
|
use Bitwise
|
||||||
|
@ -106,6 +106,56 @@ defmodule IP.Address do
|
||||||
end
|
end
|
||||||
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 """
|
@doc """
|
||||||
Convert a string representation into an IP address of specified version.
|
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}
|
@spec from_string(binary, ip_version) :: {:ok, t} | {:error, term}
|
||||||
def from_string(address, 4) when is_binary(address) do
|
def from_string(address, 4) when is_binary(address) do
|
||||||
address = address
|
case :inet.parse_ipv4_address(String.to_charlist(address)) do
|
||||||
|> String.split(".")
|
{:ok, addr} ->
|
||||||
|> Enum.map(&String.to_integer(&1))
|
addr = addr
|
||||||
|
|> Tuple.to_list()
|
||||||
|> from_bytes()
|
|> from_bytes()
|
||||||
|
{:ok, %Address{version: 4, address: addr}}
|
||||||
{:ok, %Address{version: 4, address: address}}
|
{:error, _} -> {:error, "Cannot parse IPv4 address"}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_string(address, 6) when is_binary(address) do
|
def from_string(address, 6) when is_binary(address) do
|
||||||
address = address
|
case :inet.parse_ipv6strict_address(String.to_charlist(address)) do
|
||||||
|> V6.to_integer()
|
{:ok, addr} ->
|
||||||
{:ok, %Address{version: 6, address: address}}
|
addr = addr
|
||||||
|
|> Tuple.to_list()
|
||||||
|
|> from_bytes()
|
||||||
|
{:ok, %Address{version: 6, address: addr}}
|
||||||
|
{:error, _} -> {:error, "Cannot parse IPv6 address"}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_string(_address, 4), do: {:error, "Cannot parse IPv4 address"}
|
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}"}
|
def from_string(_address, version), do: {:error, "No such IP version #{inspect version}"}
|
||||||
|
|
||||||
@doc """
|
@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.
|
`IP.Address.InvalidAddress` exception.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
@ -180,7 +237,9 @@ defmodule IP.Address do
|
||||||
b = addr >>> 0x10 &&& 0xff
|
b = addr >>> 0x10 &&& 0xff
|
||||||
c = addr >>> 0x08 &&& 0xff
|
c = addr >>> 0x08 &&& 0xff
|
||||||
d = addr &&& 0xff
|
d = addr &&& 0xff
|
||||||
"#{a}.#{b}.#{c}.#{d}"
|
{a, b, c, d}
|
||||||
|
|> :inet.ntoa()
|
||||||
|
|> List.to_string()
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_string(%Address{version: 6, address: addr}) do
|
def to_string(%Address{version: 6, address: addr}) do
|
||||||
|
@ -192,13 +251,29 @@ defmodule IP.Address do
|
||||||
f = addr >>> 0x20 &&& 0xffff
|
f = addr >>> 0x20 &&& 0xffff
|
||||||
g = addr >>> 0x10 &&& 0xffff
|
g = addr >>> 0x10 &&& 0xffff
|
||||||
h = addr &&& 0xffff
|
h = addr &&& 0xffff
|
||||||
[a, b, c, d, e, f, g, h]
|
{a, b, c, d, e, f, g, h}
|
||||||
|> Enum.map(&Integer.to_string(&1, 16))
|
|> :inet.ntoa()
|
||||||
|> Enum.join(":")
|
|> List.to_string()
|
||||||
|> V6.compress()
|
|
||||||
end
|
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
|
defp from_bytes([a, b, c, d]) do
|
||||||
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
|
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -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
32
lib/ip/prefix.ex
Normal 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
|
|
@ -6,7 +6,7 @@ defimpl String.Chars, for: IP.Address do
|
||||||
@doc ~S"""
|
@doc ~S"""
|
||||||
Convert an `address` into a string representation.
|
Convert an `address` into a string representation.
|
||||||
|
|
||||||
# Examples
|
## Examples
|
||||||
|
|
||||||
iex> "#{IP.Address.from_string!("192.0.2.1", 4)}"
|
iex> "#{IP.Address.from_string!("192.0.2.1", 4)}"
|
||||||
"192.0.2.1"
|
"192.0.2.1"
|
||||||
|
|
20
lib/string/chars/ip/prefix.ex
Normal file
20
lib/string/chars/ip/prefix.ex
Normal 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
|
|
@ -1,4 +0,0 @@
|
||||||
defmodule IPAddressV6Test do
|
|
||||||
use ExUnit.Case
|
|
||||||
doctest IP.Address.V6
|
|
||||||
end
|
|
4
test/ip/prefix_test.exs
Normal file
4
test/ip/prefix_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule IPPrefixTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest IP.Prefix
|
||||||
|
end
|
4
test/string/chars/ip/prefix_test.exs
Normal file
4
test/string/chars/ip/prefix_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule StringCharsIPPrefixTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest String.Chars.IP.Prefix
|
||||||
|
end
|
Loading…
Reference in a new issue