Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Priority extensions #162

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
@@ -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.0 / 2024-10-02

- 2 deprecations:
Expand Down Expand Up @@ -123,7 +148,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
Expand Down
194 changes: 142 additions & 52 deletions lib/mime/type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ 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
__extension_priorities

self.extensions = []

case content_type
Expand Down Expand Up @@ -167,6 +169,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
Expand All @@ -185,60 +189,28 @@ 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)
simplified <=> other.simplified
else
filtered = "silent" if other == :silent
filtered ||= "true" if other == true
filtered ||= other.to_s

simplified <=> MIME::Type.simplified(filtered)
end
return priority_compare(other) if other.is_a?(MIME::Type)
simplified <=> other
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.
# Compares the +other+ MIME::Type using the simplified representation, then
# a pre-computed sort priority value. Used by MIME::Types#[] to sort types.
#
# 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.
# While this method is public, its direct use is strongly discouraged by
# consumers of mime-types. For the next major version of MIME::Types, this
# method will become #<=> and #priority_compare will be removed.
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
if (cmp = __sort_priority <=> other.__sort_priority) == 0
simplified <=> other.simplified
else
cmp
end

pc
end

# Returns +true+ if the +other+ object is a MIME::Type and the content types
Expand Down Expand Up @@ -273,6 +245,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
Expand Down Expand Up @@ -327,6 +306,7 @@ def extensions

##
def extensions=(value) # :nodoc:
clear_sort_priority
@extensions = Set[*Array(value).flatten.compact].freeze
MIME::Types.send(:reindex_extensions, self)
end
Expand Down Expand Up @@ -355,10 +335,36 @@ def preferred_extension
def preferred_extension=(value) # :nodoc:
if value
add_extensions(value)
set_preferred_extension_priority(value)
elsif instance_variable_defined?(:@preferred_extension)
clear_extension_priority(@preferred_extension)
end
@preferred_extension = value
end

##
# Optional extension priorities for this MIME type. This is a map of
# extensions to relative priority values (+-20..20+) similar to +nice(1)+.
# Unless otherwise specified in the data, an explicitly set
# +preferred_extension+ is automatically given a relative priority of +-10+.
#
# :attr_reader: extension_priorities
attr_accessor :extension_priorities

##
# Returns the priority for the provided extension or extensions. If a priority
# is not set, the default priority is +0+. The range for priorities is
# +-20..20+, inclusive.
#
# Obsolete MIME types have a <code>+3</code> penalty applied to their
# extension priority and unregistered MIME types have a <code>+2</code>
# penalty to their extension priority, meaning that the highest priority an
# obsolete, unregistered MIME type can have is +-15+. The lowest priority is
# always <code>+20</code>.
def extension_priority(*exts)
exts.map { |ext| get_extension_priority(ext) }.min
end

##
# The encoding (+7bit+, +8bit+, <tt>quoted-printable</tt>, or +base64+)
# required to transport the data of this content type safely across a
Expand Down Expand Up @@ -408,9 +414,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

Expand Down Expand Up @@ -468,11 +482,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?
Expand Down Expand Up @@ -555,6 +585,8 @@ def encode_with(coder)
coder["registered"] = registered?
coder["provisional"] = provisional? if provisional?
coder["signature"] = signature? if signature?
coder["sort-priority"] = __sort_priority || 0b11111111
coder["extension-priorities"] = __extension_priorities unless __extension_priorities.empty?
coder
end

Expand All @@ -563,6 +595,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"]
Expand All @@ -574,8 +607,11 @@ def init_with(coder)
self.signature = coder["signature"]
self.xrefs = coder["xrefs"] || {}
self.use_instead = coder["use-instead"]
self.extension_priorities = coder["extension-priorities"]

friendly(coder["friendly"] || {})

update_sort_priority
end

def inspect # :nodoc:
Expand Down Expand Up @@ -629,8 +665,62 @@ def simplify_matchdata(matchdata, remove_x = false, joiner: "/")
end
end

def __extension_priorities # :nodoc:
@extension_priorities ||= {}
end

private

def clear_extension_priority(ext)
__extension_priorities.delete(ext) if ext
end

def get_extension_priority(ext)
[[-20, (__extension_priorities[ext] || 0) + __priority_penalty].max, 20].min
end

def set_preferred_extension_priority(ext)
__extension_priorities[ext] = -10 unless __extension_priorities.has_key?(ext)
end

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
@__priority_penalty = ((instance_variable_defined?(:@obsolete) && @obsolete) ? 3 : 0) +
((instance_variable_defined?(:@registered) && @registered) ? 0 : 2)
end

def __priority_penalty
update_sort_priority if @__priority_penalty.nil?
@__priority_penalty
end

def content_type=(type_string)
match = MEDIA_TYPE_RE.match(type_string)
fail InvalidContentType, type_string if match.nil?
Expand Down
17 changes: 16 additions & 1 deletion lib/mime/type/columnar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -39,6 +41,7 @@ def self.column(*methods, file: nil) # :nodoc:
:signature?, :provisional, :provisional=, :provisional?, file: "flags"
column :xrefs, :xrefs=, :xref_urls
column :use_instead, :use_instead=
column :extension_priorities, :extension_priorities=

def encode_with(coder) # :nodoc:
@container.send(:load_friendly)
Expand All @@ -48,9 +51,21 @@ def encode_with(coder) # :nodoc:
@container.send(:load_use_instead)
@container.send(:load_xrefs)
@container.send(:load_preferred_extension)
@container.send(:load_extension_priorities)
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
Expand Down
Loading
Loading