Module: Ask::Mime

Defined in:
lib/ask/mime.rb

Overview

Lightweight, dependency-free MIME detection and classification.

Sniffs magic bytes for common binary formats and maps extensions for text/documents; everything else falls back to application/octet-stream. Kept deliberately small — ask-core has no runtime dependencies.

Constant Summary collapse

MAGIC =

Common binary signatures → MIME type. Each entry may carry an extra proc that must also match (e.g. RIFF…WEBP for webp).

[
  ["\x89PNG\r\n\x1a\n", "image/png"],
  ["\xFF\xD8\xFF", "image/jpeg"],
  ["GIF87a", "image/gif"],
  ["GIF89a", "image/gif"],
  ["RIFF", "image/webp", ->(bytes) { bytes[8, 4] == "WEBP" }],
  ["%PDF-", "application/pdf"],
  ["OggS", "audio/ogg"]
].freeze
EXTENSIONS =

Extension → MIME type.

{
  "txt" => "text/plain",
  "csv" => "text/csv",
  "json" => "application/json",
  "xml" => "application/xml",
  "md" => "text/markdown",
  "html" => "text/html",
  "rb" => "text/x-ruby",
  "py" => "text/x-python",
  "js" => "text/javascript",
  "ts" => "text/typescript",
  "pdf" => "application/pdf",
  "png" => "image/png",
  "jpg" => "image/jpeg",
  "jpeg" => "image/jpeg",
  "gif" => "image/gif",
  "webp" => "image/webp",
  "mp3" => "audio/mpeg",
  "wav" => "audio/wav",
  "ogg" => "audio/ogg",
  "mp4" => "video/mp4",
  "mov" => "video/quicktime",
  "webm" => "video/webm",
  "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}.freeze

Class Method Summary collapse

Class Method Details

.classify(mime_type) ⇒ Symbol

Classify a MIME type into a broad category.

Parameters:

  • mime_type (String, nil)

Returns:

  • (Symbol)

    one of :image, :audio, :video, :pdf, :document, :text, :unknown



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ask/mime.rb', line 100

def classify(mime_type)
  case mime_type.to_s
  when %r{\Aimage/} then :image
  when %r{\Aaudio/} then :audio
  when %r{\Avideo/} then :video
  when "application/pdf" then :pdf
  when %r{\Atext/}, "application/json", "application/xml",
       "application/javascript",
       "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
       "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
       "application/msword", "application/vnd.ms-excel"
    then :document
  else
    :unknown
  end
end

.detect(filename: nil, bytes: nil, default: "application/octet-stream") ⇒ String

Detect a MIME type from a filename and/or raw bytes.

A known extension wins (docx/xlsx are zip containers and would otherwise sniff as application/zip); magic bytes are used when the extension is unknown.

Parameters:

  • filename (String, nil) (defaults to: nil)
  • bytes (String, nil) (defaults to: nil)
  • default (String) (defaults to: "application/octet-stream")

    fallback MIME type

Returns:

  • (String)


63
64
65
66
67
68
# File 'lib/ask/mime.rb', line 63

def detect(filename: nil, bytes: nil, default: "application/octet-stream")
  from_extension = from_extension(filename) if filename
  return from_extension if from_extension

  sniff(bytes) || default
end

.from_extension(filename) ⇒ String?

Map a filename's extension to a MIME type, or nil.

Parameters:

  • filename (String, nil)

Returns:

  • (String, nil)


90
91
92
93
# File 'lib/ask/mime.rb', line 90

def from_extension(filename)
  extension = filename.to_s.split(".").last&.downcase
  EXTENSIONS[extension]
end

.sniff(bytes) ⇒ String?

Sniff magic bytes. Returns a MIME type or nil.

Parameters:

  • bytes (String)

    raw bytes

Returns:

  • (String, nil)


74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ask/mime.rb', line 74

def sniff(bytes)
  return nil if bytes.nil? || bytes.empty?

  MAGIC.each do |(signature, mime, extra)|
    next unless bytes.start_with?(signature)
    next if extra && !extra.call(bytes)

    return mime
  end
  nil
end