Implement parsing of strings into prefixes.

This commit is contained in:
James Harton 2017-10-09 16:38:54 +13:00
parent 99596e4bf7
commit 78885b2b2d
7 changed files with 381 additions and 31 deletions

View file

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

View file

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

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

View file

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

View file

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