File: magic.rb

package info (click to toggle)
ruby-marcel 1.0.4%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 13,236 kB
  • sloc: xml: 7,071; ruby: 601; makefile: 7; javascript: 3
file content (148 lines) | stat: -rw-r--r-- 4,237 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# 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:
    # * <i>type</i>: Mime type
    # * <i>options</i>: Options hash
    #
    # Option keys:
    # * <i>:extensions</i>: String list or single string of file extensions
    # * <i>:parents</i>: String list or single string of parent mime types
    # * <i>:magic</i>: Mime magic specification
    # * <i>:comment</i>: 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).
    # * <i>type</i>: 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