├── lib ├── mime-types.rb └── mime │ ├── types │ ├── columnar.rb │ ├── version.rb │ ├── full.rb │ ├── logger.rb │ ├── deprecations.rb │ ├── cache.rb │ ├── registry.rb │ ├── container.rb │ ├── _columnar.rb │ └── loader.rb │ ├── type │ └── columnar.rb │ ├── types.rb │ └── type.rb ├── .github ├── FUNDING.yml ├── zizmor.yml ├── workflows │ ├── dco-check.yml │ ├── zizmor.yml │ ├── dependency-review.yml │ ├── publish-docs.yml │ ├── reviewdog.yml │ ├── publish-gem.yml │ └── ci.yml └── dependabot.yml ├── .typos.toml ├── .standard.yml ├── .gitignore ├── SECURITY.md ├── test ├── minitest_helper.rb ├── fixture │ ├── old-data │ ├── json.json │ └── yaml.yaml ├── bad-fixtures │ └── malformed ├── test_mime_types_loader.rb ├── test_mime_types_lazy.rb ├── test_mime_types_cache.rb ├── test_mime_types_class.rb ├── test_mime_types.rb └── test_mime_type.rb ├── Gemfile ├── .hoerc ├── Manifest.txt ├── CONTRIBUTORS.md ├── support ├── benchmarks │ ├── object_counts.rb │ ├── profile_memory.rb │ └── load.rb ├── deps.rb └── profile.rb ├── LICENCE.md ├── CONTRIBUTING.md ├── mime-types.gemspec ├── CODE_OF_CONDUCT.md ├── Rakefile ├── README.md └── CHANGELOG.md /lib/mime-types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | -------------------------------------------------------------------------------- /lib/mime/types/columnar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: halostatue 2 | buy_me_a_coffee: halostatue 3 | ko_fi: halostatue 4 | tidelift: rubygems/mime-types 5 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | artipacked: 3 | ignore: 4 | - publish-gem.yml:57 # publish-gem adds and pushes a tag. 5 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | # extend-ignore-re = ["Ned Konz"] 3 | 4 | [default.extend-identifiers] 5 | 6 | [default.extend-words] 7 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | parallel: true 3 | ruby_version: 2.3 4 | ignore: 5 | - '*.gemspec' 6 | - 'pkg/**/*' 7 | - Rakefile: 8 | - Layout/HeredocIndentation 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle/ 3 | .byebug_history 4 | .rake_tasks~ 5 | .source_index 6 | Gemfile.lock 7 | mise.toml 8 | /benchmarks/ 9 | /coverage/ 10 | /doc/ 11 | /html/ 12 | /pkg/ 13 | /publish/ 14 | /test/cache.tst 15 | /tmp/ 16 | /vendor/ 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # mime-types Security 2 | 3 | ## Security contact information 4 | 5 | To report a security vulnerability, please use the 6 | [Tidelift security contact](https://tidelift.com/security). Tidelift will 7 | coordinate the fix and disclosure. 8 | -------------------------------------------------------------------------------- /lib/mime/types/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | module MIME 5 | class Types 6 | # The released version of the mime-types library. 7 | VERSION = "3.7.0" 8 | end 9 | 10 | class Type 11 | # The released version of the mime-types library. 12 | VERSION = MIME::Types::VERSION 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/mime/types/full.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | module MIME 5 | ## 6 | class Types 7 | unless private_method_defined?(:load_mode) 8 | class << self 9 | private 10 | 11 | def load_mode 12 | {columnar: false} 13 | end 14 | end 15 | end 16 | end 17 | end 18 | 19 | require "mime/types" 20 | -------------------------------------------------------------------------------- /test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem "minitest" 4 | require "minitest/focus" 5 | require "minitest/hooks" 6 | 7 | require "fileutils" 8 | 9 | require "mime/type" 10 | ENV["RUBY_MIME_TYPES_LAZY_LOAD"] = "yes" 11 | 12 | if ENV["STRICT"] 13 | $VERBOSE = true 14 | Warning[:deprecated] = true 15 | require "minitest/error_on_warning" 16 | end 17 | -------------------------------------------------------------------------------- /test/fixture/old-data: -------------------------------------------------------------------------------- 1 | !application/smil @smi,smil :8bit 'IANA,RFC4536 =use-instead:application/smil+xml 2 | !audio/vnd.qcelp @qcp 'IANA,RFC3625 =use-instead:audio/QCELP 3 | *!image/bmp @bmp =use-instead:image/x-bmp 4 | *application/acad 'LTSW 5 | *audio/webm @webm '{WebM=http://www.webmproject.org/code/specs/container/} 6 | *image/pjpeg :base64 =Fixes a bug with IE6 and progressive JPEGs 7 | application/1d-interleaved-parityfec 'IANA,RFC6015 8 | audio/1d-interleaved-parityfec 'IANA,RFC6015 9 | mac:application/x-apple-diskimage @dmg 10 | -------------------------------------------------------------------------------- /test/bad-fixtures/malformed: -------------------------------------------------------------------------------- 1 | !application.smil @smi,smil :8bit 'IANA,RFC4536 =use-instead:application/smil+xml 2 | !audio/vnd.qcelp @qcp 'IANA,RFC3625 =use-instead:audio/QCELP 3 | *!image/bmp @bmp =use-instead:image/x-bmp 4 | *application/acad 'LTSW 5 | *audio/webm @webm '{WebM=http://www.webmproject.org/code/specs/container/} 6 | *image/pjpeg :base64 =Fixes a bug with IE6 and progressive JPEGs 7 | application/1d-interleaved-parityfec 'IANA,RFC6015 8 | audio/1d-interleaved-parityfec 'IANA,RFC6015 9 | mac:application/x-apple-diskimage @dmg 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | # frozen_string_literal: true 3 | 4 | # NOTE: This file is not the canonical source of dependencies. Edit the Rakefile, instead. 5 | 6 | source "https://rubygems.org/" 7 | 8 | if ENV["DEV"] 9 | gem "debug", platforms: [:mri] 10 | gem "ruby-prof", platforms: [:mri] 11 | gem "memory_profiler", platforms: [:mri] 12 | end 13 | 14 | if ENV["DATA"] 15 | gem "mime-types-data", path: "../mime-types-data" 16 | end 17 | 18 | if ENV["COVERAGE"] 19 | gem "simplecov", require: false, platforms: [:mri] 20 | gem "simplecov-lcov", require: false, platforms: [:mri] 21 | end 22 | 23 | gemspec 24 | -------------------------------------------------------------------------------- /.github/workflows/dco-check.yml: -------------------------------------------------------------------------------- 1 | name: Check DCO 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | check-dco: 10 | name: Check DCO 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | 16 | steps: 17 | - name: Harden the runner 18 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 19 | with: 20 | egress-policy: block 21 | disable-sudo: true 22 | allowed-endpoints: > 23 | api.github.com:443 24 | github.com:443 25 | 26 | - uses: KineticCafe/actions-dco@1c23966ecce077f76671a61caabeb13eefc72a51 # v1.3.8 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: monthly 8 | commit-message: 9 | prefix: 'deps' 10 | cooldown: 11 | default-days: 7 12 | groups: 13 | actions: 14 | applies-to: version-updates 15 | update-types: 16 | - minor 17 | - patch 18 | 19 | - package-ecosystem: bundler 20 | directory: / 21 | schedule: 22 | interval: monthly 23 | commit-message: 24 | prefix: 'deps' 25 | cooldown: 26 | default-days: 7 27 | groups: 28 | bundler: 29 | applies-to: version-updates 30 | update-types: 31 | - minor 32 | - patch 33 | ignore: 34 | - dependency-name: mime-types-data 35 | -------------------------------------------------------------------------------- /.hoerc: -------------------------------------------------------------------------------- 1 | --- 2 | exclude: !ruby/regexp '/ 3 | (?i:TAGS)$ 4 | | [Aa]ppraisals$ 5 | | [gG]emfile(?:\.lock)?$ 6 | | \.gemspec$ 7 | | \.swp$ 8 | | \.tmp$ 9 | | ^\.appveyor\.ya?ml$ 10 | | ^\.autotest$ 11 | | ^\.bundle\/ 12 | | ^\.byebug_history$ 13 | | ^\.coveralls\.ya?ml$ 14 | | ^\.DS_Store$ 15 | | ^\.fasterer\.ya?ml$ 16 | | ^\.gemtest$ 17 | | ^\.git\/ 18 | | ^\.gitattributes$ 19 | | ^\.github\/ 20 | | ^\.gitignore$ 21 | | ^\.hg\/ 22 | | ^\.hoerc$ 23 | | ^\.idea\/ 24 | | ^\.pullreview\.ya?ml$ 25 | | ^\.rubocop.*\.ya?ml$ 26 | | ^\.standard.*\.ya?ml$ 27 | | ^\.svn\/ 28 | | ^\.travis\.ya?ml$ 29 | | ^\.typos\.toml$ 30 | | ^\.unused\.ya?ml$ 31 | | ^[.]?mise\.toml$ 32 | | ^benchmarks\/ 33 | | ^coverage\/ 34 | | ^doc\/ 35 | | ^research\/ 36 | | ^support\/ 37 | | ^vendor\/ 38 | /x' 39 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | CODE_OF_CONDUCT.md 3 | CONTRIBUTING.md 4 | CONTRIBUTORS.md 5 | LICENCE.md 6 | Manifest.txt 7 | README.md 8 | Rakefile 9 | SECURITY.md 10 | lib/mime-types.rb 11 | lib/mime/type.rb 12 | lib/mime/type/columnar.rb 13 | lib/mime/types.rb 14 | lib/mime/types/_columnar.rb 15 | lib/mime/types/cache.rb 16 | lib/mime/types/columnar.rb 17 | lib/mime/types/container.rb 18 | lib/mime/types/deprecations.rb 19 | lib/mime/types/full.rb 20 | lib/mime/types/loader.rb 21 | lib/mime/types/logger.rb 22 | lib/mime/types/registry.rb 23 | lib/mime/types/version.rb 24 | test/bad-fixtures/malformed 25 | test/fixture/json.json 26 | test/fixture/old-data 27 | test/fixture/yaml.yaml 28 | test/minitest_helper.rb 29 | test/test_mime_type.rb 30 | test/test_mime_types.rb 31 | test/test_mime_types_cache.rb 32 | test/test_mime_types_class.rb 33 | test/test_mime_types_lazy.rb 34 | test/test_mime_types_loader.rb 35 | -------------------------------------------------------------------------------- /test/fixture/json.json: -------------------------------------------------------------------------------- 1 | [{"content-type":"application/smil","encoding":"8bit","extensions":["smi","smil"],"obsolete":true,"use-instead":"application/smil+xml","registered":true},{"content-type":"audio/vnd.qcelp","encoding":"base64","extensions":["qcp"],"obsolete":true,"use-instead":"audio/QCELP","registered":true},{"content-type":"image/bmp","encoding":"base64","extensions":["bmp"],"obsolete":true,"use-instead":"image/x-bmp","registered":false},{"content-type":"application/acad","encoding":"base64","registered":false},{"content-type":"audio/webm","encoding":"base64","extensions":["webm"],"registered":false},{"content-type":"image/pjpeg","docs":"Fixes a bug with IE6 and progressive JPEGs","encoding":"base64","registered":false},{"content-type":"application/1d-interleaved-parityfec","encoding":"base64","registered":true},{"content-type":"audio/1d-interleaved-parityfec","encoding":"base64","registered":true},{"content-type":"application/x-apple-diskimage","encoding":"base64","extensions":["dmg"],"registered":false}] 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - Austin Ziegler created mime-types. 4 | 5 | Thanks to everyone else who has contributed to mime-types over the years: 6 | 7 | - Aaron Patterson 8 | - Aggelos Avgerinos 9 | - Al Snow 10 | - Alex Vondrak 11 | - Andre Pankratz 12 | - Andy Brody 13 | - Arnaud Meuret 14 | - Brandon Galbraith 15 | - Burke Libbey 16 | - Chris Gat 17 | - Daniel Watkins 18 | - David Genord 19 | - Dillon Welch 20 | - Edward Betts 21 | - Eric Marden 22 | - Garret Alfert 23 | - Godfrey Chan 24 | - Greg Brockman 25 | - Hans de Graaff 26 | - Henrik Hodne 27 | - Igor Victor 28 | - Janko Marohnić 29 | - Jean Boussier 30 | - Jeremy Evans 31 | - Juanito Fatas 32 | - Jun Aruga 33 | - Keerthi Siva 34 | - Ken Ip 35 | - Kevin Menard 36 | - Koichi ITO 37 | - Łukasz Śliwa 38 | - Martin d'Allens 39 | - Masato Nakamura 40 | - Mauricio Linhares 41 | - Nana Kugayama 42 | - Nicholas La Roux 43 | - Nicolas Leger 44 | - nycvotes-dev 45 | - Olle Jonsson 46 | - Postmodern 47 | - Richard Hirner 48 | - Richard Hurt 49 | - Richard Schneeman 50 | - Robb Shecter 51 | - Tibor Szolár 52 | - Todd Carrico 53 | -------------------------------------------------------------------------------- /support/benchmarks/object_counts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Benchmarks 4 | # Benchmark object counts 5 | class ObjectCounts 6 | def self.report(columnar: false, full: false) 7 | new(columnar: columnar, full: full).report 8 | end 9 | 10 | def initialize(columnar: false, full: false) 11 | @columnar = columnar 12 | @full = full 13 | end 14 | 15 | def report 16 | collect 17 | @before.keys.grep(/T_/).map { |key| 18 | [key, @after[key] - @before[key]] 19 | }.sort_by { |_, delta| -delta }.each { |key, delta| 20 | puts "%10s +%6d" % [key, delta] 21 | } 22 | end 23 | 24 | private 25 | 26 | def collect 27 | @before = count_objects 28 | 29 | if @columnar 30 | require "mime/types" 31 | MIME::Types.first.to_h if @full 32 | else 33 | require "mime/types/full" 34 | end 35 | 36 | @after = count_objects 37 | end 38 | 39 | def count_objects 40 | GC.start 41 | ObjectSpace.count_objects 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/test_mime_types_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | describe MIME::Types::Loader do 7 | def setup 8 | @path = File.expand_path("../fixture", __FILE__) 9 | @loader = MIME::Types::Loader.new(@path) 10 | @bad_path = File.expand_path("../bad-fixtures", __FILE__) 11 | end 12 | 13 | def assert_correctly_loaded(types) 14 | assert_includes(types, "application/1d-interleaved-parityfec") 15 | assert_equal(%w[webm], types["audio/webm"].first.extensions) 16 | refute(types["audio/webm"].first.registered?) 17 | 18 | assert_equal("Fixes a bug with IE6 and progressive JPEGs", 19 | types["image/pjpeg"].first.docs) 20 | 21 | assert(types["audio/vnd.qcelp"].first.obsolete?) 22 | assert_equal("audio/QCELP", types["audio/vnd.qcelp"].first.use_instead) 23 | end 24 | 25 | it "loads YAML files correctly" do 26 | assert_correctly_loaded @loader.load_yaml 27 | end 28 | 29 | it "loads JSON files correctly" do 30 | assert_correctly_loaded @loader.load_json 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /support/deps.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "json" 3 | 4 | class Deps 5 | def self.run(args) 6 | new.run(args) 7 | end 8 | 9 | def run(args) 10 | deps = rubygems_get(gem_name: "mime-types", endpoint: "reverse_dependencies") 11 | weighted_deps = {} 12 | 13 | deps.each do |name| 14 | begin 15 | downloads = gem_downloads(name) 16 | weighted_deps[name] = downloads if downloads 17 | rescue => e 18 | puts "#{name} #{e.message}" 19 | end 20 | end 21 | 22 | weighted_deps 23 | .sort { |(_k1, v1), (_k2, v2)| v2 <=> v1 } 24 | .first(args.number || 50) 25 | .each_with_index do |(k, v), i| 26 | puts "#{i}) #{k}: #{v}" 27 | end 28 | end 29 | 30 | private 31 | 32 | def rubygems_get(gem_name: "", endpoint: "") 33 | path = File.join("/api/v1/gems/", gem_name, endpoint).chomp("/") + ".json" 34 | Net::HTTP.start("rubygems.org", use_ssl: true) do |http| 35 | JSON.parse(http.get(path).body) 36 | end 37 | end 38 | 39 | def gem_downloads(name) 40 | rubygems_get(gem_name: name)["downloads"] 41 | rescue => e 42 | puts "#{name} #{e.message}" 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /support/profile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "ruby-prof" 5 | rescue LoadError 6 | warn "Profiling requires 'ruby-prof'" 7 | exit 1 8 | end 9 | 10 | require "mime-types" 11 | 12 | MIME::Types.logger = nil 13 | 14 | def profile_columnar 15 | puts "Profiling columnar load" 16 | 17 | result = RubyProf.profile do 18 | loader = MIME::Types::Loader.new 19 | 20 | 50.times do 21 | loader.load(columnar: true) 22 | end 23 | end 24 | 25 | RubyProf::FlatPrinter.new(result).print($stdout) 26 | end 27 | 28 | def profile_columnar_full 29 | puts "Profiling columnar load, then full load" 30 | 31 | result = RubyProf.profile do 32 | loader = MIME::Types::Loader.new 33 | 34 | 50.times do 35 | loader.load(columnar: true).first.to_h 36 | end 37 | end 38 | 39 | RubyProf::FlatPrinter.new(result).print($stdout) 40 | end 41 | 42 | def profile_full 43 | puts "Profiling full load" 44 | 45 | result = RubyProf.profile do 46 | loader = MIME::Types::Loader.new 47 | 48 | 50.times do 49 | loader.load(columnar: false) 50 | end 51 | end 52 | 53 | RubyProf::FlatPrinter.new(result).print($stdout) 54 | end 55 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | permissions: {} 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | zizmor: 16 | name: zizmor latest via zizmor-action 17 | runs-on: ubuntu-latest 18 | 19 | permissions: 20 | security-events: write # Zizmor writes security events 21 | 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 25 | with: 26 | disable-sudo: true 27 | egress-policy: block 28 | allowed-endpoints: > 29 | api.github.com:443 30 | ghcr.io:443 31 | github.com:443 32 | pkg-containers.githubusercontent.com:443 33 | 34 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 35 | with: 36 | persist-credentials: false 37 | 38 | - uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0 39 | with: 40 | persona: pedantic 41 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # Licence 2 | 3 | - Copyright 2003-2025 Austin Ziegler and contributors. 4 | 5 | The software in this repository is made available under the MIT license. 6 | 7 | ## MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | this software and associated documentation files (the "Software"), to deal in 11 | the Software without restriction, including without limitation the rights to 12 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 13 | the Software, and to permit persons to whom the Software is furnished to do so, 14 | subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 21 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 22 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 23 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /test/fixture/yaml.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - !ruby/object:MIME::Type 3 | content-type: application/smil 4 | encoding: 8bit 5 | extensions: 6 | - smi 7 | - smil 8 | obsolete: true 9 | use-instead: application/smil+xml 10 | registered: true 11 | - !ruby/object:MIME::Type 12 | content-type: audio/vnd.qcelp 13 | encoding: base64 14 | extensions: 15 | - qcp 16 | obsolete: true 17 | use-instead: audio/QCELP 18 | registered: true 19 | - !ruby/object:MIME::Type 20 | content-type: image/bmp 21 | encoding: base64 22 | extensions: 23 | - bmp 24 | obsolete: true 25 | use-instead: image/x-bmp 26 | registered: false 27 | - !ruby/object:MIME::Type 28 | content-type: application/acad 29 | encoding: base64 30 | registered: false 31 | - !ruby/object:MIME::Type 32 | content-type: audio/webm 33 | encoding: base64 34 | extensions: 35 | - webm 36 | registered: false 37 | - !ruby/object:MIME::Type 38 | content-type: image/pjpeg 39 | docs: Fixes a bug with IE6 and progressive JPEGs 40 | encoding: base64 41 | registered: false 42 | - !ruby/object:MIME::Type 43 | content-type: application/1d-interleaved-parityfec 44 | encoding: base64 45 | registered: true 46 | - !ruby/object:MIME::Type 47 | content-type: audio/1d-interleaved-parityfec 48 | encoding: base64 49 | registered: true 50 | - !ruby/object:MIME::Type 51 | content-type: application/x-apple-diskimage 52 | encoding: base64 53 | extensions: 54 | - dmg 55 | registered: false 56 | -------------------------------------------------------------------------------- /lib/mime/types/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "logger" 4 | 5 | ## 6 | module MIME 7 | ## 8 | class Types 9 | class << self 10 | # Configure the MIME::Types logger. This defaults to an instance of a 11 | # logger that passes messages (unformatted) through to Kernel#warn. 12 | # :attr_accessor: logger 13 | attr_reader :logger 14 | 15 | ## 16 | def logger=(logger) # :nodoc 17 | @logger = 18 | if logger.nil? 19 | NullLogger.new 20 | else 21 | logger 22 | end 23 | end 24 | end 25 | 26 | class WarnLogger < ::Logger # :nodoc: 27 | class WarnLogDevice < ::Logger::LogDevice # :nodoc: 28 | def initialize(*) 29 | end 30 | 31 | def write(m) 32 | Kernel.warn(m) 33 | end 34 | 35 | def close 36 | end 37 | end 38 | 39 | def initialize(*) 40 | super(nil) 41 | @logdev = WarnLogDevice.new 42 | @formatter = ->(_s, _d, _p, m) { m } 43 | end 44 | end 45 | 46 | class NullLogger < ::Logger 47 | def initialize(*) 48 | super(nil) 49 | @logdev = nil 50 | end 51 | 52 | def reopen(_) 53 | self 54 | end 55 | 56 | def <<(_) 57 | end 58 | 59 | def close 60 | end 61 | 62 | def add(_severity, _message = nil, _progname = nil) 63 | end 64 | end 65 | 66 | self.logger = WarnLogger.new 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. Once 5 | # installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: {} 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | dependency-review: 20 | name: Review Dependencies 21 | permissions: 22 | contents: read 23 | 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Harden the runner 27 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 28 | with: 29 | disable-sudo: true 30 | egress-policy: block 31 | allowed-endpoints: > 32 | api.github.com:443 33 | api.securityscorecards.dev:443 34 | github.com:443 35 | 36 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 37 | with: 38 | persist-credentials: false 39 | 40 | - name: 'Dependency Review' 41 | uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 42 | -------------------------------------------------------------------------------- /support/benchmarks/profile_memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "memory_profiler" 5 | rescue Exception # standard:disable Lint/RescueException 6 | warn "Memory profiling requires the gem 'memory_profiler'." 7 | exit 1 8 | end 9 | 10 | module Benchmarks 11 | # Use Memory Profiler to profile memory 12 | class ProfileMemory 13 | def self.report(columnar: false, full: false, mime_types_only: false, top_x: nil) 14 | new(columnar, full, mime_types_only, top_x).report 15 | end 16 | 17 | def initialize(columnar, full, mime_types_only, top_x) 18 | @columnar = !!columnar 19 | @full = !!full 20 | @mime_types_only = !!mime_types_only 21 | 22 | @top_x = top_x 23 | 24 | return unless @top_x 25 | 26 | @top_x = top_x.to_i 27 | @top_x = 10 if @top_x <= 0 28 | end 29 | 30 | def report 31 | collect.pretty_print 32 | end 33 | 34 | private 35 | 36 | def collect 37 | report_params = { 38 | top: @top_x, 39 | # allow_files: @mime_types_only ? %r{mime-types/lib/mime/} : nil, 40 | ignore_files: %r{lib/logger\.rb|lib/logger} 41 | }.delete_if { |_k, v| v.nil? } 42 | 43 | if @columnar 44 | MemoryProfiler.report(**report_params) do 45 | require "mime/types" 46 | MIME::Types.first.to_h if @full 47 | end 48 | else 49 | MemoryProfiler.report(**report_params) do 50 | require "mime/types/full" 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/test_mime_types_lazy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | describe MIME::Types, "lazy loading" do 7 | def setup 8 | ENV["RUBY_MIME_TYPES_LAZY_LOAD"] = "true" 9 | end 10 | 11 | def teardown 12 | reset_mime_types 13 | ENV.delete("RUBY_MIME_TYPES_LAZY_LOAD") 14 | end 15 | 16 | def reset_mime_types 17 | MIME::Types.instance_variable_set(:@__types__, nil) 18 | MIME::Types.send(:load_default_mime_types) 19 | end 20 | 21 | describe ".lazy_load?" do 22 | it "is true when RUBY_MIME_TYPES_LAZY_LOAD is set" do 23 | assert_output "", /RUBY_MIME_TYPES_LAZY_LOAD/ do 24 | assert_equal true, MIME::Types.send(:lazy_load?) 25 | end 26 | end 27 | 28 | it "is nil when RUBY_MIME_TYPES_LAZY_LOAD is unset" do 29 | ENV["RUBY_MIME_TYPES_LAZY_LOAD"] = nil 30 | assert_output "", "" do 31 | assert_nil MIME::Types.send(:lazy_load?) 32 | end 33 | end 34 | 35 | it "is false when RUBY_MIME_TYPES_LAZY_LOAD is false" do 36 | ENV["RUBY_MIME_TYPES_LAZY_LOAD"] = "false" 37 | assert_output "", /RUBY_MIME_TYPES_LAZY_LOAD/ do 38 | assert_equal false, MIME::Types.send(:lazy_load?) 39 | end 40 | end 41 | end 42 | 43 | it "loads lazily when RUBY_MIME_TYPES_LAZY_LOAD is set" do 44 | MIME::Types.instance_variable_set(:@__types__, nil) 45 | assert_nil MIME::Types.instance_variable_get(:@__types__) 46 | refute_nil MIME::Types["text/html"].first 47 | refute_nil MIME::Types.instance_variable_get(:@__types__) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/mime/types/deprecations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types/logger" 4 | 5 | class << MIME::Types 6 | # Used to mark a method as deprecated in the mime-types interface. 7 | def deprecated(options = {}, &block) # :nodoc: 8 | message = 9 | if options[:message] 10 | options[:message] 11 | else 12 | klass = options.fetch(:class) 13 | 14 | msep = case klass 15 | when Class, Module 16 | "." 17 | else 18 | klass = klass.class 19 | "#" 20 | end 21 | 22 | method = "#{klass}#{msep}#{options.fetch(:method)}" 23 | pre = " #{options[:pre]}" if options[:pre] 24 | post = case options[:next] 25 | when :private, :protected 26 | " and will be made #{options[:next]}" 27 | when :removed 28 | " and will be removed" 29 | when nil, "" 30 | nil 31 | else 32 | " #{options[:next]}" 33 | end 34 | 35 | <<-WARNING.chomp.strip 36 | #{caller(2..2).first}: #{klass}#{msep}#{method}#{pre} is deprecated#{post}. 37 | WARNING 38 | end 39 | 40 | if !__deprecation_logged?(message, options[:once]) 41 | MIME::Types.logger.__send__(options[:level] || :debug, message) 42 | end 43 | 44 | return unless block 45 | block.call 46 | end 47 | 48 | private 49 | 50 | def __deprecation_logged?(message, once) 51 | return false unless once 52 | 53 | @__deprecations_logged = {} unless defined?(@__deprecations_logged) 54 | @__deprecations_logged.key?(message) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | workflow_call: 7 | 8 | permissions: {} 9 | 10 | concurrency: 11 | group: publish-docs-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | publish_docs: 16 | name: Publish docs from main 17 | runs-on: ubuntu-latest 18 | environment: release 19 | 20 | permissions: 21 | contents: read 22 | pages: write # Publish documentation to pages 23 | id-token: write # Publish documentation to pages 24 | 25 | steps: 26 | - name: Harden the runner 27 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 28 | with: 29 | egress-policy: audit 30 | 31 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 32 | with: 33 | persist-credentials: false 34 | 35 | - id: rubygems 36 | run: | 37 | ruby -e \ 38 | 'print "version=", Gem::Specification.load(ARGV[0]).rubygems_version, "\n"' \ 39 | mime-types.gemspec >>"${GITHUB_OUTPUT}" 40 | 41 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 42 | with: 43 | bundler-cache: false 44 | ruby-version: ruby 45 | 46 | - name: Install dependencies 47 | run: | 48 | gem update --system="${RUBYGEMS_VERSION}" 49 | bundle install --jobs 4 --retry 3 50 | env: 51 | RUBYGEMS_VERSION: ${{ steps.rubygems.outputs.version }} 52 | 53 | - name: Build documentation 54 | run: | 55 | rake docs 56 | 57 | - name: Upload documentation artifact 58 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 59 | with: 60 | path: doc 61 | 62 | - name: Deploy documentation 63 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 64 | -------------------------------------------------------------------------------- /support/benchmarks/load.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "benchmark" 4 | require "mime/types" 5 | 6 | MIME::Types.logger = MIME::Types::NullLogger.new 7 | 8 | module Benchmarks 9 | # Benchmark loading speed 10 | class Load 11 | def self.report(load_path, repeats) 12 | new(load_path, repeats.to_i).report 13 | end 14 | 15 | def initialize(load_path, repeats = nil) 16 | @cache_file = File.expand_path("../cache.mtc", __FILE__) 17 | @repeats = repeats.to_i 18 | @repeats = 50 if @repeats <= 0 19 | @load_path = load_path 20 | end 21 | 22 | def reload_mime_types(repeats = 1, force: false, columnar: false, cache: false) 23 | loader = MIME::Types::Loader.new 24 | 25 | repeats.times { 26 | types = MIME::Types::Cache.load if cache 27 | unless types 28 | types = loader.load(columnar: columnar) 29 | MIME::Types::Cache.save(types) if cache 30 | end 31 | types.first.to_h if force 32 | } 33 | end 34 | 35 | def report 36 | remove_cache 37 | 38 | Benchmark.bm(50) do |mark| 39 | mark.report("Normal") { reload_mime_types(@repeats) } 40 | mark.report("Columnar") { reload_mime_types(@repeats, columnar: true) } 41 | mark.report("Columnar Full") { reload_mime_types(@repeats, columnar: true, force: true) } 42 | 43 | ENV["RUBY_MIME_TYPES_CACHE"] = @cache_file 44 | mark.report("Cache Initialize") { reload_mime_types(cache: true) } 45 | mark.report("Cached") { reload_mime_types(@repeats, cache: true) } 46 | 47 | remove_cache 48 | ENV["RUBY_MIME_TYPES_CACHE"] = @cache_file 49 | mark.report("Columnar Cache Initialize") { reload_mime_types(columnar: true, cache: true) } 50 | mark.report("Columnar Cached") { reload_mime_types(@repeats, columnar: true, cache: true) } 51 | end 52 | ensure 53 | remove_cache 54 | end 55 | 56 | def remove_cache 57 | File.unlink(@cache_file) if File.exist?(@cache_file) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: Reviewdog 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: {} 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | typos: 14 | if: ${{ github.event.action != 'closed' }} 15 | name: Typos 16 | runs-on: ubuntu-22.04 17 | 18 | permissions: 19 | contents: read 20 | pull-requests: write # Reviewdog comments on pull requests 21 | 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 25 | with: 26 | disable-sudo: true 27 | egress-policy: block 28 | allowed-endpoints: > 29 | api.github.com:443 30 | github.com:443 31 | objects.githubusercontent.com:443 32 | raw.githubusercontent.com:443 33 | release-assets.githubusercontent.com:443 34 | 35 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 36 | with: 37 | persist-credentials: false 38 | 39 | - uses: reviewdog/action-typos@d5eb1bbcd1b3bfde596f6eeb470322727862fe98 # v1.19.0 40 | 41 | actionlint: 42 | if: ${{ github.event.action != 'closed' }} 43 | name: Actionlint 44 | runs-on: ubuntu-22.04 45 | 46 | permissions: 47 | contents: read 48 | pull-requests: write # Reviewdog comments on pull requests 49 | 50 | steps: 51 | - name: Harden Runner 52 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 53 | with: 54 | disable-sudo: true 55 | egress-policy: block 56 | allowed-endpoints: > 57 | api.github.com:443 58 | github.com:443 59 | 60 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 61 | with: 62 | persist-credentials: false 63 | 64 | - uses: reviewdog/action-actionlint@f00ad0691526c10be4021a91b2510f0a769b14d0 # v1.68.0 65 | -------------------------------------------------------------------------------- /lib/mime/types/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | MIME::Types::Cache = Struct.new(:version, :data) # :nodoc: 4 | 5 | # Caching of MIME::Types registries is advisable if you will be loading 6 | # the default registry relatively frequently. With the class methods on 7 | # MIME::Types::Cache, any MIME::Types registry can be marshaled quickly 8 | # and easily. 9 | # 10 | # The cache is invalidated on a per-data-version basis; a cache file for 11 | # version 3.2015.1118 will not be reused with version 3.2015.1201. 12 | class << MIME::Types::Cache 13 | # Attempts to load the cache from the file provided as a parameter or in 14 | # the environment variable +RUBY_MIME_TYPES_CACHE+. Returns +nil+ if the 15 | # file does not exist, if the file cannot be loaded, or if the data in 16 | # the cache version is different than this version. 17 | def load(cache_file = nil) 18 | cache_file ||= ENV["RUBY_MIME_TYPES_CACHE"] 19 | return nil unless cache_file && File.exist?(cache_file) 20 | 21 | cache = Marshal.load(File.binread(cache_file)) 22 | if cache.version == MIME::Types::Data::VERSION 23 | Marshal.load(cache.data) 24 | else 25 | MIME::Types.logger.error <<-WARNING.chomp.strip 26 | Could not load MIME::Types cache: invalid version 27 | WARNING 28 | nil 29 | end 30 | rescue => e 31 | MIME::Types.logger.error <<-WARNING.chomp.strip 32 | Could not load MIME::Types cache: #{e} 33 | WARNING 34 | nil 35 | end 36 | 37 | # Attempts to save the types provided to the cache file provided. 38 | # 39 | # If +types+ is not provided or is +nil+, the cache will contain the 40 | # current MIME::Types default registry. 41 | # 42 | # If +cache_file+ is not provided or is +nil+, the cache will be written 43 | # to the file specified in the environment variable 44 | # +RUBY_MIME_TYPES_CACHE+. If there is no cache file specified either 45 | # directly or through the environment, this method will return +nil+ 46 | def save(types = nil, cache_file = nil) 47 | cache_file ||= ENV["RUBY_MIME_TYPES_CACHE"] 48 | return nil unless cache_file 49 | 50 | types ||= MIME::Types.send(:__types__) 51 | 52 | File.binwrite(cache_file, Marshal.dump(new(MIME::Types::Data::VERSION, Marshal.dump(types)))) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/mime/type/columnar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/type" 4 | 5 | # A version of MIME::Type that works hand-in-hand with a MIME::Types::Columnar 6 | # container to load data by columns. 7 | # 8 | # When a field is has not yet been loaded, that data will be loaded for all 9 | # types in the container before forwarding the message to MIME::Type. 10 | # 11 | # More information can be found in MIME::Types::Columnar. 12 | # 13 | # MIME::Type::Columnar is *not* intended to be created except by 14 | # MIME::Types::Columnar containers. 15 | class MIME::Type::Columnar < MIME::Type 16 | def initialize(container, content_type, extensions) # :nodoc: 17 | @container = container 18 | @__priority_penalty = nil 19 | self.content_type = content_type 20 | @extensions = Set[*Array(extensions).flatten.compact].freeze 21 | clear_sort_priority 22 | end 23 | 24 | def self.column(*methods, file: nil) # :nodoc: 25 | file ||= methods.first 26 | 27 | file_method = :"load_#{file}" 28 | methods.each do |m| 29 | define_method m do |*args| 30 | @container.send(file_method) 31 | super(*args) 32 | end 33 | end 34 | end 35 | 36 | column :friendly 37 | column :encoding, :encoding= 38 | column :docs, :docs= 39 | column :preferred_extension, :preferred_extension= 40 | column :obsolete, :obsolete=, :obsolete?, :registered, :registered=, :registered?, :signature, :signature=, 41 | :signature?, :provisional, :provisional=, :provisional?, file: "flags" 42 | column :xrefs, :xrefs=, :xref_urls 43 | column :use_instead, :use_instead= 44 | 45 | def encode_with(coder) # :nodoc: 46 | @container.send(:load_friendly) 47 | @container.send(:load_encoding) 48 | @container.send(:load_docs) 49 | @container.send(:load_flags) 50 | @container.send(:load_use_instead) 51 | @container.send(:load_xrefs) 52 | @container.send(:load_preferred_extension) 53 | super 54 | end 55 | 56 | def update_sort_priority 57 | if @container.__fully_loaded? 58 | super 59 | else 60 | obsolete = (@__sort_priority & (1 << 7)) != 0 61 | registered = (@__sort_priority & (1 << 5)) == 0 62 | 63 | @__priority_penalty = (obsolete ? 3 : 0) + (registered ? 0 : 2) 64 | end 65 | end 66 | 67 | class << self 68 | undef column 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/mime/types/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types/deprecations" 4 | 5 | class << MIME::Types 6 | include Enumerable 7 | 8 | ## 9 | def new(*) # :nodoc: 10 | super.tap do |types| 11 | __instances__.add types 12 | end 13 | end 14 | 15 | # MIME::Types#[] against the default MIME::Types registry. 16 | def [](type_id, complete: false, registered: false) 17 | __types__[type_id, complete: complete, registered: registered] 18 | end 19 | 20 | # MIME::Types#count against the default MIME::Types registry. 21 | def count 22 | __types__.count 23 | end 24 | 25 | # MIME::Types#each against the default MIME::Types registry. 26 | def each 27 | if block_given? 28 | __types__.each { |t| yield t } 29 | else 30 | enum_for(:each) 31 | end 32 | end 33 | 34 | # MIME::Types#type_for against the default MIME::Types registry. 35 | def type_for(filename) 36 | __types__.type_for(filename) 37 | end 38 | alias_method :of, :type_for 39 | 40 | # MIME::Types#add against the default MIME::Types registry. 41 | def add(*types) 42 | __types__.add(*types) 43 | end 44 | 45 | private 46 | 47 | def lazy_load? 48 | return unless ENV.key?("RUBY_MIME_TYPES_LAZY_LOAD") 49 | 50 | deprecated( 51 | message: "Lazy loading ($RUBY_MIME_TYPES_LAZY_LOAD) is deprecated and will be removed." 52 | ) 53 | 54 | ENV["RUBY_MIME_TYPES_LAZY_LOAD"] != "false" 55 | end 56 | 57 | def __types__ 58 | (defined?(@__types__) && @__types__) || load_default_mime_types 59 | end 60 | 61 | unless private_method_defined?(:load_mode) 62 | def load_mode 63 | {columnar: true} 64 | end 65 | end 66 | 67 | def load_default_mime_types(mode = load_mode) 68 | if (@__types__ = MIME::Types::Cache.load) 69 | __instances__.add(@__types__) 70 | else 71 | @__types__ = MIME::Types::Loader.load(mode) 72 | MIME::Types::Cache.save(@__types__) 73 | end 74 | @__types__ 75 | end 76 | 77 | def __instances__ 78 | @__instances__ ||= Set.new 79 | end 80 | 81 | def reindex_extensions(type) 82 | __instances__.each do |instance| 83 | instance.send(:reindex_extensions!, type) 84 | end 85 | true 86 | end 87 | end 88 | 89 | ## 90 | class MIME::Types 91 | load_default_mime_types(load_mode) unless lazy_load? 92 | end 93 | -------------------------------------------------------------------------------- /.github/workflows/publish-gem.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - lib/mime/types/version.rb 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | types: 14 | - closed 15 | paths: 16 | - lib/mime/types/version.rb 17 | 18 | workflow_dispatch: 19 | 20 | permissions: {} 21 | 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | release: 28 | name: Release mime-types 29 | if: github.repository == 'mime-types/ruby-mime-types' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged)) 30 | 31 | runs-on: ubuntu-latest 32 | environment: release 33 | 34 | env: 35 | rubygems_release_gem: true 36 | 37 | permissions: 38 | contents: write # Create a new tag 39 | id-token: write # Authenticate for gem publishing 40 | 41 | steps: 42 | - name: Harden the runner 43 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 44 | with: 45 | disable-sudo: true 46 | egress-policy: block 47 | allowed-endpoints: > 48 | fulcio.sigstore.dev:443 49 | github.com:443 50 | index.rubygems.org:443 51 | objects.githubusercontent.com:443 52 | rekor.sigstore.dev:443 53 | release-assets.githubusercontent.com:443 54 | rubygems.org:443 55 | tuf-repo-cdn.sigstore.dev:443 56 | 57 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 58 | with: 59 | persist-credentials: true 60 | 61 | - id: rubygems 62 | run: | 63 | ruby -e \ 64 | 'print "version=", Gem::Specification.load(ARGV[0]).rubygems_version, "\n"' \ 65 | mime-types.gemspec >>"${GITHUB_OUTPUT}" 66 | 67 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 68 | with: 69 | bundler-cache: false 70 | ruby-version: ruby 71 | 72 | - name: Install dependencies 73 | run: | 74 | gem update --system="${RUBYGEMS_VERSION}" 75 | bundle install --jobs 4 --retry 3 76 | env: 77 | RUBYGEMS_VERSION: ${{ steps.rubygems.outputs.version }} 78 | 79 | - uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 80 | 81 | - name: Show the status if release failed 82 | if: failure() 83 | run: | 84 | git status 85 | git diff 86 | 87 | publish_docs: 88 | needs: release 89 | permissions: 90 | contents: read 91 | pages: write # Publish documentation 92 | id-token: write # Authenticate for gem publishing 93 | uses: ./.github/workflows/publish-docs.yml 94 | -------------------------------------------------------------------------------- /lib/mime/types/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "set" 4 | 5 | # MIME::Types requires a serializable keyed container that returns an empty Set 6 | # on a key miss. Hash#default_value cannot be used because, while it traverses 7 | # the Marshal format correctly, it will not survive any other serialization 8 | # format (plus, a default of a mutable object resuls in a shared mess). 9 | # Hash#default_proc cannot be used without a wrapper because it prevents 10 | # Marshal serialization (and does not survive the round-trip). 11 | class MIME::Types::Container # :nodoc: 12 | def initialize(hash = {}) 13 | @container = {} 14 | merge!(hash) 15 | end 16 | 17 | def [](key) 18 | container[key] || EMPTY_SET 19 | end 20 | 21 | def []=(key, value) 22 | container[key] = 23 | case value 24 | when Set 25 | value 26 | else 27 | Set[*value] 28 | end 29 | end 30 | 31 | def merge(other) 32 | self.class.new(other) 33 | end 34 | 35 | def merge!(other) 36 | tap { 37 | other = other.is_a?(MIME::Types::Container) ? other.container : other 38 | container.merge!(other) 39 | normalize 40 | } 41 | end 42 | 43 | def to_hash 44 | container 45 | end 46 | 47 | def ==(other) 48 | container == other 49 | end 50 | 51 | def count(*args, &block) 52 | if args.size == 0 53 | container.count 54 | elsif block 55 | container.count(&block) 56 | else 57 | container.count(args.first) 58 | end 59 | end 60 | 61 | def each_pair(&block) 62 | container.each_pair(&block) 63 | end 64 | 65 | alias_method :each, :each_pair 66 | 67 | def each_value(&block) 68 | container.each_value(&block) 69 | end 70 | 71 | def empty? 72 | container.empty? 73 | end 74 | 75 | def flat_map(&block) 76 | container.flat_map(&block) 77 | end 78 | 79 | def keys 80 | container.keys 81 | end 82 | 83 | def values 84 | container.values 85 | end 86 | 87 | def select(&block) 88 | container.select(&block) 89 | end 90 | 91 | def add(key, value) 92 | (container[key] ||= Set.new).add(value) 93 | end 94 | 95 | def marshal_dump 96 | {}.merge(container) 97 | end 98 | 99 | def marshal_load(hash) 100 | @container = hash 101 | end 102 | 103 | def encode_with(coder) 104 | container.each { |k, v| coder[k] = v.to_a } 105 | end 106 | 107 | def init_with(coder) 108 | @container = {} 109 | coder.map.each { |k, v| container[k] = Set[*v] } 110 | end 111 | 112 | protected 113 | 114 | attr_accessor :container 115 | 116 | def normalize 117 | container.each do |k, v| 118 | next if v.is_a?(Set) 119 | 120 | container[k] = Set[*v] 121 | end 122 | end 123 | 124 | EMPTY_SET = Set.new.freeze 125 | private_constant :EMPTY_SET 126 | end 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contribution to mime-types is encouraged in any form: a bug report, a feature 4 | request, or code contributions. There are a few DOs and DON'Ts for 5 | contributions. 6 | 7 | - DO: 8 | 9 | - Keep the coding style that already exists for any updated Ruby code (support 10 | or otherwise). I use [Standard Ruby][standardrb] for linting and formatting. 11 | 12 | - Use thoughtfully-named topic branches for contributions. Rebase your commits 13 | into logical chunks as necessary. 14 | 15 | - Use [quality commit messages][qcm]. 16 | 17 | - Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the 18 | `CHANGELOG.md` as a separate commit from your main change. (Follow the style 19 | in the `CHANGELOG.md` and provide a link to your PR.) 20 | 21 | - Add or update tests as appropriate for your change. The test suite is 22 | written with [minitest][minitest]. 23 | 24 | - Add or update documentation as appropriate for your change. The 25 | documentation is RDoc; mime-types does not use extensions that may be 26 | present in alternative documentation generators. 27 | 28 | - DO NOT: 29 | 30 | - Modify `VERSION` in `lib/mime/types/version.rb`. When your patch is accepted 31 | and a release is made, the version will be updated at that point. 32 | 33 | - Modify `mime-types.gemspec`; it is a generated file. (You _may_ use 34 | `rake gemspec` to regenerate it if your change involves metadata related to 35 | gem itself). 36 | 37 | - Modify the `Gemfile`. 38 | 39 | ## Adding or Modifying MIME Types 40 | 41 | The mime-types registry is managed in [mime-types-data][mtd]. 42 | 43 | ## Test Dependencies 44 | 45 | mime-types uses Ryan Davis's [Hoe][Hoe] to manage the release process, and it 46 | adds a number of rake tasks. You will mostly be interested in `rake`, which runs 47 | the tests the same way that `rake test` will do. 48 | 49 | To assist with the installation of the development dependencies for mime-types, 50 | I have provided the simplest possible Gemfile pointing to the (generated) 51 | `mime-types.gemspec` file. This will permit you to do `bundle install` to get 52 | the development dependencies. 53 | 54 | You can run tests with code coverage analysis by running `rake coverage`. 55 | 56 | ## Benchmarks 57 | 58 | mime-types offers several benchmark tasks to measure different measures of 59 | performance. 60 | 61 | There is a repeated load test, measuring how long it takes to start and load 62 | mime-types with its full registry. By default, it runs fifty loops and uses the 63 | built-in benchmark library: 64 | 65 | - `rake benchmark:load` 66 | 67 | There are two loaded object count benchmarks (for normal and columnar loads). 68 | These use `ObjectSpace.count_objects`. 69 | 70 | - `rake benchmark:objects` 71 | - `rake benchmark:objects:columnar` 72 | 73 | ## Workflow 74 | 75 | Here's the most direct way to get your work merged into the project: 76 | 77 | - Fork the project. 78 | - Clone down your fork 79 | (`git clone git://github.com//ruby-mime-types.git`). 80 | - Create a topic branch to contain your change 81 | (`git checkout -b my_awesome_feature`). 82 | - Hack away, add tests. Not necessarily in that order. 83 | - Make sure everything still passes by running `rake`. 84 | - If necessary, rebase your commits into logical chunks, without errors. 85 | - Push the branch up (`git push origin my_awesome_feature`). 86 | - Create a pull request against mime-types/ruby-mime-types and describe what 87 | your change does and the why you think it should be merged. 88 | 89 | [hoe]: https://github.com/seattlerb/hoe 90 | [minitest]: https://github.com/seattlerb/minitest 91 | [mtd]: https://github.com/mime-types/mime-types-data 92 | [qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 93 | [standardrb]: https://github.com/standardrb/standard 94 | -------------------------------------------------------------------------------- /test/test_mime_types_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | MUTEX = Mutex.new 7 | 8 | describe MIME::Types::Cache do 9 | include Minitest::Hooks 10 | 11 | def around 12 | require "fileutils" 13 | 14 | MUTEX.synchronize do 15 | @cache_file = File.expand_path("../cache.tst", __FILE__) 16 | ENV["RUBY_MIME_TYPES_CACHE"] = @cache_file 17 | clear_cache_file 18 | 19 | super 20 | 21 | clear_cache_file 22 | ENV.delete("RUBY_MIME_TYPES_CACHE") 23 | end 24 | end 25 | 26 | def reset_mime_types 27 | MIME::Types.instance_variable_set(:@__types__, nil) 28 | MIME::Types.send(:load_default_mime_types) 29 | end 30 | 31 | def clear_cache_file 32 | FileUtils.rm @cache_file if File.exist? @cache_file 33 | end 34 | 35 | describe ".load" do 36 | it "does not use cache when RUBY_MIME_TYPES_CACHE is unset" do 37 | ENV.delete("RUBY_MIME_TYPES_CACHE") 38 | assert_nil MIME::Types::Cache.load 39 | end 40 | 41 | it "does not use cache when missing" do 42 | assert_nil MIME::Types::Cache.load 43 | end 44 | 45 | it "registers the data to be updated by #add_extensions" do 46 | MIME::Types::Cache.save 47 | reset_mime_types 48 | assert_equal([], MIME::Types.type_for("foo.additional")) 49 | html = MIME::Types["text/html"][0] 50 | html.add_extensions("additional") 51 | assert_equal([html], MIME::Types.type_for("foo.additional")) 52 | end 53 | 54 | it "outputs an error when there is an invalid version" do 55 | v = MIME::Types::Data::VERSION 56 | MIME::Types::Data.send(:remove_const, :VERSION) 57 | MIME::Types::Data.const_set(:VERSION, "0.0") 58 | MIME::Types::Cache.save 59 | MIME::Types::Data.send(:remove_const, :VERSION) 60 | MIME::Types::Data.const_set(:VERSION, v) 61 | MIME::Types.instance_variable_set(:@__types__, nil) 62 | assert_output "", /MIME::Types cache: invalid version/ do 63 | MIME::Types["text/html"] 64 | end 65 | end 66 | 67 | it "outputs an error when there is a marshal file incompatibility" do 68 | MIME::Types::Cache.save 69 | data = File.binread(@cache_file).reverse 70 | File.binwrite(@cache_file, data) 71 | MIME::Types.instance_variable_set(:@__types__, nil) 72 | assert_output "", /incompatible marshal file format/ do 73 | MIME::Types["text/html"] 74 | end 75 | end 76 | end 77 | 78 | describe ".save" do 79 | it "does not create cache when RUBY_MIME_TYPES_CACHE is unset" do 80 | ENV.delete("RUBY_MIME_TYPES_CACHE") 81 | assert_nil MIME::Types::Cache.save 82 | end 83 | 84 | it "creates the cache " do 85 | assert_equal(false, File.exist?(@cache_file)) 86 | MIME::Types::Cache.save 87 | assert_equal(true, File.exist?(@cache_file)) 88 | end 89 | 90 | it "uses the cache" do 91 | MIME::Types["text/html"].first.add_extensions("hex") 92 | MIME::Types::Cache.save 93 | MIME::Types.instance_variable_set(:@__types__, nil) 94 | 95 | assert_includes MIME::Types["text/html"].first.extensions, "hex" 96 | 97 | reset_mime_types 98 | end 99 | end 100 | end 101 | 102 | describe MIME::Types::Container do 103 | it "marshals and unmarshals correctly" do 104 | container = MIME::Types::Container.new 105 | container.add("xyz", "abc") 106 | 107 | # default proc should return Set[] 108 | assert_equal(Set[], container["abc"]) 109 | assert_equal(Set["abc"], container["xyz"]) 110 | 111 | marshalled = Marshal.dump(container) 112 | loaded_container = Marshal.load(marshalled) 113 | 114 | # default proc should still return Set[] 115 | assert_equal(Set[], loaded_container["abc"]) 116 | assert_equal(Set["abc"], container["xyz"]) 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /mime-types.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: mime-types 3.7.0 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "mime-types".freeze 6 | s.version = "3.7.0".freeze 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 9 | 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= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Austin Ziegler".freeze] 12 | s.date = "2025-07-22" 13 | 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 14 | s.email = ["halostatue@gmail.com".freeze] 15 | 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] 16 | 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] 17 | s.homepage = "https://github.com/mime-types/ruby-mime-types/".freeze 18 | s.licenses = ["MIT".freeze] 19 | s.rdoc_options = ["--main".freeze, "README.md".freeze] 20 | s.required_ruby_version = Gem::Requirement.new(">= 2.0".freeze) 21 | s.rubygems_version = "3.5.22".freeze 22 | s.summary = "The mime-types library provides a library and registry for information about MIME content type definitions".freeze 23 | 24 | s.specification_version = 4 25 | 26 | s.add_runtime_dependency(%q.freeze, ["~> 3.2025".freeze, ">= 3.2025.0507".freeze]) 27 | s.add_runtime_dependency(%q.freeze, [">= 0".freeze]) 28 | s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) 29 | s.add_development_dependency(%q.freeze, ["~> 2.0".freeze]) 30 | s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) 31 | s.add_development_dependency(%q.freeze, ["~> 5.0".freeze]) 32 | s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) 33 | s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) 34 | s.add_development_dependency(%q.freeze, ["~> 1.4".freeze]) 35 | s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) 36 | s.add_development_dependency(%q.freeze, [">= 0.0".freeze]) 37 | s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) 38 | end 39 | -------------------------------------------------------------------------------- /lib/mime/types/_columnar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/type/columnar" 4 | 5 | # MIME::Types::Columnar is used to extend a MIME::Types container to load data 6 | # by columns instead of from JSON or YAML. Column loads of MIME types loaded 7 | # through the columnar store are synchronized with a Mutex. 8 | # 9 | # MIME::Types::Columnar is not intended to be used directly, but will be added 10 | # to an instance of MIME::Types when it is loaded with 11 | # MIME::Types::Loader#load_columnar. 12 | module MIME::Types::Columnar 13 | LOAD_MUTEX = Mutex.new # :nodoc: 14 | 15 | def self.extended(obj) # :nodoc: 16 | super 17 | obj.instance_variable_set(:@__mime_data__, []) 18 | obj.instance_variable_set(:@__files__, Set.new) 19 | end 20 | 21 | def __fully_loaded? # :nodoc: 22 | @__files__.size == 10 23 | end 24 | 25 | # Load the first column data file (type and extensions). 26 | def load_base_data(path) # :nodoc: 27 | @__root__ = path 28 | 29 | each_file_line("content_type", false) do |line| 30 | line = line.split 31 | content_type = line.shift 32 | extensions = line 33 | 34 | type = MIME::Type::Columnar.new(self, content_type, extensions) 35 | @__mime_data__ << type 36 | add(type) 37 | end 38 | 39 | each_file_byte("spri") do |type, byte| 40 | type.instance_variable_set(:@__sort_priority, byte) 41 | end 42 | 43 | self 44 | end 45 | 46 | private 47 | 48 | def each_file_line(name, lookup = true) 49 | LOAD_MUTEX.synchronize do 50 | next if @__files__.include?(name) 51 | 52 | i = -1 53 | column = File.join(@__root__, "mime.#{name}.column") 54 | 55 | IO.readlines(column, encoding: "UTF-8").each do |line| 56 | line.chomp! 57 | 58 | if lookup 59 | (type = @__mime_data__[i += 1]) || next 60 | yield type, line 61 | else 62 | yield line 63 | end 64 | end 65 | 66 | @__files__ << name 67 | end 68 | end 69 | 70 | def each_file_byte(name) 71 | LOAD_MUTEX.synchronize do 72 | next if @__files__.include?(name) 73 | 74 | i = -1 75 | 76 | filename = File.join(@__root__, "mime.#{name}.column") 77 | 78 | next unless File.exist?(filename) 79 | 80 | IO.binread(filename).unpack("C*").each do |byte| 81 | (type = @__mime_data__[i += 1]) || next 82 | yield type, byte 83 | end 84 | 85 | @__files__ << name 86 | end 87 | end 88 | 89 | def load_encoding 90 | each_file_line("encoding") do |type, line| 91 | pool ||= {} 92 | type.instance_variable_set(:@encoding, pool[line] ||= line) 93 | end 94 | end 95 | 96 | def load_docs 97 | each_file_line("docs") do |type, line| 98 | type.instance_variable_set(:@docs, opt(line)) 99 | end 100 | end 101 | 102 | def load_preferred_extension 103 | each_file_line("pext") do |type, line| 104 | type.instance_variable_set(:@preferred_extension, opt(line)) 105 | end 106 | end 107 | 108 | def load_flags 109 | each_file_line("flags") do |type, line| 110 | line = line.split 111 | type.instance_variable_set(:@obsolete, flag(line.shift)) 112 | type.instance_variable_set(:@registered, flag(line.shift)) 113 | type.instance_variable_set(:@signature, flag(line.shift)) 114 | type.instance_variable_set(:@provisional, flag(line.shift)) 115 | end 116 | end 117 | 118 | def load_xrefs 119 | each_file_line("xrefs") { |type, line| 120 | type.instance_variable_set(:@xrefs, dict(line, transform: :array)) 121 | } 122 | end 123 | 124 | def load_friendly 125 | each_file_line("friendly") { |type, line| 126 | type.instance_variable_set(:@friendly, dict(line)) 127 | } 128 | end 129 | 130 | def load_use_instead 131 | each_file_line("use_instead") do |type, line| 132 | type.instance_variable_set(:@use_instead, opt(line)) 133 | end 134 | end 135 | 136 | def dict(line, transform: nil) 137 | if line == "-" 138 | {} 139 | else 140 | line.split("|").each_with_object({}) { |l, h| 141 | k, v = l.split("^") 142 | v = nil if v.empty? 143 | 144 | if transform 145 | send(:"dict_#{transform}", h, k, v) 146 | else 147 | h[k] = v 148 | end 149 | } 150 | end 151 | end 152 | 153 | def dict_extension_priority(h, k, v) 154 | return if v.nil? 155 | 156 | v = v.to_i if v.is_a?(String) 157 | v = v.trunc if v.is_a?(Float) 158 | v = [[-20, v].max, 20].min 159 | 160 | return if v.zero? 161 | 162 | h[k] = v 163 | end 164 | 165 | def dict_array(h, k, v) 166 | h[k] = Array(v) 167 | end 168 | 169 | def arr(line) 170 | if line == "-" 171 | [] 172 | else 173 | line.split("|").flatten.compact.uniq 174 | end 175 | end 176 | 177 | def opt(line) 178 | line unless line == "-" 179 | end 180 | 181 | def flag(line) 182 | line == "1" 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/mime/types/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | module MIME; end 5 | 6 | ## 7 | class MIME::Types; end 8 | 9 | require "mime/types/data" 10 | 11 | # This class is responsible for initializing the MIME::Types registry from 12 | # the data files supplied with the mime-types library. 13 | # 14 | # The Loader will use one of the following paths: 15 | # 1. The +path+ provided in its constructor argument; 16 | # 2. The value of ENV["RUBY_MIME_TYPES_DATA"]; or 17 | # 3. The value of MIME::Types::Data::PATH. 18 | # 19 | # When #load is called, the +path+ will be searched recursively for all YAML 20 | # (.yml or .yaml) files. By convention, there is one file for each media 21 | # type (application.yml, audio.yml, etc.), but this is not required. 22 | class MIME::Types::Loader 23 | # The path that will be read for the MIME::Types files. 24 | attr_reader :path 25 | # The MIME::Types container instance that will be loaded. If not provided 26 | # at initialization, a new MIME::Types instance will be constructed. 27 | attr_reader :container 28 | 29 | # Creates a Loader object that can be used to load MIME::Types registries 30 | # into memory, using YAML, JSON, or Columnar registry format loaders. 31 | def initialize(path = nil, container = nil) 32 | path = path || ENV["RUBY_MIME_TYPES_DATA"] || MIME::Types::Data::PATH 33 | @container = container || MIME::Types.new 34 | @path = File.expand_path(path) 35 | end 36 | 37 | # Loads a MIME::Types registry from YAML files (*.yml or 38 | # *.yaml) recursively found in +path+. 39 | # 40 | # It is expected that the YAML objects contained within the registry array 41 | # will be tagged as !ruby/object:MIME::Type. 42 | # 43 | # Note that the YAML format is about 2½ times *slower* than the JSON format. 44 | # 45 | # NOTE: The purpose of this format is purely for maintenance reasons. 46 | def load_yaml 47 | Dir[yaml_path].sort.each do |f| 48 | container.add(*self.class.load_from_yaml(f), :silent) 49 | end 50 | container 51 | end 52 | 53 | # Loads a MIME::Types registry from JSON files (*.json) 54 | # recursively found in +path+. 55 | # 56 | # It is expected that the JSON objects will be an array of hash objects. 57 | # The JSON format is the registry format for the MIME types registry 58 | # shipped with the mime-types library. 59 | def load_json 60 | Dir[json_path].sort.each do |f| 61 | types = self.class.load_from_json(f) 62 | container.add(*types, :silent) 63 | end 64 | container 65 | end 66 | 67 | # Loads a MIME::Types registry from columnar files recursively found in 68 | # +path+. 69 | def load_columnar 70 | require "mime/types/columnar" unless defined?(MIME::Types::Columnar) 71 | container.extend(MIME::Types::Columnar) 72 | container.load_base_data(path) 73 | 74 | container 75 | end 76 | 77 | # Loads a MIME::Types registry. Loads from JSON files by default 78 | # (#load_json). 79 | # 80 | # This will load from columnar files (#load_columnar) if columnar: 81 | # true is provided in +options+ and there are columnar files in +path+. 82 | def load(options = {columnar: true}) 83 | if options[:columnar] && !Dir[columnar_path].empty? 84 | load_columnar 85 | else 86 | load_json 87 | end 88 | end 89 | 90 | class << self 91 | # Loads the default MIME::Type registry. 92 | def load(options = {columnar: false}) 93 | new.load(options) 94 | end 95 | 96 | # Loads MIME::Types from a single YAML file. 97 | # 98 | # It is expected that the YAML objects contained within the registry 99 | # array will be tagged as !ruby/object:MIME::Type. 100 | # 101 | # Note that the YAML format is about 2½ times *slower* than the JSON 102 | # format. 103 | # 104 | # NOTE: The purpose of this format is purely for maintenance reasons. 105 | def load_from_yaml(filename) 106 | begin 107 | require "psych" 108 | rescue LoadError 109 | nil 110 | end 111 | 112 | require "yaml" 113 | 114 | if old_yaml? 115 | YAML.safe_load(read_file(filename), [MIME::Type]) 116 | else 117 | YAML.safe_load(read_file(filename), permitted_classes: [MIME::Type]) 118 | end 119 | end 120 | 121 | # Loads MIME::Types from a single JSON file. 122 | # 123 | # It is expected that the JSON objects will be an array of hash objects. 124 | # The JSON format is the registry format for the MIME types registry 125 | # shipped with the mime-types library. 126 | def load_from_json(filename) 127 | require "json" 128 | JSON.parse(read_file(filename)).map { |type| MIME::Type.new(type) } 129 | end 130 | 131 | private 132 | 133 | def read_file(filename) 134 | File.open(filename, "r:UTF-8:-", &:read) 135 | end 136 | 137 | def old_yaml? 138 | @old_yaml ||= 139 | begin 140 | require "rubygems/version" 141 | Gem::Version.new(YAML::VERSION) < Gem::Version.new("3.1") 142 | end 143 | end 144 | end 145 | 146 | private 147 | 148 | def yaml_path 149 | File.join(path, "*.y{,a}ml") 150 | end 151 | 152 | def json_path 153 | File.join(path, "*.json") 154 | end 155 | 156 | def columnar_path 157 | File.join(path, "*.column") 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [INSERT CONTACT 63 | METHOD]. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at 125 | . 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [Mozilla CoC]: https://github.com/mozilla/diversity 129 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "hoe" 3 | require "rake/clean" 4 | require "minitest" 5 | require "minitest/test_task" 6 | 7 | Hoe.plugin :halostatue 8 | Hoe.plugin :rubygems 9 | 10 | Hoe.plugins.delete :debug 11 | Hoe.plugins.delete :newb 12 | Hoe.plugins.delete :publish 13 | Hoe.plugins.delete :signing 14 | Hoe.plugins.delete :test 15 | 16 | spec = Hoe.spec "mime-types" do 17 | developer("Austin Ziegler", "halostatue@gmail.com") 18 | 19 | self.trusted_release = ENV["rubygems_release_gem"] == "true" 20 | 21 | require_ruby_version ">= 2.0" 22 | 23 | license "MIT" 24 | 25 | spec_extras[:metadata] = ->(val) { 26 | val.merge!({"rubygems_mfa_required" => "true"}) 27 | } 28 | 29 | extra_deps << ["mime-types-data", "~> 3.2025", ">= 3.2025.0507"] 30 | extra_deps << ["logger", ">= 0"] 31 | 32 | extra_dev_deps << ["hoe", "~> 4.0"] 33 | extra_dev_deps << ["hoe-halostatue", "~> 2.0"] 34 | extra_dev_deps << ["hoe-rubygems", "~> 1.0"] 35 | extra_dev_deps << ["minitest", "~> 5.0"] 36 | extra_dev_deps << ["minitest-autotest", "~> 1.0"] 37 | extra_dev_deps << ["minitest-focus", "~> 1.0"] 38 | extra_dev_deps << ["minitest-hooks", "~> 1.4"] 39 | extra_dev_deps << ["rake", ">= 10.0", "< 14"] 40 | extra_dev_deps << ["rdoc", ">= 0.0"] 41 | extra_dev_deps << ["standard", "~> 1.0"] 42 | end 43 | 44 | Minitest::TestTask.create :test 45 | Minitest::TestTask.create :coverage do |t| 46 | formatters = <<-RUBY.split($/).join(" ") 47 | SimpleCov::Formatter::MultiFormatter.new([ 48 | SimpleCov::Formatter::HTMLFormatter, 49 | SimpleCov::Formatter::LcovFormatter, 50 | SimpleCov::Formatter::SimpleFormatter 51 | ]) 52 | RUBY 53 | t.test_prelude = <<-RUBY.split($/).join("; ") 54 | require "simplecov" 55 | require "simplecov-lcov" 56 | 57 | SimpleCov::Formatter::LcovFormatter.config do |config| 58 | config.report_with_single_file = true 59 | config.lcov_file_name = "lcov.info" 60 | end 61 | 62 | SimpleCov.start "test_frameworks" do 63 | enable_coverage :branch 64 | primary_coverage :branch 65 | formatter #{formatters} 66 | end 67 | RUBY 68 | end 69 | 70 | task default: :test 71 | 72 | namespace :benchmark do 73 | task :support do 74 | %w[lib support].each { |path| 75 | $LOAD_PATH.unshift(File.join(Rake.application.original_dir, path)) 76 | } 77 | end 78 | 79 | desc "Benchmark Load Times" 80 | task :load, [:repeats] => "benchmark:support" do |_, args| 81 | require "benchmarks/load" 82 | Benchmarks::Load.report( 83 | File.join(Rake.application.original_dir, "lib"), 84 | args.repeats 85 | ) 86 | end 87 | 88 | desc "Memory profiler" 89 | task :memory, [:top_x, :mime_types_only] => "benchmark:support" do |_, args| 90 | require "benchmarks/profile_memory" 91 | Benchmarks::ProfileMemory.report( 92 | mime_types_only: args.mime_types_only, 93 | top_x: args.top_x 94 | ) 95 | end 96 | 97 | desc "Columnar memory profiler" 98 | task "memory:columnar", [:top_x, :mime_types_only] => "benchmark:support" do |_, args| 99 | require "benchmarks/profile_memory" 100 | Benchmarks::ProfileMemory.report( 101 | columnar: true, 102 | mime_types_only: args.mime_types_only, 103 | top_x: args.top_x 104 | ) 105 | end 106 | 107 | desc "Columnar allocation counts (full load)" 108 | task "memory:columnar:full", [:top_x, :mime_types_only] => "benchmark:support" do |_, args| 109 | require "benchmarks/profile_memory" 110 | Benchmarks::ProfileMemory.report( 111 | columnar: true, 112 | full: true, 113 | top_x: args.top_x, 114 | mime_types_only: args.mime_types_only 115 | ) 116 | end 117 | 118 | desc "Object counts" 119 | task objects: "benchmark:support" do 120 | require "benchmarks/object_counts" 121 | Benchmarks::ObjectCounts.report 122 | end 123 | 124 | desc "Columnar object counts" 125 | task "objects:columnar" => "benchmark:support" do 126 | require "benchmarks/object_counts" 127 | Benchmarks::ObjectCounts.report(columnar: true) 128 | end 129 | 130 | desc "Columnar object counts (full load)" 131 | task "objects:columnar:full" => "benchmark:support" do 132 | require "benchmarks/object_counts" 133 | Benchmarks::ObjectCounts.report(columnar: true, full: true) 134 | end 135 | end 136 | 137 | namespace :profile do 138 | task full: "benchmark:support" do 139 | require "profile" 140 | profile_full 141 | end 142 | 143 | task columnar: "benchmark:support" do 144 | require "profile" 145 | profile_columnar 146 | end 147 | 148 | task "columnar:full" => "benchmark:support" do 149 | require "profile" 150 | profile_columnar_full 151 | end 152 | end 153 | 154 | namespace :convert do 155 | namespace :docs do 156 | task :setup do 157 | gem "rdoc" 158 | require "rdoc/rdoc" 159 | @doc_converter ||= RDoc::Markup::ToMarkdown.new 160 | end 161 | 162 | FileList["*.rdoc"].each do |name| 163 | rdoc = name 164 | mark = "#{File.basename(name, ".rdoc")}.md" 165 | 166 | file mark => [rdoc, :setup] do |t| 167 | puts "#{rdoc} => #{mark}" 168 | File.binwrite(t.name, @doc_converter.convert(IO.read(t.prerequisites.first))) 169 | end 170 | 171 | CLEAN.add mark 172 | 173 | task run: [mark] 174 | end 175 | end 176 | 177 | desc "Convert documentation from RDoc to Markdown" 178 | task docs: "convert:docs:run" 179 | end 180 | 181 | task :version do 182 | require "mime/types/version" 183 | puts MIME::Types::VERSION 184 | end 185 | 186 | namespace :deps do 187 | task :top, [:number] => "benchmark:support" do |_, args| 188 | require "deps" 189 | Deps.run(args) 190 | end 191 | end 192 | 193 | task :console do 194 | arguments = %w[irb] 195 | arguments.push(*spec.spec.require_paths.map { |dir| "-I#{dir}" }) 196 | arguments.push("-r#{spec.spec.name.gsub("-", File::SEPARATOR)}") 197 | unless system(*arguments) 198 | error "Command failed: #{show_command}" 199 | abort 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /test/test_mime_types_class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | describe MIME::Types, "registry" do 7 | def setup 8 | MIME::Types.send(:load_default_mime_types) 9 | end 10 | 11 | describe "is enumerable" do 12 | it "correctly uses an Enumerable method like #any?" do 13 | assert(MIME::Types.any? { |type| type.content_type == "text/plain" }) 14 | end 15 | 16 | it "implements each with no parameters to return an Enumerator" do 17 | assert_kind_of Enumerator, MIME::Types.each 18 | assert_kind_of Enumerator, MIME::Types.map 19 | end 20 | 21 | it "will create a lazy enumerator" do 22 | assert_kind_of Enumerator::Lazy, MIME::Types.lazy 23 | assert_kind_of Enumerator::Lazy, MIME::Types.map.lazy 24 | end 25 | 26 | it "is countable with an enumerator" do 27 | assert MIME::Types.each.count > 999 28 | assert MIME::Types.lazy.count > 999 29 | end 30 | end 31 | 32 | describe ".[]" do 33 | it "can be searched with a MIME::Type" do 34 | text_plain = MIME::Type.new("content-type" => "text/plain") 35 | assert_includes MIME::Types[text_plain], "text/plain" 36 | assert_equal 1, MIME::Types[text_plain].size 37 | end 38 | 39 | it "can be searched with a regular expression" do 40 | assert_includes MIME::Types[/plain$/], "text/plain" 41 | assert_equal 1, MIME::Types[/plain$/].size 42 | end 43 | 44 | it "sorts by priority with multiple matches" do 45 | types = MIME::Types[/gzip$/].select { |t| 46 | %w[application/gzip application/x-gzip multipart/x-gzip].include?(t) 47 | } 48 | # This is this way because of a new type ending with gzip that only 49 | # appears in some data files. 50 | assert_equal %w[application/gzip multipart/x-gzip application/x-gzip], types 51 | assert_equal 3, types.size 52 | end 53 | 54 | it "can be searched with a string" do 55 | assert_includes MIME::Types["text/plain"], "text/plain" 56 | assert_equal 1, MIME::Types["text/plain"].size 57 | end 58 | 59 | it "can be searched with the complete flag" do 60 | assert_empty MIME::Types[ 61 | "application/x-www-form-urlencoded", 62 | complete: true 63 | ] 64 | assert_includes MIME::Types["text/plain", complete: true], "text/plain" 65 | assert_equal 1, MIME::Types["text/plain", complete: true].size 66 | end 67 | 68 | it "can be searched with the registered flag" do 69 | assert_empty MIME::Types["application/x-wordperfect6.1", registered: true] 70 | refute_empty MIME::Types[ 71 | "application/x-www-form-urlencoded", 72 | registered: true 73 | ] 74 | refute_empty MIME::Types[/gzip/, registered: true] 75 | refute_equal MIME::Types[/gzip/], MIME::Types[/gzip/, registered: true] 76 | end 77 | end 78 | 79 | describe ".type_for" do 80 | it "finds all types for a given extension" do 81 | assert_equal %w[application/gzip application/x-gzip], 82 | MIME::Types.type_for("gz") 83 | end 84 | 85 | it "separates the extension from filenames" do 86 | assert_equal %w[image/jpeg], MIME::Types.of(["foo.jpeg", "bar.jpeg"]) 87 | end 88 | 89 | it "finds multiple extensions ordered by the filename list" do 90 | result = MIME::Types.type_for(%w[foo.txt foo.jpeg]) 91 | 92 | # assert_equal %w[text/plain image/jpeg], MIME::Types.type_for(%w[foo.txt foo.jpeg]) 93 | assert_equal %w[text/plain image/jpeg], result 94 | end 95 | 96 | it "does not find unknown extensions" do 97 | assert_empty MIME::Types.type_for("zzz") 98 | end 99 | 100 | it "modifying type extensions causes reindexing" do 101 | plain_text = MIME::Types["text/plain"].first 102 | plain_text.add_extensions("xtxt") 103 | assert_includes MIME::Types.type_for("xtxt"), "text/plain" 104 | end 105 | 106 | it "handles newline characters correctly" do 107 | assert_includes MIME::Types.type_for("test.pdf\n.txt"), "text/plain" 108 | assert_includes MIME::Types.type_for("test.txt\n.pdf"), "application/pdf" 109 | end 110 | 111 | it "returns a stable order for types with equal priority" do 112 | assert_equal %w[text/x-vcalendar text/x-vcard], MIME::Types[/text\/x-vca/] 113 | end 114 | end 115 | 116 | describe ".count" do 117 | it "can count the number of types inside" do 118 | assert MIME::Types.count > 999 119 | end 120 | end 121 | 122 | describe ".add" do 123 | def setup 124 | MIME::Types.instance_variable_set(:@__types__, nil) 125 | MIME::Types.send(:load_default_mime_types) 126 | end 127 | 128 | let(:eruby) { MIME::Type.new("content-type" => "application/x-eruby") } 129 | let(:jinja) { MIME::Type.new("content-type" => "application/jinja2") } 130 | 131 | it "successfully adds a new type" do 132 | MIME::Types.add(eruby) 133 | assert_equal MIME::Types["application/x-eruby"], [eruby] 134 | end 135 | 136 | it "complains about adding a duplicate type" do 137 | MIME::Types.add(eruby) 138 | assert_output "", /is already registered as a variant/ do 139 | MIME::Types.add(eruby) 140 | end 141 | assert_equal MIME::Types["application/x-eruby"], [eruby] 142 | end 143 | 144 | it "does not complain about adding a duplicate type when quiet" do 145 | MIME::Types.add(eruby) 146 | assert_silent do 147 | MIME::Types.add(eruby, :silent) 148 | end 149 | assert_equal MIME::Types["application/x-eruby"], [eruby] 150 | end 151 | 152 | it "successfully adds from an array" do 153 | MIME::Types.add([eruby, jinja]) 154 | assert_equal MIME::Types["application/x-eruby"], [eruby] 155 | assert_equal MIME::Types["application/jinja2"], [jinja] 156 | end 157 | 158 | it "successfully adds from another MIME::Types" do 159 | old_count = MIME::Types.count 160 | 161 | mt = MIME::Types.new 162 | mt.add(eruby) 163 | 164 | MIME::Types.add(mt) 165 | assert_equal old_count + 1, MIME::Types.count 166 | 167 | assert_equal MIME::Types[eruby.content_type], [eruby] 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /test/test_mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | describe MIME::Types do 7 | def mime_types 8 | @mime_types ||= MIME::Types.new.tap { |mt| 9 | mt.add( 10 | MIME::Type.new("content-type" => "text/plain", "extensions" => %w[txt]), 11 | MIME::Type.new("content-type" => "image/jpeg", "extensions" => %w[jpg jpeg]), 12 | MIME::Type.new("content-type" => "application/x-wordperfect6.1"), 13 | MIME::Type.new("content-type" => "application/x-www-form-urlencoded", "registered" => true), 14 | MIME::Type.new("content-type" => "application/x-gzip", "extensions" => %w[gz]), 15 | MIME::Type.new("content-type" => "application/gzip", "extensions" => "gz", "registered" => true), 16 | *MIME::Types.type_for("foo.webm") 17 | ) 18 | } 19 | end 20 | 21 | describe "is enumerable" do 22 | it "correctly uses an Enumerable method like #any?" do 23 | assert(mime_types.any? { |type| type.content_type == "text/plain" }) 24 | end 25 | 26 | it "implements each with no parameters to return an Enumerator" do 27 | assert_kind_of Enumerator, mime_types.each 28 | assert_kind_of Enumerator, mime_types.map 29 | end 30 | 31 | it "will create a lazy enumerator" do 32 | assert_kind_of Enumerator::Lazy, mime_types.lazy 33 | assert_kind_of Enumerator::Lazy, mime_types.map.lazy 34 | end 35 | 36 | it "is countable with an enumerator" do 37 | assert_equal 8, mime_types.each.count 38 | assert_equal 8, mime_types.lazy.count 39 | end 40 | end 41 | 42 | describe "#[]" do 43 | it "can be searched with a MIME::Type" do 44 | text_plain = MIME::Type.new("content-type" => "text/plain") 45 | assert_includes mime_types[text_plain], "text/plain" 46 | assert_equal 1, mime_types[text_plain].size 47 | end 48 | 49 | it "can be searched with a regular expression" do 50 | assert_includes mime_types[/plain$/], "text/plain" 51 | assert_equal 1, mime_types[/plain$/].size 52 | end 53 | 54 | it "sorts by priority with multiple matches" do 55 | assert_equal %w[application/gzip application/x-gzip], mime_types[/gzip$/] 56 | assert_equal 2, mime_types[/gzip$/].size 57 | end 58 | 59 | it "can be searched with a string" do 60 | assert_includes mime_types["text/plain"], "text/plain" 61 | assert_equal 1, mime_types["text/plain"].size 62 | end 63 | 64 | it "can be searched with the complete flag" do 65 | assert_empty mime_types[ 66 | "application/x-www-form-urlencoded", 67 | complete: true 68 | ] 69 | assert_includes mime_types["text/plain", complete: true], "text/plain" 70 | assert_equal 1, mime_types["text/plain", complete: true].size 71 | end 72 | 73 | it "can be searched with the registered flag" do 74 | assert_empty mime_types["application/x-wordperfect6.1", registered: true] 75 | refute_empty mime_types[ 76 | "application/x-www-form-urlencoded", 77 | registered: true 78 | ] 79 | refute_empty mime_types[/gzip/, registered: true] 80 | refute_equal mime_types[/gzip/], mime_types[/gzip/, registered: true] 81 | end 82 | 83 | it "properly returns an empty result on a regular expression miss" do 84 | assert_empty mime_types[/^foo/] 85 | assert_empty mime_types[/^foo/, registered: true] 86 | assert_empty mime_types[/^foo/, complete: true] 87 | end 88 | end 89 | 90 | describe "#add" do 91 | let(:eruby) { MIME::Type.new("content-type" => "application/x-eruby") } 92 | let(:jinja) { MIME::Type.new("content-type" => "application/jinja2") } 93 | 94 | it "successfully adds a new type" do 95 | mime_types.add(eruby) 96 | assert_equal mime_types["application/x-eruby"], [eruby] 97 | end 98 | 99 | it "complains about adding a duplicate type" do 100 | mime_types.add(eruby) 101 | assert_output "", /is already registered as a variant/ do 102 | mime_types.add(eruby) 103 | end 104 | assert_equal mime_types["application/x-eruby"], [eruby] 105 | end 106 | 107 | it "does not complain about adding a duplicate type when quiet" do 108 | mime_types.add(eruby) 109 | assert_output "", "" do 110 | mime_types.add(eruby, :silent) 111 | end 112 | assert_equal mime_types["application/x-eruby"], [eruby] 113 | end 114 | 115 | it "successfully adds from an array" do 116 | mime_types.add([eruby, jinja]) 117 | assert_equal mime_types["application/x-eruby"], [eruby] 118 | assert_equal mime_types["application/jinja2"], [jinja] 119 | end 120 | 121 | it "successfully adds from another MIME::Types" do 122 | mt = MIME::Types.new 123 | mt.add(mime_types) 124 | assert_equal mime_types.count, mt.count 125 | 126 | mime_types.each do |type| 127 | assert_equal mt[type.content_type], [type] 128 | end 129 | end 130 | end 131 | 132 | describe "#type_for" do 133 | it "finds all types for a given extension" do 134 | assert_equal %w[application/gzip application/x-gzip], 135 | mime_types.type_for("gz") 136 | end 137 | 138 | it "separates the extension from filenames" do 139 | assert_equal %w[image/jpeg], mime_types.of(["foo.jpeg", "bar.jpeg"]) 140 | end 141 | 142 | it "finds multiple extensions" do 143 | assert_equal %w[text/plain image/jpeg], 144 | mime_types.type_for(%w[foo.txt foo.jpeg]) 145 | end 146 | 147 | it "does not find unknown extensions" do 148 | keys = mime_types.instance_variable_get(:@extension_index).keys 149 | assert_empty mime_types.type_for("zzz") 150 | assert_equal keys, mime_types.instance_variable_get(:@extension_index).keys 151 | end 152 | 153 | it "modifying type extensions causes reindexing" do 154 | plain_text = mime_types["text/plain"].first 155 | plain_text.add_extensions("xtxt") 156 | assert_includes mime_types.type_for("xtxt"), "text/plain" 157 | end 158 | 159 | it "handles newline characters correctly" do 160 | assert_includes mime_types.type_for("test.pdf\n.txt"), "text/plain" 161 | end 162 | 163 | it "returns a stable order for types with equal priority" do 164 | assert_equal %w[text/x-vcalendar text/x-vcard], MIME::Types[/text\/x-vca/] 165 | end 166 | end 167 | 168 | describe "#count" do 169 | it "can count the number of types inside" do 170 | assert_equal 8, mime_types.count 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/mime/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | module MIME 5 | ## 6 | class Types 7 | end 8 | end 9 | 10 | require "mime/type" 11 | 12 | # MIME::Types is a registry of MIME types. It is both a class (created with 13 | # MIME::Types.new) and a default registry (loaded automatically or through 14 | # interactions with MIME::Types.[] and MIME::Types.type_for). 15 | # 16 | # == The Default mime-types Registry 17 | # 18 | # The default mime-types registry is loaded automatically when the library 19 | # is required (require 'mime/types'), but it may be lazily loaded 20 | # (loaded on first use) with the use of the environment variable 21 | # +RUBY_MIME_TYPES_LAZY_LOAD+ having any value other than +false+. The 22 | # initial startup is about 14× faster (~10 ms vs ~140 ms), but the 23 | # registry will be loaded at some point in the future. 24 | # 25 | # The default mime-types registry can also be loaded from a Marshal cache 26 | # file specific to the version of MIME::Types being loaded. This will be 27 | # handled automatically with the use of a file referred to in the 28 | # environment variable +RUBY_MIME_TYPES_CACHE+. MIME::Types will attempt to 29 | # load the registry from this cache file (MIME::Type::Cache.load); if it 30 | # cannot be loaded (because the file does not exist, there is an error, or 31 | # the data is for a different version of mime-types), the default registry 32 | # will be loaded from the normal JSON version and then the cache file will 33 | # be *written* to the location indicated by +RUBY_MIME_TYPES_CACHE+. Cache 34 | # file loads just over 4½× faster (~30 ms vs ~140 ms). 35 | # loads. 36 | # 37 | # Notes: 38 | # * The loading of the default registry is *not* atomic; when using a 39 | # multi-threaded environment, it is recommended that lazy loading is not 40 | # used and mime-types is loaded as early as possible. 41 | # * Cache files should be specified per application in a multiprocess 42 | # environment and should be initialized during deployment or before 43 | # forking to minimize the chance that the multiple processes will be 44 | # trying to write to the same cache file at the same time, or that two 45 | # applications that are on different versions of mime-types would be 46 | # thrashing the cache. 47 | # * Unless cache files are preinitialized, the application using the 48 | # mime-types cache file must have read/write permission to the cache file. 49 | # 50 | # == Usage 51 | # require 'mime/types' 52 | # 53 | # plaintext = MIME::Types['text/plain'] 54 | # print plaintext.media_type # => 'text' 55 | # print plaintext.sub_type # => 'plain' 56 | # 57 | # puts plaintext.extensions.join(" ") # => 'asc txt c cc h hh cpp' 58 | # 59 | # puts plaintext.encoding # => 8bit 60 | # puts plaintext.binary? # => false 61 | # puts plaintext.ascii? # => true 62 | # puts plaintext.obsolete? # => false 63 | # puts plaintext.registered? # => true 64 | # puts plaintext.provisional? # => false 65 | # puts plaintext == 'text/plain' # => true 66 | # puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip' 67 | # 68 | class MIME::Types 69 | include Enumerable 70 | 71 | # Creates a new MIME::Types registry. 72 | def initialize 73 | @type_variants = Container.new 74 | @extension_index = Container.new 75 | end 76 | 77 | # Returns the number of known type variants. 78 | def count 79 | @type_variants.values.inject(0) { |a, e| a + e.size } 80 | end 81 | 82 | def inspect # :nodoc: 83 | "#<#{self.class}: #{count} variants, #{@extension_index.count} extensions>" 84 | end 85 | 86 | # Iterates through the type variants. 87 | def each 88 | if block_given? 89 | @type_variants.each_value { |tv| tv.each { |t| yield t } } 90 | else 91 | enum_for(:each) 92 | end 93 | end 94 | 95 | @__types__ = nil 96 | 97 | # Returns a list of MIME::Type objects, which may be empty. The optional 98 | # flag parameters are :complete (finds only complete MIME::Type 99 | # objects) and :registered (finds only MIME::Types that are 100 | # registered). It is possible for multiple matches to be returned for 101 | # either type (in the example below, 'text/plain' returns two values -- 102 | # one for the general case, and one for VMS systems). 103 | # 104 | # puts "\nMIME::Types['text/plain']" 105 | # MIME::Types['text/plain'].each { |t| puts t.to_a.join(", ") } 106 | # 107 | # puts "\nMIME::Types[/^image/, complete: true]" 108 | # MIME::Types[/^image/, :complete => true].each do |t| 109 | # puts t.to_a.join(", ") 110 | # end 111 | # 112 | # If multiple type definitions are returned, returns them sorted as 113 | # follows: 114 | # 1. Complete definitions sort before incomplete ones; 115 | # 2. IANA-registered definitions sort before LTSW-recorded 116 | # definitions. 117 | # 3. Current definitions sort before obsolete ones; 118 | # 4. Obsolete definitions with use-instead clauses sort before those 119 | # without; 120 | # 5. Obsolete definitions use-instead clauses are compared. 121 | # 6. Sort on name. 122 | def [](type_id, complete: false, registered: false) 123 | matches = 124 | case type_id 125 | when MIME::Type 126 | @type_variants[type_id.simplified] 127 | when Regexp 128 | match(type_id) 129 | else 130 | @type_variants[MIME::Type.simplified(type_id)] 131 | end 132 | 133 | prune_matches(matches, complete, registered).sort 134 | end 135 | 136 | # Return the list of MIME::Types which belongs to the file based on its 137 | # filename extension. If there is no extension, the filename will be used 138 | # as the matching criteria on its own. 139 | # 140 | # This will always return a merged, flatten, priority sorted, unique array. 141 | # 142 | # puts MIME::Types.type_for('citydesk.xml') 143 | # => [application/xml, text/xml] 144 | # puts MIME::Types.type_for('citydesk.gif') 145 | # => [image/gif] 146 | # puts MIME::Types.type_for(%w(citydesk.xml citydesk.gif)) 147 | # => [application/xml, image/gif, text/xml] 148 | def type_for(filename) 149 | wanted = Array(filename).map { |fn| fn.chomp.downcase[/\.?([^.]*?)\z/m, 1] } 150 | 151 | wanted 152 | .flat_map { |ext| @extension_index[ext] } 153 | .compact 154 | .reduce(Set.new, :+) 155 | .sort { |a, b| 156 | a.__extension_priority_compare(b, wanted) 157 | } 158 | end 159 | alias_method :of, :type_for 160 | 161 | # Add one or more MIME::Type objects to the set of known types. If the 162 | # type is already known, a warning will be displayed. 163 | # 164 | # The last parameter may be the value :silent or +true+ which 165 | # will suppress duplicate MIME type warnings. 166 | def add(*types) 167 | quiet = (types.last == :silent) || (types.last == true) 168 | 169 | types.each do |mime_type| 170 | case mime_type 171 | when true, false, nil, Symbol 172 | nil 173 | when MIME::Types 174 | variants = mime_type.instance_variable_get(:@type_variants) 175 | add(*variants.values.inject(Set.new, :+).to_a, quiet) 176 | when Array 177 | add(*mime_type, quiet) 178 | else 179 | add_type(mime_type, quiet) 180 | end 181 | end 182 | end 183 | 184 | # Add a single MIME::Type object to the set of known types. If the +type+ is 185 | # already known, a warning will be displayed. The +quiet+ parameter may be a 186 | # truthy value to suppress that warning. 187 | def add_type(type, quiet = false) 188 | if !quiet && @type_variants[type.simplified].include?(type) 189 | MIME::Types.logger.debug <<-WARNING.chomp.strip 190 | Type #{type} is already registered as a variant of #{type.simplified}. 191 | WARNING 192 | end 193 | 194 | add_type_variant!(type) 195 | index_extensions!(type) 196 | end 197 | 198 | def __fully_loaded? # :nodoc: 199 | true 200 | end 201 | 202 | private 203 | 204 | def add_type_variant!(mime_type) 205 | @type_variants.add(mime_type.simplified, mime_type) 206 | end 207 | 208 | def reindex_extensions!(mime_type) 209 | return unless @type_variants[mime_type.simplified].include?(mime_type) 210 | 211 | index_extensions!(mime_type) 212 | end 213 | 214 | def index_extensions!(mime_type) 215 | mime_type.extensions.each { |ext| @extension_index.add(ext, mime_type) } 216 | end 217 | 218 | def prune_matches(matches, complete, registered) 219 | matches.delete_if { |e| !e.complete? } if complete 220 | matches.delete_if { |e| !e.registered? } if registered 221 | matches 222 | end 223 | 224 | def match(pattern) 225 | @type_variants.select { |k, _| 226 | k =~ pattern 227 | }.values.inject(Set.new, :+) 228 | end 229 | 230 | # def stable_sort(list) 231 | # list.lazy.each_with_index.sort { |(a, ai), (b, bi)| 232 | # a.priority_compare(b).nonzero? || ai <=> bi 233 | # }.map(&:first) 234 | # end 235 | end 236 | 237 | require "mime/types/cache" 238 | require "mime/types/container" 239 | require "mime/types/loader" 240 | require "mime/types/logger" 241 | require "mime/types/_columnar" 242 | require "mime/types/registry" 243 | require "mime/types/version" 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mime-types for Ruby 2 | 3 | - home :: https://github.com/mime-types/ruby-mime-types/ 4 | - issues :: https://github.com/mime-types/ruby-mime-types/issues 5 | - code :: https://github.com/mime-types/ruby-mime-types/ 6 | - rdoc :: http://rdoc.info/gems/mime-types/ 7 | - changelog :: 8 | https://github.com/mime-types/ruby-mime-types/blob/main/CHANGELOG.md 9 | - continuous integration :: [![Build Status][ci-badge]][ci-workflow] 10 | - test coverage :: [![Coverage][coveralls-badge]][coveralls] 11 | 12 | ## Description 13 | 14 | The mime-types library provides a library and registry for information about 15 | MIME content type definitions. It can be used to determine defined filename 16 | extensions for MIME types, or to use filename extensions to look up the likely 17 | MIME type definitions. 18 | 19 | Version 3.0 is a major release that requires Ruby 2.0 compatibility and removes 20 | deprecated functions. The columnar registry format introduced in 2.6 has been 21 | made the primary format; the registry data has been extracted from this library 22 | and put into [mime-types-data][data]. Additionally, mime-types is now licensed 23 | exclusively under the MIT licence and there is a code of conduct in effect. 24 | There are a number of other smaller changes described in the History file. 25 | 26 | ### About MIME Media Types 27 | 28 | MIME content types are used in MIME-compliant communications, as in e-mail or 29 | HTTP traffic, to indicate the type of content which is transmitted. The 30 | mime-types library provides the ability for detailed information about MIME 31 | entities (provided as an enumerable collection of MIME::Type objects) to be 32 | determined and used. There are many types defined by RFCs and vendors, so the 33 | list is long but by definition incomplete; don't hesitate to add additional type 34 | definitions. MIME type definitions found in mime-types are from RFCs, W3C 35 | recommendations, the [IANA Media Types registry][registry], and user 36 | contributions. It conforms to RFCs 2045 and 2231. 37 | 38 | ### mime-types 3.x 39 | 40 | Users are encouraged to upgrade to mime-types 3.x as soon as is practical. 41 | mime-types 3.x requires Ruby 2.0 compatibility and a simpler licensing scheme. 42 | 43 | ## Synopsis 44 | 45 | MIME types are used in MIME entities, as in email or HTTP traffic. It is useful 46 | at times to have information available about MIME types (or, inversely, about 47 | files). A MIME::Type stores the known information about one MIME type. 48 | 49 | ```ruby 50 | require 'mime/types' 51 | 52 | plaintext = MIME::Types['text/plain'] # => [ text/plain ] 53 | text = plaintext.first 54 | puts text.media_type # => 'text' 55 | puts text.sub_type # => 'plain' 56 | 57 | puts text.extensions.join(' ') # => 'txt asc c cc h hh cpp hpp dat hlp' 58 | puts text.preferred_extension # => 'txt' 59 | puts text.friendly # => 'Text Document' 60 | puts text.i18n_key # => 'text.plain' 61 | 62 | puts text.encoding # => quoted-printable 63 | puts text.default_encoding # => quoted-printable 64 | puts text.binary? # => false 65 | puts text.ascii? # => true 66 | puts text.obsolete? # => false 67 | puts text.registered? # => true 68 | puts text.provisional? # => false 69 | puts text.complete? # => true 70 | 71 | puts text # => 'text/plain' 72 | 73 | puts text == 'text/plain' # => true 74 | puts 'text/plain' == text # => true 75 | puts text == 'text/x-plain' # => false 76 | puts 'text/x-plain' == text # => false 77 | 78 | puts MIME::Type.simplified('x-appl/x-zip') # => 'x-appl/x-zip' 79 | puts MIME::Type.i18n_key('x-appl/x-zip') # => 'x-appl.x-zip' 80 | 81 | puts text.like?('text/x-plain') # => true 82 | puts text.like?(MIME::Type.new('x-text/x-plain')) # => true 83 | 84 | puts text.xrefs.inspect # => { "rfc" => [ "rfc2046", "rfc3676", "rfc5147" ] } 85 | puts text.xref_urls # => [ "http://www.iana.org/go/rfc2046", 86 | # "http://www.iana.org/go/rfc3676", 87 | # "http://www.iana.org/go/rfc5147" ] 88 | 89 | xtext = MIME::Type.new('x-text/x-plain') 90 | puts xtext.media_type # => 'text' 91 | puts xtext.raw_media_type # => 'x-text' 92 | puts xtext.sub_type # => 'plain' 93 | puts xtext.raw_sub_type # => 'x-plain' 94 | puts xtext.complete? # => false 95 | 96 | puts MIME::Types.any? { |type| type.content_type == 'text/plain' } # => true 97 | puts MIME::Types.all?(&:registered?) # => false 98 | 99 | # Various string representations of MIME types 100 | qcelp = MIME::Types['audio/QCELP'].first # => audio/QCELP 101 | puts qcelp.content_type # => 'audio/QCELP' 102 | puts qcelp.simplified # => 'audio/qcelp' 103 | 104 | xwingz = MIME::Types['application/x-Wingz'].first # => application/x-Wingz 105 | puts xwingz.content_type # => 'application/x-Wingz' 106 | puts xwingz.simplified # => 'application/x-wingz' 107 | ``` 108 | 109 | ### Columnar Store 110 | 111 | mime-types uses as its primary registry storage format a columnar storage format 112 | reducing the default memory footprint. This is done by selectively loading the 113 | data on a per-attribute basis. When the registry is first loaded from the 114 | columnar store, only the canonical MIME content type and known extensions and 115 | the MIME type will be connected to its loading registry. When other data about 116 | the type is required (including `preferred_extension`, `obsolete?`, and 117 | `registered?`) that data is loaded from its own column file for all types in the 118 | registry. 119 | 120 | The load of any column data is performed with a Mutex to ensure that types are 121 | updated safely in a multithreaded environment. Benchmarks show that while 122 | columnar data loading is slower than the JSON store, it cuts the memory use by a 123 | third over the JSON store. 124 | 125 | If you prefer to load all the data at once, this can be specified in your 126 | application Gemfile as: 127 | 128 | ```ruby 129 | gem 'mime-types', require: 'mime/types/full' 130 | ``` 131 | 132 | Projects that do not use Bundler should `require` the same: 133 | 134 | ```ruby 135 | require 'mime/types/full' 136 | ``` 137 | 138 | Libraries that use mime-types are discouraged from choosing the JSON store. 139 | 140 | For applications and clients that used mime-types 2.6 when the columnar store 141 | was introduced, the require used previously will still work through at least 142 | [version 4][pull-96-comment] and possibly beyond; it is effectively an empty 143 | operation. You are recommended to change your Gemfile as soon as is practical. 144 | 145 | ```ruby 146 | require 'mime/types/columnar' 147 | ``` 148 | 149 | Note that MIME::Type::Columnar and MIME::Types::Columnar are considered private 150 | variant implementations of MIME::Type and MIME::Types and the specific 151 | implementation should not be relied upon by consumers of the mime-types library. 152 | Instead, depend on the public implementations (MIME::Type and MIME::Types) only. 153 | 154 | ### Cached Storage 155 | 156 | mime-types supports a cache of MIME types using `Marshal.dump`. The cache is 157 | invalidated for each version of the mime-types-data gem so that data version 158 | 3.2015.1201 will not be reused with data version 3.2016.0101. If the environment 159 | variable `RUBY_MIME_TYPES_CACHE` is set to a cache file, mime-types will attempt 160 | to load the MIME type registry from the cache file. If it cannot, it will load 161 | the types normally and then saves the registry to the cache file. 162 | 163 | The caching works with both full stores and columnar stores. Only the data that 164 | has been loaded prior to saving the cache will be stored. 165 | 166 | ## mime-types Modified Semantic Versioning 167 | 168 | The mime-types library has one version number, but this single version number 169 | tracks both API changes and registry data changes; this is not wholly compatible 170 | with all aspects of [Semantic Versioning][semver]; removing a MIME type from the 171 | registry _could_ be considered a breaking change under some interpretations of 172 | semantic versioning (as lookups for that particular type would no longer work by 173 | default). 174 | 175 | mime-types itself uses a modified semantic versioning scheme. Given the version 176 | `MAJOR.MINOR`: 177 | 178 | 1. If an incompatible API (code) change is made, the `MAJOR` version will be 179 | incremented and both `MINOR` and `PATCH` will be set to zero. Major version 180 | updates will also generally break Ruby version compatibility guarantees. 181 | 182 | 2. If an API (code) feature is added that does not break compatibility, the 183 | `MINOR` version will be incremented and `PATCH` will be set to zero. 184 | 185 | 3. If there is a bug fix to a feature added in the most recent `MAJOR.MINOR` 186 | release, the `PATCH` value will be incremented. 187 | 188 | In practical terms, there will be fewer releases of mime-types focussing on 189 | features because of the existence of the [mime-types-data][data] gem, and if 190 | features are marked deprecated in the course of mime-types 3.x, they will not be 191 | removed until mime-types 4.x or possibly later. 192 | 193 | [pull-96-comment]: https://github.com/mime-types/ruby-mime-types/pull/96#issuecomment-100725400 194 | [semver]: https://semver.org 195 | [data]: https://github.com/mime-types/mime-types-data 196 | [ci-badge]: https://github.com/mime-types/ruby-mime-types/actions/workflows/ci.yml/badge.svg 197 | [ci-workflow]: https://github.com/mime-types/ruby-mime-types/actions/workflows/ci.yml 198 | [coveralls-badge]: https://coveralls.io/repos/mime-types/ruby-mime-types/badge.svg?branch=main&service=github 199 | [coveralls]: https://coveralls.io/github/mime-types/ruby-mime-types?branch=main 200 | [registry]: https://www.iana.org/assignments/media-types/media-types.xhtml 201 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | contents: read 22 | 23 | steps: 24 | - name: Harden the runner 25 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 26 | with: 27 | disable-sudo: true 28 | egress-policy: block 29 | allowed-endpoints: > 30 | github.com:443 31 | index.rubygems.org:443 32 | objects.githubusercontent.com:443 33 | release-assets.githubusercontent.com:443 34 | rubygems.org:443 35 | 36 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 37 | with: 38 | persist-credentials: false 39 | 40 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 41 | with: 42 | ruby-version: '3.4' 43 | rubygems: latest 44 | bundler: 2 45 | bundler-cache: true 46 | 47 | - name: Run standardrb 48 | run: | 49 | bundle exec standardrb 50 | 51 | coverage: 52 | name: Generate Coverage Report 53 | runs-on: ubuntu-latest 54 | 55 | permissions: 56 | contents: read 57 | 58 | steps: 59 | - name: Harden the runner 60 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 61 | with: 62 | disable-sudo: true 63 | egress-policy: block 64 | allowed-endpoints: > 65 | coveralls.io:443 66 | github.com:443 67 | index.rubygems.org:443 68 | objects.githubusercontent.com:443 69 | release-assets.githubusercontent.com:443 70 | rubygems.org:443 71 | 72 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 73 | with: 74 | persist-credentials: false 75 | 76 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 77 | with: 78 | ruby-version: '3.4' 79 | rubygems: latest 80 | bundler: 2 81 | bundler-cache: false 82 | 83 | - name: Run coverage 84 | run: | 85 | bundle install --jobs 4 --retry 3 86 | bundle exec ruby -S rake coverage --trace 87 | env: 88 | COVERAGE: true 89 | 90 | - uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b #v2.3.6 91 | 92 | required-ubuntu: 93 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 94 | 95 | permissions: 96 | contents: read 97 | 98 | strategy: 99 | fail-fast: false 100 | matrix: 101 | os: 102 | - ubuntu-22.04 103 | - ubuntu-24.04 104 | ruby: 105 | - '2.6' 106 | - '2.7' 107 | - '3.1' 108 | - '3.2' 109 | - '3.3' 110 | - '3.4' 111 | - truffleruby 112 | 113 | runs-on: ${{ matrix.os }} 114 | 115 | steps: 116 | - name: Harden the runner 117 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 118 | with: 119 | disable-sudo: true 120 | egress-policy: block 121 | allowed-endpoints: > 122 | github.com:443 123 | index.rubygems.org:443 124 | objects.githubusercontent.com:443 125 | release-assets.githubusercontent.com:443 126 | rubygems.org:443 127 | 128 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 129 | with: 130 | persist-credentials: false 131 | 132 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 133 | with: 134 | ruby-version: ${{ matrix.ruby }} 135 | rubygems: latest 136 | bundler: 2 137 | bundler-cache: true 138 | 139 | - run: bundle exec ruby -S rake test --trace 140 | 141 | required-macos: 142 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 143 | 144 | permissions: 145 | contents: read 146 | 147 | strategy: 148 | fail-fast: false 149 | matrix: 150 | os: 151 | - macos-14 152 | - macos-15 153 | - macos-26 154 | ruby: 155 | - '2.6' 156 | - '2.7' 157 | - '3.1' 158 | - '3.2' 159 | - '3.3' 160 | - '3.4' 161 | 162 | runs-on: ${{ matrix.os }} 163 | 164 | steps: 165 | - name: Harden the runner 166 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 167 | with: 168 | disable-sudo: true 169 | egress-policy: block 170 | allowed-endpoints: > 171 | github.com:443 172 | index.rubygems.org:443 173 | objects.githubusercontent.com:443 174 | release-assets.githubusercontent.com:443 175 | rubygems.org:443 176 | 177 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 178 | with: 179 | persist-credentials: false 180 | 181 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 182 | with: 183 | ruby-version: ${{ matrix.ruby }} 184 | rubygems: latest 185 | bundler: 2 186 | bundler-cache: true 187 | 188 | - run: bundle exec ruby -S rake test --trace 189 | 190 | required-windows: 191 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 192 | 193 | permissions: 194 | contents: read 195 | 196 | strategy: 197 | fail-fast: false 198 | matrix: 199 | os: 200 | - windows-2022 201 | - windows-2025 202 | ruby: 203 | - '2.6' 204 | - '2.7' 205 | - '3.0' 206 | - '3.1' 207 | - '3.2' 208 | - '3.3' 209 | - '3.4' 210 | - mingw 211 | - mswin 212 | - ucrt 213 | 214 | runs-on: ${{ matrix.os }} 215 | 216 | steps: 217 | - name: Harden the runner 218 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 219 | with: 220 | disable-sudo: true 221 | egress-policy: block 222 | allowed-endpoints: > 223 | github.com:443 224 | index.rubygems.org:443 225 | objects.githubusercontent.com:443 226 | release-assets.githubusercontent.com:443 227 | rubygems.org:443 228 | 229 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 230 | with: 231 | persist-credentials: false 232 | 233 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 234 | with: 235 | ruby-version: ${{ matrix.ruby }} 236 | rubygems: latest 237 | bundler: 2 238 | bundler-cache: true 239 | 240 | - run: bundle exec ruby -S rake test --trace 241 | 242 | jruby-optional: 243 | name: JRuby ${{ matrix.ruby }} - ${{ matrix.os }} 244 | 245 | permissions: 246 | contents: read 247 | 248 | strategy: 249 | fail-fast: false 250 | 251 | matrix: 252 | os: 253 | - ubuntu-22.04 254 | - ubuntu-24.04 255 | ruby: 256 | - jruby-10 257 | 258 | continue-on-error: true 259 | runs-on: ${{ matrix.os }} 260 | 261 | steps: 262 | - name: Harden the runner 263 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 264 | with: 265 | disable-sudo: true 266 | egress-policy: block 267 | allowed-endpoints: > 268 | github.com:443 269 | index.rubygems.org:443 270 | objects.githubusercontent.com:443 271 | release-assets.githubusercontent.com:443 272 | repo.maven.apache.org:443 273 | rubygems.org:443 274 | 275 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 276 | with: 277 | persist-credentials: false 278 | 279 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 280 | with: 281 | ruby-version: ${{ matrix.ruby }} 282 | rubygems: latest 283 | bundler: 2 284 | bundler-cache: true 285 | 286 | - run: bundle exec ruby -S rake test --trace 287 | 288 | ruby-head-optional: 289 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} (optional) 290 | 291 | permissions: 292 | contents: read 293 | 294 | strategy: 295 | fail-fast: false 296 | 297 | matrix: 298 | ruby: 299 | - head 300 | os: 301 | - macos-latest 302 | - ubuntu-latest 303 | - windows-latest 304 | 305 | continue-on-error: true 306 | runs-on: ${{ matrix.os }} 307 | 308 | steps: 309 | - name: Harden the runner 310 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 311 | with: 312 | disable-sudo: true 313 | egress-policy: block 314 | allowed-endpoints: > 315 | github.com:443 316 | index.rubygems.org:443 317 | objects.githubusercontent.com:443 318 | release-assets.githubusercontent.com:443 319 | rubygems.org:443 320 | 321 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 322 | with: 323 | persist-credentials: false 324 | 325 | - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 326 | with: 327 | ruby-version: ${{ matrix.ruby }} 328 | rubygems: latest 329 | bundler: 2 330 | bundler-cache: true 331 | 332 | - run: bundle exec ruby -S rake test --trace 333 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## NEXT / YYYY-MM-DD 4 | 5 | ### Governance 6 | 7 | As of 2025-10-15, all contribution commits require `Signed-off-by` trailers to 8 | certify that the contributor is able to contribute the changes, per the 9 | [Developer Certificate of Origin][dco]. Further documentation and updates will 10 | be added in the coming weeks. 11 | 12 | ## 3.7.0 / 2025-05-07 13 | 14 | - Deprecated `MIME::Type#priority_compare`. In a future release, this will be 15 | will be renamed to `MIME::Type#<=>`. This method is used in tight loops, so 16 | there is no warning message for either `MIME::Type#priority_compare` or 17 | `MIME::Type#<=>`. 18 | 19 | - Improved the performance of sorting by eliminating the complex comparison flow 20 | from `MIME::Type#priority_compare`. The old version shows under 600 i/s, and 21 | the new version shows over 900 i/s. In sorting the full set of MIME data, 22 | there are three differences between the old and new versions; after 23 | comparison, these differences are considered acceptable. 24 | 25 | - Simplified the default compare implementation (`MIME::Type#<=>`) to use the 26 | new `MIME::Type#priority_compare` operation and simplify the fallback to 27 | `String` comparison. This _may_ result in exceptions where there had been 28 | none, as explicit support for several special values (which should have caused 29 | errors in any case) have been removed. 30 | 31 | - When sorting the result of `MIME::Types#type_for`, provided a priority boost 32 | if one of the target extensions is the type's preferred extension. This means 33 | that for the case in [#148][issue-148], when getting the type for `foo.webm`, 34 | the type `video/webm` will be returned before the type `audio/webm`, because 35 | `.webm` is the preferred extension for `video/webm` but not `audio/webm` 36 | (which has a preferred extension of `.weba`). Added tests to ensure MIME types 37 | are retrieved in a stable order (which is alphabetical). 38 | 39 | ## 3.6.2 / 2025-03-25 40 | 41 | - Updated the reference to the changelog in the README, fixing RubyGems metadata 42 | on the next release. Fixed in [#189][pull-189] by nna774. 43 | 44 | - Daniel Watkins fixed an error in the repo tag for this release because the 45 | modified gemspec was not included in the release. Fixed in [#196][pull-196]. 46 | 47 | ## 3.6.1 / 2025-03-15 48 | 49 | - Restructure project structure to be more consistent with mime-types-data. 50 | 51 | - Increased GitHub action security. Added Ruby 3.4, dropped macOS 12, added 52 | macOS 15. 53 | 54 | - Added [trusted publishing][tp] for fully automated releases. 55 | 56 | - Added `MIME::Types::NullLogger` to completely silence MIME::Types logging. 57 | 58 | - Improved the development experience with updates to the Gemfile. 59 | 60 | - Worked around various issues with the benchmarks and profiling code. 61 | 62 | - Removed Forwardable from MIME::Types::Container. 63 | 64 | - Added coverage support (back). 65 | 66 | ## 3.6.0 / 2024-10-02 67 | 68 | - 2 deprecations: 69 | 70 | - Array-based MIME::Type initialization 71 | - String-based MIME::Type initialization 72 | 73 | Use of these these will result in deprecation warnings. 74 | 75 | - Added `logger` to the gemspec to suppress a bundled gem warning with Ruby 76 | 3.3.5. This warning should not be showing up until Ruby 3.4.0 is released and 77 | will be suppressed in Ruby 3.3.6. 78 | 79 | - Reworked the deprecation message code to be somewhat more flexible and allow 80 | for outputting certain warnings once. Because there will be at least one other 81 | release after 3.6, we do not need to make the type initialization deprecations 82 | frequent with this release. 83 | 84 | ## 3.5.2 / 2024-01-02 85 | 86 | There are no primary code changes, but we are releasing this as an update as 87 | there are some validation changes and updated code with formatting. 88 | 89 | - Dependency and CI updates: 90 | 91 | - Masato Nakamura added Ruby 3.3 to the CI workflow in [#179][pull-179]. 92 | 93 | - Fixed regressions in standard formatting in [#180][pull-180]. 94 | 95 | - Removed `minitest-bonus-assertions` because of a bundler resolution issue. 96 | Created a better replacement in-line. 97 | 98 | ## 3.5.1 / 2023-08-21 99 | 100 | - 1 bug fix: 101 | 102 | - Better handle possible line-termination strings (legal in Unix filenames) 103 | such as `\n` in `MIME::Types.type_for`. Reported by ooooooo-q in 104 | [#177][issue-177], resolved in [#178][pull-178]. 105 | 106 | ## 3.5.0 / 2023-08-07 107 | 108 | - 1 minor enhancement: 109 | 110 | - Robb Shecter changed the default log level for duplicate type variant from 111 | `warn` to `debug` in [#170][pull-170]. This works because 112 | `MIME::Types.logger` is intended to fit the `::Logger` interface, and the 113 | default logger (`WarnLogger`) is a subclass of `::Logger` that passes 114 | through to `Kernel.warn`. 115 | 116 | - Further consideration has changed cache load messages from `warn` to 117 | `error` and deprecation messages from `warn` to `debug`. 118 | 119 | - 1 bug fix: 120 | 121 | - Added a definition of `MIME::Type#hash`. Contributed by Alex Vondrak in 122 | [#167][pull-167], fixing [#166][issue-166]. 123 | 124 | - Dependency and CI updates: 125 | 126 | - Update the .github/workflows/ci.yml workflow to test Ruby 3.2 and more 127 | reliably test certain combinations rather than depending on exclusions. 128 | 129 | - Change `.standard.yml` configuration to format for Ruby 2.3 as certain files 130 | are not properly detected with Ruby 2.0. 131 | 132 | - Change from `hoe-git` to `hoe-git2` to support Hoe version 4. 133 | 134 | - Apply `standardrb --fix`. 135 | 136 | - The above changes have resulted in the Soft deprecation of Ruby versions 137 | below 2.6. Any errors reported for Ruby versions 2.0, 2.1, 2.2, 2.3, 2.4, 138 | and 2.5 will be resolved, but maintaining CI for these versions is 139 | unsustainable. 140 | 141 | ## 3.4.1 / 2021-11-16 142 | 143 | - 1 bug fix: 144 | 145 | - Fixed a Ruby < 2.3 incompatibility introduced by the use of standardrb, 146 | where `<<-` heredocs were converted to `<<~` heredocs. These have been 147 | reverted back to `<<-` with the indentation kept and a `.strip` call to 148 | prevent excess whitespace. 149 | 150 | ## 3.4.0 / 2021-11-15 151 | 152 | - 1 minor enhancement: 153 | 154 | - Added a new field to `MIME::Type` for checking provisional registrations 155 | from IANA. [#157] 156 | 157 | - Documentation: 158 | 159 | - Kevin Menard synced the documentation so that all examples are correct. 160 | [#153] 161 | 162 | - Administrivia: 163 | 164 | - Added Ruby 3.0 to the CI test matrix. Added `windows/jruby` to the CI 165 | exclusion list; it refuses to run successfully. 166 | - Removed the Travis CI configuration and changed it to Github Workflows 167 | [#150][pull-150]. Removed Coveralls configuration. 168 | - Igor Victor added TruffleRuby to the Travis CI configuration. [#149] 169 | - Koichi ITO loosened an excessively tight dependency. [#147] 170 | - Started using `standardrb` for Ruby formatting and validation. 171 | - Moved `deps:top` functionality to a support file. 172 | 173 | ## 3.3.1 / 2019-12-26 174 | 175 | - 1 minor bug fix: 176 | 177 | - Al Snow fixed a warning with MIME::Types::Logger producing a warning because 178 | Ruby 2.7 introduces numbered block parameters. Because of the way that the 179 | MIME::Types::Logger works for deprecation messages, the initializer 180 | parameters had been named `_1`, `_2`, and `_3`. This has now been resolved. 181 | [#146] 182 | 183 | - Administrivia: 184 | 185 | - Olle Jonsson removed an outdated Travis configuration option. 186 | [#142][pull-142] 187 | 188 | ## 3.3 / 2019-09-04 189 | 190 | - 1 minor enhancement: 191 | 192 | - Jean Boussier reduced memory usage for Ruby versions 2.3 or higher by 193 | interning various string values in each type. This is done with a 194 | backwards-compatible call that _freezes_ the strings on older versions of 195 | Ruby. [#141] 196 | 197 | - Administrivia: 198 | 199 | - Nicholas La Roux updated Travis build configurations. [#139] 200 | 201 | ## 3.2.2 / 2018-08-12 202 | 203 | - Hiroto Fukui removed a stray `debugger` statement that I had used in producing 204 | v3.2.1. [#137] 205 | 206 | ## 3.2.1 / 2018-08-12 207 | 208 | - A few bugs related to MIME::Types::Container and its use in the 209 | mime-types-data helper tools reared their head because I released 3.2 before 210 | verifying against mime-types-data. 211 | 212 | ## 3.2 / 2018-08-12 213 | 214 | - 2 minor enhancements 215 | 216 | - Janko Marohnić contributed a change to `MIME::Type#priority_order` that 217 | should improve on strict sorting when dealing with MIME types that appear to 218 | be in the same family even if strict sorting would cause an unregistered 219 | type to be sorted first. [#132] 220 | 221 | - Dillon Welch contributed a change that added `frozen_string_literal: true` 222 | to files so that modern Rubies can automatically reduce duplicate string 223 | allocations. [#135] 224 | 225 | - 2 bug fixes 226 | 227 | - Burke Libbey fixed a problem with cached data loading. [#126] 228 | 229 | - Resolved an issue where Enumerable#inject returns `nil` when provided an 230 | empty enumerable and a default value has not been provided. This is because 231 | when Enumerable#inject isn't provided a starting value, the first value is 232 | used as the default value. In every case where this error was happening, the 233 | result was supposed to be an array containing Set objects so they can be 234 | reduced to a single Set. [#117][issue-117], [#127][issue-127], 235 | [#134][issue-134] 236 | 237 | - Fixed an uncontrolled growth bug in MIME::Types::Container where a key miss 238 | would create a new entry with an empty Set in the container. This was 239 | working as designed (this particular feature was heavily used during 240 | MIME::Type registry construction), but the design was flawed in that it did 241 | not have any way of determining the difference between construction and 242 | querying. This would mean that, if you have a function in your web app that 243 | queries the MIME::Types registry by extension, the extension registry would 244 | grow uncontrollably. [#136] 245 | 246 | - Deprecations: 247 | 248 | - Lazy loading (`$RUBY_MIME_TYPES_LAZY_LOAD`) has been deprecated. 249 | 250 | - Documentation Changes: 251 | 252 | - Supporting files are now Markdown instead of rdoc, except for the README. 253 | 254 | - The history file has been modified to remove all history prior to 3.0. This 255 | history can be found in previous commits. 256 | 257 | - A spelling error was corrected by Edward Betts ([#129][pull-129]). 258 | 259 | - Administrivia: 260 | 261 | - CI configuration for more modern versions of Ruby were added by Nicolas 262 | Leger ([#130][pull-130]), Jun Aruga ([#125][pull-125]), and Austin Ziegler. 263 | Removed ruby-head-clang and rbx (Rubinius) from CI. 264 | 265 | - Fixed tests which were asserting equality against nil, which will become an 266 | error in Minitest 6. 267 | 268 | ## 3.1 / 2016-05-22 269 | 270 | - 1 documentation change: 271 | 272 | - Tim Smith (@tas50) updated the build badges to be SVGs to improve 273 | readability on high-density (retina) screens with pull request 274 | [#112][pull-112]. 275 | 276 | - 3 bug fixes 277 | 278 | - A test for `MIME::Types::Cache` fails under Ruby 2.3 because of frozen 279 | strings, [#118][pull-118]. This has been fixed. 280 | 281 | - The JSON data has been incorrectly encoded since the release of mime-types 3 282 | on the `xrefs` field, because of the switch to using a Set to store 283 | cross-reference information. This has been fixed. 284 | 285 | - A tentative fix for [#117][issue-117] has been applied, removing the only 286 | circular require dependencies that exist (and for which there was code to 287 | prevent, but the current fix is simpler). I have no way to verify this fix 288 | and depending on how things are loaded by `delayed_job`, this fix may not be 289 | sufficient. 290 | 291 | - 1 governance change 292 | 293 | - Updated to Contributor Covenant 1.4. 294 | 295 | ## 3.0 / 2015-11-21 296 | 297 | - 2 governance changes 298 | 299 | - This project and the related mime-types-data project are now exclusively MIT 300 | licensed. Resolves [#95][pull-95]. 301 | 302 | - All projects under the mime-types organization now have a standard code of 303 | conduct adapted from the [Contributor Covenant][contributor covenant]. This 304 | text can be found in the [Code of Conduct][code of conduct] file. 305 | 306 | - 3 major changes 307 | 308 | - All methods deprecated in mime-types 2.x have been removed. 309 | 310 | - mime-types now requires Ruby 2.0 compatibility or later. Resolves 311 | [#97][pull-97]. 312 | 313 | - The registry data has been removed from mime-types and put into 314 | mime-types-data, maintained and released separately. It can be found at 315 | [mime-types-data][mime-types-data]. 316 | 317 | - 17 minor changes: 318 | 319 | - `MIME::Type` changes: 320 | 321 | - Changed the way that simplified types representations are created to 322 | reflect the fact that `x-` prefixes are no longer considered special 323 | according to IANA. A simplified MIME type is case-folded to lowercase. A 324 | new keyword parameter, `remove_x_prefix`, can be provided to remove `x-` 325 | prefixes. 326 | 327 | - Improved initialization with an Array works so that extensions do not need 328 | to be wrapped in another array. This means that `%w(text/yaml yaml yml)` 329 | works in the same way that `['text/yaml', %w(yaml yml)]` did (and still 330 | does). 331 | 332 | - Changed `priority_compare` to conform with attributes that no longer 333 | exist. 334 | 335 | - Changed the internal implementation of extensions to use a frozen Set. 336 | 337 | - When extensions are set or modified with `add_extensions`, the primary 338 | registry will be informed of a need to re-index extensions. Resolves 339 | [#84][pull-84]. 340 | 341 | - The preferred extension can be set explicitly. If not set, it will be the 342 | first extension. If the preferred extension is not in the extension list, 343 | it will be added. 344 | 345 | - Improved how xref URLs are generated. 346 | 347 | - Converted `obsolete`, `registered` and `signature` to `attr_accessors`. 348 | 349 | - `MIME::Types` changes: 350 | 351 | - Modified `MIME::Types.new` to track instances of `MIME::Types` so that 352 | they can be told to reindex the extensions as necessary. 353 | 354 | - Removed `data_version` attribute. 355 | 356 | - Changed `#[]` so that the `complete` and `registered` flags are keywords 357 | instead of a generic options parameter. 358 | 359 | - Extracted the class methods to a separate file. 360 | 361 | - Changed the container implementation to use a Set instead of an Array to 362 | prevent data duplication. Resolves [#79][pull-79]. 363 | 364 | - `MIME::Types::Cache` changes: 365 | 366 | - Caching is now based on the data gem version instead of the mime-types 367 | version. 368 | 369 | - Caching is compatible with columnar registry stores. 370 | 371 | - `MIME::Types::Loader` changes: 372 | 373 | - `MIME::Types::Loader::PATH` has been removed and replaced with 374 | `MIME::Types::Data::PATH` from the mime-types-data gem. The environment 375 | variable `RUBY_MIME_TYPES_DATA` is still used. 376 | 377 | - Support for the long-deprecated mime-types v1 format has been removed. 378 | 379 | - The registry is default loaded from the columnar store by default. The 380 | internal format of the columnar store has changed; many of the boolean 381 | flags are now loaded from a single file. Resolves [#85][pull-85]. 382 | 383 | [code of conduct]: CODE_OF_CONDUCT.md 384 | [contributor covenant]: http://contributor-covenant.org 385 | [issue-117]: https://github.com/mime-types/ruby-mime-types/issues/117 386 | [issue-127]: https://github.com/mime-types/ruby-mime-types/issues/127 387 | [issue-134]: https://github.com/mime-types/ruby-mime-types/issues/134 388 | [issue-136]: https://github.com/mime-types/ruby-mime-types/issues/136 389 | [issue-148]: https://github.com/mime-types/ruby-mime-types/issues/148 390 | [issue-166]: https://github.com/mime-types/ruby-mime-types/issues/166 391 | [issue-177]: https://github.com/mime-types/ruby-mime-types/issues/177 392 | [mime-types-data]: https://github.com/mime-types/mime-types-data 393 | [pull-112]: https://github.com/mime-types/ruby-mime-types/pull/112 394 | [pull-118]: https://github.com/mime-types/ruby-mime-types/pull/118 395 | [pull-125]: https://github.com/mime-types/ruby-mime-types/pull/125 396 | [pull-126]: https://github.com/mime-types/ruby-mime-types/pull/126 397 | [pull-129]: https://github.com/mime-types/ruby-mime-types/pull/129 398 | [pull-130]: https://github.com/mime-types/ruby-mime-types/pull/130 399 | [pull-132]: https://github.com/mime-types/ruby-mime-types/pull/132 400 | [pull-135]: https://github.com/mime-types/ruby-mime-types/pull/135 401 | [pull-137]: https://github.com/mime-types/ruby-mime-types/pull/137 402 | [pull-139]: https://github.com/mime-types/ruby-mime-types/pull/139 403 | [pull-141]: https://github.com/mime-types/ruby-mime-types/pull/141 404 | [pull-142]: https://github.com/mime-types/ruby-mime-types/pull/142 405 | [pull-146]: https://github.com/mime-types/ruby-mime-types/pull/146 406 | [pull-147]: https://github.com/mime-types/ruby-mime-types/pull/147 407 | [pull-149]: https://github.com/mime-types/ruby-mime-types/pull/149 408 | [pull-150]: https://github.com/mime-types/ruby-mime-types/pull/150 409 | [pull-153]: https://github.com/mime-types/ruby-mime-types/pull/153 410 | [pull-167]: https://github.com/mime-types/ruby-mime-types/pull/167 411 | [pull-170]: https://github.com/mime-types/ruby-mime-types/pull/170 412 | [pull-178]: https://github.com/mime-types/ruby-mime-types/pull/178 413 | [pull-179]: https://github.com/mime-types/ruby-mime-types/pull/179 414 | [pull-180]: https://github.com/mime-types/ruby-mime-types/pull/180 415 | [pull-189]: https://github.com/mime-types/ruby-mime-types/pull/189 416 | [pull-196]: https://github.com/mime-types/ruby-mime-types/pull/196 417 | [pull-79]: https://github.com/mime-types/ruby-mime-types/pull/79 418 | [pull-84]: https://github.com/mime-types/ruby-mime-types/pull/84 419 | [pull-85]: https://github.com/mime-types/ruby-mime-types/pull/85 420 | [pull-95]: https://github.com/mime-types/ruby-mime-types/pull/95 421 | [pull-97]: https://github.com/mime-types/ruby-mime-types/pull/97 422 | [tp]: https://guides.rubygems.org/trusted-publishing/ 423 | [dco]: https://developercertificate.org 424 | -------------------------------------------------------------------------------- /test/test_mime_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mime/types" 4 | require "minitest_helper" 5 | 6 | describe MIME::Type do 7 | def mime_type(content_type) 8 | MIME::Type.new(content_type) { |mt| yield mt if block_given? } 9 | end 10 | 11 | let(:x_appl_x_zip) { 12 | mime_type("content-type" => "x-appl/x-zip") { |t| t.extensions = %w[zip zp] } 13 | } 14 | let(:text_plain) { mime_type("content-type" => "text/plain") } 15 | let(:text_html) { mime_type("content-type" => "text/html") } 16 | let(:image_jpeg) { mime_type("content-type" => "image/jpeg") } 17 | let(:application_javascript) { 18 | mime_type("content-type" => "application/javascript") do |js| 19 | js.friendly("en" => "JavaScript") 20 | js.xrefs = { 21 | "rfc" => %w[rfc4239 rfc4239], 22 | "template" => %w[application/javascript] 23 | } 24 | js.encoding = "8bit" 25 | js.extensions = %w[js sj] 26 | js.registered = true 27 | end 28 | } 29 | let(:text_x_yaml) { 30 | mime_type("content-type" => "text/x-yaml") do |yaml| 31 | yaml.extensions = %w[yaml yml] 32 | yaml.encoding = "8bit" 33 | yaml.friendly("en" => "YAML Structured Document") 34 | end 35 | } 36 | let(:text_x_yaml_with_docs) { 37 | text_x_yaml.dup.tap do |yaml| 38 | yaml.docs = "Test YAML" 39 | end 40 | } 41 | 42 | describe ".simplified" do 43 | it "leaves normal types alone" do 44 | assert_equal "text/plain", MIME::Type.simplified("text/plain") 45 | end 46 | 47 | it "does not remove x- prefixes by default" do 48 | assert_equal "application/x-msword", 49 | MIME::Type.simplified("application/x-msword") 50 | assert_equal "x-xyz/abc", MIME::Type.simplified("x-xyz/abc") 51 | end 52 | 53 | it "removes x- prefixes when requested" do 54 | assert_equal "application/msword", 55 | MIME::Type.simplified("application/x-msword", remove_x_prefix: true) 56 | assert_equal "xyz/abc", 57 | MIME::Type.simplified("x-xyz/abc", remove_x_prefix: true) 58 | end 59 | 60 | it "lowercases mixed-case types" do 61 | assert_equal "text/vcard", MIME::Type.simplified("text/vCard") 62 | end 63 | 64 | it "returns nil when the value provided is not a valid content type" do 65 | assert_nil MIME::Type.simplified("text") 66 | end 67 | end 68 | 69 | describe ".i18n_key" do 70 | it "converts text/plain to text.plain" do 71 | assert_equal "text.plain", MIME::Type.i18n_key("text/plain") 72 | end 73 | 74 | it "does not remove x-prefixes" do 75 | assert_equal "application.x-msword", 76 | MIME::Type.i18n_key("application/x-msword") 77 | end 78 | 79 | it "converts text/vCard to text.vcard" do 80 | assert_equal "text.vcard", MIME::Type.i18n_key("text/vCard") 81 | end 82 | 83 | it "returns nil when the value provided is not a valid content type" do 84 | assert_nil MIME::Type.i18n_key("text") 85 | end 86 | end 87 | 88 | describe ".new" do 89 | it "fails if an invalid content type is provided" do 90 | exception = assert_raises MIME::Type::InvalidContentType do 91 | MIME::Type.new("content-type" => "apps") 92 | end 93 | assert_equal 'Invalid Content-Type "apps"', exception.to_s 94 | end 95 | 96 | it "creates a valid content type just from a string" do 97 | assert_output "", /MIME::Type.new when called with a String is deprecated\./ do 98 | type = MIME::Type.new("text/x-yaml") 99 | 100 | assert_instance_of MIME::Type, type 101 | assert_equal "text/x-yaml", type.content_type 102 | end 103 | end 104 | 105 | it "yields the content type in a block" do 106 | MIME::Type.new("content-type" => "text/x-yaml") do |type| 107 | assert_instance_of MIME::Type, type 108 | assert_equal "text/x-yaml", type.content_type 109 | end 110 | end 111 | 112 | it "creates a valid content type from a hash" do 113 | type = MIME::Type.new( 114 | "content-type" => "text/x-yaml", 115 | "obsolete" => true 116 | ) 117 | assert_instance_of MIME::Type, type 118 | assert_equal "text/x-yaml", type.content_type 119 | assert type.obsolete? 120 | end 121 | 122 | it "creates a valid content type from an array" do 123 | assert_output "", /MIME::Type.new when called with an Array is deprecated\./ do 124 | type = MIME::Type.new(%w[text/x-yaml yaml yml yz]) 125 | assert_instance_of MIME::Type, type 126 | assert_equal "text/x-yaml", type.content_type 127 | assert_equal %w[yaml yml yz], type.extensions 128 | end 129 | end 130 | end 131 | 132 | describe "#like?" do 133 | it "compares two MIME::Types on #simplified values without x- prefixes" do 134 | assert text_plain.like?(text_plain) 135 | refute text_plain.like?(text_html) 136 | end 137 | 138 | it "compares MIME::Type against string without x- prefixes" do 139 | assert text_plain.like?(text_plain.to_s) 140 | refute text_plain.like?(text_html.to_s) 141 | end 142 | end 143 | 144 | describe "#<=>" do 145 | it "correctly compares identical types" do 146 | assert_equal text_plain, text_plain 147 | end 148 | 149 | it "correctly compares equivalent types" do 150 | right = mime_type("content-type" => "text/Plain") 151 | refute_same text_plain, right 152 | assert_equal text_plain, right 153 | end 154 | 155 | it "correctly compares types that sort earlier" do 156 | refute_equal text_html, text_plain 157 | assert_operator text_html, :<, text_plain 158 | end 159 | 160 | it "correctly compares types that sort later" do 161 | refute_equal text_plain, text_html 162 | assert_operator text_plain, :>, text_html 163 | end 164 | 165 | it "correctly compares types against equivalent strings" do 166 | assert_equal text_plain, "text/plain" 167 | end 168 | 169 | it "correctly compares types against strings that sort earlier" do 170 | refute_equal text_html, "text/plain" 171 | assert_operator text_html, :<, "text/plain" 172 | end 173 | 174 | it "correctly compares types against strings that sort later" do 175 | refute_equal text_plain, "text/html" 176 | assert_operator text_plain, :>, "text/html" 177 | end 178 | end 179 | 180 | describe "#ascii?" do 181 | it "defaults to true for text/* types" do 182 | assert text_plain.ascii? 183 | end 184 | 185 | it "defaults to false for non-text/* types" do 186 | refute image_jpeg.ascii? 187 | end 188 | end 189 | 190 | describe "#binary?" do 191 | it "defaults to false for text/* types" do 192 | refute text_plain.binary? 193 | end 194 | 195 | it "defaults to true for non-text/* types" do 196 | assert image_jpeg.binary? 197 | end 198 | end 199 | 200 | describe "#complete?" do 201 | it "is true when there are extensions" do 202 | assert text_x_yaml.complete? 203 | end 204 | 205 | it "is false when there are no extensions" do 206 | refute mime_type("content-type" => "text/plain").complete? 207 | end 208 | end 209 | 210 | describe "#content_type" do 211 | it "preserves the original case" do 212 | assert_equal "text/plain", text_plain.content_type 213 | assert_equal "text/vCard", mime_type("content-type" => "text/vCard").content_type 214 | end 215 | 216 | it "does not remove x- prefixes" do 217 | assert_equal "x-appl/x-zip", x_appl_x_zip.content_type 218 | end 219 | end 220 | 221 | describe "#default_encoding" do 222 | it "is quoted-printable for text/* types" do 223 | assert_equal "quoted-printable", text_plain.default_encoding 224 | end 225 | 226 | it "is base64 for non-text/* types" do 227 | assert_equal "base64", image_jpeg.default_encoding 228 | end 229 | end 230 | 231 | describe "#encoding, #encoding=" do 232 | it "returns #default_encoding if not set explicitly" do 233 | assert_equal "quoted-printable", text_plain.encoding 234 | assert_equal "base64", image_jpeg.encoding 235 | end 236 | 237 | it "returns the set value when set" do 238 | text_plain.encoding = "8bit" 239 | assert_equal "8bit", text_plain.encoding 240 | end 241 | 242 | it "resets to the default encoding when set to nil or :default" do 243 | text_plain.encoding = "8bit" 244 | text_plain.encoding = nil 245 | assert_equal text_plain.default_encoding, text_plain.encoding 246 | text_plain.encoding = :default 247 | assert_equal text_plain.default_encoding, text_plain.encoding 248 | end 249 | 250 | it "raises a MIME::Type::InvalidEncoding for an invalid encoding" do 251 | exception = assert_raises MIME::Type::InvalidEncoding do 252 | text_plain.encoding = "binary" 253 | end 254 | assert_equal 'Invalid Encoding "binary"', exception.to_s 255 | end 256 | end 257 | 258 | describe "#eql?" do 259 | it "is not true for a non-MIME::Type" do 260 | refute text_plain.eql?("text/plain") 261 | end 262 | 263 | it "is not true for a different MIME::Type" do 264 | refute text_plain.eql?(image_jpeg) 265 | end 266 | 267 | it "is true for an equivalent MIME::Type" do 268 | assert text_plain.eql?(mime_type("content-type" => "text/Plain")) 269 | end 270 | 271 | it "is true for an equivalent subclass of MIME::Type" do 272 | subclass = Class.new(MIME::Type) 273 | assert text_plain.eql?(subclass.new("content-type" => "text/plain")) 274 | end 275 | end 276 | 277 | describe "#hash" do 278 | it "is the same between #eql? MIME::Type instances" do 279 | assert_equal text_plain.hash, mime_type("content-type" => "text/plain").hash 280 | end 281 | 282 | it "is the same between #eql? MIME::Type instances of different classes" do 283 | subclass = Class.new(MIME::Type) 284 | assert_equal text_plain.hash, subclass.new("content-type" => "text/plain").hash 285 | end 286 | 287 | it "uses the #simplified value" do 288 | assert_equal text_plain.hash, mime_type("content-type" => "text/Plain").hash 289 | end 290 | end 291 | 292 | describe "#extensions, #extensions=" do 293 | it "returns an array of extensions" do 294 | assert_equal %w[yaml yml], text_x_yaml.extensions 295 | assert_equal %w[zip zp], x_appl_x_zip.extensions 296 | end 297 | 298 | it "sets a single extension when provided a single value" do 299 | text_x_yaml.extensions = "yaml" 300 | assert_equal %w[yaml], text_x_yaml.extensions 301 | end 302 | 303 | it "deduplicates extensions" do 304 | text_x_yaml.extensions = %w[yaml yaml] 305 | assert_equal %w[yaml], text_x_yaml.extensions 306 | end 307 | end 308 | 309 | describe "#add_extensions" do 310 | it "does not modify extensions when provided nil" do 311 | text_x_yaml.add_extensions(nil) 312 | assert_equal %w[yaml yml], text_x_yaml.extensions 313 | end 314 | 315 | it "remains deduplicated with duplicate values" do 316 | text_x_yaml.add_extensions("yaml") 317 | assert_equal %w[yaml yml], text_x_yaml.extensions 318 | text_x_yaml.add_extensions(%w[yaml yz]) 319 | assert_equal %w[yaml yml yz], text_x_yaml.extensions 320 | end 321 | end 322 | 323 | describe "#priority_compare" do 324 | def priority(type) 325 | priority = "OpRceXtN" 326 | .chars 327 | .zip(("%08b" % type.__sort_priority).chars) 328 | .map { |e| e.join(":") } 329 | .join(" ") 330 | 331 | "#{type} (#{priority} / #{type.__sort_priority})" 332 | end 333 | 334 | def assert_priority_less(left, right) 335 | assert_equal(-1, left.priority_compare(right), "#{priority(left)} is not less than #{priority(right)}") 336 | end 337 | 338 | def assert_priority_same(left, right) 339 | assert_equal 0, left.priority_compare(right), "#{priority(left)} is not equal to #{priority(right)}" 340 | end 341 | 342 | def assert_priority_more(left, right) 343 | assert_equal 1, left.priority_compare(right), "#{priority(left)} is not more than #{priority(right)}" 344 | end 345 | 346 | def assert_priority(left, middle, right) 347 | assert_priority_less left, right 348 | assert_priority_same left, middle 349 | assert_priority_more right, middle 350 | end 351 | 352 | let(:text_1) { mime_type("content-type" => "text/1") } 353 | let(:text_1p) { mime_type("content-type" => "text/1") } 354 | let(:text_2) { mime_type("content-type" => "text/2") } 355 | 356 | it "sorts based on the simplified type when the sort priorities are the same" do 357 | assert_priority text_1, text_1p, text_2 358 | end 359 | 360 | it "sorts obsolete types higher than non-obsolete types" do 361 | text_1.obsolete = text_1p.obsolete = false 362 | text_1b = mime_type(text_1) { |t| t.obsolete = true } 363 | 364 | assert_priority_less text_1, text_1b 365 | 366 | assert_priority text_1, text_1p, text_1b 367 | end 368 | 369 | it "sorts provisional types higher than non-provisional types" do 370 | text_1.provisional = text_1p.provisional = false 371 | text_1b = mime_type(text_1) { |t| t.provisional = true } 372 | 373 | assert_priority text_1, text_1p, text_1b 374 | end 375 | 376 | it "sorts (3) based on the registration state" do 377 | text_1.registered = text_1p.registered = true 378 | text_1b = mime_type(text_1) { |t| t.registered = false } 379 | 380 | assert_priority text_1, text_1p, text_1b 381 | end 382 | 383 | it "sorts (4) based on the completeness" do 384 | text_1.extensions = text_1p.extensions = "1" 385 | text_1b = mime_type(text_1) { |t| t.extensions = nil } 386 | 387 | assert_priority text_1, text_1p, text_1b 388 | end 389 | 390 | it "sorts based on extensions (more extensions sort lower)" do 391 | text_1.extensions = ["foo", "bar"] 392 | text_2.extensions = ["foo"] 393 | 394 | assert_priority_less text_1, text_2 395 | end 396 | end 397 | 398 | describe "#raw_media_type" do 399 | it "extracts the media type as case-preserved" do 400 | assert_equal "Text", mime_type("content-type" => "Text/plain").raw_media_type 401 | end 402 | 403 | it "does not remove x- prefixes" do 404 | assert_equal("x-appl", x_appl_x_zip.raw_media_type) 405 | end 406 | end 407 | 408 | describe "#media_type" do 409 | it "extracts the media type as lowercase" do 410 | assert_equal "text", text_plain.media_type 411 | end 412 | 413 | it "does not remove x- prefixes" do 414 | assert_equal("x-appl", x_appl_x_zip.media_type) 415 | end 416 | end 417 | 418 | describe "#raw_media_type" do 419 | it "extracts the media type as case-preserved" do 420 | assert_equal "Text", mime_type("content-type" => "Text/plain").raw_media_type 421 | end 422 | 423 | it "does not remove x- prefixes" do 424 | assert_equal("x-appl", x_appl_x_zip.raw_media_type) 425 | end 426 | end 427 | 428 | describe "#sub_type" do 429 | it "extracts the sub type as lowercase" do 430 | assert_equal "plain", text_plain.sub_type 431 | end 432 | 433 | it "does not remove x- prefixes" do 434 | assert_equal("x-zip", x_appl_x_zip.sub_type) 435 | end 436 | end 437 | 438 | describe "#raw_sub_type" do 439 | it "extracts the sub type as case-preserved" do 440 | assert_equal "Plain", mime_type("content-type" => "text/Plain").raw_sub_type 441 | end 442 | 443 | it "does not remove x- prefixes" do 444 | assert_equal("x-zip", x_appl_x_zip.raw_sub_type) 445 | end 446 | end 447 | 448 | describe "#to_h" do 449 | let(:t) { mime_type("content-type" => "a/b") } 450 | 451 | def assert_has_keys(wanted_keys, actual, msg = nil) 452 | wanted_keys = Array(wanted_keys).uniq.sort 453 | actual_keys = if actual.is_a?(Hash) 454 | actual.keys 455 | else 456 | actual.to_h.keys 457 | end 458 | 459 | missing = wanted_keys - actual_keys 460 | pretty_wanted_keys = (wanted_keys + actual_keys).uniq.sort 461 | 462 | msg = message(msg) { 463 | "#{mu_pp(actual)} is missing attribute values\n#{diff(pretty_wanted_keys, actual_keys)}" 464 | } 465 | 466 | assert missing.empty?, msg 467 | end 468 | 469 | it "has the required keys (content-type, registered, encoding)" do 470 | assert_has_keys %w[content-type registered encoding], t 471 | end 472 | 473 | it "has the docs key if there are documents" do 474 | assert_has_keys "docs", mime_type(t) { |v| v.docs = "a" } 475 | end 476 | 477 | it "has the extensions key if set" do 478 | assert_has_keys "extensions", mime_type(t) { |v| v.extensions = "a" } 479 | end 480 | 481 | it "has the preferred-extension key if set" do 482 | assert_has_keys "preferred-extension", mime_type(t) { |v| v.preferred_extension = "a" } 483 | end 484 | 485 | it "has the obsolete key if set" do 486 | assert_has_keys "obsolete", mime_type(t) { |v| v.obsolete = true } 487 | end 488 | 489 | it "has the obsolete and use-instead keys if set" do 490 | assert_has_keys %w[obsolete use-instead], mime_type(t) { |v| 491 | v.obsolete = true 492 | v.use_instead = "c/d" 493 | } 494 | end 495 | 496 | it "has the signature key if set" do 497 | assert_has_keys "signature", mime_type(t) { |v| v.signature = true } 498 | end 499 | end 500 | 501 | describe "#to_json" do 502 | let(:expected_1) { 503 | '{"content-type":"a/b","encoding":"base64","registered":false,"sort-priority":48}' 504 | } 505 | let(:expected_2) { 506 | '{"content-type":"a/b","encoding":"base64","registered":true,"provisional":true,"sort-priority":80}' 507 | } 508 | 509 | it "converts to JSON when requested" do 510 | assert_equal expected_1, mime_type("content-type" => "a/b").to_json 511 | end 512 | 513 | it "converts to JSON with provisional when requested" do 514 | type = mime_type("content-type" => "a/b") do |t| 515 | t.registered = true 516 | t.provisional = true 517 | end 518 | assert_equal expected_2, type.to_json 519 | end 520 | end 521 | 522 | describe "#to_s, #to_str" do 523 | it "represents itself as a string of the canonical content_type" do 524 | assert_equal "text/plain", text_plain.to_s 525 | end 526 | 527 | it "acts like a string of the canonical content_type for comparison" do 528 | assert_equal text_plain, "text/plain" 529 | end 530 | 531 | it "acts like a string for other purposes" do 532 | assert_equal "stringy", "text/plain".sub(text_plain, "stringy") 533 | end 534 | end 535 | 536 | describe "#xrefs, #xrefs=" do 537 | let(:expected) { 538 | MIME::Types::Container.new("rfc" => Set["rfc1234", "rfc5678"]) 539 | } 540 | 541 | it "returns the expected results" do 542 | application_javascript.xrefs = { 543 | "rfc" => %w[rfc5678 rfc1234 rfc1234] 544 | } 545 | 546 | assert_equal expected, application_javascript.xrefs 547 | end 548 | end 549 | 550 | describe "#xref_urls" do 551 | let(:expected) { 552 | [ 553 | "http://www.iana.org/go/draft1", 554 | "http://www.iana.org/assignments/media-types/a/b", 555 | "http://www.iana.org/assignments/media-types/media-types.xhtml#p-1", 556 | "http://www.iana.org/go/rfc-1", 557 | "http://www.rfc-editor.org/errata_search.php?eid=err-1", 558 | "http://example.org", 559 | "text" 560 | ] 561 | } 562 | 563 | let(:type) { 564 | mime_type("content-type" => "a/b").tap do |t| 565 | t.xrefs = { 566 | "draft" => ["RFC1"], 567 | "template" => ["a/b"], 568 | "person" => ["p-1"], 569 | "rfc" => ["rfc-1"], 570 | "rfc-errata" => ["err-1"], 571 | "uri" => ["http://example.org"], 572 | "text" => ["text"] 573 | } 574 | end 575 | } 576 | 577 | it "translates according to given rules" do 578 | assert_equal expected, type.xref_urls 579 | end 580 | end 581 | 582 | describe "#use_instead" do 583 | it "is nil unless the type is obsolete" do 584 | assert_nil text_plain.use_instead 585 | end 586 | 587 | it "is nil if not set and the type is obsolete" do 588 | text_plain.obsolete = true 589 | assert_nil text_plain.use_instead 590 | end 591 | 592 | it "is a different type if set and the type is obsolete" do 593 | text_plain.obsolete = true 594 | text_plain.use_instead = "text/html" 595 | assert_equal "text/html", text_plain.use_instead 596 | end 597 | end 598 | 599 | describe "#preferred_extension, #preferred_extension=" do 600 | it "is nil when not set and there are no extensions" do 601 | assert_nil text_plain.preferred_extension 602 | end 603 | 604 | it "is the first extension when not set but there are extensions" do 605 | assert_equal "yaml", text_x_yaml.preferred_extension 606 | end 607 | 608 | it "is the extension provided when set" do 609 | text_x_yaml.preferred_extension = "yml" 610 | assert_equal "yml", text_x_yaml.preferred_extension 611 | end 612 | 613 | it "is adds the preferred extension if it does not exist" do 614 | text_x_yaml.preferred_extension = "yz" 615 | assert_equal "yz", text_x_yaml.preferred_extension 616 | assert_includes text_x_yaml.extensions, "yz" 617 | end 618 | end 619 | 620 | describe "#friendly" do 621 | it "returns English by default" do 622 | assert_equal "YAML Structured Document", text_x_yaml.friendly 623 | end 624 | 625 | it "returns English when requested" do 626 | assert_equal "YAML Structured Document", text_x_yaml.friendly("en") 627 | assert_equal "YAML Structured Document", text_x_yaml.friendly(:en) 628 | end 629 | 630 | it "returns nothing for an unknown language" do 631 | assert_nil text_x_yaml.friendly("zz") 632 | end 633 | 634 | it "merges new values from an array parameter" do 635 | expected = {"en" => "Text files"} 636 | assert_equal expected, text_plain.friendly(["en", "Text files"]) 637 | expected.update("fr" => "des fichiers texte") 638 | assert_equal expected, 639 | text_plain.friendly(["fr", "des fichiers texte"]) 640 | end 641 | 642 | it "merges new values from a hash parameter" do 643 | expected = {"en" => "Text files"} 644 | assert_equal expected, text_plain.friendly(expected) 645 | french = {"fr" => "des fichiers texte"} 646 | expected.update(french) 647 | assert_equal expected, text_plain.friendly(french) 648 | end 649 | 650 | it "raises an ArgumentError if an unknown value is provided" do 651 | exception = assert_raises ArgumentError do 652 | text_plain.friendly(1) 653 | end 654 | 655 | assert_equal "Expected a language or translation set, not 1", 656 | exception.message 657 | end 658 | end 659 | end 660 | -------------------------------------------------------------------------------- /lib/mime/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | module MIME 5 | end 6 | 7 | require "mime/types/deprecations" 8 | 9 | # The definition of one MIME content-type. 10 | # 11 | # == Usage 12 | # require "mime/types" 13 | # 14 | # plaintext = MIME::Types["text/plain"] # => [ text/plain ] 15 | # text = plaintext.first 16 | # puts text.media_type # => "text" 17 | # puts text.sub_type # => "plain" 18 | # 19 | # puts text.extensions.join(" ") # => "txt asc c cc h hh cpp hpp dat hlp" 20 | # puts text.preferred_extension # => "txt" 21 | # puts text.friendly # => "Text Document" 22 | # puts text.i18n_key # => "text.plain" 23 | # 24 | # puts text.encoding # => quoted-printable 25 | # puts text.default_encoding # => quoted-printable 26 | # puts text.binary? # => false 27 | # puts text.ascii? # => true 28 | # puts text.obsolete? # => false 29 | # puts text.registered? # => true 30 | # puts text.provisional? # => false 31 | # puts text.complete? # => true 32 | # 33 | # puts text # => "text/plain" 34 | # 35 | # puts text == "text/plain" # => true 36 | # puts "text/plain" == text # => true 37 | # puts text == "text/x-plain" # => false 38 | # puts "text/x-plain" == text # => false 39 | # 40 | # puts MIME::Type.simplified("x-appl/x-zip") # => "x-appl/x-zip" 41 | # puts MIME::Type.i18n_key("x-appl/x-zip") # => "x-appl.x-zip" 42 | # 43 | # puts text.like?("text/x-plain") # => true 44 | # puts text.like?(MIME::Type.new("content-type" => "x-text/x-plain")) # => true 45 | # 46 | # puts text.xrefs.inspect # => { "rfc" => [ "rfc2046", "rfc3676", "rfc5147" ] } 47 | # puts text.xref_urls # => [ "http://www.iana.org/go/rfc2046", 48 | # # "http://www.iana.org/go/rfc3676", 49 | # # "http://www.iana.org/go/rfc5147" ] 50 | # 51 | # xtext = MIME::Type.new("x-text/x-plain") 52 | # puts xtext.media_type # => "text" 53 | # puts xtext.raw_media_type # => "x-text" 54 | # puts xtext.sub_type # => "plain" 55 | # puts xtext.raw_sub_type # => "x-plain" 56 | # puts xtext.complete? # => false 57 | # 58 | # puts MIME::Types.any? { |type| type.content_type == "text/plain" } # => true 59 | # puts MIME::Types.all?(&:registered?) # => false 60 | # 61 | # # Various string representations of MIME types 62 | # qcelp = MIME::Types["audio/QCELP"].first # => audio/QCELP 63 | # puts qcelp.content_type # => "audio/QCELP" 64 | # puts qcelp.simplified # => "audio/qcelp" 65 | # 66 | # xwingz = MIME::Types["application/x-Wingz"].first # => application/x-Wingz 67 | # puts xwingz.content_type # => "application/x-Wingz" 68 | # puts xwingz.simplified # => "application/x-wingz" 69 | class MIME::Type 70 | # Reflects a MIME content-type specification that is not correctly 71 | # formatted (it is not +type+/+subtype+). 72 | class InvalidContentType < ArgumentError 73 | # :stopdoc: 74 | def initialize(type_string) 75 | @type_string = type_string 76 | end 77 | 78 | def to_s 79 | "Invalid Content-Type #{@type_string.inspect}" 80 | end 81 | # :startdoc: 82 | end 83 | 84 | # Reflects an unsupported MIME encoding. 85 | class InvalidEncoding < ArgumentError 86 | # :stopdoc: 87 | def initialize(encoding) 88 | @encoding = encoding 89 | end 90 | 91 | def to_s 92 | "Invalid Encoding #{@encoding.inspect}" 93 | end 94 | # :startdoc: 95 | end 96 | 97 | include Comparable 98 | 99 | # :stopdoc: 100 | # Full conformance with RFC 6838 §4.2 (the recommendation for < 64 characters is not 101 | # enforced or reported because MIME::Types mostly deals with registered data). RFC 4288 102 | # §4.2 does not restrict the first character to alphanumeric, but the total length of 103 | # each part is limited to 127 characters. RFCC 2045 §5.1 does not restrict the character 104 | # composition except for whitespace, but MIME::Type was always more strict than this. 105 | restricted_name_first = "[0-9a-zA-Z]" 106 | restricted_name_chars = "[-!#{$&}^_.+0-9a-zA-Z]{0,126}" 107 | restricted_name = "#{restricted_name_first}#{restricted_name_chars}" 108 | MEDIA_TYPE_RE = %r{(#{restricted_name})/(#{restricted_name})}.freeze 109 | I18N_RE = /[^[:alnum:]]/.freeze 110 | BINARY_ENCODINGS = %w[base64 8bit].freeze 111 | ASCII_ENCODINGS = %w[7bit quoted-printable].freeze 112 | # :startdoc: 113 | 114 | private_constant :MEDIA_TYPE_RE, :I18N_RE, :BINARY_ENCODINGS, 115 | :ASCII_ENCODINGS 116 | 117 | # Builds a MIME::Type object from the +content_type+, a MIME Content Type 118 | # value (e.g., "text/plain" or "application/x-eruby"). The constructed object 119 | # is yielded to an optional block for additional configuration, such as 120 | # associating extensions and encoding information. 121 | # 122 | # * When provided a Hash or a MIME::Type, the MIME::Type will be 123 | # constructed with #init_with. 124 | # 125 | # There are two deprecated initialization forms: 126 | # 127 | # * When provided an Array, the MIME::Type will be constructed using 128 | # the first element as the content type and the remaining flattened 129 | # elements as extensions. 130 | # * Otherwise, the content_type will be used as a string. 131 | # 132 | # Yields the newly constructed +self+ object. 133 | def initialize(content_type) # :yields: self 134 | @friendly = {} 135 | @obsolete = @registered = @provisional = false 136 | @preferred_extension = @docs = @use_instead = @__sort_priority = nil 137 | 138 | self.extensions = [] 139 | 140 | case content_type 141 | when Hash 142 | init_with(content_type) 143 | when Array 144 | MIME::Types.deprecated( 145 | class: MIME::Type, 146 | method: :new, 147 | pre: "when called with an Array", 148 | once: true 149 | ) 150 | self.content_type = content_type.shift 151 | self.extensions = content_type.flatten 152 | when MIME::Type 153 | init_with(content_type.to_h) 154 | else 155 | MIME::Types.deprecated( 156 | class: MIME::Type, 157 | method: :new, 158 | pre: "when called with a String", 159 | once: true 160 | ) 161 | self.content_type = content_type 162 | end 163 | 164 | self.encoding ||= :default 165 | self.xrefs ||= {} 166 | 167 | yield self if block_given? 168 | 169 | update_sort_priority 170 | end 171 | 172 | # Indicates that a MIME type is like another type. This differs from 173 | # == because x- prefixes are removed for this comparison. 174 | def like?(other) 175 | other = 176 | if other.respond_to?(:simplified) 177 | MIME::Type.simplified(other.simplified, remove_x_prefix: true) 178 | else 179 | MIME::Type.simplified(other.to_s, remove_x_prefix: true) 180 | end 181 | MIME::Type.simplified(simplified, remove_x_prefix: true) == other 182 | end 183 | 184 | # Compares the +other+ MIME::Type against the exact content type or the 185 | # simplified type (the simplified type will be used if comparing against 186 | # something that can be treated as a String with #to_s). In comparisons, this 187 | # is done against the lowercase version of the MIME::Type. 188 | # 189 | # Note that this implementation of #<=> is deprecated and will be changed 190 | # in the next major version to be the same as #priority_compare. 191 | # 192 | # Note that MIME::Types no longer compare against nil. 193 | def <=>(other) 194 | return priority_compare(other) if other.is_a?(MIME::Type) 195 | simplified <=> other 196 | end 197 | 198 | # Compares the +other+ MIME::Type using a pre-computed sort priority value, 199 | # then the simplified representation for an alphabetical sort. 200 | # 201 | # For the next major version of MIME::Types, this method will become #<=> and 202 | # #priority_compare will be removed. 203 | def priority_compare(other) 204 | if (cmp = __sort_priority <=> other.__sort_priority) == 0 205 | simplified <=> other.simplified 206 | else 207 | cmp 208 | end 209 | end 210 | 211 | # Uses a modified pre-computed sort priority value based on whether one of the provided 212 | # extensions is the preferred extension for a type. 213 | # 214 | # This is an internal function. If an extension provided is a preferred extension either 215 | # for this instance or the compared instance, the corresponding extension has its top 216 | # _extension_ bit cleared from its sort priority. That means that a type with between 217 | # 0 and 8 extensions will be treated as if it had 9 extensions. 218 | def __extension_priority_compare(other, exts) # :nodoc: 219 | tsp = __sort_priority 220 | 221 | if exts.include?(preferred_extension) && tsp & 0b1000 != 0 222 | tsp = tsp & 0b11110111 | 0b0111 223 | end 224 | 225 | osp = other.__sort_priority 226 | 227 | if exts.include?(other.preferred_extension) && osp & 0b1000 != 0 228 | osp = osp & 0b11110111 | 0b0111 229 | end 230 | 231 | if (cmp = tsp <=> osp) == 0 232 | simplified <=> other.simplified 233 | else 234 | cmp 235 | end 236 | end 237 | 238 | # Returns +true+ if the +other+ object is a MIME::Type and the content types 239 | # match. 240 | def eql?(other) 241 | other.is_a?(MIME::Type) && (self == other) 242 | end 243 | 244 | # Returns a hash based on the #simplified value. 245 | # 246 | # This maintains the invariant that two #eql? instances must have the same 247 | # #hash (although having the same #hash does *not* imply that the objects are 248 | # #eql?). 249 | # 250 | # To see why, suppose a MIME::Type instance +a+ is compared to another object 251 | # +b+, and that a.eql?(b) is true. By the definition of #eql?, 252 | # we know the following: 253 | # 254 | # 1. +b+ is a MIME::Type instance itself. 255 | # 2. a == b is true. 256 | # 257 | # Due to the first point, we know that +b+ should respond to the #simplified 258 | # method. Thus, per the definition of #<=>, we know that +a.simplified+ must 259 | # be equal to +b.simplified+, as compared by the <=> method corresponding to 260 | # +a.simplified+. 261 | # 262 | # Presumably, if a.simplified <=> b.simplified is +0+, then 263 | # +a.simplified+ has the same hash as +b.simplified+. So we assume it is 264 | # suitable for #hash to delegate to #simplified in service of the #eql? 265 | # invariant. 266 | def hash 267 | simplified.hash 268 | end 269 | 270 | # The computed sort priority value. This is _not_ intended to be used by most 271 | # callers. 272 | def __sort_priority # :nodoc: 273 | update_sort_priority if !instance_variable_defined?(:@__sort_priority) || @__sort_priority.nil? 274 | @__sort_priority 275 | end 276 | 277 | # Returns the whole MIME content-type string. 278 | # 279 | # The content type is a presentation value from the MIME type registry and 280 | # should not be used for comparison. The case of the content type is 281 | # preserved, and extension markers (x-) are kept. 282 | # 283 | # text/plain => text/plain 284 | # x-chemical/x-pdb => x-chemical/x-pdb 285 | # audio/QCELP => audio/QCELP 286 | attr_reader :content_type 287 | # A simplified form of the MIME content-type string, suitable for 288 | # case-insensitive comparison, with the content_type converted to lowercase. 289 | # 290 | # text/plain => text/plain 291 | # x-chemical/x-pdb => x-chemical/x-pdb 292 | # audio/QCELP => audio/qcelp 293 | attr_reader :simplified 294 | # Returns the media type of the simplified MIME::Type. 295 | # 296 | # text/plain => text 297 | # x-chemical/x-pdb => x-chemical 298 | # audio/QCELP => audio 299 | attr_reader :media_type 300 | # Returns the media type of the unmodified MIME::Type. 301 | # 302 | # text/plain => text 303 | # x-chemical/x-pdb => x-chemical 304 | # audio/QCELP => audio 305 | attr_reader :raw_media_type 306 | # Returns the sub-type of the simplified MIME::Type. 307 | # 308 | # text/plain => plain 309 | # x-chemical/x-pdb => pdb 310 | # audio/QCELP => QCELP 311 | attr_reader :sub_type 312 | # Returns the media type of the unmodified MIME::Type. 313 | # 314 | # text/plain => plain 315 | # x-chemical/x-pdb => x-pdb 316 | # audio/QCELP => qcelp 317 | attr_reader :raw_sub_type 318 | 319 | ## 320 | # The list of extensions which are known to be used for this MIME::Type. 321 | # Non-array values will be coerced into an array with #to_a. Array values 322 | # will be flattened, +nil+ values removed, and made unique. 323 | # 324 | # :attr_accessor: extensions 325 | def extensions 326 | @extensions.to_a 327 | end 328 | 329 | ## 330 | def extensions=(value) # :nodoc: 331 | clear_sort_priority 332 | @extensions = Set[*Array(value).flatten.compact].freeze 333 | MIME::Types.send(:reindex_extensions, self) 334 | end 335 | 336 | # Merge the +extensions+ provided into this MIME::Type. The extensions added 337 | # will be merged uniquely. 338 | def add_extensions(*extensions) 339 | self.extensions += extensions 340 | end 341 | 342 | ## 343 | # The preferred extension for this MIME type. If one is not set and there are 344 | # exceptions defined, the first extension will be used. 345 | # 346 | # When setting #preferred_extensions, if #extensions does not contain this 347 | # extension, this will be added to #extensions. 348 | # 349 | # :attr_accessor: preferred_extension 350 | 351 | ## 352 | def preferred_extension 353 | @preferred_extension || extensions.first 354 | end 355 | 356 | ## 357 | def preferred_extension=(value) # :nodoc: 358 | add_extensions(value) if value 359 | @preferred_extension = value 360 | end 361 | 362 | ## 363 | # The encoding (+7bit+, +8bit+, quoted-printable, or +base64+) 364 | # required to transport the data of this content type safely across a 365 | # network, which roughly corresponds to Content-Transfer-Encoding. A value of 366 | # +nil+ or :default will reset the #encoding to the 367 | # #default_encoding for the MIME::Type. Raises ArgumentError if the encoding 368 | # provided is invalid. 369 | # 370 | # If the encoding is not provided on construction, this will be either 371 | # "quoted-printable" (for text/* media types) and "base64" for eveything 372 | # else. 373 | # 374 | # :attr_accessor: encoding 375 | 376 | ## 377 | attr_reader :encoding 378 | 379 | ## 380 | def encoding=(enc) # :nodoc: 381 | if enc.nil? || (enc == :default) 382 | @encoding = default_encoding 383 | elsif BINARY_ENCODINGS.include?(enc) || ASCII_ENCODINGS.include?(enc) 384 | @encoding = enc 385 | else 386 | fail InvalidEncoding, enc 387 | end 388 | end 389 | 390 | # Returns the default encoding for the MIME::Type based on the media type. 391 | def default_encoding 392 | (@media_type == "text") ? "quoted-printable" : "base64" 393 | end 394 | 395 | ## 396 | # Returns the media type or types that should be used instead of this media 397 | # type, if it is obsolete. If there is no replacement media type, or it is 398 | # not obsolete, +nil+ will be returned. 399 | # 400 | # :attr_accessor: use_instead 401 | 402 | ## 403 | def use_instead 404 | obsolete? ? @use_instead : nil 405 | end 406 | 407 | ## 408 | attr_writer :use_instead 409 | 410 | # Returns +true+ if the media type is obsolete. 411 | # 412 | # :attr_accessor: obsolete 413 | attr_reader :obsolete 414 | alias_method :obsolete?, :obsolete 415 | 416 | ## 417 | def obsolete=(value) 418 | clear_sort_priority 419 | @obsolete = !!value 420 | end 421 | 422 | # The documentation for this MIME::Type. 423 | attr_accessor :docs 424 | 425 | # A friendly short description for this MIME::Type. 426 | # 427 | # call-seq: 428 | # text_plain.friendly # => "Text File" 429 | # text_plain.friendly("en") # => "Text File" 430 | def friendly(lang = "en") 431 | @friendly ||= {} 432 | 433 | case lang 434 | when String, Symbol 435 | @friendly[lang.to_s] 436 | when Array 437 | @friendly.update(Hash[*lang]) 438 | when Hash 439 | @friendly.update(lang) 440 | else 441 | fail ArgumentError, 442 | "Expected a language or translation set, not #{lang.inspect}" 443 | end 444 | end 445 | 446 | # A key suitable for use as a lookup key for translations, such as with 447 | # the I18n library. 448 | # 449 | # call-seq: 450 | # text_plain.i18n_key # => "text.plain" 451 | # 3gpp_xml.i18n_key # => "application.vnd-3gpp-bsf-xml" 452 | # # from application/vnd.3gpp.bsf+xml 453 | # x_msword.i18n_key # => "application.word" 454 | # # from application/x-msword 455 | attr_reader :i18n_key 456 | 457 | ## 458 | # The cross-references list for this MIME::Type. 459 | # 460 | # :attr_accessor: xrefs 461 | 462 | ## 463 | attr_reader :xrefs 464 | 465 | ## 466 | def xrefs=(xrefs) # :nodoc: 467 | @xrefs = MIME::Types::Container.new(xrefs) 468 | end 469 | 470 | # The decoded cross-reference URL list for this MIME::Type. 471 | def xref_urls 472 | xrefs.flat_map { |type, values| 473 | name = :"xref_url_for_#{type.tr("-", "_")}" 474 | respond_to?(name, true) && xref_map(values, name) || values.to_a 475 | } 476 | end 477 | 478 | # Indicates whether the MIME type has been registered with IANA. 479 | # 480 | # :attr_accessor: registered 481 | attr_reader :registered 482 | alias_method :registered?, :registered 483 | 484 | ## 485 | def registered=(value) 486 | clear_sort_priority 487 | @registered = !!value 488 | end 489 | 490 | # Indicates whether the MIME type's registration with IANA is provisional. 491 | # 492 | # :attr_accessor: provisional 493 | attr_reader :provisional 494 | 495 | ## 496 | def provisional=(value) 497 | clear_sort_priority 498 | @provisional = !!value 499 | end 500 | 501 | # Indicates whether the MIME type's registration with IANA is provisional. 502 | def provisional? 503 | registered? && @provisional 504 | end 505 | 506 | # MIME types can be specified to be sent across a network in particular 507 | # formats. This method returns +true+ when the MIME::Type encoding is set 508 | # to base64. 509 | def binary? 510 | BINARY_ENCODINGS.include?(encoding) 511 | end 512 | 513 | # MIME types can be specified to be sent across a network in particular 514 | # formats. This method returns +false+ when the MIME::Type encoding is 515 | # set to base64. 516 | def ascii? 517 | ASCII_ENCODINGS.include?(encoding) 518 | end 519 | 520 | # Indicateswhether the MIME type is declared as a signature type. 521 | attr_accessor :signature 522 | alias_method :signature?, :signature 523 | 524 | # Returns +true+ if the MIME::Type specifies an extension list, 525 | # indicating that it is a complete MIME::Type. 526 | def complete? 527 | !@extensions.empty? 528 | end 529 | 530 | # Returns the MIME::Type as a string. 531 | def to_s 532 | content_type 533 | end 534 | 535 | # Returns the MIME::Type as a string for implicit conversions. This allows 536 | # MIME::Type objects to appear on either side of a comparison. 537 | # 538 | # "text/plain" == MIME::Type.new("content-type" => "text/plain") 539 | def to_str 540 | content_type 541 | end 542 | 543 | # Converts the MIME::Type to a JSON string. 544 | def to_json(*args) 545 | require "json" 546 | to_h.to_json(*args) 547 | end 548 | 549 | # Converts the MIME::Type to a hash. The output of this method can also be 550 | # used to initialize a MIME::Type. 551 | def to_h 552 | encode_with({}) 553 | end 554 | 555 | # Populates the +coder+ with attributes about this record for 556 | # serialization. The structure of +coder+ should match the structure used 557 | # with #init_with. 558 | # 559 | # This method should be considered a private implementation detail. 560 | def encode_with(coder) 561 | coder["content-type"] = @content_type 562 | coder["docs"] = @docs unless @docs.nil? || @docs.empty? 563 | coder["friendly"] = @friendly unless @friendly.nil? || @friendly.empty? 564 | coder["encoding"] = @encoding 565 | coder["extensions"] = @extensions.to_a unless @extensions.empty? 566 | coder["preferred-extension"] = @preferred_extension if @preferred_extension 567 | if obsolete? 568 | coder["obsolete"] = obsolete? 569 | coder["use-instead"] = use_instead if use_instead 570 | end 571 | unless xrefs.empty? 572 | {}.tap do |hash| 573 | xrefs.each do |k, v| 574 | hash[k] = v.to_a.sort 575 | end 576 | coder["xrefs"] = hash 577 | end 578 | end 579 | coder["registered"] = registered? 580 | coder["provisional"] = provisional? if provisional? 581 | coder["signature"] = signature? if signature? 582 | coder["sort-priority"] = __sort_priority || 0b11111111 583 | coder 584 | end 585 | 586 | # Initialize an empty object from +coder+, which must contain the 587 | # attributes necessary for initializing an empty object. 588 | # 589 | # This method should be considered a private implementation detail. 590 | def init_with(coder) 591 | @__sort_priority = 0 592 | self.content_type = coder["content-type"] 593 | self.docs = coder["docs"] || "" 594 | self.encoding = coder["encoding"] 595 | self.extensions = coder["extensions"] || [] 596 | self.preferred_extension = coder["preferred-extension"] 597 | self.obsolete = coder["obsolete"] || false 598 | self.registered = coder["registered"] || false 599 | self.provisional = coder["provisional"] || false 600 | self.signature = coder["signature"] 601 | self.xrefs = coder["xrefs"] || {} 602 | self.use_instead = coder["use-instead"] 603 | 604 | friendly(coder["friendly"] || {}) 605 | 606 | update_sort_priority 607 | end 608 | 609 | def inspect # :nodoc: 610 | # We are intentionally lying here because MIME::Type::Columnar is an 611 | # implementation detail. 612 | "#" 613 | end 614 | 615 | class << self 616 | # MIME media types are case-insensitive, but are typically presented in a 617 | # case-preserving format in the type registry. This method converts 618 | # +content_type+ to lowercase. 619 | # 620 | # In previous versions of mime-types, this would also remove any extension 621 | # prefix (x-). This is no longer default behaviour, but may be 622 | # provided by providing a truth value to +remove_x_prefix+. 623 | def simplified(content_type, remove_x_prefix: false) 624 | simplify_matchdata(match(content_type), remove_x_prefix) 625 | end 626 | 627 | # Converts a provided +content_type+ into a translation key suitable for 628 | # use with the I18n library. 629 | def i18n_key(content_type) 630 | simplify_matchdata(match(content_type), joiner: ".") { |e| 631 | e.gsub!(I18N_RE, "-") 632 | } 633 | end 634 | 635 | # Return a +MatchData+ object of the +content_type+ against pattern of 636 | # media types. 637 | def match(content_type) 638 | case content_type 639 | when MatchData 640 | content_type 641 | else 642 | MEDIA_TYPE_RE.match(content_type) 643 | end 644 | end 645 | 646 | private 647 | 648 | def simplify_matchdata(matchdata, remove_x = false, joiner: "/") 649 | return nil unless matchdata 650 | 651 | matchdata.captures.map { |e| 652 | e.downcase! 653 | e.sub!(/^x-/, "") if remove_x 654 | yield e if block_given? 655 | e 656 | }.join(joiner) 657 | end 658 | end 659 | 660 | private 661 | 662 | def clear_sort_priority 663 | @__sort_priority = nil 664 | end 665 | 666 | # Update the __sort_priority value. Lower numbers sort better, so the 667 | # bitmapping may seem a little odd. The _best_ sort priority is 0. 668 | # 669 | # | bit | meaning | details | 670 | # | --- | --------------- | --------- | 671 | # | 7 | obsolete | 1 if true | 672 | # | 6 | provisional | 1 if true | 673 | # | 5 | registered | 0 if true | 674 | # | 4 | complete | 0 if true | 675 | # | 3 | # of extensions | see below | 676 | # | 2 | # of extensions | see below | 677 | # | 1 | # of extensions | see below | 678 | # | 0 | # of extensions | see below | 679 | # 680 | # The # of extensions is marked as the number of extensions subtracted from 681 | # 16, to a minimum of 0. 682 | def update_sort_priority 683 | extension_count = @extensions.length 684 | obsolete = (instance_variable_defined?(:@obsolete) && @obsolete) ? 1 << 7 : 0 685 | provisional = (instance_variable_defined?(:@provisional) && @provisional) ? 1 << 6 : 0 686 | registered = (instance_variable_defined?(:@registered) && @registered) ? 0 : 1 << 5 687 | complete = extension_count.nonzero? ? 0 : 1 << 4 688 | extension_count = [0, 16 - extension_count].max 689 | 690 | @__sort_priority = obsolete | registered | provisional | complete | extension_count 691 | end 692 | 693 | def content_type=(type_string) 694 | match = MEDIA_TYPE_RE.match(type_string) 695 | fail InvalidContentType, type_string if match.nil? 696 | 697 | @content_type = intern_string(type_string) 698 | @raw_media_type, @raw_sub_type = match.captures 699 | @simplified = intern_string(MIME::Type.simplified(match)) 700 | @i18n_key = intern_string(MIME::Type.i18n_key(match)) 701 | @media_type, @sub_type = MEDIA_TYPE_RE.match(@simplified).captures 702 | 703 | @raw_media_type = intern_string(@raw_media_type) 704 | @raw_sub_type = intern_string(@raw_sub_type) 705 | @media_type = intern_string(@media_type) 706 | @sub_type = intern_string(@sub_type) 707 | end 708 | 709 | if String.method_defined?(:-@) 710 | def intern_string(string) 711 | -string 712 | end 713 | else 714 | # MRI 2.2 and older do not have a method for string interning, 715 | # so we simply freeze them for keeping a similar interface 716 | def intern_string(string) 717 | string.freeze 718 | end 719 | end 720 | 721 | def xref_map(values, helper) 722 | values.map { |value| send(helper, value) } 723 | end 724 | 725 | def xref_url_for_rfc(value) 726 | "http://www.iana.org/go/%s" % value 727 | end 728 | 729 | def xref_url_for_draft(value) 730 | "http://www.iana.org/go/%s" % value.sub(/\ARFC/, "draft") 731 | end 732 | 733 | def xref_url_for_rfc_errata(value) 734 | "http://www.rfc-editor.org/errata_search.php?eid=%s" % value 735 | end 736 | 737 | def xref_url_for_person(value) 738 | "http://www.iana.org/assignments/media-types/media-types.xhtml#%s" % value 739 | end 740 | 741 | def xref_url_for_template(value) 742 | "http://www.iana.org/assignments/media-types/%s" % value 743 | end 744 | end 745 | 746 | require "mime/types/version" 747 | --------------------------------------------------------------------------------