# -*- ruby encoding: utf-8 -*- # The namespace for MIME applications, tools, and libraries. module MIME # Reflects a MIME Content-Type which is in invalid format (e.g., it isn't # in the form of type/subtype). class InvalidContentType < RuntimeError; end # The definition of one MIME content-type. # # == Usage # require 'mime/types' # # plaintext = MIME::Types['text/plain'] # print plaintext.media_type # => 'text' # print plaintext.sub_type # => 'plain' # # puts plaintext.extensions.join(" ") # => 'asc txt c cc h hh cpp' # # puts plaintext.encoding # => 8bit # puts plaintext.binary? # => false # puts plaintext.ascii? # => true # puts plaintext == 'text/plain' # => true # puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip' # class Type # The released version of Ruby MIME::Types VERSION = '1.18' include Comparable MEDIA_TYPE_RE = %r{([-\w.+]+)/([-\w.+]*)}o UNREG_RE = %r{[Xx]-}o ENCODING_RE = %r{(?:base64|7bit|8bit|quoted\-printable)}o PLATFORM_RE = %r|#{RUBY_PLATFORM}|o SIGNATURES = %w(application/pgp-keys application/pgp application/pgp-signature application/pkcs10 application/pkcs7-mime application/pkcs7-signature text/vcard) 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" LTSW_URL = "http://www.ltsw.se/knbase/internet/%s.htp" CONTACT_URL = "http://www.iana.org/assignments/contact-people.htm#%s" # Returns +true+ if the simplified type matches the current def like?(other) if other.respond_to?(:simplified) @simplified == other.simplified else @simplified == Type.simplified(other) end end # Compares the 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 <=> Type.simplified(other.to_s) else @content_type.downcase <=> other.downcase end end # Compares the 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. Generic definitions > platform definitions. # 3. Complete definitions > incomplete definitions. # 4. Current definitions > obsolete definitions. # 5. Obselete with use-instead references > obsolete without. # 6. Obsolete use-instead definitions are compared. def priority_compare(other) pc = simplified <=> other.simplified if pc.zero? and registered? != other.registered? pc = registered? ? -1 : 1 end if pc.zero? and platform? != other.platform? pc = platform? ? 1 : -1 end if pc.zero? and complete? != other.complete? pc = complete? ? -1 : 1 end if pc.zero? and obsolete? != other.obsolete? pc = obsolete? ? 1 : -1 end if pc.zero? and obsolete? and (use_instead != other.use_instead) pc = if use_instead.nil? -1 elsif other.use_instead.nil? 1 else use_instead <=> other.use_instead 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. # # text/plain => text/plain # x-chemical/x-pdb => x-chemical/x-pdb attr_reader :content_type # 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 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 can disappear, adds to the confusing # proliferation of MIME types. The simplified string has the x- # removed and are translated to lowercase. # # text/plain => text/plain # x-chemical/x-pdb => chemical/pdb attr_reader :simplified # 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 and +nil+ values removed. attr_accessor :extensions remove_method :extensions= ; def extensions=(ext) #:nodoc: @extensions = [ext].flatten.compact 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 remove_method :encoding= ; def encoding=(enc) #:nodoc: if enc.nil? or enc == :default @encoding = self.default_encoding elsif enc =~ ENCODING_RE @encoding = enc else raise ArgumentError, "The encoding must be nil, :default, base64, 7bit, 8bit, or quoted-printable." end end # The regexp for the operating system that this MIME::Type is specific # to. attr_accessor :system remove_method :system= ; def system=(os) #:nodoc: if os.nil? or os.kind_of?(Regexp) @system = os else @system = %r|#{os}| end end # Returns the default encoding for the MIME::Type based on the media # type. attr_reader :default_encoding remove_method :default_encoding 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_reader :use_instead remove_method :use_instead def use_instead return nil unless @obsolete @use_instead end # Returns +true+ if the media type is obsolete. def obsolete? @obsolete ? true : false end # Sets the obsolescence indicator for this media type. attr_writer :obsolete # The documentation for this MIME::Type. Documentation about media # types will be found on a media type definition as a comment. # Documentation will be found through #docs. attr_accessor :docs remove_method :docs= ; def docs=(d) if d a = d.scan(%r{use-instead:#{MEDIA_TYPE_RE}}) if a.empty? @use_instead = nil else @use_instead = a.map { |el| "#{el[0]}/#{el[1]}" } end end @docs = d end # The encoded URL list for this MIME::Type. See #urls for more # information. attr_accessor :url # The decoded URL list for this MIME::Type. # The special URL value IANA will be translated into: # http://www.iana.org/assignments/media-types// # # The special URL value RFC### will be translated into: # http://www.rfc-editor.org/rfc/rfc###.txt # # The special URL value DRAFT:name will be translated into: # https://datatracker.ietf.org/public/idindex.cgi? # command=id_detail&filename= # # The special URL value LTSW will be translated into: # http://www.ltsw.se/knbase/internet/.htp # # The special URL value [token] will be translated into: # http://www.iana.org/assignments/contact-people.htm# # # These values will be accessible through #urls, which always returns an # array. def urls @url.map do |el| case el when %r{^IANA$} IANA_URL % [ @media_type, @sub_type ] when %r{^RFC(\d+)$} RFC_URL % $1 when %r{^DRAFT:(.+)$} DRAFT_URL % $1 when %r{^LTSW$} LTSW_URL % @media_type when %r<^\{([^=]+)=([^\]]+)\}> [$1, $2] when %r{^\[([^=]+)=([^\]]+)\]} [$1, CONTACT_URL % $2] when %r{^\[([^\]]+)\]} CONTACT_URL % $1 else el end end 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 can disappear, adds to the confusing # proliferation of MIME types. The simplified string has the # x- removed and are translated to lowercase. def simplified(content_type) matchdata = MEDIA_TYPE_RE.match(content_type) if matchdata.nil? simplified = nil else media_type = matchdata.captures[0].downcase.gsub(UNREG_RE, '') subtype = matchdata.captures[1].downcase.gsub(UNREG_RE, '') simplified = "#{media_type}/#{subtype}" end simplified end # Creates a MIME::Type from an array in the form of: # [type-name, [extensions], encoding, system] # # +extensions+, +encoding+, and +system+ are optional. # # MIME::Type.from_array("application/x-ruby", ['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 def from_array(*args) #:yields MIME::Type.new: # Dereferences the array one level, if necessary. args = args[0] if args[0].kind_of?(Array) if args.size.between?(1, 8) m = MIME::Type.new(args[0]) do |t| t.extensions = args[1] if args.size > 1 t.encoding = args[2] if args.size > 2 t.system = args[3] if args.size > 3 t.obsolete = args[4] if args.size > 4 t.docs = args[5] if args.size > 5 t.url = args[6] if args.size > 6 t.registered = args[7] if args.size > 7 end yield m if block_given? else raise ArgumentError, "Array provided must contain between one and eight elements." end m 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. # # 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 def from_hash(hash) #:yields MIME::Type.new: type = {} hash.each_pair do |k, v| type[k.to_s.tr('A-Z', 'a-z').gsub(/-/, '_').to_sym] = v end m = MIME::Type.new(type[:content_type]) do |t| t.extensions = type[:extensions] t.encoding = type[:content_transfer_encoding] t.system = type[:system] t.obsolete = type[:obsolete] t.docs = type[:docs] t.url = type[:url] t.registered = type[:registered] end yield m if block_given? m end # Essentially a copy constructor. # # 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 def from_mime_type(mime_type) #:yields the new MIME::Type: m = MIME::Type.new(mime_type.content_type.dup) do |t| t.extensions = mime_type.extensions.map { |e| e.dup } t.url = mime_type.url && mime_type.url.map { |e| e.dup } mime_type.system && t.system = mime_type.system.dup mime_type.encoding && t.encoding = mime_type.encoding.dup t.obsolete = mime_type.obsolete? t.registered = mime_type.registered? mime_type.docs && t.docs = mime_type.docs.dup end yield m if block_given? end end # Builds a MIME::Type object from the provided 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. def initialize(content_type) #:yields self: matchdata = MEDIA_TYPE_RE.match(content_type) if matchdata.nil? raise InvalidContentType, "Invalid Content-Type provided ('#{content_type}')" end @content_type = content_type @raw_media_type = matchdata.captures[0] @raw_sub_type = matchdata.captures[1] @simplified = MIME::Type.simplified(@content_type) matchdata = MEDIA_TYPE_RE.match(@simplified) @media_type = matchdata.captures[0] @sub_type = matchdata.captures[1] self.extensions = nil self.encoding = :default self.system = nil self.registered = true self.url = nil self.obsolete = nil self.docs = nil yield self if block_given? end # MIME content-types which are not regestered by IANA nor defined in # RFCs are required to start with x-. This counts as well for # a new media type as well as a new sub-type of an existing media # type. If either the media-type or the content-type begins with # x-, this method will return +false+. def registered? if (@raw_media_type =~ UNREG_RE) || (@raw_sub_type =~ UNREG_RE) false else @registered end end attr_writer :registered #:nodoc: # 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? @encoding == 'base64' 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? not binary? end # Returns +true+ when the simplified MIME type is in the list of known # digital signatures. def signature? SIGNATURES.include?(@simplified.downcase) end # Returns +true+ if the MIME::Type is specific to an operating system. def system? not @system.nil? end # Returns +true+ if the MIME::Type is specific to the current operating # system as represented by RUBY_PLATFORM. def platform? system? and (RUBY_PLATFORM =~ @system) end # Returns +true+ if the MIME::Type specifies an extension list, # indicating that it is a complete MIME::Type. def complete? not @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. def to_str @content_type end # Returns the MIME type as an array suitable for use with # MIME::Type.from_array. def to_a [ @content_type, @extensions, @encoding, @system, @obsolete, @docs, @url, registered? ] end # Returns the MIME type as an array suitable for use with # MIME::Type.from_hash. def to_hash { 'Content-Type' => @content_type, 'Content-Transfer-Encoding' => @encoding, 'Extensions' => @extensions, 'System' => @system, 'Obsolete' => @obsolete, 'Docs' => @docs, 'URL' => @url, 'Registered' => registered?, } end end # = MIME::Types # MIME types are used in MIME-compliant communications, as in e-mail or # HTTP traffic, to indicate the type of content which is transmitted. # MIME::Types provides the ability for detailed information about MIME # entities (provided as a set of MIME::Type objects) to be determined and # used programmatically. There are many types defined by RFCs and vendors, # so the list is long but not complete; don't hesitate to ask to add # additional information. This library follows the IANA collection of MIME # types (see below for reference). # # == Description # MIME types are used in MIME entities, as in email or HTTP traffic. It is # useful at times to have information available about MIME types (or, # inversely, about files). A MIME::Type stores the known information about # one MIME type. # # == Usage # require 'mime/types' # # plaintext = MIME::Types['text/plain'] # print plaintext.media_type # => 'text' # print plaintext.sub_type # => 'plain' # # puts plaintext.extensions.join(" ") # => 'asc txt c cc h hh cpp' # # puts plaintext.encoding # => 8bit # puts plaintext.binary? # => false # puts plaintext.ascii? # => true # puts plaintext.obsolete? # => false # puts plaintext.registered? # => true # puts plaintext == 'text/plain' # => true # puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip' # # This module is built to conform to the MIME types of RFCs 2045 and 2231. # It follows the official IANA registry at # http://www.iana.org/assignments/media-types/ and # ftp://ftp.iana.org/assignments/media-types with some unofficial types # added from the the collection at # http://www.ltsw.se/knbase/internet/mime.htp # # This is originally based on Perl MIME::Types by Mark Overmeer. # # = Author # Copyright:: Copyright (c) 2002 - 2009 by Austin Ziegler # # Version:: 1.18 # Based On:: Perl # MIME::Types[http://search.cpan.org/author/MARKOV/MIME-Types-1.27/MIME/Types.pm], # Copyright (c) 2001 - 2009 by Mark Overmeer # . # Licence:: Ruby's, Perl Artistic, or GPL version 2 (or later) # See Also:: http://www.iana.org/assignments/media-types/ # http://www.ltsw.se/knbase/internet/mime.htp # class Types # The released version of Ruby MIME::Types VERSION = MIME::Type::VERSION # The data version. attr_reader :data_version def initialize(data_version = nil) @type_variants = Hash.new { |h, k| h[k] = [] } @extension_index = Hash.new { |h, k| h[k] = [] } @data_version = data_version end def add_type_variant(mime_type) #:nodoc: @type_variants[mime_type.simplified] << mime_type end def index_extensions(mime_type) #:nodoc: mime_type.extensions.each { |ext| @extension_index[ext] << mime_type } end def defined_types #:nodoc: @type_variants.values.flatten end @__types__ = self.new(VERSION) # Returns a list of MIME::Type objects, which may be empty. The optional # flag parameters are :complete (finds only complete MIME::Type objects) # and :platform (finds only MIME::Types for the current platform). It is # possible for multiple matches to be returned for either type (in the # example below, 'text/plain' returns two values -- one for the general # case, and one for VMS systems. # # puts "\nMIME::Types['text/plain']" # MIME::Types['text/plain'].each { |t| puts t.to_a.join(", ") } # # puts "\nMIME::Types[/^image/, :complete => true]" # MIME::Types[/^image/, :complete => true].each do |t| # puts t.to_a.join(", ") # end # # If multiple type definitions are returned, returns them sorted as # follows: # 1. Complete definitions sort before incomplete ones; # 2. IANA-registered definitions sort before LTSW-recorded # definitions. # 3. Generic definitions sort before platform-specific ones; # 4. Current definitions sort before obsolete ones; # 5. Obsolete definitions with use-instead clauses sort before those # without; # 6. Obsolete definitions use-instead clauses are compared. # 7. Sort on name. def [](type_id, flags = {}) if type_id.kind_of?(Regexp) matches = [] @type_variants.each_key do |k| matches << @type_variants[k] if k =~ type_id end matches.flatten! elsif type_id.kind_of?(MIME::Type) matches = [type_id] else matches = @type_variants[MIME::Type.simplified(type_id)] end matches.delete_if { |e| not e.complete? } if flags[:complete] matches.delete_if { |e| not e.platform? } if flags[:platform] matches.sort { |a, b| a.priority_compare(b) } end # Return the list of MIME::Types which belongs to the file based on its # filename extension. If +platform+ is +true+, then only file types that # are specific to the current platform will be returned. # # This will always return an array. # # puts "MIME::Types.type_for('citydesk.xml') # => [application/xml, text/xml] # puts "MIME::Types.type_for('citydesk.gif') # => [image/gif] def type_for(filename, platform = false) ext = filename.chomp.downcase.gsub(/.*\./o, '') list = @extension_index[ext] list.delete_if { |e| not e.platform? } if platform list end # A synonym for MIME::Types.type_for def of(filename, platform = false) type_for(filename, platform) end # Add one or more MIME::Type objects to the set of known types. Each # type should be experimental (e.g., 'application/x-ruby'). If the type # is already known, a warning will be displayed. # # Please inform the maintainer of this module when registered # types are missing. def add(*types) types.each do |mime_type| if mime_type.kind_of? MIME::Types add(*mime_type.defined_types) else if @type_variants.include?(mime_type.simplified) if @type_variants[mime_type.simplified].include?(mime_type) warn "Type #{mime_type} already registered as a variant of #{mime_type.simplified}." unless defined? MIME::Types::STARTUP end end add_type_variant(mime_type) index_extensions(mime_type) end end end class << self def add_type_variant(mime_type) #:nodoc: @__types__.add_type_variant(mime_type) end def index_extensions(mime_type) #:nodoc: @__types__.index_extensions(mime_type) end # The regular expression used to match a file-based MIME type # definition. TEXT_FORMAT_RE = %r{ ^ \s* ([*])? # 0: Unregistered? (!)? # 1: Obsolete? (?:(\w+):)? # 2: Platform marker #{MIME::Type::MEDIA_TYPE_RE} # 3,4: Media type (?:\s+@([^\s]+))? # 5: Extensions (?:\s+:(#{MIME::Type::ENCODING_RE}))? # 6: Encoding (?:\s+'(.+))? # 7: URL list (?:\s+=(.+))? # 8: Documentation \s* $ }x # Build the type list from a file in the format: # # [*][!][os:]mt/st[@ext][:enc]['url-list][=docs] # # == * # An unofficial MIME type. This should be used if and only if the MIME type # is not properly specified (that is, not under either x-type or # vnd.name.type). # # == ! # An obsolete MIME type. May be used with an unofficial MIME type. # # == os: # Platform-specific MIME type definition. # # == mt # The media type. # # == st # The media subtype. # # == @ext # The list of comma-separated extensions. # # == :enc # The encoding. # # == 'url-list # The list of comma-separated URLs. # # == =docs # The documentation string. # # That is, everything except the media type and the subtype is optional. The # more information that's available, though, the richer the values that can # be provided. def load_from_file(filename) #:nodoc: if defined? ::Encoding data = File.open(filename, 'r:UTF-8') { |f| f.read } else data = File.open(filename) { |f| f.read } end data = data.split($/) mime = MIME::Types.new data.each_with_index { |line, index| item = line.chomp.strip.gsub(%r{#.*}o, '') next if item.empty? begin m = TEXT_FORMAT_RE.match(item).captures rescue Exception puts "#{filename}:#{index}: Parsing error in MIME type definitions." puts "=> #{line}" raise end unregistered, obsolete, platform, mediatype, subtype, extensions, encoding, urls, docs = *m extensions &&= extensions.split(/,/) urls &&= urls.split(/,/) mime_type = MIME::Type.new("#{mediatype}/#{subtype}") do |t| t.extensions = extensions t.encoding = encoding t.system = platform t.obsolete = obsolete t.registered = false if unregistered t.docs = docs t.url = urls end mime.add(mime_type) } mime end # Returns a list of MIME::Type objects, which may be empty. The # optional flag parameters are :complete (finds only complete # MIME::Type objects) and :platform (finds only MIME::Types for the # current platform). It is possible for multiple matches to be # returned for either type (in the example below, 'text/plain' returns # two values -- one for the general case, and one for VMS systems. # # puts "\nMIME::Types['text/plain']" # MIME::Types['text/plain'].each { |t| puts t.to_a.join(", ") } # # puts "\nMIME::Types[/^image/, :complete => true]" # MIME::Types[/^image/, :complete => true].each do |t| # puts t.to_a.join(", ") # end def [](type_id, flags = {}) @__types__[type_id, flags] end # Return the list of MIME::Types which belongs to the file based on # its filename extension. If +platform+ is +true+, then only file # types that are specific to the current platform will be returned. # # This will always return an array. # # puts "MIME::Types.type_for('citydesk.xml') # => [application/xml, text/xml] # puts "MIME::Types.type_for('citydesk.gif') # => [image/gif] def type_for(filename, platform = false) @__types__.type_for(filename, platform) end # A synonym for MIME::Types.type_for def of(filename, platform = false) @__types__.type_for(filename, platform) end # Add one or more MIME::Type objects to the set of known types. Each # type should be experimental (e.g., 'application/x-ruby'). If the # type is already known, a warning will be displayed. # # Please inform the maintainer of this module when registered # types are missing. def add(*types) @__types__.add(*types) end end files = Dir[File.join(File.dirname(__FILE__), 'types', '*')] MIME::Types::STARTUP = true unless $DEBUG files.sort.each { |file| add load_from_file(file) } remove_const :STARTUP if defined? STARTUP end end # vim: ft=ruby