# frozen_string_literal: true ## module MIME end # The definition of one MIME content-type. # # == Usage # require 'mime/types' # # plaintext = MIME::Types['text/plain'] # => [ text/plain ] # text = plaintext.first # puts text.media_type # => 'text' # puts text.sub_type # => 'plain' # # puts text.extensions.join(' ') # => 'txt asc c cc h hh cpp hpp dat hlp' # puts text.preferred_extension # => 'txt' # puts text.friendly # => 'Text Document' # puts text.i18n_key # => 'text.plain' # # puts text.encoding # => quoted-printable # puts text.default_encoding # => quoted-printable # puts text.binary? # => false # puts text.ascii? # => true # puts text.obsolete? # => false # puts text.registered? # => true # puts text.provisional? # => false # puts text.complete? # => true # # puts text # => 'text/plain' # # puts text == 'text/plain' # => true # puts 'text/plain' == text # => true # puts text == 'text/x-plain' # => false # puts 'text/x-plain' == text # => false # # puts MIME::Type.simplified('x-appl/x-zip') # => 'x-appl/x-zip' # puts MIME::Type.i18n_key('x-appl/x-zip') # => 'x-appl.x-zip' # # puts text.like?('text/x-plain') # => true # puts text.like?(MIME::Type.new('x-text/x-plain')) # => true # # puts text.xrefs.inspect # => { "rfc" => [ "rfc2046", "rfc3676", "rfc5147" ] } # puts text.xref_urls # => [ "http://www.iana.org/go/rfc2046", # # "http://www.iana.org/go/rfc3676", # # "http://www.iana.org/go/rfc5147" ] # # xtext = MIME::Type.new('x-text/x-plain') # puts xtext.media_type # => 'text' # puts xtext.raw_media_type # => 'x-text' # puts xtext.sub_type # => 'plain' # puts xtext.raw_sub_type # => 'x-plain' # puts xtext.complete? # => false # # puts MIME::Types.any? { |type| type.content_type == 'text/plain' } # => true # puts MIME::Types.all?(&:registered?) # => false # # # Various string representations of MIME types # qcelp = MIME::Types['audio/QCELP'].first # => audio/QCELP # puts qcelp.content_type # => 'audio/QCELP' # puts qcelp.simplified # => 'audio/qcelp' # # xwingz = MIME::Types['application/x-Wingz'].first # => application/x-Wingz # puts xwingz.content_type # => 'application/x-Wingz' # puts xwingz.simplified # => 'application/x-wingz' 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 = "3.4.1" include Comparable # :stopdoc: # TODO verify mime-type character restrictions; I am pretty sure that this is # too wide open. MEDIA_TYPE_RE = %r{([-\w.+]+)/([-\w.+]*)}.freeze I18N_RE = /[^[:alnum:]]/.freeze BINARY_ENCODINGS = %w[base64 8bit].freeze ASCII_ENCODINGS = %w[7bit quoted-printable].freeze # :startdoc: private_constant :MEDIA_TYPE_RE, :I18N_RE, :BINARY_ENCODINGS, :ASCII_ENCODINGS # Builds a MIME::Type object from the +content_type+, a MIME Content Type # value (e.g., 'text/plain' or 'application/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 using # the first element as the content type and the remaining flattened # elements as extensions. # * Otherwise, the content_type will be used as a string. # # Yields the newly constructed +self+ object. def initialize(content_type) # :yields: self @friendly = {} @obsolete = @registered = @provisional = false @preferred_extension = @docs = @use_instead = nil self.extensions = [] case content_type when Hash init_with(content_type) when Array self.content_type = content_type.shift self.extensions = content_type.flatten when MIME::Type init_with(content_type.to_h) else self.content_type = content_type end self.encoding ||= :default self.xrefs ||= {} yield self if block_given? end # Indicates that a MIME type is like another type. This differs from # == because x- prefixes are removed for this comparison. def like?(other) other = if other.respond_to?(:simplified) MIME::Type.simplified(other.simplified, remove_x_prefix: true) else MIME::Type.simplified(other.to_s, remove_x_prefix: true) end MIME::Type.simplified(simplified, remove_x_prefix: true) == other 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.nil? -1 elsif other.respond_to?(:simplified) simplified <=> other.simplified else filtered = "silent" if other == :silent filtered ||= "true" if other == true filtered ||= other.to_s simplified <=> MIME::Type.simplified(filtered) 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? || !(extensions & other.extensions).empty? 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 && ((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.is_a?(MIME::Type) && (self == other) end # Returns a hash based on the #simplified value. # # This maintains the invariant that two #eql? instances must have the same # #hash (although having the same #hash does *not* imply that the objects are # #eql?). # # To see why, suppose a MIME::Type instance +a+ is compared to another object # +b+, and that a.eql?(b) is true. By the definition of #eql?, # we know the following: # # 1. +b+ is a MIME::Type instance itself. # 2. a == b is true. # # Due to the first point, we know that +b+ should respond to the #simplified # method. Thus, per the definition of #<=>, we know that +a.simplified+ must # be equal to +b.simplified+, as compared by the <=> method corresponding to # +a.simplified+. # # Presumably, if a.simplified <=> b.simplified is +0+, then # +a.simplified+ has the same hash as +b.simplified+. So we assume it's # suitable for #hash to delegate to #simplified in service of the #eql? # invariant. def hash simplified.hash 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 the content_type converted to lowercase. # # text/plain => text/plain # x-chemical/x-pdb => x-chemical/x-pdb # audio/QCELP => audio/qcelp attr_reader :simplified # Returns the media type of the simplified MIME::Type. # # text/plain => text # x-chemical/x-pdb => x-chemical # audio/QCELP => audio attr_reader :media_type # Returns the media type of the unmodified MIME::Type. # # text/plain => text # x-chemical/x-pdb => x-chemical # audio/QCELP => audio attr_reader :raw_media_type # Returns the sub-type of the simplified MIME::Type. # # text/plain => plain # x-chemical/x-pdb => pdb # audio/QCELP => QCELP attr_reader :sub_type # Returns the media type of the unmodified MIME::Type. # # text/plain => plain # x-chemical/x-pdb => x-pdb # audio/QCELP => qcelp 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_accessor: extensions def extensions @extensions.to_a end ## def extensions=(value) # :nodoc: @extensions = Set[*Array(value).flatten.compact].freeze MIME::Types.send(:reindex_extensions, self) end # Merge the +extensions+ provided into this MIME::Type. The extensions added # will be merged uniquely. def add_extensions(*extensions) self.extensions += extensions end ## # The preferred extension for this MIME type. If one is not set and there are # exceptions defined, the first extension will be used. # # When setting #preferred_extensions, if #extensions does not contain this # extension, this will be added to #xtensions. # # :attr_accessor: preferred_extension ## def preferred_extension @preferred_extension || extensions.first end ## def preferred_extension=(value) # :nodoc: add_extensions(value) if value @preferred_extension = value 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 enc.nil? || (enc == :default) @encoding = default_encoding elsif BINARY_ENCODINGS.include?(enc) || ASCII_ENCODINGS.include?(enc) @encoding = enc else fail InvalidEncoding, enc end 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 obsolete? ? @use_instead : nil end ## attr_writer :use_instead # Returns +true+ if the media type is obsolete. attr_accessor :obsolete alias_method :obsolete?, :obsolete # 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") @friendly ||= {} case lang when String, Symbol @friendly[lang.to_s] when Array @friendly.update(Hash[*lang]) when Hash @friendly.update(lang) else fail ArgumentError, "Expected a language or translation set, not #{lang.inspect}" 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 ## # The cross-references list for this MIME::Type. # # :attr_accessor: xrefs ## attr_reader :xrefs ## def xrefs=(xrefs) # :nodoc: @xrefs = MIME::Types::Container.new(xrefs) end # The decoded cross-reference URL list for this MIME::Type. def xref_urls xrefs.flat_map { |type, values| name = :"xref_url_for_#{type.tr("-", "_")}" respond_to?(name, true) && xref_map(values, name) || values.to_a } end # Indicates whether the MIME type has been registered with IANA. attr_accessor :registered alias_method :registered?, :registered # Indicates whether the MIME type's registration with IANA is provisional. attr_accessor :provisional # Indicates whether the MIME type's registration with IANA is provisional. def provisional? registered? && @provisional 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? ASCII_ENCODINGS.include?(encoding) end # Indicateswhether the MIME type is declared as a signature type. attr_accessor :signature alias_method :signature?, :signature # 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 # 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. 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? || @docs.empty? coder["friendly"] = @friendly unless @friendly.nil? || @friendly.empty? coder["encoding"] = @encoding coder["extensions"] = @extensions.to_a unless @extensions.empty? coder["preferred-extension"] = @preferred_extension if @preferred_extension if obsolete? coder["obsolete"] = obsolete? coder["use-instead"] = use_instead if use_instead end unless xrefs.empty? {}.tap do |hash| xrefs.each do |k, v| hash[k] = v.to_a.sort end coder["xrefs"] = hash end end coder["registered"] = registered? coder["provisional"] = provisional? if provisional? 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"] || "" self.encoding = coder["encoding"] self.extensions = coder["extensions"] || [] self.preferred_extension = coder["preferred-extension"] self.obsolete = coder["obsolete"] || false self.registered = coder["registered"] || false self.provisional = coder["provisional"] || false self.signature = coder["signature"] self.xrefs = coder["xrefs"] || {} self.use_instead = coder["use-instead"] friendly(coder["friendly"] || {}) end def inspect # :nodoc: # We are intentionally lying here because MIME::Type::Columnar is an # implementation detail. "#" end class << self # MIME media types are case-insensitive, but are typically presented in a # case-preserving format in the type registry. This method converts # +content_type+ to lowercase. # # In previous versions of mime-types, this would also remove any extension # prefix (x-). This is no longer default behaviour, but may be # provided by providing a truth value to +remove_x_prefix+. def simplified(content_type, remove_x_prefix: false) simplify_matchdata(match(content_type), remove_x_prefix) end # Converts a provided +content_type+ into a translation key suitable for # use with the I18n library. def i18n_key(content_type) simplify_matchdata(match(content_type), joiner: ".") { |e| e.gsub!(I18N_RE, "-") } end # Return a +MatchData+ object of the +content_type+ against pattern of # media types. def match(content_type) case content_type when MatchData content_type else MEDIA_TYPE_RE.match(content_type) end end private def simplify_matchdata(matchdata, remove_x = false, joiner: "/") return nil unless matchdata matchdata.captures.map { |e| e.downcase! e.sub!(/^x-/, "") if remove_x yield e if block_given? e }.join(joiner) end end private def content_type=(type_string) match = MEDIA_TYPE_RE.match(type_string) fail InvalidContentType, type_string if match.nil? @content_type = intern_string(type_string) @raw_media_type, @raw_sub_type = match.captures @simplified = intern_string(MIME::Type.simplified(match)) @i18n_key = intern_string(MIME::Type.i18n_key(match)) @media_type, @sub_type = MEDIA_TYPE_RE.match(@simplified).captures @raw_media_type = intern_string(@raw_media_type) @raw_sub_type = intern_string(@raw_sub_type) @media_type = intern_string(@media_type) @sub_type = intern_string(@sub_type) end if String.method_defined?(:-@) def intern_string(string) -string end else # MRI 2.2 and older don't have a method for string interning, # so we simply freeze them for keeping a similar interface def intern_string(string) string.freeze end end def xref_map(values, helper) values.map { |value| send(helper, value) } end def xref_url_for_rfc(value) "http://www.iana.org/go/%s" % value end def xref_url_for_draft(value) "http://www.iana.org/go/%s" % value.sub(/\ARFC/, "draft") end def xref_url_for_rfc_errata(value) "http://www.rfc-editor.org/errata_search.php?eid=%s" % value end def xref_url_for_person(value) "http://www.iana.org/assignments/media-types/media-types.xhtml#%s" % value end def xref_url_for_template(value) "http://www.iana.org/assignments/media-types/%s" % value end end