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