lib/infer.ex

defmodule Infer do
  @moduledoc """
  A dependency free library to infer file and MIME type by checking the [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)) signature.

  An elixir adaption of the [`infer`](https://github.com/bojand/infer) rust library.
  """

  defmodule Type do
    defstruct [:matcher_type, :mime_type, :extension, :matcher]

    @type matcher_type() :: :app | :archive | :audio | :book | :image | :font | :text
    @type mime_type() :: binary()
    @type extension() :: binary()
    @type matcher_fun() :: fun((binary() -> boolean()))
    @type t() :: %{matcher_type: matcher_type(), mime_type: mime_type(), extension: extension(), matcher: matcher_fun()}
  end

  # Load matchers on compilation
  @matchers Infer.Matchers.list()

  @doc """
  Takes the binary file contents as argument and returns the `t:Infer.Type.t/0` if the file matches one of the supported types. Returns `nil` otherwise.

  ## Examples

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.get(binary)
      %Infer.Type{extension: "png", matcher: &Infer.Image.png?/1, matcher_type: :image, mime_type: "image/png"}

  """
  @spec get(binary()) :: Infer.Type.t() | nil
  def get(binary), do: Enum.find(@matchers, & &1.matcher.(binary))

  @doc """
  Same as `Infer.get/1`, but takes the file path and byte size as argument.

  ## Examples

      iex> Infer.get_from_path("test/images/sample.png", 128)
      %Infer.Type{extension: "png", matcher: &Infer.Image.png?/1, matcher_type: :image, mime_type: "image/png"}

      iex> Infer.get_from_path("test/docs/sample.pptx")
      %Infer.Type{extension: "pptx", matcher: &Infer.Doc.pptx?/1, matcher_type: :doc, mime_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation"}

  """
  @spec get_from_path(binary()) :: Infer.Type.t() | nil
  def get_from_path(path, byte_size \\ 2048) do
    with {:ok, io_device} <- :file.open(path, [:read, :binary]),
         {:ok, binary} <- :file.read(io_device, byte_size) do
      Enum.find(@matchers, & &1.matcher.(binary))
    end
  end

  @doc """
  Takes the binary content and the file extension as arguments. Returns whether the file content is
  of the given extension.

  ## Examples

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.is?(binary, "png")
      true

  """
  @spec is?(binary(), Infer.Type.extension()) :: boolean()
  def is?(binary, extension), do: Enum.any?(@matchers, &(&1.extension == extension && &1.matcher.(binary)))

  @doc """
  Takes the binary content and the file extension as arguments. Returns whether the file content is
  of the given mime type.

  ## Examples

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.mime?(binary, "image/png")
      true

  """
  @spec mime?(binary(), Infer.Type.mime_type()) :: boolean()
  def mime?(binary, mime_type), do: Enum.any?(@matchers, &(&1.mime_type == mime_type && &1.matcher.(binary)))

  @doc """
  Returns whether the given extension is supported.

  ## Examples

      iex> Infer.supported?("png")
      true

  """
  @spec supported?(Infer.Type.extension()) :: boolean()
  def supported?(extension), do: Enum.any?(@matchers, &(&1.extension == extension))

  @doc """
  Returns whether the given mime type is supported.

  ## Examples

      iex> Infer.mime_supported?("image/png")
      true

  """
  @spec mime_supported?(Infer.Type.mime_type()) :: boolean()
  def mime_supported?(mime_type), do: Enum.any?(@matchers, &(&1.mime_type == mime_type))

  @doc """
  Takes the binary file contents as argument and returns whether the file is an application or not.

  ## Examples

      iex> binary = File.read!("test/app/sample.wasm")
      iex> Infer.app?(binary)
      true

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.app?(binary)
      false

  """
  @spec app?(binary()) :: boolean()
  def app?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :app && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is an archive or not.

  ## Examples

      iex> binary = File.read!("test/archives/sample.zip")
      iex> Infer.archive?(binary)
      true

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.archive?(binary)
      false

  """
  @spec archive?(binary()) :: boolean()
  def archive?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :archive && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is an archive or not.

  ## Examples

      iex> binary = File.read!("test/audio/sample.mp3")
      iex> Infer.audio?(binary)
      true

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.audio?(binary)
      false

  """
  @spec audio?(binary()) :: boolean()
  def audio?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :audio && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is an book (epub or mobi) or not.

  ## Examples

      iex> binary = File.read!("test/books/sample.epub")
      iex> Infer.book?(binary)
      true

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.book?(binary)
      false

  """
  @spec book?(binary()) :: boolean()
  def book?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :book && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is a document (microsoft office, open office)

  ## Examples

      iex> binary = File.read!("test/docs/sample.xlsx")
      iex> Infer.document?(binary)
      true

      iex> binary = File.read!("test/docs/sample.pptx")
      iex> Infer.document?(binary)
      true

      iex> binary = File.read!("test/docs/sample.odp")
      iex> Infer.document?(binary)
      true

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.document?(binary)
      false

  """
  @spec document?(binary()) :: boolean()
  def document?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :doc && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is a font or not.

  ## Examples

      iex> binary = File.read!("test/fonts/sample.ttf")
      iex> Infer.font?(binary)
      true

      iex> binary = File.read!("test/app/sample.wasm")
      iex> Infer.font?(binary)
      false

  """
  @spec font?(binary()) :: boolean()
  def font?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :font && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is an image or not.

  ## Examples

      iex> binary = File.read!("test/images/sample.png")
      iex> Infer.image?(binary)
      true

      iex> binary = File.read!("test/app/sample.wasm")
      iex> Infer.image?(binary)
      false

  """
  @spec image?(binary()) :: boolean()
  def image?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :image && &1.matcher.(binary)))

  @doc """
  Takes the binary file contents as argument and returns whether the file is a video or not.

  ## Examples

      iex> binary = File.read!("test/videos/sample.mp4")
      iex> Infer.video?(binary)
      true

      iex> binary = File.read!("test/app/sample.wasm")
      iex> Infer.video?(binary)
      false

  """
  @spec video?(binary()) :: boolean()
  def video?(binary), do: Enum.any?(@matchers, &(&1.matcher_type == :video && &1.matcher.(binary)))
end