From 5e7166bc116c1014d9477abf1c1cd86464ee913e Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 16 Dec 2021 21:37:01 -0500 Subject: [PATCH 1/3] Deprecate MIME::Types.new(Array) --- CHANGELOG.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac73b2..1d610f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## NEXT / YYYY-MM-DD + +- 1 deprecation: + + - Deprecated `MIME::Type#priority_compare`. In a future release, this will be + will be renamed to `MIME::Type#<=>`. This method is used in tight loops, so + there is no warning message for either `MIME::Type#priority_compare` or + `MIME::Type#<=>`. + +- 1 enhancement: + + - Improved the performance of sorting by eliminating the complex comparison + flow from `MIME::Type#priority_compare`. The old version shows under 600 + i/s, and the new version shows over 900 i/s. In sorting the full set of MIME + data, there are three differences between the old and new versions; after + comparison, these differences are considered acceptable. + +- 1 bug fix: + + - Simplified the default compare implementation (`MIME::Type#<=>`) to use the + new `MIME::Type#priority_compare` operation and simplify the fallback to + `String` comparison. This _may_ result in exceptions where there had been + none, as explicit support for several special values (which should have + caused errors in any case) have been removed. + ## 3.6.2 / 2025-03-25 - Updated the reference to the changelog in the README, fixing RubyGems metadata @@ -151,7 +176,7 @@ there are some validation changes and updated code with formatting. ## 3.3 / 2019-09-04 -- 1 minor enhancement +- 1 minor enhancement: - Jean Boussier reduced memory usage for Ruby versions 2.3 or higher by interning various string values in each type. This is done with a From cd93b9565567be77f1b0f3809551961a2c777346 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 3 Apr 2025 00:00:57 -0400 Subject: [PATCH 2/3] Improve comparison performance with pre-computed priority Closes: #151 Resolves: #148 --- CHANGELOG.md | 51 ++++++----- Rakefile | 11 ++- lib/mime/type.rb | 166 +++++++++++++++++++++++----------- lib/mime/type/columnar.rb | 15 ++- lib/mime/types.rb | 26 ++++-- lib/mime/types/_columnar.rb | 55 ++++++++++- lib/mime/types/loader.rb | 2 +- lib/mime/types/version.rb | 2 +- mime-types.gemspec | 17 ++-- test/test_mime_type.rb | 66 +++++++------- test/test_mime_types.rb | 15 ++- test/test_mime_types_class.rb | 14 ++- 12 files changed, 298 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d610f9..9844d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,31 @@ # Changelog -## NEXT / YYYY-MM-DD - -- 1 deprecation: - - - Deprecated `MIME::Type#priority_compare`. In a future release, this will be - will be renamed to `MIME::Type#<=>`. This method is used in tight loops, so - there is no warning message for either `MIME::Type#priority_compare` or - `MIME::Type#<=>`. - -- 1 enhancement: - - - Improved the performance of sorting by eliminating the complex comparison - flow from `MIME::Type#priority_compare`. The old version shows under 600 - i/s, and the new version shows over 900 i/s. In sorting the full set of MIME - data, there are three differences between the old and new versions; after - comparison, these differences are considered acceptable. - -- 1 bug fix: - - - Simplified the default compare implementation (`MIME::Type#<=>`) to use the - new `MIME::Type#priority_compare` operation and simplify the fallback to - `String` comparison. This _may_ result in exceptions where there had been - none, as explicit support for several special values (which should have - caused errors in any case) have been removed. +## 3.7.0.pre2 / YYYY-MM-DD + +- Deprecated `MIME::Type#priority_compare`. In a future release, this will be + will be renamed to `MIME::Type#<=>`. This method is used in tight loops, so + there is no warning message for either `MIME::Type#priority_compare` or + `MIME::Type#<=>`. + +- Improved the performance of sorting by eliminating the complex comparison flow + from `MIME::Type#priority_compare`. The old version shows under 600 i/s, and + the new version shows over 900 i/s. In sorting the full set of MIME data, + there are three differences between the old and new versions; after + comparison, these differences are considered acceptable. + +- Simplified the default compare implementation (`MIME::Type#<=>`) to use the + new `MIME::Type#priority_compare` operation and simplify the fallback to + `String` comparison. This _may_ result in exceptions where there had been + none, as explicit support for several special values (which should have caused + errors in any case) have been removed. + +- When sorting the result of `MIME::Types#type_for`, provided a priority boost + if one of the target extensions is the type's preferred extension. This means + that for the case in [#148][issue-148], when getting the type for `foo.webm`, + the type `video/webm` will be returned before the type `audio/webm`, because + `.webm` is the preferred extension for `video/webm` but not `audio/webm` + (which has a preferred extension of `.weba`). Added tests to ensure MIME types + are retrieved in a stable order (which is alphabetical). ## 3.6.2 / 2025-03-25 @@ -375,6 +377,7 @@ there are some validation changes and updated code with formatting. [issue-127]: https://github.com/mime-types/ruby-mime-types/issues/127 [issue-134]: https://github.com/mime-types/ruby-mime-types/issues/134 [issue-136]: https://github.com/mime-types/ruby-mime-types/issues/136 +[issue-148]: https://github.com/mime-types/ruby-mime-types/issues/148 [issue-166]: https://github.com/mime-types/ruby-mime-types/issues/166 [issue-177]: https://github.com/mime-types/ruby-mime-types/issues/177 [mime-types-data]: https://github.com/mime-types/mime-types-data diff --git a/Rakefile b/Rakefile index 40c1759..af0cc12 100644 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,7 @@ require "rubygems" require "hoe" require "rake/clean" require "minitest" +require "minitest/test_task" Hoe.plugin :halostatue Hoe.plugin :rubygems @@ -10,6 +11,7 @@ Hoe.plugins.delete :debug Hoe.plugins.delete :newb Hoe.plugins.delete :publish Hoe.plugins.delete :signing +Hoe.plugins.delete :test spec = Hoe.spec "mime-types" do developer("Austin Ziegler", "halostatue@gmail.com") @@ -24,7 +26,7 @@ spec = Hoe.spec "mime-types" do val.merge!({"rubygems_mfa_required" => "true"}) } - extra_deps << ["mime-types-data", "~> 3.2015"] + extra_deps << ["mime-types-data", "~> 3.2025", ">= 3.2025.0506.pre2"] extra_deps << ["logger", ">= 0"] extra_dev_deps << ["hoe", "~> 4.0"] @@ -65,6 +67,8 @@ Minitest::TestTask.create :coverage do |t| RUBY end +task default: :test + namespace :benchmark do task :support do %w[lib support].each { |path| @@ -174,6 +178,11 @@ namespace :convert do task docs: "convert:docs:run" end +task :version do + require "mime/types/version" + puts MIME::Types::VERSION +end + namespace :deps do task :top, [:number] => "benchmark:support" do |_, args| require "deps" diff --git a/lib/mime/type.rb b/lib/mime/type.rb index 40688e8..5e46de8 100644 --- a/lib/mime/type.rb +++ b/lib/mime/type.rb @@ -133,7 +133,8 @@ def to_s def initialize(content_type) # :yields: self @friendly = {} @obsolete = @registered = @provisional = false - @preferred_extension = @docs = @use_instead = nil + @preferred_extension = @docs = @use_instead = @__sort_priority = nil + self.extensions = [] case content_type @@ -164,6 +165,8 @@ def initialize(content_type) # :yields: self self.xrefs ||= {} yield self if block_given? + + update_sort_priority end # Indicates that a MIME type is like another type. This differs from @@ -182,60 +185,54 @@ def like?(other) # 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. + # + # Note that this implementation of #<=> is deprecated and will be changed + # in the next major version to be the same as #priority_compare. + # + # Note that MIME::Types no longer compare against nil. def <=>(other) - if other.nil? - -1 - elsif other.respond_to?(:simplified) + return priority_compare(other) if other.is_a?(MIME::Type) + simplified <=> other + end + + # Compares the +other+ MIME::Type using a pre-computed sort priority value, + # then the simplified representation for an alphabetical sort. + # + # For the next major version of MIME::Types, this method will become #<=> and + # #priority_compare will be removed. + def priority_compare(other) + if (cmp = __sort_priority <=> other.__sort_priority) == 0 simplified <=> other.simplified else - filtered = "silent" if other == :silent - filtered ||= "true" if other == true - filtered ||= other.to_s - - simplified <=> MIME::Type.simplified(filtered) + cmp 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 - # do not 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. + # Uses a modified pre-computed sort priority value based on whether one of the provided + # extensions is the preferred extension for a type. # - # 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 + # This is an internal function. If an extension provided is a preferred extension either + # for this instance or the compared instance, the corresponding extension has its top + # _extension_ bit cleared from its sort priority. That means that a type with between + # 0 and 8 extensions will be treated as if it had 9 extensions. + def __extension_priority_compare(other, exts) # :nodoc: + tsp = __sort_priority + + if exts.include?(preferred_extension) && tsp & 0b1000 != 0 + tsp = tsp & 0b11110111 | 0b0111 + end + + osp = other.__sort_priority + + if exts.include?(other.preferred_extension) && osp & 0b1000 != 0 + osp = osp & 0b11110111 | 0b0111 end - pc + if (cmp = tsp <=> osp) == 0 + simplified <=> other.simplified + else + cmp + end end # Returns +true+ if the +other+ object is a MIME::Type and the content types @@ -270,6 +267,13 @@ def hash simplified.hash end + # The computed sort priority value. This is _not_ intended to be used by most + # callers. + def __sort_priority # :nodoc: + update_sort_priority if !instance_variable_defined?(:@__sort_priority) || @__sort_priority.nil? + @__sort_priority + end + # Returns the whole MIME content-type string. # # The content type is a presentation value from the MIME type registry and @@ -324,6 +328,7 @@ def extensions ## def extensions=(value) # :nodoc: + clear_sort_priority @extensions = Set[*Array(value).flatten.compact].freeze MIME::Types.send(:reindex_extensions, self) end @@ -350,9 +355,7 @@ def preferred_extension ## def preferred_extension=(value) # :nodoc: - if value - add_extensions(value) - end + add_extensions(value) if value @preferred_extension = value end @@ -405,9 +408,17 @@ def use_instead attr_writer :use_instead # Returns +true+ if the media type is obsolete. - attr_accessor :obsolete + # + # :attr_accessor: obsolete + attr_reader :obsolete alias_method :obsolete?, :obsolete + ## + def obsolete=(value) + clear_sort_priority + @obsolete = !!value + end + # The documentation for this MIME::Type. attr_accessor :docs @@ -465,11 +476,27 @@ def xref_urls end # Indicates whether the MIME type has been registered with IANA. - attr_accessor :registered + # + # :attr_accessor: registered + attr_reader :registered alias_method :registered?, :registered + ## + def registered=(value) + clear_sort_priority + @registered = !!value + end + # Indicates whether the MIME type's registration with IANA is provisional. - attr_accessor :provisional + # + # :attr_accessor: provisional + attr_reader :provisional + + ## + def provisional=(value) + clear_sort_priority + @provisional = !!value + end # Indicates whether the MIME type's registration with IANA is provisional. def provisional? @@ -552,6 +579,7 @@ def encode_with(coder) coder["registered"] = registered? coder["provisional"] = provisional? if provisional? coder["signature"] = signature? if signature? + coder["sort-priority"] = __sort_priority || 0b11111111 coder end @@ -560,6 +588,7 @@ def encode_with(coder) # # This method should be considered a private implementation detail. def init_with(coder) + @__sort_priority = 0 self.content_type = coder["content-type"] self.docs = coder["docs"] || "" self.encoding = coder["encoding"] @@ -573,6 +602,8 @@ def init_with(coder) self.use_instead = coder["use-instead"] friendly(coder["friendly"] || {}) + + update_sort_priority end def inspect # :nodoc: @@ -628,6 +659,37 @@ def simplify_matchdata(matchdata, remove_x = false, joiner: "/") private + def clear_sort_priority + @__sort_priority = nil + end + + # Update the __sort_priority value. Lower numbers sort better, so the + # bitmapping may seem a little odd. The _best_ sort priority is 0. + # + # | bit | meaning | details | + # | --- | --------------- | --------- | + # | 7 | obsolete | 1 if true | + # | 6 | provisional | 1 if true | + # | 5 | registered | 0 if true | + # | 4 | complete | 0 if true | + # | 3 | # of extensions | see below | + # | 2 | # of extensions | see below | + # | 1 | # of extensions | see below | + # | 0 | # of extensions | see below | + # + # The # of extensions is marked as the number of extensions subtracted from + # 16, to a minimum of 0. + def update_sort_priority + extension_count = @extensions.length + obsolete = (instance_variable_defined?(:@obsolete) && @obsolete) ? 1 << 7 : 0 + provisional = (instance_variable_defined?(:@provisional) && @provisional) ? 1 << 6 : 0 + registered = (instance_variable_defined?(:@registered) && @registered) ? 0 : 1 << 5 + complete = extension_count.nonzero? ? 0 : 1 << 4 + extension_count = [0, 16 - extension_count].max + + @__sort_priority = obsolete | registered | provisional | complete | extension_count + end + def content_type=(type_string) match = MEDIA_TYPE_RE.match(type_string) fail InvalidContentType, type_string if match.nil? diff --git a/lib/mime/type/columnar.rb b/lib/mime/type/columnar.rb index 1b7c3ca..7006fab 100644 --- a/lib/mime/type/columnar.rb +++ b/lib/mime/type/columnar.rb @@ -15,8 +15,10 @@ class MIME::Type::Columnar < MIME::Type def initialize(container, content_type, extensions) # :nodoc: @container = container + @__priority_penalty = nil self.content_type = content_type - self.extensions = extensions + @extensions = Set[*Array(extensions).flatten.compact].freeze + clear_sort_priority end def self.column(*methods, file: nil) # :nodoc: @@ -51,6 +53,17 @@ def encode_with(coder) # :nodoc: super end + def update_sort_priority + if @container.__fully_loaded? + super + else + obsolete = (@__sort_priority & (1 << 7)) != 0 + registered = (@__sort_priority & (1 << 5)) == 0 + + @__priority_penalty = (obsolete ? 3 : 0) + (registered ? 0 : 2) + end + end + class << self undef column end diff --git a/lib/mime/types.rb b/lib/mime/types.rb index 0aba5b1..c251232 100644 --- a/lib/mime/types.rb +++ b/lib/mime/types.rb @@ -130,9 +130,7 @@ def [](type_id, complete: false, registered: false) @type_variants[MIME::Type.simplified(type_id)] end - prune_matches(matches, complete, registered).sort { |a, b| - a.priority_compare(b) - } + prune_matches(matches, complete, registered).sort end # Return the list of MIME::Types which belongs to the file based on its @@ -148,10 +146,14 @@ def [](type_id, complete: false, registered: false) # puts MIME::Types.type_for(%w(citydesk.xml citydesk.gif)) # => [application/xml, image/gif, text/xml] def type_for(filename) - Array(filename).flat_map { |fn| - @extension_index[fn.chomp.downcase[/\.?([^.]*?)\z/m, 1]] - }.compact.inject(Set.new, :+).sort { |a, b| - a.priority_compare(b) + wanted = Array(filename).map { |fn| fn.chomp.downcase[/\.?([^.]*?)\z/m, 1] } + + wanted + .flat_map { |ext| @extension_index[ext] } + .compact + .reduce(Set.new, :+) + .sort { |a, b| + a.__extension_priority_compare(b, wanted) } end alias_method :of, :type_for @@ -193,6 +195,10 @@ def add_type(type, quiet = false) index_extensions!(type) end + def __fully_loaded? # :nodoc: + true + end + private def add_type_variant!(mime_type) @@ -220,6 +226,12 @@ def match(pattern) k =~ pattern }.values.inject(Set.new, :+) end + + # def stable_sort(list) + # list.lazy.each_with_index.sort { |(a, ai), (b, bi)| + # a.priority_compare(b).nonzero? || ai <=> bi + # }.map(&:first) + # end end require "mime/types/cache" diff --git a/lib/mime/types/_columnar.rb b/lib/mime/types/_columnar.rb index b862800..766617e 100644 --- a/lib/mime/types/_columnar.rb +++ b/lib/mime/types/_columnar.rb @@ -18,6 +18,10 @@ def self.extended(obj) # :nodoc: obj.instance_variable_set(:@__files__, Set.new) end + def __fully_loaded? # :nodoc: + @__files__.size == 10 + end + # Load the first column data file (type and extensions). def load_base_data(path) # :nodoc: @__root__ = path @@ -26,13 +30,16 @@ def load_base_data(path) # :nodoc: line = line.split content_type = line.shift extensions = line - # content_type, *extensions = line.split type = MIME::Type::Columnar.new(self, content_type, extensions) @__mime_data__ << type add(type) end + each_file_byte("spri") do |type, byte| + type.instance_variable_set(:@__sort_priority, byte) + end + self end @@ -60,6 +67,25 @@ def each_file_line(name, lookup = true) end end + def each_file_byte(name) + LOAD_MUTEX.synchronize do + next if @__files__.include?(name) + + i = -1 + + filename = File.join(@__root__, "mime.#{name}.column") + + next unless File.exist?(filename) + + IO.binread(filename).unpack("C*").each do |byte| + (type = @__mime_data__[i += 1]) || next + yield type, byte + end + + @__files__ << name + end + end + def load_encoding each_file_line("encoding") do |type, line| pool ||= {} @@ -91,7 +117,7 @@ def load_flags def load_xrefs each_file_line("xrefs") { |type, line| - type.instance_variable_set(:@xrefs, dict(line, array: true)) + type.instance_variable_set(:@xrefs, dict(line, transform: :array)) } end @@ -107,18 +133,39 @@ def load_use_instead end end - def dict(line, array: false) + def dict(line, transform: nil) if line == "-" {} else line.split("|").each_with_object({}) { |l, h| k, v = l.split("^") v = nil if v.empty? - h[k] = array ? Array(v) : v + + if transform + send(:"dict_#{transform}", h, k, v) + else + h[k] = v + end } end end + def dict_extension_priority(h, k, v) + return if v.nil? + + v = v.to_i if v.is_a?(String) + v = v.trunc if v.is_a?(Float) + v = [[-20, v].max, 20].min + + return if v.zero? + + h[k] = v + end + + def dict_array(h, k, v) + h[k] = Array(v) + end + def arr(line) if line == "-" [] diff --git a/lib/mime/types/loader.rb b/lib/mime/types/loader.rb index 79c4f98..7336c25 100644 --- a/lib/mime/types/loader.rb +++ b/lib/mime/types/loader.rb @@ -79,7 +79,7 @@ def load_columnar # # This will load from columnar files (#load_columnar) if columnar: # true is provided in +options+ and there are columnar files in +path+. - def load(options = {columnar: false}) + def load(options = {columnar: true}) if options[:columnar] && !Dir[columnar_path].empty? load_columnar else diff --git a/lib/mime/types/version.rb b/lib/mime/types/version.rb index 876a36e..c5f21bd 100644 --- a/lib/mime/types/version.rb +++ b/lib/mime/types/version.rb @@ -4,7 +4,7 @@ module MIME class Types # The released version of the mime-types library. - VERSION = "3.6.2" + VERSION = "3.7.0.pre2" end class Type diff --git a/mime-types.gemspec b/mime-types.gemspec index c4f1961..cc15dc2 100644 --- a/mime-types.gemspec +++ b/mime-types.gemspec @@ -1,15 +1,15 @@ # -*- encoding: utf-8 -*- -# stub: mime-types 3.6.2 ruby lib +# stub: mime-types 3.7.0.pre2 ruby lib Gem::Specification.new do |s| s.name = "mime-types".freeze - s.version = "3.6.2".freeze + s.version = "3.7.0.pre2".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= - s.metadata = { "bug_tracker_uri" => "https://github.com/mime-types/ruby-mime-types/issues", "changelog_uri" => "https://github.com/mime-types/ruby-mime-types/blob/master/History.md", "homepage_uri" => "https://github.com/mime-types/ruby-mime-types/", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/mime-types/ruby-mime-types/" } if s.respond_to? :metadata= + s.metadata = { "bug_tracker_uri" => "https://github.com/mime-types/ruby-mime-types/issues", "changelog_uri" => "https://github.com/mime-types/ruby-mime-types/blob/main/CHANGELOG.md", "homepage_uri" => "https://github.com/mime-types/ruby-mime-types/", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/mime-types/ruby-mime-types/" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Austin Ziegler".freeze] - s.date = "2025-03-24" + s.date = "2025-04-07" s.description = "The mime-types library provides a library and registry for information about\nMIME content type definitions. It can be used to determine defined filename\nextensions for MIME types, or to use filename extensions to look up the likely\nMIME type definitions.\n\nVersion 3.0 is a major release that requires Ruby 2.0 compatibility and removes\ndeprecated functions. The columnar registry format introduced in 2.6 has been\nmade the primary format; the registry data has been extracted from this library\nand put into {mime-types-data}[https://github.com/mime-types/mime-types-data].\nAdditionally, mime-types is now licensed exclusively under the MIT licence and\nthere is a code of conduct in effect. There are a number of other smaller\nchanges described in the History file.".freeze s.email = ["halostatue@gmail.com".freeze] s.extra_rdoc_files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "SECURITY.md".freeze] @@ -18,20 +18,21 @@ Gem::Specification.new do |s| s.licenses = ["MIT".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.0".freeze) - s.rubygems_version = "3.6.2".freeze + s.rubygems_version = "3.6.6".freeze s.summary = "The mime-types library provides a library and registry for information about MIME content type definitions".freeze s.specification_version = 4 - s.add_runtime_dependency(%q.freeze, ["~> 3.2015".freeze]) + s.add_runtime_dependency(%q.freeze, ["~> 3.2025".freeze, ">= 3.2025.0506.pre2".freeze]) s.add_runtime_dependency(%q.freeze, [">= 0".freeze]) - s.add_development_dependency(%q.freeze, ["~> 5.25".freeze]) s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 2.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) + s.add_development_dependency(%q.freeze, ["~> 5.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.4".freeze]) - s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14.0".freeze]) + s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) + s.add_development_dependency(%q.freeze, [">= 0.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) end diff --git a/test/test_mime_type.rb b/test/test_mime_type.rb index a2349ca..1190ced 100644 --- a/test/test_mime_type.rb +++ b/test/test_mime_type.rb @@ -175,11 +175,6 @@ def mime_type(content_type) refute_equal text_plain, "text/html" assert_operator text_plain, :>, "text/html" end - - it "correctly compares against nil" do - refute_equal text_html, nil - assert_operator text_plain, :<, nil - end end describe "#ascii?" do @@ -326,41 +321,56 @@ def mime_type(content_type) end describe "#priority_compare" do + def priority(type) + priority = "OpRceXtN" + .chars + .zip(("%08b" % type.__sort_priority).chars) + .map { |e| e.join(":") } + .join(" ") + + "#{type} (#{priority} / #{type.__sort_priority})" + end + def assert_priority_less(left, right) - assert_equal(-1, left.priority_compare(right)) + assert_equal(-1, left.priority_compare(right), "#{priority(left)} is not less than #{priority(right)}") end def assert_priority_same(left, right) - assert_equal 0, left.priority_compare(right) + assert_equal 0, left.priority_compare(right), "#{priority(left)} is not equal to #{priority(right)}" end def assert_priority_more(left, right) - assert_equal 1, left.priority_compare(right) + assert_equal 1, left.priority_compare(right), "#{priority(left)} is not more than #{priority(right)}" end def assert_priority(left, middle, right) assert_priority_less left, right assert_priority_same left, middle - assert_priority_more right, left + assert_priority_more right, middle end let(:text_1) { mime_type("content-type" => "text/1") } let(:text_1p) { mime_type("content-type" => "text/1") } let(:text_2) { mime_type("content-type" => "text/2") } - it "sorts (1) based on the simplified type" do + it "sorts based on the simplified type when the sort priorities are the same" do assert_priority text_1, text_1p, text_2 end - it "sorts (2) based on extensions" do - text_1.extensions = ["foo", "bar"] - text_2.extensions = ["foo"] + it "sorts obsolete types higher than non-obsolete types" do + text_1.obsolete = text_1p.obsolete = false + text_1b = mime_type(text_1) { |t| t.obsolete = true } + + assert_priority_less text_1, text_1b - assert_priority_same text_1, text_2 + assert_priority text_1, text_1p, text_1b + end - text_2.registered = true + it "sorts provisional types higher than non-provisional types" do + text_1.provisional = text_1p.provisional = false + text_1b = mime_type(text_1) { |t| t.provisional = true } - assert_priority_more text_1, text_2 + assert_priority text_1, text_1p, text_1b end it "sorts (3) based on the registration state" do @@ -377,23 +387,11 @@ def assert_priority(left, middle, right) assert_priority text_1, text_1p, text_1b end - it "sorts (5) based on obsolete status" do - text_1.obsolete = text_1p.obsolete = false - text_1b = mime_type(text_1) { |t| t.obsolete = true } - - assert_priority text_1, text_1p, text_1b - end - - it "sorts (5) based on the use-instead value" do - text_1.obsolete = text_1p.obsolete = true - text_1.use_instead = text_1p.use_instead = "abc/xyz" - text_1b = mime_type(text_1) { |t| t.use_instead = nil } - - assert_priority text_1, text_1p, text_1b - - text_1b.use_instead = "abc/zzz" + it "sorts based on extensions (more extensions sort lower)" do + text_1.extensions = ["foo", "bar"] + text_2.extensions = ["foo"] - assert_priority text_1, text_1p, text_1b + assert_priority_less text_1, text_2 end end @@ -502,10 +500,10 @@ def assert_has_keys(wanted_keys, actual, msg = nil) describe "#to_json" do let(:expected_1) { - '{"content-type":"a/b","encoding":"base64","registered":false}' + '{"content-type":"a/b","encoding":"base64","registered":false,"sort-priority":48}' } let(:expected_2) { - '{"content-type":"a/b","encoding":"base64","registered":true,"provisional":true}' + '{"content-type":"a/b","encoding":"base64","registered":true,"provisional":true,"sort-priority":80}' } it "converts to JSON when requested" do diff --git a/test/test_mime_types.rb b/test/test_mime_types.rb index 574be8a..c4c5af2 100644 --- a/test/test_mime_types.rb +++ b/test/test_mime_types.rb @@ -12,7 +12,8 @@ def mime_types MIME::Type.new("content-type" => "application/x-wordperfect6.1"), MIME::Type.new("content-type" => "application/x-www-form-urlencoded", "registered" => true), MIME::Type.new("content-type" => "application/x-gzip", "extensions" => %w[gz]), - MIME::Type.new("content-type" => "application/gzip", "extensions" => "gz", "registered" => true) + MIME::Type.new("content-type" => "application/gzip", "extensions" => "gz", "registered" => true), + *MIME::Types.type_for("foo.webm") ) } end @@ -33,8 +34,8 @@ def mime_types end it "is countable with an enumerator" do - assert_equal 6, mime_types.each.count - assert_equal 6, mime_types.lazy.count + assert_equal 8, mime_types.each.count + assert_equal 8, mime_types.lazy.count end end @@ -139,7 +140,7 @@ def mime_types end it "finds multiple extensions" do - assert_equal %w[image/jpeg text/plain], + assert_equal %w[text/plain image/jpeg], mime_types.type_for(%w[foo.txt foo.jpeg]) end @@ -158,11 +159,15 @@ def mime_types it "handles newline characters correctly" do assert_includes mime_types.type_for("test.pdf\n.txt"), "text/plain" end + + it "returns a stable order for types with equal priority" do + assert_equal %w[text/x-vcalendar text/x-vcard], MIME::Types[/text\/x-vca/] + end end describe "#count" do it "can count the number of types inside" do - assert_equal 6, mime_types.count + assert_equal 8, mime_types.count end end end diff --git a/test/test_mime_types_class.rb b/test/test_mime_types_class.rb index c4b8a33..05494dd 100644 --- a/test/test_mime_types_class.rb +++ b/test/test_mime_types_class.rb @@ -47,7 +47,7 @@ def setup } # This is this way because of a new type ending with gzip that only # appears in some data files. - assert_equal %w[application/gzip application/x-gzip multipart/x-gzip], types + assert_equal %w[application/gzip multipart/x-gzip application/x-gzip], types assert_equal 3, types.size end @@ -86,9 +86,11 @@ def setup assert_equal %w[image/jpeg], MIME::Types.of(["foo.jpeg", "bar.jpeg"]) end - it "finds multiple extensions" do - assert_equal %w[image/jpeg text/plain], - MIME::Types.type_for(%w[foo.txt foo.jpeg]) + it "finds multiple extensions ordered by the filename list" do + result = MIME::Types.type_for(%w[foo.txt foo.jpeg]) + + # assert_equal %w[text/plain image/jpeg], MIME::Types.type_for(%w[foo.txt foo.jpeg]) + assert_equal %w[text/plain image/jpeg], result end it "does not find unknown extensions" do @@ -105,6 +107,10 @@ def setup assert_includes MIME::Types.type_for("test.pdf\n.txt"), "text/plain" assert_includes MIME::Types.type_for("test.txt\n.pdf"), "application/pdf" end + + it "returns a stable order for types with equal priority" do + assert_equal %w[text/x-vcalendar text/x-vcard], MIME::Types[/text\/x-vca/] + end end describe ".count" do From 23be01910d2e7c2af0fb0c345a1e04bacc624569 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Tue, 6 May 2025 21:22:23 -0400 Subject: [PATCH 3/3] Prep for release --- Rakefile | 2 +- lib/mime/types/version.rb | 2 +- mime-types.gemspec | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Rakefile b/Rakefile index af0cc12..c995b1e 100644 --- a/Rakefile +++ b/Rakefile @@ -26,7 +26,7 @@ spec = Hoe.spec "mime-types" do val.merge!({"rubygems_mfa_required" => "true"}) } - extra_deps << ["mime-types-data", "~> 3.2025", ">= 3.2025.0506.pre2"] + extra_deps << ["mime-types-data", "~> 3.2025", ">= 3.2025.0507"] extra_deps << ["logger", ">= 0"] extra_dev_deps << ["hoe", "~> 4.0"] diff --git a/lib/mime/types/version.rb b/lib/mime/types/version.rb index c5f21bd..b36cd15 100644 --- a/lib/mime/types/version.rb +++ b/lib/mime/types/version.rb @@ -4,7 +4,7 @@ module MIME class Types # The released version of the mime-types library. - VERSION = "3.7.0.pre2" + VERSION = "3.7.0" end class Type diff --git a/mime-types.gemspec b/mime-types.gemspec index cc15dc2..d117d66 100644 --- a/mime-types.gemspec +++ b/mime-types.gemspec @@ -1,16 +1,16 @@ # -*- encoding: utf-8 -*- -# stub: mime-types 3.7.0.pre2 ruby lib +# stub: mime-types 3.7.0 ruby lib Gem::Specification.new do |s| s.name = "mime-types".freeze - s.version = "3.7.0.pre2".freeze + s.version = "3.7.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/mime-types/ruby-mime-types/issues", "changelog_uri" => "https://github.com/mime-types/ruby-mime-types/blob/main/CHANGELOG.md", "homepage_uri" => "https://github.com/mime-types/ruby-mime-types/", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/mime-types/ruby-mime-types/" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Austin Ziegler".freeze] - s.date = "2025-04-07" - s.description = "The mime-types library provides a library and registry for information about\nMIME content type definitions. It can be used to determine defined filename\nextensions for MIME types, or to use filename extensions to look up the likely\nMIME type definitions.\n\nVersion 3.0 is a major release that requires Ruby 2.0 compatibility and removes\ndeprecated functions. The columnar registry format introduced in 2.6 has been\nmade the primary format; the registry data has been extracted from this library\nand put into {mime-types-data}[https://github.com/mime-types/mime-types-data].\nAdditionally, mime-types is now licensed exclusively under the MIT licence and\nthere is a code of conduct in effect. There are a number of other smaller\nchanges described in the History file.".freeze + s.date = "2025-05-07" + s.description = "The mime-types library provides a library and registry for information about\nMIME content type definitions. It can be used to determine defined filename\nextensions for MIME types, or to use filename extensions to look up the likely\nMIME type definitions.\n\nVersion 3.0 is a major release that requires Ruby 2.0 compatibility and removes\ndeprecated functions. The columnar registry format introduced in 2.6 has been\nmade the primary format; the registry data has been extracted from this library\nand put into mime-types-data. Additionally, mime-types is now licensed\nexclusively under the MIT licence and there is a code of conduct in effect.\nThere are a number of other smaller changes described in the History file.".freeze s.email = ["halostatue@gmail.com".freeze] s.extra_rdoc_files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "SECURITY.md".freeze] s.files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "lib/mime-types.rb".freeze, "lib/mime/type.rb".freeze, "lib/mime/type/columnar.rb".freeze, "lib/mime/types.rb".freeze, "lib/mime/types/_columnar.rb".freeze, "lib/mime/types/cache.rb".freeze, "lib/mime/types/columnar.rb".freeze, "lib/mime/types/container.rb".freeze, "lib/mime/types/deprecations.rb".freeze, "lib/mime/types/full.rb".freeze, "lib/mime/types/loader.rb".freeze, "lib/mime/types/logger.rb".freeze, "lib/mime/types/registry.rb".freeze, "lib/mime/types/version.rb".freeze, "test/bad-fixtures/malformed".freeze, "test/fixture/json.json".freeze, "test/fixture/old-data".freeze, "test/fixture/yaml.yaml".freeze, "test/minitest_helper.rb".freeze, "test/test_mime_type.rb".freeze, "test/test_mime_types.rb".freeze, "test/test_mime_types_cache.rb".freeze, "test/test_mime_types_class.rb".freeze, "test/test_mime_types_lazy.rb".freeze, "test/test_mime_types_loader.rb".freeze] @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.specification_version = 4 - s.add_runtime_dependency(%q.freeze, ["~> 3.2025".freeze, ">= 3.2025.0506.pre2".freeze]) + s.add_runtime_dependency(%q.freeze, ["~> 3.2025".freeze, ">= 3.2025.0507".freeze]) s.add_runtime_dependency(%q.freeze, [">= 0".freeze]) s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 2.0".freeze])