Skip to main content

lib/systemd/error.ex

defmodule Systemd.Error do
  @moduledoc """
  Structured error returned by systemd and D-Bus operations.
  """

  @type source :: :dbus | :connection | :protocol | :validation
  @type category :: :permission | :not_found | :timeout | :invalid | :unsupported | :unknown

  @type t :: %__MODULE__{
          source: source(),
          reason: atom(),
          category: category(),
          message: String.t(),
          dbus_name: String.t() | nil,
          body: [term()],
          details: term()
        }

  defexception [:source, :reason, :message, :dbus_name, :category, body: [], details: nil]

  @doc false
  @spec dbus_error(String.t() | nil, [term()]) :: t()
  def dbus_error(name, body) when is_list(body) do
    %__MODULE__{
      source: :dbus,
      reason: reason_from_dbus_name(name),
      category: category_from_reason(reason_from_dbus_name(name)),
      message: message_from_body(body, name),
      dbus_name: name,
      body: body
    }
  end

  @doc false
  @spec connection_error(term()) :: t()
  def connection_error(reason) do
    %__MODULE__{
      source: :connection,
      reason: normalize_reason(reason),
      category: category_from_reason(normalize_reason(reason)),
      message: "D-Bus connection failed: #{inspect(reason)}",
      details: reason
    }
  end

  @doc false
  @spec encoding_error(term()) :: t()
  def encoding_error(details) do
    %__MODULE__{
      source: :protocol,
      reason: :unsupported_type,
      category: :unsupported,
      message: "Unsupported D-Bus value or signature: #{inspect(details)}",
      details: details
    }
  end

  @doc false
  @spec protocol_error(term()) :: t()
  def protocol_error(details) do
    %__MODULE__{
      source: :protocol,
      reason: :unexpected_reply,
      category: :unsupported,
      message: "Unexpected D-Bus reply: #{inspect(details)}",
      details: details
    }
  end

  @doc false
  @spec validation_error(term()) :: t()
  def validation_error(reason) do
    %__MODULE__{
      source: :validation,
      reason: :invalid_call,
      category: :invalid,
      message: "Invalid D-Bus call: #{inspect(reason)}",
      details: reason
    }
  end

  defp message_from_body([message | _], _name) when is_binary(message), do: message
  defp message_from_body(_body, name) when is_binary(name), do: name
  defp message_from_body(_body, _name), do: "D-Bus method call failed"

  @doc """
  Returns true when the error is a systemd/D-Bus permission or polkit denial.
  """
  @spec permission?(t()) :: boolean()
  def permission?(%__MODULE__{category: :permission}), do: true
  def permission?(_error), do: false

  defp reason_from_dbus_name("org.freedesktop.DBus.Error.AccessDenied"), do: :access_denied
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.AuthFailed"), do: :auth_failed
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.FileNotFound"), do: :file_not_found

  defp reason_from_dbus_name("org.freedesktop.DBus.Error.InteractiveAuthorizationRequired"),
    do: :interactive_authorization_required

  defp reason_from_dbus_name("org.freedesktop.DBus.Error.InvalidArgs"), do: :invalid_args
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.NoReply"), do: :no_reply
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.ServiceUnknown"), do: :service_unknown
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.UnknownMethod"), do: :unknown_method
  defp reason_from_dbus_name("org.freedesktop.DBus.Error.UnknownObject"), do: :unknown_object
  defp reason_from_dbus_name("org.freedesktop.systemd1.NoSuchJob"), do: :no_such_job
  defp reason_from_dbus_name("org.freedesktop.systemd1.NoSuchUnit"), do: :no_such_unit
  defp reason_from_dbus_name("org.freedesktop.systemd1.UnitExists"), do: :unit_exists
  defp reason_from_dbus_name(_name), do: :dbus_error

  defp category_from_reason(reason)
       when reason in [:access_denied, :auth_failed, :interactive_authorization_required],
       do: :permission

  defp category_from_reason(reason)
       when reason in [
              :file_not_found,
              :no_such_job,
              :no_such_unit,
              :service_unknown,
              :unknown_object
            ],
       do: :not_found

  defp category_from_reason(reason) when reason in [:invalid_args, :invalid_call], do: :invalid
  defp category_from_reason(reason) when reason in [:no_reply, :timeout], do: :timeout

  defp category_from_reason(reason) when reason in [:unexpected_reply, :unsupported_type],
    do: :unsupported

  defp category_from_reason(_reason), do: :unknown

  defp normalize_reason(reason) when is_atom(reason), do: reason
  defp normalize_reason({reason, _}) when is_atom(reason), do: reason
  defp normalize_reason(_reason), do: :error
end