Initial parsing of various formats for IP addresses.
This commit is contained in:
parent
e03433107c
commit
1855e9a1eb
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
18
.gitlab-ci.yml
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
# IP
|
||||
|
||||
**TODO: Add description**
|
||||
Simple IP Address representations.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
13
lib/ip.ex
13
lib/ip.ex
|
@ -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
167
lib/ip/address.ex
Normal 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
41
lib/ip/address/helpers.ex
Normal 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
|
8
lib/ip/address/invalid_address.ex
Normal file
8
lib/ip/address/invalid_address.ex
Normal 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
108
lib/ip/address/v6.ex
Normal 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
17
mix.exs
|
@ -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
6
mix.lock
Normal 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], []}}
|
4
test/ip/address/v6_test.exs
Normal file
4
test/ip/address/v6_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPAddressV6Test do
|
||||
use ExUnit.Case
|
||||
doctest IP.Address.V6
|
||||
end
|
4
test/ip/address_test.exs
Normal file
4
test/ip/address_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPAddressTest do
|
||||
use ExUnit.Case
|
||||
doctest IP.Address
|
||||
end
|
|
@ -1,8 +1,4 @@
|
|||
defmodule IPTest do
|
||||
use ExUnit.Case
|
||||
doctest IP
|
||||
|
||||
test "greets the world" do
|
||||
assert IP.hello() == :world
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue