Elegant way to check filetypes?

Hi
Recently I wanted to check if a File is a Picture, and I don’t like checking just the extension.
So with the help of this:


I came to the following solution:
  def is_picture(file) do
    {_, content} = :file.open(file, [:read, :binary])
    fileHeader = :file.read(content, 8)
    :file.close(content)
    case fileHeader do
      {:ok, <<0xFF, 0xD8, _, _, _, _, _, _>>} -> true #jpg
      {:ok, <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>>} -> true #png
      {:ok, <<0x47, 0x49, 0x46, 0x38, 0x37, 0x61, _, _>>} -> true #gif standard
      {:ok, <<0x47, 0x49, 0x46, 0x38, 0x39, 0x61, _, _>>} -> true #gif animated
      _ -> false
    end
  end

As I’m still not experienced with Elixir, I wonder how would you write such a function in an elegant way?

2 Likes

Maybe this package can help, if You are on Linux.

1 Like

Assuming you don’t care about false positives and strictly don’t care about corrupted files, looks good. I would advocate using elixir File module (so you don’t have to open, you can go straight to read) instead of erlang :file module and maybe consider putting it in a with statement. Closing the file I’d put either in an after block, or possibly even not bother (when the calling process dies, the file descriptor will be reclaimed)

2 Likes

Your solution is basically the same as mine, but I have an additional check that the file extension is in the list of allowed file extensions, and then I also check that the magic bytes match the file extension given. It’s not a bulletproof solution but it’s “good enough”.

defmodule MyApp.FileValidation do
  @moduledoc """
  Checks if uploaded files are valid.

  Checks images for both filenames and magic bytes matching.
  """

  @extensions ~w(.jpg .jpeg .png)

  @doc """
  Checks if an image is valid.

  Only JPG/JPEG and PNG are valid files. Checks the file name of the uploaded file as well as the
  magic bytes of the image.
  """
  def image_valid?(file) do
    ext = Path.extname(file.file_name)

    Enum.member?(@extensions, ext) &&
      ((is_jpg?(file.path) && ext in ~w(.jpg .jpeg)) || (is_png?(file.path) && ext == ".png"))
  end

  @doc """
  Checks if a file is a JPG

  Checks the magic bytes. JPG magic bytes: 0xffd8
  """
  @spec is_jpg?(path :: String.t()) :: boolean
  def is_jpg?(path) do
    with {:ok, file_content} <- :file.open(path, [:read, :binary]),
         {:ok, <<255, 216>>} <- :file.read(file_content, 2) do
      true
    else
      _error ->
        false
    end
  end

  @doc """
  Checks if a file is a PNG

  Checks the magic bytes. PNG magic bytes: 0x89504e470d0a1a0a
  """
  @spec is_png?(path :: String.t()) :: boolean
  def is_png?(path) do
    with {:ok, file_content} <- :file.open(path, [:read, :binary]),
         {:ok, <<137, 80, 78, 71, 13, 10, 26, 10>>} <- :file.read(file_content, 8) do
      true
    else
      _error ->
        false
    end
  end
end

This is one of those things where I throw my hands up and go

System.cmd("/usr/bin/file", ["-i", file_path])

There’s just not really anything that does a better job. If I ever get the chance, I’d like to make it into a nif.

Thanks for the replies :slight_smile:

I didn’t know about the with statement until now :smiley:

Haven’t thought about such a simple solution, also a good idea :stuck_out_tongue:

I’ll try to improve my function with your suggestions :+1:t3:

1 Like