Skip to main content

lib/linx/ip/subnet.ex

defmodule Linx.IP.Subnet do
  @moduledoc """
  An IPv4 or IPv6 subnet — a network address and a prefix length, parsed
  from CIDR notation.

      iex> Linx.IP.Subnet.parse("10.0.0.0/24")
      {:ok, ~IP"10.0.0.0/24"}

      iex> import Linx.IP
      iex> ~IP"fc00::/16"
      ~IP"fc00::/16"

  ## Subnet operations

      iex> import Linx.IP
      iex> Linx.IP.Subnet.contains?(~IP"10.0.0.0/8", ~IP"10.99.0.5")
      true
      iex> Linx.IP.Subnet.network(~IP"10.0.42.5/24")
      ~IP"10.0.42.0"
      iex> Linx.IP.Subnet.broadcast(~IP"10.0.42.5/24")
      ~IP"10.0.42.255"
  """

  import Bitwise
  alias Linx.IP

  @enforce_keys [:address, :prefix]
  defstruct [:address, :prefix]

  @type t :: %__MODULE__{address: IP.t(), prefix: 0..128}

  @doc """
  Parses a CIDR string (`"10.0.0.0/24"`, `"fc00::/16"`) into a subnet.
  """
  @spec parse(binary) :: {:ok, t} | {:error, term}
  def parse(string) when is_binary(string) do
    case String.split(string, "/") do
      [ip_str, prefix_str] ->
        with {prefix, ""} <- Integer.parse(prefix_str),
             {:ok, %IP{family: family} = ip} <- IP.parse(ip_str),
             :ok <- check_prefix(family, prefix) do
          {:ok, %__MODULE__{address: ip, prefix: prefix}}
        else
          _ -> {:error, {:bad_subnet, string}}
        end

      _ ->
        {:error, {:bad_subnet, string}}
    end
  end

  @doc """
  Returns whether `subnet` contains `ip`.

  False when the address families differ.
  """
  @spec contains?(t, IP.t()) :: boolean
  def contains?(
        %__MODULE__{address: %IP{family: family, bytes: net}, prefix: prefix},
        %IP{family: family, bytes: addr}
      )
      when bit_size(net) == bit_size(addr) do
    <<net_bits::bits-size(prefix), _::bits>> = net
    <<addr_bits::bits-size(prefix), _::bits>> = addr
    net_bits == addr_bits
  end

  def contains?(_, _), do: false

  @doc """
  Returns `subnet`'s network address — the input address with host bits
  zeroed.
  """
  @spec network(t) :: IP.t()
  def network(%__MODULE__{address: %IP{family: family, bytes: bytes}, prefix: prefix}) do
    host_bits = bit_size(bytes) - prefix
    <<net::bits-size(prefix), _::bits-size(host_bits)>> = bytes
    %IP{family: family, bytes: <<net::bits, 0::size(host_bits)>>}
  end

  @doc """
  Returns the IPv4 broadcast address of `subnet`, or `nil` for IPv6 (which
  has no broadcast).
  """
  @spec broadcast(t) :: IP.t() | nil
  def broadcast(%__MODULE__{address: %IP{family: :inet, bytes: bytes}, prefix: prefix}) do
    host_bits = 32 - prefix
    <<net::bits-size(prefix), _::bits-size(host_bits)>> = bytes
    ones = if host_bits == 0, do: 0, else: (1 <<< host_bits) - 1
    %IP{family: :inet, bytes: <<net::bits, ones::size(host_bits)>>}
  end

  def broadcast(%__MODULE__{address: %IP{family: :inet6}}), do: nil

  defp check_prefix(:inet, p) when p in 0..32, do: :ok
  defp check_prefix(:inet6, p) when p in 0..128, do: :ok
  defp check_prefix(_, _), do: :error

  defimpl Inspect do
    def inspect(%{address: ip, prefix: prefix}, _opts) do
      ~s|~IP"#{Linx.IP.to_string(ip)}/#{prefix}"|
    end
  end
end