Implement parsing of strings into prefixes.
This commit is contained in:
parent
99596e4bf7
commit
78885b2b2d
7 changed files with 381 additions and 31 deletions
|
@ -171,7 +171,7 @@ defmodule IP.Address do
|
|||
"""
|
||||
@spec from_string(binary, ip_version) :: {:ok, t} | {:error, term}
|
||||
def from_string(address, 4) when is_binary(address) do
|
||||
case :inet.parse_ipv4_address(String.to_charlist(address)) do
|
||||
case :inet.parse_ipv4strict_address(String.to_charlist(address)) do
|
||||
{:ok, addr} ->
|
||||
addr = addr
|
||||
|> Tuple.to_list()
|
||||
|
@ -269,6 +269,80 @@ defmodule IP.Address do
|
|||
:: Prefix.t
|
||||
def to_prefix(%Address{} = address, length), do: IP.Prefix.new(address, length)
|
||||
|
||||
@doc """
|
||||
Returns the IP version of the address.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1"
|
||||
...> |> IP.Address.from_string!
|
||||
...> |> IP.Address.version()
|
||||
4
|
||||
|
||||
iex> "2001:db8::1"
|
||||
...> |> IP.Address.from_string!
|
||||
...> |> IP.Address.version()
|
||||
6
|
||||
"""
|
||||
@spec version(t) :: 4 | 6
|
||||
def version(%Address{version: version}), do: version
|
||||
|
||||
@doc """
|
||||
Returns the IP Address as an integer
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1"
|
||||
...> |> IP.Address.from_string!
|
||||
...> |> IP.Address.to_integer()
|
||||
3221225985
|
||||
|
||||
iex> "2001:db8::1"
|
||||
...> |> IP.Address.from_string!
|
||||
...> |> IP.Address.to_integer()
|
||||
42540766411282592856903984951653826561
|
||||
"""
|
||||
@spec to_integer(t) :: ipv4 | ipv6
|
||||
def to_integer(%Address{address: address}), do: address
|
||||
|
||||
@doc """
|
||||
Returns true if `address` is version 6.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1"
|
||||
...> |> IP.Address.from_string!()
|
||||
...> |> IP.Address.v6?
|
||||
false
|
||||
|
||||
iex> "2001:db8::"
|
||||
...> |> IP.Address.from_string!()
|
||||
...> |> IP.Address.v6?
|
||||
true
|
||||
"""
|
||||
@spec v6?(t) :: true | false
|
||||
def v6?(%Address{version: 6} = _address), do: true
|
||||
def v6?(_address), do: false
|
||||
|
||||
@doc """
|
||||
Returns true if `address` is version 4.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1"
|
||||
...> |> IP.Address.from_string!()
|
||||
...> |> IP.Address.v4?
|
||||
true
|
||||
|
||||
iex> "2001:db8::"
|
||||
...> |> IP.Address.from_string!()
|
||||
...> |> IP.Address.v4?
|
||||
false
|
||||
"""
|
||||
@spec v4?(t) :: true | false
|
||||
def v4?(%Address{version: 4} = _address), do: true
|
||||
def v4?(_address), do: false
|
||||
|
||||
defp from_bytes([a, b, c, d]) do
|
||||
(a <<< 24) + (b <<< 16) + (c <<< 8) + d
|
||||
end
|
||||
|
|
194
lib/ip/prefix.ex
194
lib/ip/prefix.ex
|
@ -1,7 +1,9 @@
|
|||
defmodule IP.Prefix do
|
||||
alias IP.{Prefix, Address}
|
||||
alias IP.Prefix.{Parser, InvalidPrefix, Helpers}
|
||||
defstruct ~w(address mask)a
|
||||
use Bitwise
|
||||
import Helpers
|
||||
|
||||
@moduledoc """
|
||||
Defines an IP prefix, otherwise known as a subnet.
|
||||
|
@ -36,13 +38,108 @@ defmodule IP.Prefix do
|
|||
%Prefix{address: Address.from_integer!(address &&& mask, 6), mask: mask}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a prefix by attempting to parse a string of unknown version.
|
||||
|
||||
Calling `from_string/2` is faster if you know the IP version of the prefix.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/24"
|
||||
...> |> IP.Prefix.from_string()
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/24>}"
|
||||
|
||||
iex> "192.0.2.1/255.255.255.0"
|
||||
...> |> IP.Prefix.from_string()
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/24>}"
|
||||
|
||||
iex> "2001:db8::/64"
|
||||
...> |> IP.Prefix.from_string()
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<2001:db8::/64>}"
|
||||
"""
|
||||
@spec from_string(binary) :: {:ok, t} | {:error, term}
|
||||
def from_string(prefix), do: Parser.parse(prefix)
|
||||
|
||||
@doc """
|
||||
Create a prefix by attempting to parse a string of specified IP version.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/24"
|
||||
...> |> IP.Prefix.from_string(4)
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/24>}"
|
||||
|
||||
iex> "192.0.2.1/255.255.255.0"
|
||||
...> |> IP.Prefix.from_string(4)
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/24>}"
|
||||
|
||||
iex> "2001:db8::/64"
|
||||
...> |> IP.Prefix.from_string(4)
|
||||
{:error, "Error parsing IPv4 prefix"}
|
||||
"""
|
||||
@spec from_string(binary, 4 | 6) :: {:ok, t} | {:error, term}
|
||||
def from_string(prefix, version), do: Parser.parse(prefix, version)
|
||||
|
||||
@doc """
|
||||
Create a prefix by attempting to parse a string of unknown version.
|
||||
|
||||
Calling `from_string!/2` is faster if you know the IP version of the prefix.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/24"
|
||||
...> |> IP.Prefix.from_string!()
|
||||
#IP.Prefix<192.0.2.0/24>
|
||||
|
||||
iex> "192.0.2.1/255.255.255.0"
|
||||
...> |> IP.Prefix.from_string!()
|
||||
#IP.Prefix<192.0.2.0/24>
|
||||
|
||||
iex> "2001:db8::/64"
|
||||
...> |> IP.Prefix.from_string!()
|
||||
#IP.Prefix<2001:db8::/64>
|
||||
"""
|
||||
@spec from_string!(binary) :: t
|
||||
def from_string!(prefix) do
|
||||
case from_string(prefix) do
|
||||
{:ok, prefix} -> prefix
|
||||
{:error, msg} -> raise(InvalidPrefix, message: msg)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a prefix by attempting to parse a string of specified IP version.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/24"
|
||||
...> |> IP.Prefix.from_string!(4)
|
||||
#IP.Prefix<192.0.2.0/24>
|
||||
|
||||
iex> "192.0.2.1/255.255.255.0"
|
||||
...> |> IP.Prefix.from_string!(4)
|
||||
#IP.Prefix<192.0.2.0/24>
|
||||
"""
|
||||
@spec from_string!(binary, 4 | 6) :: t
|
||||
def from_string!(prefix, version) do
|
||||
case from_string(prefix, version) do
|
||||
{:ok, prefix} -> prefix
|
||||
{:error, msg} -> raise(InvalidPrefix, message: msg)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the bit-length of the prefix.
|
||||
|
||||
## Example
|
||||
|
||||
iex> IP.Address.from_string!("192.0.2.1")
|
||||
...> |> IP.Address.to_prefix(24)
|
||||
iex> "192.0.2.1/24"
|
||||
...> |> IP.Prefix.from_string!()
|
||||
...> |> IP.Prefix.length()
|
||||
24
|
||||
"""
|
||||
|
@ -54,26 +151,57 @@ defmodule IP.Prefix do
|
|||
|
||||
## Example
|
||||
|
||||
iex> IP.Address.from_string!("192.0.2.1")
|
||||
...> |> IP.Address.to_prefix(24)
|
||||
iex> IP.Prefix.from_string!("192.0.2.1/24")
|
||||
...> |> IP.Prefix.mask()
|
||||
0b11111111111111111111111100000000
|
||||
"""
|
||||
@spec mask(t) :: non_neg_integer
|
||||
def mask(%Prefix{mask: mask}), do: mask
|
||||
|
||||
@doc """
|
||||
Returns an old-fashioned subnet mask for IPv4 prefixes.
|
||||
|
||||
## Example
|
||||
|
||||
iex> IP.Prefix.from_string!("192.0.2.0/24")
|
||||
...> |> IP.Prefix.subnet_mask()
|
||||
"255.255.255.0"
|
||||
"""
|
||||
@spec subnet_mask(t) :: binary
|
||||
def subnet_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
|
||||
mask
|
||||
|> Address.from_integer!(4)
|
||||
|> Address.to_string()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an "cisco style" wildcard mask for IPv4 prefixes.
|
||||
|
||||
## Example
|
||||
|
||||
iex> IP.Prefix.from_string!("192.0.2.0/24")
|
||||
...> |> IP.Prefix.wildcard_mask()
|
||||
"0.0.0.255"
|
||||
"""
|
||||
@spec wildcard_mask(t) :: binary
|
||||
def wildcard_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
|
||||
mask
|
||||
|> bnot()
|
||||
|> band(@ipv4_mask)
|
||||
|> Address.from_integer!(4)
|
||||
|> Address.to_string()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first address in the prefix.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IP.Address.from_string!("192.0.2.128")
|
||||
...> |> IP.Address.to_prefix(24)
|
||||
iex> IP.Prefix.from_string!("192.0.2.128/24")
|
||||
...> |> IP.Prefix.first()
|
||||
#IP.Address<192.0.2.0>
|
||||
|
||||
iex> IP.Address.from_string!("2001:db8::128")
|
||||
...> |> IP.Address.to_prefix(64)
|
||||
iex> IP.Prefix.from_string!("2001:db8::128/64")
|
||||
...> |> IP.Prefix.first()
|
||||
#IP.Address<2001:db8::>
|
||||
"""
|
||||
|
@ -87,13 +215,11 @@ defmodule IP.Prefix do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> IP.Address.from_string!("192.0.2.128")
|
||||
...> |> IP.Address.to_prefix(24)
|
||||
iex> IP.Prefix.from_string!("192.0.2.128/24")
|
||||
...> |> IP.Prefix.last()
|
||||
#IP.Address<192.0.2.255>
|
||||
|
||||
iex> IP.Address.from_string!("2001:db8::128")
|
||||
...> |> IP.Address.to_prefix(64)
|
||||
iex> IP.Prefix.from_string!("2001:db8::128/64")
|
||||
...> |> IP.Prefix.last()
|
||||
#IP.Address<2001:db8::ffff:ffff:ffff:ffff>
|
||||
"""
|
||||
|
@ -106,15 +232,38 @@ defmodule IP.Prefix do
|
|||
Address.from_integer!(address, 6)
|
||||
end
|
||||
|
||||
def contains?(%Prefix{address: %Address{address: addr0, version: 4}, mask: mask}, %Address{address: addr1, version: 4})
|
||||
@doc """
|
||||
Returns `true` or `false` depending on whether the supplied `address` is
|
||||
contained within `prefix`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IP.Prefix.from_string!("192.0.2.0/24")
|
||||
...> |> IP.Prefix.contains?(IP.Address.from_string!("192.0.2.127"))
|
||||
true
|
||||
|
||||
iex> IP.Prefix.from_string!("192.0.2.0/24")
|
||||
...> |> IP.Prefix.contains?(IP.Address.from_string!("198.51.100.1"))
|
||||
false
|
||||
|
||||
iex> IP.Prefix.from_string!("2001:db8::/64")
|
||||
...> |> IP.Prefix.contains?(IP.Address.from_string!("2001:db8::1"))
|
||||
true
|
||||
|
||||
iex> IP.Prefix.from_string!("2001:db8::/64")
|
||||
...> |> IP.Prefix.contains?(IP.Address.from_string!("2001:db8:1::1"))
|
||||
false
|
||||
"""
|
||||
def contains?(%Prefix{address: %Address{address: addr0, version: 4}, mask: mask} = _prefix,
|
||||
%Address{address: addr1, version: 4} = _address)
|
||||
when (addr0 &&& mask) <= addr1
|
||||
and ((addr0 &&& mask) + (~~~(mask) &&& @ipv4_mask)) >= addr1
|
||||
do
|
||||
true
|
||||
end
|
||||
|
||||
def contains?(%Prefix{address: %Address{address: addr0, version: 6}, mask: mask},
|
||||
%Address{address: addr1, version: 6})
|
||||
def contains?(%Prefix{address: %Address{address: addr0, version: 6}, mask: mask} = _prefix,
|
||||
%Address{address: addr1, version: 6} = _address)
|
||||
when (addr0 &&& mask) <= addr1
|
||||
and ((addr0 &&& mask) + (~~~(mask) &&& @ipv6_mask)) >= addr1
|
||||
do
|
||||
|
@ -123,19 +272,4 @@ defmodule IP.Prefix do
|
|||
|
||||
def contains?(_prefix, _address), do: false
|
||||
|
||||
defp calculate_mask_from_length(length, mask_length) do
|
||||
pad = mask_length - length
|
||||
0..(length - 1)
|
||||
|> Enum.reduce(0, fn (i, mask) -> mask + (1 <<< i + pad) end)
|
||||
end
|
||||
|
||||
defp calculate_length_from_mask(mask) do
|
||||
mask
|
||||
|> Integer.digits(2)
|
||||
|> Stream.filter(fn
|
||||
1 -> true
|
||||
0 -> false
|
||||
end)
|
||||
|> Enum.count()
|
||||
end
|
||||
end
|
||||
|
|
23
lib/ip/prefix/helpers.ex
Normal file
23
lib/ip/prefix/helpers.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule IP.Prefix.Helpers do
|
||||
use Bitwise
|
||||
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
def calculate_mask_from_length(length, mask_length) do
|
||||
pad = mask_length - length
|
||||
0..(length - 1)
|
||||
|> Enum.reduce(0, fn (i, mask) -> mask + (1 <<< i + pad) end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def calculate_length_from_mask(mask) do
|
||||
mask
|
||||
|> Integer.digits(2)
|
||||
|> Stream.filter(fn
|
||||
1 -> true
|
||||
0 -> false
|
||||
end)
|
||||
|> Enum.count()
|
||||
end
|
||||
end
|
8
lib/ip/prefix/invalid_prefix.ex
Normal file
8
lib/ip/prefix/invalid_prefix.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule IP.Prefix.InvalidPrefix do
|
||||
defexception ~w(message)a
|
||||
|
||||
@moduledoc """
|
||||
An exception raised by the bang-version functions in `IP.Prefix` when the
|
||||
supplied value is invalid.
|
||||
"""
|
||||
end
|
103
lib/ip/prefix/parser.ex
Normal file
103
lib/ip/prefix/parser.ex
Normal file
|
@ -0,0 +1,103 @@
|
|||
defmodule IP.Prefix.Parser do
|
||||
alias IP.{Prefix, Address}
|
||||
import IP.Prefix.Helpers
|
||||
|
||||
@moduledoc """
|
||||
Used internally by `IP.Prefix.from_string` to parse IP prefixes.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Attempts to parse a `prefix` of unknown IP version.
|
||||
|
||||
This attempts to parse as IPv4 and then as IPv6. Obviously it's slower
|
||||
than parsing a specific version if you know that at call time.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/25"
|
||||
...> |> IP.Prefix.Parser.parse()
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/25>}"
|
||||
|
||||
iex> "2001:db8::/64"
|
||||
...> |> IP.Prefix.Parser.parse()
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<2001:db8::/64>}"
|
||||
"""
|
||||
@spec parse(binary) :: Prefix.t
|
||||
def parse(prefix) do
|
||||
case parse(prefix, 4) do
|
||||
{:ok, prefix} -> {:ok, prefix}
|
||||
_ ->
|
||||
case parse(prefix, 6) do
|
||||
{:ok, prefix} -> {:ok, prefix}
|
||||
_ -> {:error, "Unable to parse IP prefix"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Attempts to parse a `prefix` of a specific IP `version` from a string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> "192.0.2.1/25"
|
||||
...> |> IP.Prefix.Parser.parse(4)
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<192.0.2.0/25>}"
|
||||
|
||||
iex> "2001:db8::/64"
|
||||
...> |> IP.Prefix.Parser.parse(6)
|
||||
...> |> inspect()
|
||||
"{:ok, #IP.Prefix<2001:db8::/64>}"
|
||||
"""
|
||||
@spec parse(binary, 4 | 6) :: Prefix.t
|
||||
def parse(prefix, 4 = _version) do
|
||||
with {:ok, address, mask} <- ensure_contains_slash(prefix),
|
||||
{:ok, address} <- Address.from_string(address, 4),
|
||||
{:ok, mask} <- parse_v4_mask(mask)
|
||||
do
|
||||
{:ok, Prefix.new(address, mask)}
|
||||
else
|
||||
_ -> {:error, "Error parsing IPv4 prefix"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(prefix, 6 = _version) do
|
||||
with {:ok, address, mask} <- ensure_contains_slash(prefix),
|
||||
{:ok, address} <- Address.from_string(address, 6),
|
||||
{:ok, mask} <- parse_v6_mask(mask)
|
||||
do
|
||||
{:ok, Prefix.new(address, mask)}
|
||||
else
|
||||
_ -> {:error, "Error parsing IPv6 prefix"}
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_contains_slash(prefix) do
|
||||
case String.split(prefix, "/") do
|
||||
[address, mask] -> {:ok, address, mask}
|
||||
_ -> {:error, "Missing \"/\" in IP prefix"}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_v4_mask(mask) do
|
||||
case Address.from_string(mask, 4) do
|
||||
{:ok, address} ->
|
||||
mask = address
|
||||
|> Address.to_integer()
|
||||
|> calculate_length_from_mask()
|
||||
{:ok, mask}
|
||||
_ ->
|
||||
{:ok, String.to_integer(mask)}
|
||||
end
|
||||
rescue
|
||||
ArgumentError -> {:error, "Unable to parse IPv4 mask"}
|
||||
end
|
||||
|
||||
defp parse_v6_mask(mask) do
|
||||
{:ok, String.to_integer(mask)}
|
||||
rescue
|
||||
ArgumentError -> {:error, "Unable to parse IPv6 mask"}
|
||||
end
|
||||
end
|
4
test/ip/prefix/helpers_test.exs
Normal file
4
test/ip/prefix/helpers_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPPrefixHelpersTest do
|
||||
use ExUnit.Case
|
||||
doctest IP.Prefix.Helpers
|
||||
end
|
4
test/ip/prefix/parser_test.exs
Normal file
4
test/ip/prefix/parser_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule IPPrefixParserTest do
|
||||
use ExUnit.Case
|
||||
doctest IP.Prefix.Parser
|
||||
end
|
Loading…
Reference in a new issue