Initial parsing of various formats for IP addresses.

This commit is contained in:
James Harton 2017-10-08 19:07:51 +13:00
parent e03433107c
commit 1855e9a1eb
13 changed files with 373 additions and 20 deletions

1
.gitignore vendored
View file

@ -9,6 +9,7 @@
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
/docs/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

18
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,18 @@
image: elixir:1.5
variables:
MIX_ENV: "test"
before_script:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get --only test
test:
script:
- mix test
- mix credo --strict
- mix inch --pedantic
after_script:
- mix inch.report

View file

@ -1,6 +1,6 @@
# IP
**TODO: Add description**
Simple IP Address representations.
## Installation

View file

@ -2,17 +2,4 @@ defmodule IP do
@moduledoc """
Documentation for IP.
"""
@doc """
Hello world.
## Examples
iex> IP.hello
:world
"""
def hello do
:world
end
end

167
lib/ip/address.ex Normal file
View file

@ -0,0 +1,167 @@
defmodule IP.Address do
alias __MODULE__
alias IP.Address.{InvalidAddress, V6, Helpers}
defstruct ~w(address version)a
import Helpers
use Bitwise
@moduledoc """
Simple representations of IP Addresses.
"""
@type t :: %Address{}
@type ipv4 :: 0..0xffffffff
@type ipv6 :: 0..0xffffffffffffffffffffffffffffffff
@type ip_version :: 4 | 6
@doc """
Convert from (packed) binary representations (either 32 or 128 bits long) into an address.
## Examples
iex> <<192, 0, 2, 1>>
...> |> IP.Address.from_binary()
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
...> |> IP.Address.from_binary()
{:ok, %IP.Address{address: 42540766411282592856903984951653826560, version: 6}}
iex> "192.0.2.1"
...> |> IP.Address.from_binary()
{:error, "Unable to convert binary to address"}
"""
@spec from_binary(binary) :: {:ok, t} | {:error, term}
def from_binary(<<address :: unsigned-integer-size(32)>>), do: {:ok, %Address{address: address, version: 4}}
def from_binary(<<address :: unsigned-integer-size(128)>>), do: {:ok, %Address{address: address, version: 6}}
def from_binary(_address), do: {:error, "Unable to convert binary to address"}
@doc """
Convert from a packed binary presentation to an address or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> <<192, 0, 2, 1>>
...> |> IP.Address.from_binary!()
%IP.Address{address: 3221225985, version: 4}
iex> <<32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>
...> |> IP.Address.from_binary!()
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_binary!(binary) :: t
def from_binary!(address) do
case from_binary(address) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, message: msg)
end
end
@doc """
Convert an integer into an IP address of specified version.
## Examples
iex> 3221225985
...> |> IP.Address.from_integer(4)
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> 42540766411282592856903984951653826561
...> |> IP.Address.from_integer(6)
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@spec from_integer(ipv4 | ipv6, ip_version) :: {:ok, t} | {:error, term}
def from_integer(address, 4) when valid_ipv4_integer?(address) do
{:ok, %Address{address: address, version: 4}}
end
def from_integer(address, 6) when valid_ipv6_integer?(address) do
{:ok, %Address{address: address, version: 6}}
end
def from_integer(_address, 4), do: {:error, "Supplied address not within IPv4 address space"}
def from_integer(_address, 6), do: {:error, "Supplied address not within IPv6 address space"}
def from_integer(_address, version), do: {:error, "No such IP version #{inspect version}"}
@doc """
Convert an integer into an IP address of specified version or raise an
`IP.Address.InvalidAddress` exception.
## Examples
iex> 3221225985
...> |> IP.Address.from_integer!(4)
%IP.Address{address: 3221225985, version: 4}
iex> 42540766411282592856903984951653826561
...> |> IP.Address.from_integer!(6)
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_integer!(ipv4 | ipv6, ip_version) :: t
def from_integer!(address, version) do
case from_integer(address, version) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, message: msg)
end
end
@doc """
Convert a string representation into an IP address of specified version.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string(4)
{:ok, %IP.Address{address: 3221225985, version: 4}}
iex> "2001:db8::1"
...> |> IP.Address.from_string(6)
{:ok, %IP.Address{address: 42540766411282592856903984951653826561, version: 6}}
"""
@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}}
end
def from_string(address, 6) when is_binary(address) do
address = address
|> V6.to_integer()
{:ok, %Address{version: 6, address: address}}
end
def from_string(_address, 4), do: {:error, "Cannot parse IPv4 address"}
def from_string(_address, 6), do: {:error, "Cannot parse IPv6 address"}
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
`IP.Address.InvalidAddress` exception.
## Examples
iex> "192.0.2.1"
...> |> IP.Address.from_string!(4)
%IP.Address{address: 3221225985, version: 4}
iex> "2001:db8::1"
...> |> IP.Address.from_string!(6)
%IP.Address{address: 42540766411282592856903984951653826561, version: 6}
"""
@spec from_string!(binary, ip_version) :: t
def from_string!(address, version) do
case from_string(address, version) do
{:ok, address} -> address
{:error, msg} -> raise(InvalidAddress, msg)
end
end
defp from_bytes([a, b, c, d]) do
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
end
end

41
lib/ip/address/helpers.ex Normal file
View file

@ -0,0 +1,41 @@
defmodule IP.Address.Helpers do
@moduledoc """
Helpful macros related to IP addresses.
"""
@doc """
Guard clause macro for "between 0 and 0xffffffff"
"""
defmacro valid_ipv4_integer?(n) do
quote do
is_integer(unquote(n)) and unquote(n) >= 0 and unquote(n) <= 0xffffffff
end
end
@doc """
Guard clause macro for "between 0 and 0xffffffffffffffffffffffffffffffff"
"""
defmacro valid_ipv6_integer?(n) do
quote do
is_integer(unquote(n))
and unquote(n) >= 0
and unquote(n) <= 0xffffffffffffffffffffffffffffffff
end
end
@doc """
Guard clause macro for "4 or 6"
"""
defmacro valid_ip_version?(4), do: quote do: true
defmacro valid_ip_version?(6), do: quote do: true
defmacro valid_ip_version?(_), do: quote do: false
@doc """
Guard clause macro for "between 0 and 0xff"
"""
defmacro valid_byte?(n) do
quote do
is_integer(unquote(n)) and unquote(n) >= 0 and unquote(n) <= 0xff
end
end
end

View file

@ -0,0 +1,8 @@
defmodule IP.Address.InvalidAddress do
defexception ~w(message)a
@moduledoc """
An exception raised by the bang-version functions in `IP.Address` when the
supplied value is invalid.
"""
end

108
lib/ip/address/v6.ex Normal file
View file

@ -0,0 +1,108 @@
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
"""
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

17
mix.exs
View file

@ -7,10 +7,21 @@ defmodule IP.Mixfile do
version: "0.1.0",
elixir: "~> 1.5",
start_permanent: Mix.env == :prod,
package: package(),
deps: deps()
]
end
def package do
[
maintainers: [ "James Harton <james@automat.nz>" ],
licenses: [ "MIT" ],
links: %{
"Source" => "https://gitlab.com/jimsy/ip"
}
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
@ -21,8 +32,10 @@ defmodule IP.Mixfile do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
{:ex_doc, ">= 0.0.0", only: :dev},
{:earmark, ">= 0.0.0", only: :dev},
{:credo, "~> 0.6", only: ~w(dev test)a, runtime: false},
{:inch_ex, "~> 0.5", only: ~w(dev test)a, runtime: false}
]
end
end

6
mix.lock Normal file
View file

@ -0,0 +1,6 @@
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
"credo": {:hex, :credo, "0.8.7", "b1aad9cd3aa7acdbaea49765bfc9f1605dc4555023a037dc9ea7a70539615bc8", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]},
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
"inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}}

View file

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

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

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

View file

@ -1,8 +1,4 @@
defmodule IPTest do
use ExUnit.Case
doctest IP
test "greets the world" do
assert IP.hello() == :world
end
end