%%============================================================================== %% Copyright 2026 Jan Henry Nystrom %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%============================================================================== %%%------------------------------------------------------------------- %%% @doc %%% A media type determining library based on a subset of the types found in: %%% %%% https://www.iana.org/assignments/media-types/media-types.xhtml %%% %%% %%% The trivially determined types are: %%% %%% application/applefile %%% application/gzip (rfc6713) %%% application/msword %%% application/pdf (rfc8118) %%% application/postscript (rfc2045) %%% application/rtf %%% application/sqlite3 %%% application/vnd.ms-excel %%% application/vnd.ms-powerpoint %%% application/vnd.visio %%% application/zip %%% image/bmp (rfc7903) %%% image/jpeg (rfc2046) %%% image/png (https://www.w3.org/TR/png/) %%% image/vnd.microsoft.icon %%% image/webp (rfc9649) %%% text/calendar (rfc5545) %%% %%% The following types are not registered with IANA but of interest %%% %%% application/x-snappy-framed %%% %%% The following types requires deeper analysis possibly including %%% uncompressing the file and are only determined when the deep or %%% {deep, true} option is provided, if not the encapsulating type, e.g., %%% application/zip. %%% %%% application/atom+xml (rfc4287) %%% application/epub+zip (https://www.w3.org/TR/epub-overview-33/) %%% application/vnd.ms-visio.drawing.main+xml %%% application/vnd.openxmlformats-officedocument.wordprocessingml.document %%% application/ %%% vnd.openxmlformats-officedocument.presentationml.presentation %%% application/vnd.openxmlformats-officedocument.spreadsheetml.sheet %%% text/plain (rfc2046]) %%% %%% The following types are relaxed in that they might contain "non-standard" %%% content as a ^D at the start of the file due to historical reasons %%% and are only determined when the relax or {relax, true} option is %%% provided, if not undefined might be returned. %%% This will only scan a maximum 1024 octets of the data unless deep option %%% is set. %%% %%% application/pdf (rfc8118) %%% application/postscript (rfc2045) %%% %%% So far only a few types are supported but more will be added. %%% %%% @end %% @author Jan Henry Nystrom %% @copyright (C) 2026, Jan Henry Nystrom %%%------------------------------------------------------------------- -module(jhn_magic). -copyright('Jan Henry Nystrom '). %% Library functions -export([check/2, check/3, media_type/1, media_type/2]). %% Exported types -export_type([media_type/0]). %% Includes -include_lib("stdlib/include/zip.hrl"). %% Records -record(opts, {deep = false :: boolean(), relax = false :: boolean() }). %% Types -type media_type() :: binary(). -type opts() :: [opt()]. -type opt() :: deep | relax | {deep, boolean()} | {relax, boolean()}. %% Defines -define(APPLEFILE_SINGLE_MAGIC, 16#00, 16#05, 16#16, 16#00). -define(APPLEFILE_DOUBLE_MAGIC, 16#00, 16#05, 16#16, 16#07). -define(GZIP_MAGIC, 16#1F, 16#8B). -define(MS_MAGIC, 16#D0, 16#CF, 16#11, 16#E0, 16#A1, 16#B1, 16#1A, 16#E1). -define(BMP_MAGIC, 16#42, 16#4D). -define(GIF_MAGIC, "GIF8"). -define(ICO_MAGIC, 16#00, 16#00, 16#01, 16#00). -define(JPEG_MAGIC, 16#FF, 16#D8, 16#FF). -define(PNG_MAGIC, 16#89, 16#50, 16#4E, 16#47, 16#0D, 16#0A, 16#1A, 16#0A). -define(WEBP_MAGIC, "RIFF", _:4/binary, "WEBPVP8"). -define(SNAPPY_MAGIC, 16#FF, 6:24/integer-little, "sNaPpY"). -define(HUMAN_READABLE(X), X > 7, X < 14; X =:= 27; X > 31, X < 127; X == 133; X > 159). -define(UTF8_BOM, 16#EF, 16#BB, 16#BF). -define(UTF16BE_BOM, 16#FE, 16#FF). -define(UTF16LE_BOM, 16#FF, 16#FE). -define(UTF32BE_BOM, 16#0, 16#0, 16#FE, 16#FF). -define(UTF32LE_BOM, 16#FF, 16#FE, 16#0, 16#0). %% =================================================================== %% Library functions. %% =================================================================== %%-------------------------------------------------------------------- %% Function: check(MediaType(s), File) -> Boolean %% @doc %% Equivalent of calling check(MediaType(s) File, []) %% @end %%-------------------------------------------------------------------- -spec check(binary() | [binary()], binary()) -> boolean(). %%-------------------------------------------------------------------- check(Types, File) -> check(Types, File, []). %%-------------------------------------------------------------------- %% Function: check(MediaType(s), File, Options) -> Boolean %% @doc %% The files media type is checked against the type or types provided and %% if they are the same or a member true is returned otherwise false. %% %% Options are: %% {deep, boolean()} -> does more expensive analysis default false %% %% @end %%-------------------------------------------------------------------- -spec check(binary() | [binary()], binary(), opts()) -> boolean(). %%-------------------------------------------------------------------- check(Types = [_|_], File, Opts) -> lists:member(media_type(File, Opts), Types); check(Type = <<_/binary>>, File, Opts) -> Type == media_type(File, Opts). %%-------------------------------------------------------------------- %% Function: media_type(File) -> MediaType. %% @doc %% Equivalent of calling media_type(File, []) %% @end %%-------------------------------------------------------------------- -spec media_type(binary()) -> media_type() | undefined. %%-------------------------------------------------------------------- media_type(File) -> media_type(File, []). %%-------------------------------------------------------------------- %% Function: media_type(File, Options) -> MediaType or undefined %% @doc %% The files media type is returned. %% %% Options are: %% {deep, boolean()} -> does more expensive analysis default false %% %% @end %%-------------------------------------------------------------------- -spec media_type(binary(), opts()) -> media_type() | undefined. %%-------------------------------------------------------------------- media_type(File, Opts) -> gen(File, lists:foldr(fun opt/2, #opts{}, Opts)). %% =================================================================== %% Internal functions. %% =================================================================== opt({deep, Bool}, Opts) when is_boolean(Bool) -> Opts#opts{deep = Bool}; opt({relax, Bool}, Opts) when is_boolean(Bool) -> Opts#opts{relax = Bool}; opt(deep, Opts) -> Opts#opts{deep = true}; opt(relax, Opts) -> Opts#opts{relax = true}. gen(<>, _) -> ~"application/applefile"; gen(<>, _) -> ~"application/applefile"; gen(<>, _) -> ~"application/gzip"; gen(<>, _) -> ms(T); gen(<<"%PDF-", _/binary>>, _) -> ~"application/pdf"; gen(<<"%!PS", _/binary>>, _) -> ~"application/postscript"; gen(<<"{\rtf", _/binary>>, _) -> ~"application/rtf"; gen(<>, _) -> ~"application/snappy-framed"; gen(<<"SQLite format 3", _/binary>>, _) -> ~"application/sqlite3"; gen(ZIP = <<"PK", 3, 4, _/binary>>, #opts{deep = true}) -> zip(ZIP); gen(<<"PK", 3, 4, _/binary>>, #opts{deep = false}) -> ~"application/zip"; gen(XML = <<">, #opts{deep = true}) -> xml(XML); gen(<<">, #opts{deep = false}) -> ~"application/xml"; gen(<>, _) -> ~"image/bmp"; gen(<>, _) -> ~"image/gif"; gen(<>, _) -> ~"image/vnd.microsoft.icon"; gen(<>, _) -> ~"image/jpeg"; gen(<>, _) -> ~"image/png"; gen(<>, _) -> ~"image/webp"; gen(<<"BEGIN:VCALENDAR", _/binary>>, _) -> ~"text/calendar"; gen(<>, _) -> ~"text/plain"; gen(<>, _) -> ~"text/plain"; gen(<>, _) -> ~"text/plain"; gen(<>, _) -> ~"text/plain"; gen(<>, _) -> ~"text/plain"; gen(Data, #opts{deep = true, relax = true}) -> deep_relax_scan(Data, true); gen(TEXT, #opts{deep = true}) -> deep_scan(TEXT); gen(Data, #opts{relax = true}) -> relax_scan(Data, 1024); gen(_, _) -> undefined. xml(XML) -> case xml_type(XML) of none -> ~"application/xml"; ~"http://www.w3.org/2005/Atom" -> ~"application/atom+xml" end. xml_type(~"") -> none; xml_type(<<"xmlns", XML/binary>>) -> xmlns(XML, ~"=\""); xml_type(<<_, T/binary>>) -> xml_type(T). xmlns(<>, ~"") -> xmlns1(T, ~""); xmlns(<>, <>) -> xmlns(T, T1); xmlns(<<_, T/binary>>, Left) -> xmlns(T, Left). xmlns1(<<$\", _/binary>>, Acc) -> Acc; xmlns1(<>, Acc) -> xmlns1(T, <>). ms(~"") -> undefined; ms(<<"Excel", _/binary>>) -> ~"application/vnd.ms-excel"; ms(<<"PowerPoint", _/binary>>) -> ~"application/vnd.ms-powerpoint"; ms(<<"Visio", _/binary>>) -> ~"application/vnd.visio"; ms(<<"Word", _/binary>>) -> ~"application/msword"; ms(<<_, T/binary>>) -> ms(T). zip(Z) -> {ok, Files} = zip:list_dir(Z), Names = [Name || #zip_file{name = Name} <- Files], case zip_category(Names) of zip -> ~"application/zip"; msooxml -> msooxml([hd(string:tokens(N, "/")) || N <- Names]); epub -> epub(Z) end. zip_category([]) -> zip; zip_category(["[Content_Types].xml" | _]) -> msooxml; zip_category(["mimetype" | _]) -> epub; zip_category(["Metadata" | _]) -> apple; zip_category([_ | T]) -> zip_category(T). msooxml([]) -> ~"application/zip"; msooxml(["word" | _]) -> ~"application/vnd.openxmlformats-officedocument.wordprocessingml.document"; msooxml(["ppt" | _]) -> <<"application/" "vnd.openxmlformats-officedocument.presentationml.presentation">>; msooxml(["xl" | _]) -> ~"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; msooxml(["visio" | _]) -> ~"application/vnd.ms-visio.drawing.main+xml"; msooxml([_ | T]) -> msooxml(T). epub(Z) -> {ok, H} = zip:zip_open(Z, [memory]), {ok, {_, MimeType}} = zip:zip_get("mimetype", H), zip:zip_close(H), case MimeType of <<"application/epub+zip", _/binary>> -> ~"application/epub+zip"; _ -> ~"application/zip" end. deep_scan(<<>>) -> ~"text/plain"; deep_scan(<>) when ?HUMAN_READABLE(H) -> deep_scan(T); deep_scan(_) -> ~"application/octet-stream". deep_relax_scan(<<>>, true) -> ~"text/plain"; deep_relax_scan(<<>>, false) -> ~"application/octet-stream"; deep_relax_scan(<<"%PDF-", _/binary>>, _) -> ~"application/pdf"; deep_relax_scan(<<"%!PS", _/binary>>, _) -> ~"application/postscript"; deep_relax_scan(<>, F) when ?HUMAN_READABLE(H) -> deep_relax_scan(T, F); deep_relax_scan(<<_, T/binary>>, _) -> deep_relax_scan(T, false). relax_scan(<<>>, _) -> undefined; relax_scan(_, 0) -> undefined; relax_scan(<<"%PDF-", _/binary>>, _) -> ~"application/pdf"; relax_scan(<<"%!PS", _/binary>>, _) -> ~"application/postscript"; relax_scan(<<_, T/binary>>, N) -> relax_scan(T, N - 1).