# frozen_string_literal: true # Code in this file adapted from the mimemagic gem, released under the MIT License. # Copyright (c) 2011 Daniel Mendler. Available at https://github.com/mimemagicrb/mimemagic. require 'marcel/tables' require 'stringio' module Marcel # Mime type detection class Magic attr_reader :type, :mediatype, :subtype # Mime type by type string def initialize(type) @type = type @mediatype, @subtype = type.split('/', 2) end # Add custom mime type. Arguments: # * type: Mime type # * options: Options hash # # Option keys: # * :extensions: String list or single string of file extensions # * :parents: String list or single string of parent mime types # * :magic: Mime magic specification # * :comment: Comment string def self.add(type, options) extensions = [options[:extensions]].flatten.compact TYPE_EXTS[type] = extensions parents = [options[:parents]].flatten.compact TYPE_PARENTS[type] = parents unless parents.empty? extensions.each {|ext| EXTENSIONS[ext] = type } MAGIC.unshift [type, options[:magic]] if options[:magic] end # Removes a mime type from the dictionary. You might want to do this if # you're seeing impossible conflicts (for instance, application/x-gmc-link). # * type: The mime type to remove. All associated extensions and magic are removed too. def self.remove(type) EXTENSIONS.delete_if {|ext, t| t == type } MAGIC.delete_if {|t, m| t == type } TYPE_EXTS.delete(type) TYPE_PARENTS.delete(type) end # Returns true if type is a text format def text?; mediatype == 'text' || child_of?('text/plain'); end # Mediatype shortcuts def image?; mediatype == 'image'; end def audio?; mediatype == 'audio'; end def video?; mediatype == 'video'; end # Returns true if type is child of parent type def child_of?(parent) self.class.child?(type, parent) end # Get string list of file extensions def extensions TYPE_EXTS[type] || [] end # Get mime comment def comment nil # deprecated end # Lookup mime type by file extension def self.by_extension(ext) ext = ext.to_s.downcase mime = ext[0..0] == '.' ? EXTENSIONS[ext[1..-1]] : EXTENSIONS[ext] mime && new(mime) end # Lookup mime type by filename def self.by_path(path) by_extension(File.extname(path)) end # Lookup mime type by magic content analysis. # This is a slow operation. def self.by_magic(io) mime = magic_match(io, :find) mime && new(mime[0]) end # Lookup all mime types by magic content analysis. # This is a slower operation. def self.all_by_magic(io) magic_match(io, :select).map { |mime| new(mime[0]) } end # Return type as string def to_s type end # Allow comparison with string def eql?(other) type == other.to_s end def hash type.hash end alias == eql? def self.child?(child, parent) child == parent || TYPE_PARENTS[child]&.any? {|p| child?(p, parent) } end def self.magic_match(io, method) return magic_match(StringIO.new(io.to_s), method) unless io.respond_to?(:read) io.binmode if io.respond_to?(:binmode) io.set_encoding(Encoding::BINARY) if io.respond_to?(:set_encoding) buffer = "".encode(Encoding::BINARY) MAGIC.send(method) { |type, matches| magic_match_io(io, matches, buffer) } end def self.magic_match_io(io, matches, buffer) matches.any? do |offset, value, children| match = if value if Range === offset io.read(offset.begin, buffer) x = io.read(offset.end - offset.begin + value.bytesize, buffer) x && x.include?(value) else io.read(offset, buffer) io.read(value.bytesize, buffer) == value end end io.rewind match && (!children || magic_match_io(io, children, buffer)) end end private_class_method :magic_match, :magic_match_io end end