lib/ip/prefix.ex

defmodule IP.Prefix do
  alias IP.{Address, Prefix}
  alias IP.Prefix.{EUI64, Helpers, InvalidPrefix, Parser}
  defstruct ~w(address mask)a
  use Bitwise
  import Helpers

  @moduledoc """
  Defines an IP prefix, otherwise known as a subnet.
  """

  @ipv4_mask 0xFFFFFFFF
  @ipv6_mask 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

  @typedoc "Valid IPv4 prefix lengths from 0 to 32."
  @type ipv4_prefix_length :: 0..32

  @typedoc "Valid IPv6 prefix lengths from 0 to 128."
  @type ipv6_prefix_length :: 0..128

  @typedoc "Valid IP prefix length."
  @type prefix_length :: ipv4_prefix_length | ipv6_prefix_length

  @typedoc "The main prefix type, contains an address and a mask value."
  @type t :: %Prefix{address: Address.t(), mask: Address.ip()}

  @doc """
  Create an IP prefix from an `IP.Address` and `length`.

  ## Examples

      iex> IP.Prefix.new(~i(192.0.2.1), 24)
      #IP.Prefix<192.0.2.0/24 DOCUMENTATION>

      iex> IP.Prefix.new(~i(2001:db8::1), 64)
      #IP.Prefix<2001:db8::/64 DOCUMENTATION>
  """
  @spec new(Address.t(), prefix_length) :: t
  def new(%Address{address: address, version: 4}, length) when length >= 0 and length <= 32 do
    mask = calculate_mask_from_length(length, 32)
    %Prefix{address: Address.from_integer!(address, 4), mask: mask}
  end

  def new(%Address{address: address, version: 6}, length) when length >= 0 and length <= 128 do
    mask = calculate_mask_from_length(length, 128)
    %Prefix{address: Address.from_integer!(address, 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 DOCUMENTATION>}"

      iex> "192.0.2.1/255.255.255.0"
      ...> |> IP.Prefix.from_string()
      ...> |> inspect()
      "{:ok, #IP.Prefix<192.0.2.0/24 DOCUMENTATION>}"

      iex> "2001:db8::/64"
      ...> |> IP.Prefix.from_string()
      ...> |> inspect()
      "{:ok, #IP.Prefix<2001:db8::/64 DOCUMENTATION>}"
  """
  @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 DOCUMENTATION>}"

      iex> "192.0.2.1/255.255.255.0"
      ...> |> IP.Prefix.from_string(4)
      ...> |> inspect()
      "{:ok, #IP.Prefix<192.0.2.0/24 DOCUMENTATION>}"

      iex> "2001:db8::/64"
      ...> |> IP.Prefix.from_string(4)
      {:error, "Error parsing IPv4 prefix"}
  """
  @spec from_string(binary, Address.version()) :: {: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 DOCUMENTATION>

      iex> "192.0.2.1/255.255.255.0"
      ...> |> IP.Prefix.from_string!()
      #IP.Prefix<192.0.2.0/24 DOCUMENTATION>

      iex> "2001:db8::/64"
      ...> |> IP.Prefix.from_string!()
      #IP.Prefix<2001:db8::/64 DOCUMENTATION>
  """
  @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 DOCUMENTATION>

      iex> "192.0.2.1/255.255.255.0"
      ...> |> IP.Prefix.from_string!(4)
      #IP.Prefix<192.0.2.0/24 DOCUMENTATION>
  """
  @spec from_string!(binary, Address.version()) :: 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> ~i(192.0.2.1/24)
      ...> |> IP.Prefix.length()
      24
  """
  @spec length(t) :: prefix_length
  def length(%Prefix{mask: mask}), do: calculate_length_from_mask(mask)

  @doc """
  Alter the bit-`length` of the `prefix`.

  ## Example

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.length(25)
      #IP.Prefix<192.0.2.0/25 DOCUMENTATION>
  """
  @spec length(t, prefix_length) :: t
  def length(%Prefix{address: %Address{version: 4}} = prefix, length)
      when is_number(length) and length >= 0 and length <= 32 do
    %{prefix | mask: calculate_mask_from_length(length, 32)}
  end

  def length(%Prefix{address: %Address{version: 6}} = prefix, length)
      when is_number(length) and length >= 0 and length <= 128 do
    %{prefix | mask: calculate_mask_from_length(length, 128)}
  end

  @doc """
  Returns the calculated mask of the prefix.

  ## Example

      iex> ~i(192.0.2.1/24)
      ...> |> IP.Prefix.mask()
      0b11111111111111111111111100000000
  """
  @spec mask(t) :: Address.ip()
  def mask(%Prefix{mask: mask}), do: mask

  @doc """
  Returns an old-fashioned subnet mask for IPv4 prefixes.

  ## Example

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.subnet_mask()
      #IP.Address<255.255.255.0 RESERVED>
  """
  @spec subnet_mask(t) :: Address.t()
  def subnet_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
    mask
    |> Address.from_integer!(4)
  end

  @doc """
  Returns an "cisco style" wildcard mask for IPv4 prefixes.

  ## Example

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.wildcard_mask()
      #IP.Address<0.0.0.255 CURRENT NETWORK>
  """
  @spec wildcard_mask(t) :: Address.t()
  def wildcard_mask(%Prefix{mask: mask, address: %Address{version: 4}}) do
    mask
    |> bnot()
    |> band(@ipv4_mask)
    |> Address.from_integer!(4)
  end

  @doc """
  Returns the first address in the prefix.

  ## Examples

      iex> ~i(192.0.2.128/24)
      ...> |> IP.Prefix.first()
      #IP.Address<192.0.2.0 DOCUMENTATION>

      iex> ~i(2001:db8::128/64)
      ...> |> IP.Prefix.first()
      #IP.Address<2001:db8:: DOCUMENTATION>
  """
  @spec first(t) :: Address.t()
  def first(%Prefix{address: %Address{address: address, version: version}, mask: mask}) do
    Address.from_integer!(lowest_address(address, mask), version)
  end

  @doc """
  Returns the last address in the prefix.

  ## Examples

      iex> ~i(192.0.2.128/24)
      ...> |> IP.Prefix.last()
      #IP.Address<192.0.2.255 DOCUMENTATION>

      iex> ~i(2001:db8::128/64)
      ...> |> IP.Prefix.last()
      #IP.Address<2001:db8::ffff:ffff:ffff:ffff DOCUMENTATION>
  """
  @spec last(t) :: Address.t()
  def last(%Prefix{address: %Address{address: address, version: 4}, mask: mask}) do
    Address.from_integer!(highest_address(address, mask, 4), 4)
  end

  def last(%Prefix{address: %Address{address: address, version: 6}, mask: mask}) do
    Address.from_integer!(highest_address(address, mask, 6), 6)
  end

  @doc """
  Returns `true` or `false` depending on whether the supplied `address` is
  contained within `prefix`.

  ## Examples

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.contains_address?(~i(192.0.2.127))
      true

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.contains_address?(~i(198.51.100.1))
      false

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.contains_address?(~i(2001:db8::1))
      true

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.contains_address?(~i(2001:db8:1::1))
      false

      iex> outside = ~i(2001:db8::/64)
      ...> inside  = IP.Prefix.eui_64!(outside, "60:f8:1d:ad:d8:90")
      ...> IP.Prefix.contains_address?(outside, inside)
      true

  """
  @spec contains_prefix?(t, Address.t()) :: boolean
  def contains_address?(
        %Prefix{address: %Address{address: addr0, version: 4}, mask: mask} = _prefix,
        %Address{address: addr1, version: 4} = _address
      )
      when lowest_address(addr0, mask) <= addr1 and highest_address(addr0, mask, 4) >= addr1 do
    true
  end

  def contains_address?(
        %Prefix{address: %Address{address: addr0, version: 6}, mask: mask} = _prefix,
        %Address{address: addr1, version: 6} = _address
      )
      when lowest_address(addr0, mask) <= addr1 and highest_address(addr0, mask, 6) >= addr1 do
    true
  end

  def contains_address?(_prefix, _address), do: false

  @doc """
  Returns `true` or `false` depending on whether the supplied `inside` is
  completely contained by `outside`.

  ## Examples

      iex> outside = ~i(192.0.2.0/24)
      ...> inside  = ~i(192.0.2.128/25)
      ...> IP.Prefix.contains_prefix?(outside, inside)
      true

      iex> outside = ~i(192.0.2.128/25)
      ...> inside  = ~i(192.0.2.0/24)
      ...> IP.Prefix.contains_prefix?(outside, inside)
      false

      iex> outside = ~i(2001:db8::/64)
      ...> inside  = ~i(2001:db8::/128)
      ...> IP.Prefix.contains_prefix?(outside, inside)
      true

      iex> outside = ~i(2001:db8::/128)
      ...> inside  = ~i(2001:db8::/64)
      ...> IP.Prefix.contains_prefix?(outside, inside)
      false

  """
  @spec contains_prefix?(t, t) :: boolean
  def contains_prefix?(
        %Prefix{address: %Address{address: oaddr, version: 4}, mask: omask} = _outside,
        %Prefix{address: %Address{address: iaddr, version: 4}, mask: imask} = _inside
      )
      when lowest_address(oaddr, omask) <= lowest_address(iaddr, imask) and
             highest_address(oaddr, omask, 4) >= highest_address(iaddr, imask, 4) do
    true
  end

  def contains_prefix?(
        %Prefix{address: %Address{address: oaddr, version: 6}, mask: omask} = _outside,
        %Prefix{address: %Address{address: iaddr, version: 6}, mask: imask} = _inside
      )
      when lowest_address(oaddr, omask) <= lowest_address(iaddr, imask) and
             highest_address(oaddr, omask, 6) >= highest_address(iaddr, imask, 6) do
    true
  end

  def contains_prefix?(_outside, _inside), do: false

  @doc """
  Generate an EUI-64 host address within the specifed IPv6 `prefix`.

  EUI-64 addresses can only be generated for 64 bit long IPv6 prefixes.

  ## Examples

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.eui_64("60:f8:1d:ad:d8:90")
      ...> |> inspect()
      "{:ok, #IP.Address<2001:db8::62f8:1dff:fead:d890 DOCUMENTATION>}"
  """
  @spec eui_64(t, binary) :: {:ok, Address.t()} | {:error, term}
  def eui_64(
        %Prefix{address: %Address{version: 6}, mask: 0xFFFFFFFFFFFFFFFF0000000000000000} = prefix,
        mac
      ) do
    with {:ok, eui_portion} <- EUI64.eui_portion(mac),
         address <- Prefix.first(prefix),
         address <- Address.to_integer(address),
         address <- address + eui_portion do
      Address.from_integer(address, 6)
    end
  end

  @doc """
  Generate an EUI-64 host address within the specifed IPv6 `prefix`.

  EUI-64 addresses can only be generated for 64 bit long IPv6 prefixes.

  ## Examples

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.eui_64!("60:f8:1d:ad:d8:90")
      #IP.Address<2001:db8::62f8:1dff:fead:d890 DOCUMENTATION>
  """
  @spec eui_64!(t, binary) :: Address.t()
  def eui_64!(prefix, mac) do
    case eui_64(prefix, mac) do
      {:ok, address} -> address
      {:error, msg} -> raise(InvalidPrefix, msg)
    end
  end

  @doc """
  Return the address space within this address.

  ## Examples

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.space()
      256

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.space()
      18446744073709551616
  """
  @spec space(t) :: non_neg_integer
  def space(%Prefix{address: %Address{address: address, version: 4}, mask: mask}) do
    first = address &&& mask
    last = first + (~~~mask &&& @ipv4_mask)
    last - first + 1
  end

  def space(%Prefix{address: %Address{address: address, version: 6}, mask: mask}) do
    first = address &&& mask
    last = first + (~~~mask &&& @ipv6_mask)
    last - first + 1
  end

  @doc """
  Return the usable IP address space within this address.

  ## Examples

      iex> ~i(192.0.2.0/24)
      ...> |> IP.Prefix.usable()
      254

      iex> ~i(2001:db8::/64)
      ...> |> IP.Prefix.usable()
      18446744073709551616
  """
  @spec usable(t) :: non_neg_integer
  def usable(%Prefix{address: %Address{version: 4}} = prefix) do
    space =
      prefix
      |> Prefix.space()

    space - 2
  end

  def usable(%Prefix{address: %Address{version: 6}} = prefix) do
    Prefix.space(prefix)
  end
end