Module: Philiprehberger::MimeType
- Defined in:
- lib/philiprehberger/mime_type.rb,
lib/philiprehberger/mime_type/magic.rb,
lib/philiprehberger/mime_type/version.rb,
lib/philiprehberger/mime_type/extensions.rb
Defined Under Namespace
Classes: Error
Constant Summary collapse
- MAGIC_SIGNATURES =
Magic byte signatures for content-based detection Each entry: [offset, byte_pattern, mime_type]
[ # Images [0, [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], 'image/png'], [0, [0xFF, 0xD8, 0xFF], 'image/jpeg'], [0, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], 'image/gif'], [0, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 'image/gif'], [0, [0x42, 0x4D], 'image/bmp'], [0, [0x00, 0x00, 0x01, 0x00], 'image/x-icon'], [0, [0x00, 0x00, 0x02, 0x00], 'image/x-icon'], [8, [0x57, 0x45, 0x42, 0x50], 'image/webp'], [4, [0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63], 'image/heic'], [0, [0x49, 0x49, 0x2A, 0x00], 'image/tiff'], [0, [0x4D, 0x4D, 0x00, 0x2A], 'image/tiff'], # Documents [0, [0x25, 0x50, 0x44, 0x46], 'application/pdf'], [0, [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], 'application/msword'], # Archives [0, [0x50, 0x4B, 0x03, 0x04], 'application/zip'], [0, [0x50, 0x4B, 0x05, 0x06], 'application/zip'], [0, [0x50, 0x4B, 0x07, 0x08], 'application/zip'], [0, [0x1F, 0x8B], 'application/gzip'], [0, [0x42, 0x5A, 0x68], 'application/x-bzip2'], [0, [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 'application/x-7z-compressed'], [0, [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], 'application/vnd.rar'], [0, [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00], 'application/x-xz'], [257, [0x75, 0x73, 0x74, 0x61, 0x72], 'application/x-tar'], # Audio [0, [0x49, 0x44, 0x33], 'audio/mpeg'], [0, [0xFF, 0xFB], 'audio/mpeg'], [0, [0xFF, 0xF3], 'audio/mpeg'], [0, [0xFF, 0xF2], 'audio/mpeg'], [0, [0x52, 0x49, 0x46, 0x46], 'audio/wav'], [0, [0x4F, 0x67, 0x67, 0x53], 'audio/ogg'], [0, [0x66, 0x4C, 0x61, 0x43], 'audio/flac'], # Video [4, [0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D], 'video/mp4'], [4, [0x66, 0x74, 0x79, 0x70, 0x4D, 0x53, 0x4E, 0x56], 'video/mp4'], [4, [0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32], 'video/mp4'], [0, [0x1A, 0x45, 0xDF, 0xA3], 'video/webm'], [0, [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70], 'video/mp4'], # Fonts [0, [0x77, 0x4F, 0x46, 0x46], 'font/woff'], [0, [0x77, 0x4F, 0x46, 0x32], 'font/woff2'], [0, [0x00, 0x01, 0x00, 0x00], 'font/ttf'], [0, [0x4F, 0x54, 0x54, 0x4F], 'font/otf'], # Binary [0, [0x4D, 0x5A], 'application/vnd.microsoft.portable-executable'], [0, [0x7F, 0x45, 0x4C, 0x46], 'application/x-elf'], [0, [0xCA, 0xFE, 0xBA, 0xBE], 'application/java-vm'], # WebAssembly [0, [0x00, 0x61, 0x73, 0x6D], 'application/wasm'], # SQLite [0, [0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74], 'application/vnd.sqlite3'] ].freeze
- VERSION =
'0.3.0'- EXTENSION_MAP =
Extension to MIME type mappings
{ # Text '.txt' => 'text/plain', '.html' => 'text/html', '.htm' => 'text/html', '.css' => 'text/css', '.csv' => 'text/csv', '.tsv' => 'text/tab-separated-values', '.xml' => 'text/xml', '.rtf' => 'text/rtf', '.md' => 'text/markdown', '.yaml' => 'text/yaml', '.yml' => 'text/yaml', '.ics' => 'text/calendar', '.vcf' => 'text/vcard', '.vtt' => 'text/vtt', # JavaScript / JSON '.js' => 'text/javascript', '.mjs' => 'text/javascript', '.json' => 'application/json', '.jsonld' => 'application/ld+json', '.map' => 'application/json', # Images '.png' => 'image/png', '.jpg' => 'image/jpeg', '.jpeg' => 'image/jpeg', '.gif' => 'image/gif', '.bmp' => 'image/bmp', '.ico' => 'image/x-icon', '.svg' => 'image/svg+xml', '.webp' => 'image/webp', '.avif' => 'image/avif', '.tiff' => 'image/tiff', '.tif' => 'image/tiff', '.heic' => 'image/heic', '.heif' => 'image/heif', '.jxl' => 'image/jxl', '.apng' => 'image/apng', '.psd' => 'image/vnd.adobe.photoshop', # Audio '.mp3' => 'audio/mpeg', '.wav' => 'audio/wav', '.ogg' => 'audio/ogg', '.oga' => 'audio/ogg', '.flac' => 'audio/flac', '.aac' => 'audio/aac', '.m4a' => 'audio/mp4', '.wma' => 'audio/x-ms-wma', '.opus' => 'audio/opus', '.mid' => 'audio/midi', '.midi' => 'audio/midi', '.weba' => 'audio/webm', '.aiff' => 'audio/aiff', # Video '.mp4' => 'video/mp4', '.m4v' => 'video/mp4', '.avi' => 'video/x-msvideo', '.mov' => 'video/quicktime', '.wmv' => 'video/x-ms-wmv', '.flv' => 'video/x-flv', '.webm' => 'video/webm', '.mkv' => 'video/x-matroska', '.ogv' => 'video/ogg', '.mpeg' => 'video/mpeg', '.mpg' => 'video/mpeg', '.3gp' => 'video/3gpp', '.3g2' => 'video/3gpp2', '.m2ts' => 'video/mp2t', '.m3u8' => 'application/vnd.apple.mpegurl', # Application / Documents '.pdf' => 'application/pdf', '.doc' => 'application/msword', '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xls' => 'application/vnd.ms-excel', '.xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.ppt' => 'application/vnd.ms-powerpoint', '.pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', '.odt' => 'application/vnd.oasis.opendocument.text', '.ods' => 'application/vnd.oasis.opendocument.spreadsheet', '.odp' => 'application/vnd.oasis.opendocument.presentation', '.epub' => 'application/epub+zip', # Archives '.zip' => 'application/zip', '.gz' => 'application/gzip', '.gzip' => 'application/gzip', '.tar' => 'application/x-tar', '.bz2' => 'application/x-bzip2', '.7z' => 'application/x-7z-compressed', '.rar' => 'application/vnd.rar', '.xz' => 'application/x-xz', '.zst' => 'application/zstd', '.lz' => 'application/x-lzip', '.lzma' => 'application/x-lzma', '.cab' => 'application/vnd.ms-cab-compressed', '.dmg' => 'application/x-apple-diskimage', '.iso' => 'application/x-iso9660-image', # Executables / Binary '.exe' => 'application/vnd.microsoft.portable-executable', '.dll' => 'application/vnd.microsoft.portable-executable', '.deb' => 'application/vnd.debian.binary-package', '.rpm' => 'application/x-rpm', '.msi' => 'application/x-msi', '.apk' => 'application/vnd.android.package-archive', '.jar' => 'application/java-archive', '.war' => 'application/java-archive', '.class' => 'application/java-vm', # Fonts '.woff' => 'font/woff', '.woff2' => 'font/woff2', '.ttf' => 'font/ttf', '.otf' => 'font/otf', '.eot' => 'application/vnd.ms-fontobject', # Data / Config '.sql' => 'application/sql', '.sqlite' => 'application/vnd.sqlite3', '.db' => 'application/octet-stream', '.wasm' => 'application/wasm', '.graphql' => 'application/graphql', '.toml' => 'application/toml', # Programming '.rb' => 'text/x-ruby', '.py' => 'text/x-python', '.java' => 'text/x-java-source', '.c' => 'text/x-c', '.cpp' => 'text/x-c++', '.h' => 'text/x-c', '.hpp' => 'text/x-c++', '.rs' => 'text/x-rust', '.go' => 'text/x-go', '.swift' => 'text/x-swift', '.kt' => 'text/x-kotlin', '.ts' => 'text/typescript', '.tsx' => 'text/tsx', '.jsx' => 'text/jsx', '.php' => 'text/x-php', '.sh' => 'application/x-sh', '.bash' => 'application/x-sh', '.bat' => 'application/x-msdos-program', '.ps1' => 'application/x-powershell', '.lua' => 'text/x-lua', '.r' => 'text/x-r', '.scala' => 'text/x-scala', '.pl' => 'text/x-perl', '.dart' => 'application/vnd.dart', '.ex' => 'text/x-elixir', '.erl' => 'text/x-erlang', '.hs' => 'text/x-haskell', '.clj' => 'text/x-clojure', '.vim' => 'text/x-vim', '.dockerfile' => 'text/x-dockerfile', # Misc '.swf' => 'application/x-shockwave-flash', '.atom' => 'application/atom+xml', '.rss' => 'application/rss+xml', '.m3u' => 'audio/x-mpegurl', '.pls' => 'audio/x-scpls', '.torrent' => 'application/x-bittorrent', '.bin' => 'application/octet-stream', '.dat' => 'application/octet-stream', '.crt' => 'application/x-x509-ca-cert', '.pem' => 'application/x-pem-file', '.p12' => 'application/x-pkcs12', '.key' => 'application/x-pem-file', '.asc' => 'application/pgp-signature', '.gpg' => 'application/pgp-encrypted', '.sig' => 'application/pgp-signature', '.eml' => 'message/rfc822', '.mbox' => 'application/mbox', '.xpi' => 'application/x-xpinstall', '.crx' => 'application/x-chrome-extension' }.freeze
- MIME_TO_EXTENSIONS =
Reverse mapping: MIME type to extensions
EXTENSION_MAP.each_with_object({}) do |(ext, mime), hash| (hash[mime] ||= []) << ext end.freeze
Class Method Summary collapse
-
.application?(mime) ⇒ Boolean
Check if a MIME type is an application type.
-
.audio?(mime) ⇒ Boolean
Check if a MIME type is an audio type.
-
.best_match(available, accept_header) ⇒ String?
Content negotiation: find the best matching MIME type.
-
.binary?(mime) ⇒ Boolean
Check if a MIME type is a binary (non-text) type.
-
.category(mime) ⇒ Symbol?
Get the category of a MIME type.
-
.charset(mime) ⇒ String?
Return the default charset for a MIME type.
-
.extensions(mime) ⇒ Array<String>
Get file extensions for a MIME type.
-
.font?(mime) ⇒ Boolean
Check if a MIME type is a font type.
-
.for_content(bytes) ⇒ String?
Detect MIME type from file content using magic bytes.
-
.for_extension(ext) ⇒ String?
Detect MIME type from a file extension.
-
.for_filename(name) ⇒ String?
Detect MIME type from a filename.
-
.image?(mime) ⇒ Boolean
Check if a MIME type is an image type.
-
.message?(mime) ⇒ Boolean
Check if a MIME type is a message type.
-
.multipart?(mime) ⇒ Boolean
Check if a MIME type is a multipart type.
-
.parse_accept(header) ⇒ Array<Hash>
Parse an HTTP Accept header string.
-
.register(ext, mime_type) ⇒ void
Register a custom MIME type mapping for an extension.
-
.text?(mime) ⇒ Boolean
Check if a MIME type is a text type.
-
.unregister(ext) ⇒ void
Remove a custom registration for an extension.
-
.valid?(mime) ⇒ Boolean
Check if a MIME type string is valid.
-
.video?(mime) ⇒ Boolean
Check if a MIME type is a video type.
Class Method Details
.application?(mime) ⇒ Boolean
Check if a MIME type is an application type
165 166 167 |
# File 'lib/philiprehberger/mime_type.rb', line 165 def self.application?(mime) category_prefix?(mime, 'application') end |
.audio?(mime) ⇒ Boolean
Check if a MIME type is an audio type
149 150 151 |
# File 'lib/philiprehberger/mime_type.rb', line 149 def self.audio?(mime) category_prefix?(mime, 'audio') end |
.best_match(available, accept_header) ⇒ String?
Content negotiation: find the best matching MIME type
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/philiprehberger/mime_type.rb', line 234 def self.best_match(available, accept_header) return nil if available.nil? || available.empty? parsed = parse_accept(accept_header) return nil if parsed.empty? parsed.each do |entry| accepted = entry[:type] if accepted == '*/*' return available.first end # Check for type/* wildcard if accepted.end_with?('/*') prefix = accepted.split('/').first match = available.find { |a| a.start_with?("#{prefix}/") } return match if match elsif available.include?(accepted) return accepted end end nil end |
.binary?(mime) ⇒ Boolean
Check if a MIME type is a binary (non-text) type
197 198 199 200 201 202 |
# File 'lib/philiprehberger/mime_type.rb', line 197 def self.binary?(mime) type = mime.to_s.strip.downcase return false if type.empty? !type.start_with?('text/') end |
.category(mime) ⇒ Symbol?
Get the category of a MIME type
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/philiprehberger/mime_type.rb', line 89 def self.category(mime) type = mime.to_s.strip.downcase return nil if type.empty? major = type.split('/').first case major when 'text' then :text when 'image' then :image when 'audio' then :audio when 'video' then :video when 'application' then :application when 'font' then :font when 'message' then :message when 'multipart' then :multipart when 'model' then :model end end |
.charset(mime) ⇒ String?
Return the default charset for a MIME type
122 123 124 125 126 127 |
# File 'lib/philiprehberger/mime_type.rb', line 122 def self.charset(mime) type = mime.to_s.strip.downcase return nil if type.empty? type.start_with?('text/') ? 'utf-8' : nil end |
.extensions(mime) ⇒ Array<String>
Get file extensions for a MIME type
81 82 83 |
# File 'lib/philiprehberger/mime_type.rb', line 81 def self.extensions(mime) MIME_TO_EXTENSIONS[mime.to_s.strip.downcase] || [] end |
.font?(mime) ⇒ Boolean
Check if a MIME type is a font type
173 174 175 |
# File 'lib/philiprehberger/mime_type.rb', line 173 def self.font?(mime) category_prefix?(mime, 'font') end |
.for_content(bytes) ⇒ String?
Detect MIME type from file content using magic bytes
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/philiprehberger/mime_type.rb', line 59 def self.for_content(bytes) return nil if bytes.nil? || bytes.empty? raw = bytes.b MAGIC_SIGNATURES.each do |offset, pattern, mime| next if raw.length < offset + pattern.length match = pattern.each_with_index.all? do |byte, i| raw.getbyte(offset + i) == byte end return mime if match end nil end |
.for_extension(ext) ⇒ String?
Detect MIME type from a file extension
38 39 40 41 42 |
# File 'lib/philiprehberger/mime_type.rb', line 38 def self.for_extension(ext) normalized = ext.to_s.strip.downcase normalized = ".#{normalized}" unless normalized.start_with?('.') @custom_extensions[normalized] || EXTENSION_MAP[normalized] end |
.for_filename(name) ⇒ String?
Detect MIME type from a filename
48 49 50 51 52 53 |
# File 'lib/philiprehberger/mime_type.rb', line 48 def self.for_filename(name) ext = File.extname(name.to_s.strip).downcase return nil if ext.empty? @custom_extensions[ext] || EXTENSION_MAP[ext] end |
.image?(mime) ⇒ Boolean
Check if a MIME type is an image type
141 142 143 |
# File 'lib/philiprehberger/mime_type.rb', line 141 def self.image?(mime) category_prefix?(mime, 'image') end |
.message?(mime) ⇒ Boolean
Check if a MIME type is a message type
189 190 191 |
# File 'lib/philiprehberger/mime_type.rb', line 189 def self.(mime) category_prefix?(mime, 'message') end |
.multipart?(mime) ⇒ Boolean
Check if a MIME type is a multipart type
181 182 183 |
# File 'lib/philiprehberger/mime_type.rb', line 181 def self.multipart?(mime) category_prefix?(mime, 'multipart') end |
.parse_accept(header) ⇒ Array<Hash>
Parse an HTTP Accept header string
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/philiprehberger/mime_type.rb', line 208 def self.parse_accept(header) return [] if header.nil? || header.strip.empty? entries = header.split(',').map do |part| segments = part.strip.split(';').map(&:strip) type = segments.first q = 1.0 segments[1..].each do |param| key, value = param.split('=', 2).map(&:strip) if key == 'q' q = value.to_f end end { type: type, q: q } end entries.sort_by { |e| -e[:q] } end |
.register(ext, mime_type) ⇒ void
This method returns an undefined value.
Register a custom MIME type mapping for an extension
18 19 20 21 22 |
# File 'lib/philiprehberger/mime_type.rb', line 18 def self.register(ext, mime_type) normalized = ext.to_s.strip.downcase normalized = ".#{normalized}" unless normalized.start_with?('.') @custom_extensions[normalized] = mime_type end |
.text?(mime) ⇒ Boolean
Check if a MIME type is a text type
133 134 135 |
# File 'lib/philiprehberger/mime_type.rb', line 133 def self.text?(mime) category_prefix?(mime, 'text') end |
.unregister(ext) ⇒ void
This method returns an undefined value.
Remove a custom registration for an extension
28 29 30 31 32 |
# File 'lib/philiprehberger/mime_type.rb', line 28 def self.unregister(ext) normalized = ext.to_s.strip.downcase normalized = ".#{normalized}" unless normalized.start_with?('.') @custom_extensions.delete(normalized) end |
.valid?(mime) ⇒ Boolean
Check if a MIME type string is valid
111 112 113 114 115 116 |
# File 'lib/philiprehberger/mime_type.rb', line 111 def self.valid?(mime) type = mime.to_s.strip return false if type.empty? type.match?(%r{\A[a-zA-Z0-9][a-zA-Z0-9!\#$\-^_.+]*/[a-zA-Z0-9][a-zA-Z0-9!\#$\-^_.+]*\z}) end |
.video?(mime) ⇒ Boolean
Check if a MIME type is a video type
157 158 159 |
# File 'lib/philiprehberger/mime_type.rb', line 157 def self.video?(mime) category_prefix?(mime, 'video') end |