# -*- ruby encoding: utf-8 -*- # The definition of one MIME content-type. # # == Usage # require 'mime/types' # # plaintext = MIME::Types['text/plain'].first # # returns [text/plain, text/plain] # text = plaintext.first # print text.media_type # => 'text' # print text.sub_type # => 'plain' # # puts text.extensions.join(" ") # => 'asc txt c cc h hh cpp' # # puts text.encoding # => 8bit # puts text.binary? # => false # puts text.ascii? # => true # puts text == 'text/plain' # => true # puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip' # # puts MIME::Types.any? { |type| # type.content_type == 'text/plain' # } # => true # puts MIME::Types.all?(&:registered?) # # => false class MIME::Type # Reflects a MIME content-type specification that is not correctly # formatted (it isn't +type+/+subtype+). class InvalidContentType < ArgumentError # :stopdoc: def initialize(type_string) @type_string = type_string end def to_s "Invalid Content-Type #{@type_string.inspect}" end # :startdoc: end # Reflects an unsupported MIME encoding. class InvalidEncoding < ArgumentError # :stopdoc: def initialize(encoding) @encoding = encoding end def to_s "Invalid Encoding #{@encoding.inspect}" end # :startdoc: end # The released version of the mime-types library. VERSION = '2.99' include Comparable # :stopdoc: MEDIA_TYPE_RE = %r{([-\w.+]+)/([-\w.+]*)}o UNREGISTERED_RE = %r{[Xx]-}o I18N_RE = %r{[^[:alnum:]]}o PLATFORM_RE = %r{#{RUBY_PLATFORM}}o DEFAULT_ENCODINGS = [ nil, :default ] BINARY_ENCODINGS = %w(base64 8bit) TEXT_ENCODINGS = %w(7bit quoted-printable) VALID_ENCODINGS = DEFAULT_ENCODINGS + BINARY_ENCODINGS + TEXT_ENCODINGS IANA_URL = 'http://www.iana.org/assignments/media-types/%s/%s' RFC_URL = 'http://rfc-editor.org/rfc/rfc%s.txt' DRAFT_URL = 'http://datatracker.ietf.org/public/idindex.cgi?command=id_details&filename=%s' # rubocop:disable Metrics/LineLength CONTACT_URL = 'http://www.iana.org/assignments/contact-people.htm#%s' # :startdoc: if respond_to? :private_constant private_constant :MEDIA_TYPE_RE, :UNREGISTERED_RE, :I18N_RE, :PLATFORM_RE, :DEFAULT_ENCODINGS, :BINARY_ENCODINGS, :TEXT_ENCODINGS, :VALID_ENCODINGS, :IANA_URL, :RFC_URL, :DRAFT_URL, :CONTACT_URL end # Builds a MIME::Type object from the +content_type+, a MIME Content Type # value (e.g., 'text/plain' or 'applicaton/x-eruby'). The constructed object # is yielded to an optional block for additional configuration, such as # associating extensions and encoding information. # # * When provided a Hash or a MIME::Type, the MIME::Type will be # constructed with #init_with. # * When provided an Array, the MIME::Type will be constructed only using # the first two elements of the array as the content type and # extensions. # * Otherwise, the content_type will be used as a string. # # Yields the newly constructed +self+ object. def initialize(content_type) # :yields self: @friendly = {} self.obsolete = false self.registered = nil self.use_instead = nil self.signature = nil case content_type when Hash init_with(content_type) when Array self.content_type = content_type[0] self.extensions = content_type[1] || [] when MIME::Type init_with(content_type.to_h) else self.content_type = content_type end self.extensions ||= [] self.docs ||= [] self.encoding ||= :default self.xrefs ||= {} yield self if block_given? end # Returns +true+ if the +other+ simplified type matches the current type. def like?(other) if other.respond_to?(:simplified) @simplified == other.simplified else @simplified == MIME::Type.simplified(other) end end # Compares the +other+ MIME::Type against the exact content type or the # simplified type (the simplified type will be used if comparing against # something that can be treated as a String with #to_s). In comparisons, this # is done against the lowercase version of the MIME::Type. def <=>(other) if other.respond_to?(:content_type) @content_type.downcase <=> other.content_type.downcase elsif other.respond_to?(:to_s) @simplified <=> MIME::Type.simplified(other.to_s) end end # Compares the +other+ MIME::Type based on how reliable it is before doing a # normal <=> comparison. Used by MIME::Types#[] to sort types. The # comparisons involved are: # # 1. self.simplified <=> other.simplified (ensures that we # don't try to compare different types) # 2. IANA-registered definitions < other definitions. # 3. Complete definitions < incomplete definitions. # 4. Current definitions < obsolete definitions. # 5. Obselete with use-instead names < obsolete without. # 6. Obsolete use-instead definitions are compared. # # While this method is public, its use is strongly discouraged by consumers # of mime-types. In mime-types 3, this method is likely to see substantial # revision and simplification to ensure current registered content types sort # before unregistered or obsolete content types. def priority_compare(other) pc = simplified <=> other.simplified if pc.zero? pc = if (reg = registered?) != other.registered? reg ? -1 : 1 # registered < unregistered elsif (comp = complete?) != other.complete? comp ? -1 : 1 # complete < incomplete elsif (obs = obsolete?) != other.obsolete? obs ? 1 : -1 # current < obsolete elsif obs and ((ui = use_instead) != (oui = other.use_instead)) if ui.nil? 1 elsif oui.nil? -1 else ui <=> oui end else 0 end end pc end # Returns +true+ if the +other+ object is a MIME::Type and the content types # match. def eql?(other) other.kind_of?(MIME::Type) and self == other end # Returns the whole MIME content-type string. # # The content type is a presentation value from the MIME type registry and # should not be used for comparison. The case of the content type is # preserved, and extension markers (x-) are kept. # # text/plain => text/plain # x-chemical/x-pdb => x-chemical/x-pdb # audio/QCELP => audio/QCELP attr_reader :content_type # A simplified form of the MIME content-type string, suitable for # case-insensitive comparison, with any extension markers (x- text/plain # x-chemical/x-pdb => chemical/pdb # audio/QCELP => audio/qcelp attr_reader :simplified # Returns the media type of the simplified MIME::Type. # # text/plain => text # x-chemical/x-pdb => chemical attr_reader :media_type # Returns the media type of the unmodified MIME::Type. # # text/plain => text # x-chemical/x-pdb => x-chemical attr_reader :raw_media_type # Returns the sub-type of the simplified MIME::Type. # # text/plain => plain # x-chemical/x-pdb => pdb attr_reader :sub_type # Returns the media type of the unmodified MIME::Type. # # text/plain => plain # x-chemical/x-pdb => x-pdb attr_reader :raw_sub_type # The list of extensions which are known to be used for this MIME::Type. # Non-array values will be coerced into an array with #to_a. Array values # will be flattened, +nil+ values removed, and made unique. attr_reader :extensions def extensions=(ext) # :nodoc: @extensions = Array(ext).flatten.compact.uniq # TODO: In mime-types 3.x, we probably want to have a clue about the # container(s) we belong to so we can trigger reindexing when this is done. end # Merge the +extensions+ provided into this MIME::Type. The extensions added # will be merged uniquely. def add_extensions(*extensions) self.extensions = self.extensions + extensions end ## # The preferred extension for this MIME type, if one is set. # # :attr_reader: preferred_extension ## def preferred_extension extensions.first end ## # The encoding (+7bit+, +8bit+, quoted-printable, or +base64+) # required to transport the data of this content type safely across a # network, which roughly corresponds to Content-Transfer-Encoding. A value of # +nil+ or :default will reset the #encoding to the # #default_encoding for the MIME::Type. Raises ArgumentError if the encoding # provided is invalid. # # If the encoding is not provided on construction, this will be either # 'quoted-printable' (for text/* media types) and 'base64' for eveything # else. # # :attr_accessor: encoding ## attr_reader :encoding def encoding=(enc) # :nodoc: if DEFAULT_ENCODINGS.include?(enc) @encoding = default_encoding elsif BINARY_ENCODINGS.include?(enc) or TEXT_ENCODINGS.include?(enc) @encoding = enc else fail InvalidEncoding, enc end end # Returns +nil+ and assignments are ignored. Prior to mime-types 2.99, this # would return the regular expression for the operating system indicated if # the MIME::Type is a system-specific MIME::Type, # # This information about MIME content types is deprecated and will be removed # in mime-types 3. def system MIME::Types.deprecated(self, __method__) nil end def system=(_os) # :nodoc: MIME::Types.deprecated(self, __method__) end # Returns the default encoding for the MIME::Type based on the media type. def default_encoding (@media_type == 'text') ? 'quoted-printable' : 'base64' end ## # Returns the media type or types that should be used instead of this # media type, if it is obsolete. If there is no replacement media type, or # it is not obsolete, +nil+ will be returned. # # :attr_accessor: use_instead ## def use_instead return nil unless obsolete? @use_instead end ## attr_writer :use_instead # Returns +true+ if the media type is obsolete. def obsolete? !!@obsolete end def obsolete=(v) # :nodoc: @obsolete = !!v end # The documentation for this MIME::Type. attr_accessor :docs # A friendly short description for this MIME::Type. # # call-seq: # text_plain.friendly # => "Text File" # text_plain.friendly('en') # => "Text File" def friendly(lang = 'en'.freeze) @friendly ||= {} case lang when String @friendly[lang] when Array @friendly.merge!(Hash[*lang]) when Hash @friendly.merge!(lang) else fail ArgumentError end end # A key suitable for use as a lookup key for translations, such as with # the I18n library. # # call-seq: # text_plain.i18n_key # => "text.plain" # 3gpp_xml.i18n_key # => "application.vnd-3gpp-bsf-xml" # # from application/vnd.3gpp.bsf+xml # x_msword.i18n_key # => "application.word" # # from application/x-msword attr_reader :i18n_key ## # Returns an empty array and warns that this method has been deprecated. # Assignments are ignored. Prior to mime-types 2.99, this was the encoded # references URL list for this MIME::Type. # # This was previously called #url. # # #references has been deprecated and both versions (#references and #url) # will be removed in mime-types 3. # # :attr_accessor: references ## def references(*) MIME::Types.deprecated(self, __method__) [] end ## def references=(_r) # :nodoc: MIME::Types.deprecated(self, __method__) end ## # Returns an empty array and warns that this method has been deprecated. # Assignments are ignored. Prior to mime-types 2.99, this was the encoded # references URL list for this MIME::Type. See #urls for more information. # # #url has been deprecated and both versions (#references and #url) will be # removed in mime-types 3. # # :attr_accessor: url ## def url MIME::Types.deprecated(self, __method__) [] end ## def url=(_r) # :nodoc: MIME::Types.deprecated(self, __method__) end ## # The cross-references list for this MIME::Type. # # :attr_accessor: xrefs ## attr_reader :xrefs ## def xrefs=(x) # :nodoc: @xrefs = MIME::Types::Container.new.merge(x) @xrefs.each_value(&:sort!) @xrefs.each_value(&:uniq!) end # Returns an empty array. Prior to mime-types 2.99, this returned the decoded # URL list for this MIME::Type. # # The special URL value IANA was translated into: # http://www.iana.org/assignments/media-types// # # The special URL value RFC### was translated into: # http://www.rfc-editor.org/rfc/rfc###.txt # # The special URL value DRAFT:name was translated into: # https://datatracker.ietf.org/public/idindex.cgi? # command=id_detail&filename= # # The special URL value [token] was translated into: # http://www.iana.org/assignments/contact-people.htm# # # These values were accessible through #urls, which always returns an array. # # This method is deprecated and will be removed in mime-types 3. def urls MIME::Types.deprecated(self, __method__) [] end # The decoded cross-reference URL list for this MIME::Type. def xref_urls xrefs.flat_map { |(type, values)| case type when 'rfc'.freeze values.map { |data| 'http://www.iana.org/go/%s'.freeze % data } when 'draft'.freeze values.map { |data| 'http://www.iana.org/go/%s'.freeze % data.sub(/\ARFC/, 'draft') } when 'rfc-errata'.freeze values.map { |data| 'http://www.rfc-editor.org/errata_search.php?eid=%s'.freeze % data } when 'person'.freeze values.map { |data| 'http://www.iana.org/assignments/media-types/media-types.xhtml#%s'.freeze % data # rubocop:disable Metrics/LineLength } when 'template'.freeze values.map { |data| 'http://www.iana.org/assignments/media-types/%s'.freeze % data } else # 'uri', 'text', etc. values end } end ## # Prior to BCP 178 (RFC 6648), it could be assumed that MIME content types # that start with x- were unregistered MIME. Per this BCP, this # assumption is no longer being made by default in this library. # # There are three possible registration states for a MIME::Type: # - Explicitly registered, like application/x-www-url-encoded. # - Explicitly not registered, like image/webp. # - Unspecified, in which case the media-type and the content-type will be # scanned to see if they start with x-, indicating that they # are assumed unregistered. # # In mime-types 3, only a MIME content type that is explicitly registered # will be used; there will be assumption that x- types are # unregistered. def registered? if @registered.nil? (@raw_media_type !~ UNREGISTERED_RE) and (@raw_sub_type !~ UNREGISTERED_RE) else !!@registered end end def registered=(v) # :nodoc: @registered = v.nil? ? v : !!v end # MIME types can be specified to be sent across a network in particular # formats. This method returns +true+ when the MIME::Type encoding is set # to base64. def binary? BINARY_ENCODINGS.include?(@encoding) end # MIME types can be specified to be sent across a network in particular # formats. This method returns +false+ when the MIME::Type encoding is # set to base64. def ascii? !binary? end # Returns +true+ when the simplified MIME::Type is one of the known digital # signature types. def signature? !!@signature end def signature=(v) # :nodoc: @signature = !!v end # Returns +false+. Prior to mime-types 2.99, would return +true+ if the # MIME::Type is specific to an operating system. # # This method is deprecated and will be removed in mime-types 3. def system?(*) MIME::Types.deprecated(self, __method__) false end # Returns +false+. Prior to mime-types 2.99, would return +true+ if the # MIME::Type is specific to the current operating system as represented by # RUBY_PLATFORM. # # This method is deprecated and will be removed in mime-types 3. def platform?(*) MIME::Types.deprecated(self, __method__) false end # Returns +true+ if the MIME::Type specifies an extension list, # indicating that it is a complete MIME::Type. def complete? !@extensions.empty? end # Returns the MIME::Type as a string. def to_s content_type end # Returns the MIME::Type as a string for implicit conversions. This allows # MIME::Type objects to appear on either side of a comparison. # # 'text/plain' == MIME::Type.new('text/plain') def to_str content_type end # Returns the MIME::Type as an array suitable for use with # MIME::Type.from_array. # # This method is deprecated and will be removed in mime-types 3. def to_a MIME::Types.deprecated(self, __method__) [ @content_type, @extensions, @encoding, nil, obsolete?, @docs, [], registered? ] end # Returns the MIME::Type as an array suitable for use with # MIME::Type.from_hash. # # This method is deprecated and will be removed in mime-types 3. def to_hash MIME::Types.deprecated(self, __method__) { 'Content-Type' => @content_type, 'Content-Transfer-Encoding' => @encoding, 'Extensions' => @extensions, 'System' => nil, 'Obsolete' => obsolete?, 'Docs' => @docs, 'URL' => [], 'Registered' => registered?, } end # Converts the MIME::Type to a JSON string. def to_json(*args) require 'json' to_h.to_json(*args) end # Converts the MIME::Type to a hash suitable for use in JSON. The output # of this method can also be used to initialize a MIME::Type. def to_h encode_with({}) end # Populates the +coder+ with attributes about this record for # serialization. The structure of +coder+ should match the structure used # with #init_with. # # This method should be considered a private implementation detail. def encode_with(coder) coder['content-type'] = @content_type coder['docs'] = @docs unless @docs.nil? or @docs.empty? coder['friendly'] = @friendly unless @friendly.empty? coder['encoding'] = @encoding coder['extensions'] = @extensions unless @extensions.empty? if obsolete? coder['obsolete'] = obsolete? coder['use-instead'] = use_instead if use_instead end coder['xrefs'] = xrefs unless xrefs.empty? coder['registered'] = registered? coder['signature'] = signature? if signature? coder end # Initialize an empty object from +coder+, which must contain the # attributes necessary for initializing an empty object. # # This method should be considered a private implementation detail. def init_with(coder) self.content_type = coder['content-type'] self.docs = coder['docs'] || [] friendly(coder['friendly'] || {}) self.encoding = coder['encoding'] self.extensions = coder['extensions'] || [] self.obsolete = coder['obsolete'] self.registered = coder['registered'] self.signature = coder['signature'] self.xrefs = coder['xrefs'] || {} self.use_instead = coder['use-instead'] end class << self # The MIME types main- and sub-label can both start with x-, # which indicates that it is a non-registered name. Of course, after # registration this flag may disappear, adds to the confusing # proliferation of MIME types. The simplified +content_type+ string has the # x- removed and is translated to lowercase. def simplified(content_type) matchdata = case content_type when MatchData content_type else MEDIA_TYPE_RE.match(content_type) end return unless matchdata matchdata.captures.map { |e| e.downcase! e.gsub!(UNREGISTERED_RE, ''.freeze) e }.join('/'.freeze) end # Converts a provided +content_type+ into a translation key suitable for # use with the I18n library. def i18n_key(content_type) matchdata = case content_type when MatchData content_type else MEDIA_TYPE_RE.match(content_type) end return unless matchdata matchdata.captures.map { |e| e.downcase! e.gsub!(UNREGISTERED_RE, ''.freeze) e.gsub!(I18N_RE, '-'.freeze) e }.join('.'.freeze) end # Creates a MIME::Type from an +args+ array in the form of: # [ type-name, [ extensions ], encoding, system ] # # +extensions+, and +encoding+ are optional; +system+ is ignored. # # MIME::Type.from_array('application/x-ruby', %w(rb), '8bit') # MIME::Type.from_array([ 'application/x-ruby', [ 'rb' ], '8bit' ]) # # These are equivalent to: # # MIME::Type.new('application/x-ruby') do |t| # t.extensions = %w(rb) # t.encoding = '8bit' # end # # It will yield the type (+t+) if a block is given. # # This method is deprecated and will be removed in mime-types 3. def from_array(*args) # :yields t: MIME::Types.deprecated(self, __method__) # Dereferences the array one level, if necessary. args = args.first if args.first.kind_of? Array unless args.size.between?(1, 8) fail ArgumentError, 'Array provided must contain between one and eight elements.' end MIME::Type.new(args.shift) do |t| t.extensions, t.encoding, _system, t.obsolete, t.docs, _references, t.registered = *args yield t if block_given? end end # Creates a MIME::Type from a +hash+. Keys are case-insensitive, dashes # may be replaced with underscores, and the internal Symbol of the # lowercase-underscore version can be used as well. That is, # Content-Type can be provided as content-type, Content_Type, # content_type, or :content_type. # # Known keys are Content-Type, # Content-Transfer-Encoding, Extensions, and # System. +System+ is ignored. # # MIME::Type.from_hash('Content-Type' => 'text/x-yaml', # 'Content-Transfer-Encoding' => '8bit', # 'System' => 'linux', # 'Extensions' => ['yaml', 'yml']) # # This is equivalent to: # # MIME::Type.new('text/x-yaml') do |t| # t.encoding = '8bit' # t.system = 'linux' # t.extensions = ['yaml', 'yml'] # end # # It will yield the constructed type +t+ if a block has been provided. # # # This method is deprecated and will be removed in mime-types 3. def from_hash(hash) # :yields t: MIME::Types.deprecated(self, __method__) type = {} hash.each_pair do |k, v| type[k.to_s.tr('A-Z', 'a-z').gsub(/-/, '_').to_sym] = v end MIME::Type.new(type[:content_type]) do |t| t.extensions = type[:extensions] t.encoding = type[:content_transfer_encoding] t.obsolete = type[:obsolete] t.docs = type[:docs] t.url = type[:url] t.registered = type[:registered] yield t if block_given? end end # Essentially a copy constructor for +mime_type+. # # MIME::Type.from_mime_type(plaintext) # # is equivalent to: # # MIME::Type.new(plaintext.content_type.dup) do |t| # t.extensions = plaintext.extensions.dup # t.system = plaintext.system.dup # t.encoding = plaintext.encoding.dup # end # # It will yield the type (+t+) if a block is given. # # This method is deprecated and will be removed in mime-types 3. def from_mime_type(mime_type) # :yields the new MIME::Type: MIME::Types.deprecated(self, __method__) new(mime_type) end end private def content_type=(type_string) match = MEDIA_TYPE_RE.match(type_string) fail InvalidContentType, type_string if match.nil? @content_type = type_string @raw_media_type, @raw_sub_type = match.captures @simplified = MIME::Type.simplified(match) @i18n_key = MIME::Type.i18n_key(match) @media_type, @sub_type = MEDIA_TYPE_RE.match(@simplified).captures end end