file-validators-3.0.0/0000755000175000017510000000000014124557274013427 5ustar rmb571rmb571file-validators-3.0.0/.travis.yml0000644000175000017510000000235414124557274015544 0ustar rmb571rmb571language: ruby rvm: - 2.2.3 - 2.5.0 - 3.0.0 - ruby-head - jruby-9.1.7.0 - jruby-9.2.13.0 gemfile: - gemfiles/activemodel_6.1.gemfile - gemfiles/activemodel_6.0.gemfile - gemfiles/activemodel_5.0.gemfile - gemfiles/activemodel_4.0.gemfile - gemfiles/activemodel_3.2.gemfile matrix: exclude: - rvm: 2.2.3 gemfile: gemfiles/activemodel_6.0.gemfile - rvm: 2.2.3 gemfile: gemfiles/activemodel_6.1.gemfile - rvm: 2.5.0 gemfile: gemfiles/activemodel_3.2.gemfile - rvm: 2.5.0 gemfile: gemfiles/activemodel_4.0.gemfile - rvm: 3.0.0 gemfile: gemfiles/activemodel_3.2.gemfile - rvm: 3.0.0 gemfile: gemfiles/activemodel_4.0.gemfile - rvm: 3.0.0 gemfile: gemfiles/activemodel_5.0.gemfile - rvm: ruby-head gemfile: gemfiles/activemodel_3.2.gemfile - rvm: ruby-head gemfile: gemfiles/activemodel_4.0.gemfile - rvm: ruby-head gemfile: gemfiles/activemodel_5.0.gemfile - rvm: jruby-9.1.7.0 gemfile: gemfiles/activemodel_6.0.gemfile - rvm: jruby-9.1.7.0 gemfile: gemfiles/activemodel_6.1.gemfile - rvm: jruby-9.2.13.0 gemfile: gemfiles/activemodel_3.2.gemfile - rvm: jruby-9.2.13.0 gemfile: gemfiles/activemodel_4.0.gemfile allow_failures: - rvm: ruby-head file-validators-3.0.0/spec/0000755000175000017510000000000014124557274014361 5ustar rmb571rmb571file-validators-3.0.0/spec/lib/0000755000175000017510000000000014124557274015127 5ustar rmb571rmb571file-validators-3.0.0/spec/lib/file_validators/0000755000175000017510000000000014124557274020276 5ustar rmb571rmb571file-validators-3.0.0/spec/lib/file_validators/mime_type_analyzer_spec.rb0000644000175000017510000001035114124557274025532 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' require 'rack/test/uploaded_file' describe FileValidators::MimeTypeAnalyzer do it 'rises error when tool is invalid' do expect { described_class.new(:invalid) }.to raise_error(FileValidators::Error) end before :all do @cute_path = File.join(File.dirname(__FILE__), '../../fixtures/cute.jpg') @spoofed_file_path = File.join(File.dirname(__FILE__), '../../fixtures/spoofed.jpg') end let(:cute_image) { Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } let(:spoofed_file) { Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') } describe ':file analyzer' do let(:analyzer) { described_class.new(:file) } it 'determines MIME type from file contents' do expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'returns text/plain for unidentified MIME types' do expect(analyzer.call(fakeio('a' * 5 * 1024 * 1024))).to eq('text/plain') end it 'is able to determine MIME type for spoofed files' do expect(analyzer.call(spoofed_file)).to eq('text/plain') end it 'is able to determine MIME type for non-files' do expect(analyzer.call(fakeio(cute_image.read))).to eq('image/jpeg') end it 'returns nil for empty IOs' do expect(analyzer.call(fakeio(''))).to eq(nil) end it 'raises error if file command is not found' do allow(Open3).to receive(:popen3).and_raise(Errno::ENOENT) expect { analyzer.call(fakeio) }.to raise_error(FileValidators::Error, 'file command-line tool is not installed') end end describe ':fastimage analyzer' do let(:analyzer) { described_class.new(:fastimage) } it 'extracts MIME type of any IO' do expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'returns nil for unidentified MIME types' do expect(analyzer.call(fakeio('😃'))).to eq nil end it 'returns nil for empty IOs' do expect(analyzer.call(fakeio(''))).to eq nil end end describe ':mimemagic analyzer' do let(:analyzer) { described_class.new(:mimemagic) } it 'extracts MIME type of any IO' do expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'returns nil for unidentified MIME types' do expect(analyzer.call(fakeio('😃'))).to eq nil end it 'returns nil for empty IOs' do expect(analyzer.call(fakeio(''))).to eq nil end end if RUBY_VERSION >= '2.2.0' describe ':marcel analyzer' do let(:analyzer) { described_class.new(:marcel) } it 'extracts MIME type of any IO' do expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'returns application/octet-stream for unidentified MIME types' do expect(analyzer.call(fakeio('😃'))).to eq 'application/octet-stream' end it 'returns nil for empty IOs' do expect(analyzer.call(fakeio(''))).to eq nil end end end describe ':mime_types analyzer' do let(:analyzer) { described_class.new(:mime_types) } it 'extract MIME type from the file extension' do expect(analyzer.call(fakeio(filename: 'image.png'))).to eq('image/png') expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'extracts MIME type from file extension when IO is empty' do expect(analyzer.call(fakeio('', filename: 'image.png'))).to eq('image/png') end it 'returns nil on unknown extension' do expect(analyzer.call(fakeio(filename: 'file.foo'))).to eq(nil) end it 'returns nil when input is not a file' do expect(analyzer.call(fakeio)).to eq(nil) end end describe ':mini_mime analyzer' do let(:analyzer) { described_class.new(:mini_mime) } it 'extract MIME type from the file extension' do expect(analyzer.call(fakeio(filename: 'image.png'))).to eq('image/png') expect(analyzer.call(cute_image)).to eq('image/jpeg') end it 'extracts MIME type from file extension when IO is empty' do expect(analyzer.call(fakeio('', filename: 'image.png'))).to eq('image/png') end it 'returns nil on unkown extension' do expect(analyzer.call(fakeio(filename: 'file.foo'))).to eq(nil) end it 'returns nil when input is not a file' do expect(analyzer.call(fakeio)).to eq(nil) end end end file-validators-3.0.0/spec/lib/file_validators/validators/0000755000175000017510000000000014124557274022446 5ustar rmb571rmb571file-validators-3.0.0/spec/lib/file_validators/validators/file_size_validator_spec.rb0000644000175000017510000002077614124557274030037 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' describe ActiveModel::Validations::FileSizeValidator do class Dummy include ActiveModel::Validations end def storage_units if defined?(ActiveSupport::NumberHelper) # Rails 4.0+ { 5120 => '5 KB', 10_240 => '10 KB' } else { 5120 => '5120 Bytes', 10_240 => '10240 Bytes' } end end before :all do @storage_units = storage_units end subject { Dummy } def build_validator(options) @validator = described_class.new(options.merge(attributes: :avatar)) end context 'with :in option' do context 'as a range' do before { build_validator in: (5.kilobytes..10.kilobytes) } it { is_expected.to allow_file_size(7.kilobytes, @validator) } it { is_expected.not_to allow_file_size(4.kilobytes, @validator) } it { is_expected.not_to allow_file_size(11.kilobytes, @validator) } end context 'as a proc' do before { build_validator in: ->(_record) { (5.kilobytes..10.kilobytes) } } it { is_expected.to allow_file_size(7.kilobytes, @validator) } it { is_expected.not_to allow_file_size(4.kilobytes, @validator) } it { is_expected.not_to allow_file_size(11.kilobytes, @validator) } end end context 'with :greater_than_or_equal_to option' do context 'as a number' do before { build_validator greater_than_or_equal_to: 10.kilobytes } it { is_expected.to allow_file_size(11.kilobytes, @validator) } it { is_expected.to allow_file_size(10.kilobytes, @validator) } it { is_expected.not_to allow_file_size(9.kilobytes, @validator) } end context 'as a proc' do before { build_validator greater_than_or_equal_to: ->(_record) { 10.kilobytes } } it { is_expected.to allow_file_size(11.kilobytes, @validator) } it { is_expected.to allow_file_size(10.kilobytes, @validator) } it { is_expected.not_to allow_file_size(9.kilobytes, @validator) } end end context 'with :less_than_or_equal_to option' do context 'as a number' do before { build_validator less_than_or_equal_to: 10.kilobytes } it { is_expected.to allow_file_size(9.kilobytes, @validator) } it { is_expected.to allow_file_size(10.kilobytes, @validator) } it { is_expected.not_to allow_file_size(11.kilobytes, @validator) } end context 'as a proc' do before { build_validator less_than_or_equal_to: ->(_record) { 10.kilobytes } } it { is_expected.to allow_file_size(9.kilobytes, @validator) } it { is_expected.to allow_file_size(10.kilobytes, @validator) } it { is_expected.not_to allow_file_size(11.kilobytes, @validator) } end end context 'with :greater_than option' do context 'as a number' do before { build_validator greater_than: 10.kilobytes } it { is_expected.to allow_file_size(11.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end context 'as a proc' do before { build_validator greater_than: ->(_record) { 10.kilobytes } } it { is_expected.to allow_file_size(11.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end end context 'with :less_than option' do context 'as a number' do before { build_validator less_than: 10.kilobytes } it { is_expected.to allow_file_size(9.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end context 'as a proc' do before { build_validator less_than: ->(_record) { 10.kilobytes } } it { is_expected.to allow_file_size(9.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end end context 'with :greater_than and :less_than option' do context 'as a number' do before { build_validator greater_than: 5.kilobytes, less_than: 10.kilobytes } it { is_expected.to allow_file_size(7.kilobytes, @validator) } it { is_expected.not_to allow_file_size(5.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end context 'as a proc' do before do build_validator greater_than: ->(_record) { 5.kilobytes }, less_than: ->(_record) { 10.kilobytes } end it { is_expected.to allow_file_size(7.kilobytes, @validator) } it { is_expected.not_to allow_file_size(5.kilobytes, @validator) } it { is_expected.not_to allow_file_size(10.kilobytes, @validator) } end end context 'with :message option' do before do build_validator in: (5.kilobytes..10.kilobytes), message: 'is invalid. (Between %{min} and %{max} please.)' end it do is_expected.not_to allow_file_size( 11.kilobytes, @validator, message: "Avatar is invalid. (Between #{@storage_units[5120]}" \ " and #{@storage_units[10_240]} please.)" ) end it do is_expected.to allow_file_size( 7.kilobytes, @validator, message: "Avatar is invalid. (Between #{@storage_units[5120]}" \ " and #{@storage_units[10_240]} please.)" ) end end context 'default error message' do context 'given :in options' do before { build_validator in: 5.kilobytes..10.kilobytes } it do is_expected.not_to allow_file_size( 11.kilobytes, @validator, message: "Avatar file size must be between #{@storage_units[5120]}" \ " and #{@storage_units[10_240]}" ) end it do is_expected.not_to allow_file_size( 4.kilobytes, @validator, message: "Avatar file size must be between #{@storage_units[5120]}" \ " and #{@storage_units[10_240]}" ) end end context 'given :greater_than and :less_than options' do before { build_validator greater_than: 5.kilobytes, less_than: 10.kilobytes } it do is_expected.not_to allow_file_size( 11.kilobytes, @validator, message: "Avatar file size must be less than #{@storage_units[10_240]}" ) end it do is_expected.not_to allow_file_size( 4.kilobytes, @validator, message: "Avatar file size must be greater than #{@storage_units[5120]}" ) end end context 'given :greater_than_or_equal_to and :less_than_or_equal_to options' do before do build_validator greater_than_or_equal_to: 5.kilobytes, less_than_or_equal_to: 10.kilobytes end it do is_expected.not_to allow_file_size( 11.kilobytes, @validator, message: "Avatar file size must be less than or equal to #{@storage_units[10_240]}" ) end it do is_expected.not_to allow_file_size( 4.kilobytes, @validator, message: "Avatar file size must be greater than or equal to #{@storage_units[5120]}" ) end end end context 'exceptional file size' do before { build_validator less_than: 3.kilobytes } it { is_expected.to allow_file_size(0, @validator) } # zero-byte file it { is_expected.not_to allow_file_size(nil, @validator) } end context 'using the helper' do before { Dummy.validates_file_size :avatar, in: (5.kilobytes..10.kilobytes) } it 'adds the validator to the class' do expect(Dummy.validators_on(:avatar)).to include(described_class) end end context 'given options' do it 'raises argument error if no required argument was given' do expect { build_validator message: 'Some message' }.to raise_error(ArgumentError) end (described_class::CHECKS.keys - [:in]).each do |argument| it "does not raise argument error if :#{argument} is numeric or a proc" do expect { build_validator argument => 5.kilobytes }.not_to raise_error expect { build_validator argument => ->(_record) { 5.kilobytes } }.not_to raise_error end it "raises error if :#{argument} is neither a number nor a proc" do expect { build_validator argument => 5.kilobytes..10.kilobytes }.to raise_error(ArgumentError) end end it 'does not raise argument error if :in is a range or a proc' do expect { build_validator in: 5.kilobytes..10.kilobytes }.not_to raise_error expect { build_validator in: ->(_record) { 5.kilobytes..10.kilobytes } }.not_to raise_error end it 'raises error if :in is neither a range nor a proc' do expect { build_validator in: 5.kilobytes }.to raise_error(ArgumentError) end end end file-validators-3.0.0/spec/lib/file_validators/validators/file_content_type_validator_spec.rb0000644000175000017510000001565614124557274031601 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' describe ActiveModel::Validations::FileContentTypeValidator do class Dummy include ActiveModel::Validations end subject { Dummy } def build_validator(options) @validator = described_class.new(options.merge(attributes: :avatar)) end context 'whitelist format' do context 'with an allowed type' do context 'as a string' do before { build_validator allow: 'image/jpg' } it { is_expected.to allow_file_content_type('image/jpg', @validator) } end context 'as an regexp' do before { build_validator allow: /^image\/.*/ } it { is_expected.to allow_file_content_type('image/png', @validator) } end context 'as a list' do before { build_validator allow: ['image/png', 'image/jpg', 'image/jpeg'] } it { is_expected.to allow_file_content_type('image/png', @validator) } end context 'as a proc' do before { build_validator allow: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } } it { is_expected.to allow_file_content_type('image/png', @validator) } end end context 'with a disallowed type' do context 'as a string' do before { build_validator allow: 'image/png' } it { is_expected.not_to allow_file_content_type('image/jpeg', @validator) } end context 'as a regexp' do before { build_validator allow: /^text\/.*/ } it { is_expected.not_to allow_file_content_type('image/png', @validator) } end context 'as a proc' do before { build_validator allow: ->(_record) { /^text\/.*/ } } it { is_expected.not_to allow_file_content_type('image/png', @validator) } end context 'with :message option' do context 'without interpolation' do before do build_validator allow: 'image/png', message: 'should be a PNG image' end it do is_expected.not_to allow_file_content_type( 'image/jpeg', @validator, message: 'Avatar should be a PNG image' ) end end context 'with interpolation' do before do build_validator allow: 'image/png', message: 'should have content type %{types}' end it do is_expected.not_to allow_file_content_type( 'image/jpeg', @validator, message: 'Avatar should have content type image/png' ) end it do is_expected.to allow_file_content_type( 'image/png', @validator, message: 'Avatar should have content type image/png' ) end end end context 'default message' do before { build_validator allow: 'image/png' } it do is_expected.not_to allow_file_content_type( 'image/jpeg', @validator, message: 'Avatar file should be one of image/png' ) end end end end context 'blacklist format' do context 'with an allowed type' do context 'as a string' do before { build_validator exclude: 'image/gif' } it { is_expected.to allow_file_content_type('image/png', @validator) } end context 'as an regexp' do before { build_validator exclude: /^text\/.*/ } it { is_expected.to allow_file_content_type('image/png', @validator) } end context 'as a list' do before { build_validator exclude: ['image/png', 'image/jpg', 'image/jpeg'] } it { is_expected.to allow_file_content_type('image/gif', @validator) } end context 'as a proc' do before { build_validator exclude: ->(_record) { ['image/png', 'image/jpg', 'image/jpeg'] } } it { is_expected.to allow_file_content_type('image/gif', @validator) } end end context 'with a disallowed type' do context 'as a string' do before { build_validator exclude: 'image/gif' } it { is_expected.not_to allow_file_content_type('image/gif', @validator) } end context 'as an regexp' do before { build_validator exclude: /^text\/.*/ } it { is_expected.not_to allow_file_content_type('text/plain', @validator) } end context 'as an proc' do before { build_validator exclude: ->(_record) { /^text\/.*/ } } it { is_expected.not_to allow_file_content_type('text/plain', @validator) } end context 'with :message option' do context 'without interpolation' do before do build_validator exclude: 'image/png', message: 'should not be a PNG image' end it do is_expected.not_to allow_file_content_type( 'image/png', @validator, message: 'Avatar should not be a PNG image' ) end end context 'with interpolation' do before do build_validator exclude: 'image/png', message: 'should not have content type %{types}' end it do is_expected.not_to allow_file_content_type( 'image/png', @validator, message: 'Avatar should not have content type image/png' ) end it do is_expected.to allow_file_content_type( 'image/jpeg', @validator, message: 'Avatar should not have content type image/jpeg' ) end end end context 'default message' do before { build_validator exclude: 'image/png' } it do is_expected.not_to allow_file_content_type( 'image/png', @validator, message: 'Avatar file cannot be image/png' ) end end end end context 'using the helper' do before { Dummy.validates_file_content_type :avatar, allow: 'image/jpg' } it 'adds the validator to the class' do expect(Dummy.validators_on(:avatar)).to include(described_class) end end context 'given options' do it 'raises argument error if no required argument was given' do expect { build_validator message: 'Some message' }.to raise_error(ArgumentError) end described_class::CHECKS.each do |argument| it "does not raise error if :#{argument} is string, array, regexp or a proc" do expect { build_validator argument => 'image/jpg' }.not_to raise_error expect { build_validator argument => ['image/jpg'] }.not_to raise_error expect { build_validator argument => /^image\/.*/ }.not_to raise_error expect { build_validator argument => ->(_record) { 'image/jpg' } }.not_to raise_error end it "raises argument error if :#{argument} is neither a string, array, regexp nor proc" do expect { build_validator argument => 5.kilobytes }.to raise_error(ArgumentError) end end end end file-validators-3.0.0/spec/support/0000755000175000017510000000000014124557274016075 5ustar rmb571rmb571file-validators-3.0.0/spec/support/helpers.rb0000644000175000017510000000020514124557274020061 0ustar rmb571rmb571# frozen_string_literal: true module Helpers def fakeio(content = 'file', **options) FakeIO.new(content, **options) end end file-validators-3.0.0/spec/support/fakeio.rb0000644000175000017510000000057114124557274017663 0ustar rmb571rmb571# frozen_string_literal: true require 'forwardable' require 'stringio' class FakeIO attr_reader :original_filename, :content_type def initialize(content, filename: nil, content_type: nil) @io = StringIO.new(content) @original_filename = filename @content_type = content_type end extend Forwardable delegate %i[read rewind eof? close size] => :@io end file-validators-3.0.0/spec/support/matchers/0000755000175000017510000000000014124557274017703 5ustar rmb571rmb571file-validators-3.0.0/spec/support/matchers/allow_content_type.rb0000644000175000017510000000110014124557274024131 0ustar rmb571rmb571# frozen_string_literal: true RSpec::Matchers.define :allow_file_content_type do |content_type, validator, message| match do |model| value = double('file', path: content_type, original_filename: content_type) allow_any_instance_of(model).to receive(:read_attribute_for_validation).and_return(value) allow(validator).to receive(:get_content_type).and_return(content_type) dummy = model.new validator.validate(dummy) if message.present? dummy.errors.full_messages.exclude?(message[:message]) else dummy.errors.empty? end end end file-validators-3.0.0/spec/support/matchers/allow_file_size.rb0000644000175000017510000000067314124557274023405 0ustar rmb571rmb571# frozen_string_literal: true RSpec::Matchers.define :allow_file_size do |size, validator, message| match do |model| value = double('file', size: size) allow_any_instance_of(model).to receive(:read_attribute_for_validation).and_return(value) dummy = model.new validator.validate(dummy) if message.present? dummy.errors.full_messages.exclude?(message[:message]) else dummy.errors.empty? end end end file-validators-3.0.0/spec/spec_helper.rb0000644000175000017510000000120414124557274017174 0ustar rmb571rmb571# frozen_string_literal: true ENV['RAILS_ENV'] ||= 'test' require 'active_support' require 'active_support/deprecation' require 'active_support/core_ext' require 'file_validators' require 'rspec' require 'coveralls' Coveralls.wear! locale_path = Dir.glob(File.dirname(__FILE__) + '/locale/*.yml') I18n.load_path += locale_path unless I18n.load_path.include?(locale_path) I18n.enforce_available_locales = false Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } RSpec.configure do |config| config.include Helpers # Suppress stdout in the console config.before { allow($stdout).to receive(:write) } end file-validators-3.0.0/spec/locale/0000755000175000017510000000000014124557274015620 5ustar rmb571rmb571file-validators-3.0.0/spec/locale/en.yml0000755000175000017510000000034614124557274016753 0ustar rmb571rmb571en: number: human: storage_units: format: "%n %u" units: byte: one: "Byte" other: "Bytes" kb: "KB" mb: "MB" gb: "GB" tb: "TB" file-validators-3.0.0/spec/fixtures/0000755000175000017510000000000014124557274016232 5ustar rmb571rmb571file-validators-3.0.0/spec/fixtures/chubby_cute.png0000755000175000017510000012727114124557274021251 0ustar rmb571rmb571PNG  IHDR!b =zTXtRaw profile type exifxڭi$7r:c9V3@MjfLCΌ@? |?K(zF(#O_ϟ?ү7v_ۯ8Q7_/ ſ~?w{f%EX{=θS'i̥_*4K7~Yc77~漳}ukymђ} ;-J{-S'qhN,;{;RTLZ~TF9o_/5:_;789'q_u-()\YL2Zk2O5ŖsKX4hl%/:aUz3-}f?3*ǭZ7&*O+ M7/^y3TzUlJV[k6ҽzϑ1>h1䚳OOse,_uXs]{ySzgyJvw>篾oگkW)~wW[ӉS$gt,DǛ:z{*%sY"]9)ΐ*-,7ew[ot.u?_t ױ)TM1}k汩8K܊{W?K{JY}ԘOn1hjlI|<w}euFϨ̼:UW]h>}\{m=;wr jwz'߶n[mTZG.!{˒ޛs8,c{\x^޲OkQvZAMέTNЇ;j\(g̑v7o1ˡ2s@UWTon0P֋SzR^g7{ĺ}l4Mޛ+{~oN9gxr99 ri{90Zb?nzV=mD_(>0UG[w?ݙBC=ep8+$fKa!QL&QL ks:"Ϙk86rj3v8қ,8>B7sdj9.fWCll 8|v2sSf}A'ӴoF"Q~3c*L|^ۏj#1U8-ZlcP {ތ4$1 ~3;S+}gn+3εk¸k<[g9s;Fyō0oLy9Ĝ #(QIr SNY doe:g7.&F!hF>PkFfwz8&k}Lqye}?gHlt1ph(6EEi{YhqZO8 h h <ōAToQzkÄpKUT>vf;䯙r7쁳i o!A1B2@ydF5ePK@(ۨEӐJbMi{  `axOC[#%gG9 N#@a4zlLbŊ;kʺAPETXd 'pj(ąI |XuؙƃiX41okC^CX-E1q;AzQdx5+PӫköE{l CGs$Bi.),za& 1IPHa/Cm70GӍY9msTL]d}( L'gks!lr~m9!b \n"H*l /].Łc.9<\;O_A}ejMf@,@kWP[FgJ 7c9" &7CT0m`hG>+.`dXÐ9 @9iMT x'3yH }FuڹV\,~cz]quqtSa0܃ 1^~<ӻKC u!>9iDΉ̸]ZAyLchd!Jv|5+ݡ!mp)j$@z>qZrעj>kE,Rok~ , "9 ui$(!f6eDFaIO]ABuفSCc+ ' P*hIXɃ$cIh펐aI I|?"1b!XH7ڿn f(h7ֆwf 4q\Ș:>+`/Q8IüO*R >B'Aᜏߩ[D pk$fzmPz@jƵk+#3Ƿ&@q$TE6-8Lcsq_c>kj|16g]-fy#5%]ji,n**h8쎖ہs2(TV:;bhp;^zȓ0KXK;e (YX& 7ЀR:j%<Q ohɕ#bap|2s9A e?DA{c W3@:H;jZj-b(6ċTh:蒳nX(<fޏC\ hW{Vo6O,y aOKɶvN? $3j<|1fuJ~$hB[9PWt%j{r/0E7Q2fߑi:^҈1di4* $Ŏma}P0A1PmI/ǀ1mRː6;" IM2ز)$aNsW h2J"-6ԌJ\O ztE8P&P$[W3Z@ʺw< ׈R]^,C$QX!7+MKՆO KN*SSC "+cC;!ϦYPWWMh0dhm5>pcJI`1>IC#Nnۍ2A+mTX(i8U16MˠN@[}((x`r@ɺқ\ -]Qi#u=8cU=Eč |tPlt#1Ȃ;Qґt#HxǔaVTj]0ߘ(/h/L0wD[`!Ф9;KY?BfK_Bo[i,EB֋2> n=S 2VP!?bw?{1[I} '#FvgC- xk<tc$GJ7`ԃVitzj'au"m7l0!ehg9O~ I< +hwBu3LdMۛ<47%1/umH&6!A8 ?'f_͘+mh\ H?Su]5 gTThGnNcIvվP]g;75b~.#'F6[o-mG Fs 8Г"$ *F*q\l`sfHE^0r]{qJ8dl,jFgh:7*@n:(@l_2<&R*B 훤EcZπk3#X6 jfL)f#jMD: hA{)^-Hփl2'^L`J ۝KZ]OwULQqʓngi˔κ_vC0N 9i"/W?7YDRMiPDV+q@*jʹ;4z,E[iaO,rYc,VL4ǓD6 §e OᅑJ=1M АČ{oe^{{*3X>-QVJAY6}:i\}YͿ=/ ]W1)?e%jz@.kQ1BUk\(˼hvoK{a~'R38² IDATxڄiluHM8Ȅ@Z(J Yǿd3HeAbIM l ݍu{U?{oެz eȗy}? @D0c@$HRj)sY<:O\DH.1@0)Bޯ& ¾K_sy2<WTDV[բzڡ{!B⪊jg1>b`jFG{k-x=}Fఏ|8#DUpgV-"1늠c+w]j1Z5 AIPIӲ*UTVD; bS1ZJRvU6:C-芨(_VW-o6FvhZ.K)ff-Ο y1߆|lDɘ^y3o<˻Zp CuַO|SKSo|QLxOGJ"Wl$QANyθu<)US!h(kEZ .I8Z4˝>mlZjkAU47%}/~՗*>8B:Ha)"Z AP/.ཙ04Fk- z@q0v]ߵh#.($R㒡W6]NtqAS ,uJu"!!@Lҗ?-BЀBPB(*b%E" 9eWu̸ i Iȱ3n@2 Gc0>x8_yg= \\ju~WS p+:&v2[3:XfHD4@H6 6x;/|P GUE ZJQ"* D2eH5dPD$d5MwQ6oz>+;L#|<麵ZksJ?S3nj^v41 :E`EU=D""ӛP֫U:U 62p悭E)Ň&Sg1"SuS7zg*鼁à7h֚)"ES|G "G$ x8,2-50x|ů‡~L!f@h5sUx"RD4H9qfihgL_<4o 7{FUi1NR9-0͉h` =Ͼ_y^En]-ꪚy #Re?E*"sBT ٲL mU7g#tw>vQ6Q31%AFAU&V:_RJF jYun7d`4QM̌Ws?S/#k s "DI,ҕBJ!P( AT[%I~|Oi"d'ifVJQ+S&KvNB*!8z\I> SO&Lq |?Icm"~gv,U麲YV]U)2e6=a7yvǡj@&SB>'K1Îd@pww{h<fy"<=(b!>%$=='3=oN\P1}I-4i1Z$c>[!5@)XOUJW+T+(e.4fBJy'W/JA &$M(e@8fT3g拉?6_0DPM7uFAsLg{92#*RU".H!f 9Пz.ME539?dTr/ zTŴǕtျ M_{}g_xu(L[Ea%*ՂĊ'U !j#61)K(cAmh'&ze#[D/gsH: )]&qD)ߔm%ˮR 'B6fۭj*Z[k TE4YkqG*\"P5omjP#H OLR$uo=RoQ9/mk16%Lljet=-s'{`\U 9⣪c,D@VRPIU+wx:i5u1]: TL- ƶvev||>1Wi4_}{o RWe慔巙jn>a&b cP'X (P 4gw`ffVj~jZnLv񢫌)1?յCweYvR¡׿U3DUY 8} E\5AU'EX &b ,qy7 u b|-O<-o޳b82}o0'˛qg^܅@JsnZ$-]ֆa~v9]0 7nVwLNgc8]9[M҂k-Rbw\_^ﭫarϧzkes?Sύdt%Y \&&-x~de\ꣿ&t(JLaW{'iQbVMB*)Gi[%fEj}*lz%/a"˚6`8B1)^ȱi ~|? 4jv{qN9DBiY68A+euA8"?HbGwJqLsHO3>) ,*Vwd+!@Ld` ]2 O8'Aꎕ~kM;IǨ- H WH'?ZP`SC!6]fLz4@0AB >l|-h*x ?+T`ĀpCAjk=StET)fcGҚT^b#o-[wqqfU#DHҏ ",+[*U C-ڭY-3UM{wJS%<Pu])glADub+hU-0X/ 0lӀS ۉ»w/֛BDE'~H8C5Ϸ57Zk)5oy֚`#!v#][ZF㜣wVZk3Ꞧy#]_&ӰߎB2JKx0#; 3`Zw_WUCjRUٵSDL`,]Wj]ѮXWtiסH5S XU4ȍĜ\ިtv456|*d +nu%f,?#3 N4)!jA:gc DL5daZk<ۄf2r9ge^G0#3_jmAȯ{G˂5DT+5S*sCd^-O]\K /˓==*fwRmh7hލ@TՒmZ+.v4e0 2iWZҷt+lZzI)0>nm5[s׀ ^+?|s_֠+g+ E$bVHG pIhkŅ;pstcHLKZ-p8in~073H=C荝s6Lw^W>O$@m9Q25Q3-EMJWKA(FPqSjt4R2{q')4fb]"ȁUEK)ZUq$Bc 5՗?目F8AU JUIإQEN6bl8kXDAE`͎2 83FYxK1(lC;X~7E_~9g2wUOD B*QmlIs _~c| R80D3tEQ ja0irpZ%u1wq <,Pl@ 1$k,czH_@x;|ŗ!;u&bLc%Vą}6h2;=fVҠ4HQQѐAnW~8$7SҼdHk].4 :%WI+ Zsqafn2O,nEtED0n\8>|exbf9L /nC9GW>j1s9t2NŌh3ժls`; G LI IMHx)2P U+T;\CcaYgL,f?p4OϿ2۪)0N-{'O\'л,*{2 }pwbh> W}{;?>hgC*%H]f7ߘOZH$"c͙CfΠS`i3[Kpy7I@<\]1&Qk7x+`쮮?GCj8Mof"0,Q@,3QRqu.|S9}4F ȁ0a"r[gvS`A4¹t aDsgTڡ ~_myDUESj8݄٪N`I뇡v3p{J>&7oӾWNČGcN'Oo??x*41kRB0`~6+GE9QbtG2 ߐڍ&X2mXvVZk͋ Kvʾ{5Tj^uC|Rs?}׻v!xb{?{v}omUݪd )#d.b,2 Z;:tl>۷w_=>~ARYIڢYy%grmvf@ 7'3]Q97\7{|(yrlzKe &))F?׾?PdTSDj)%TI2IB,IS3;2UO:@ IDATOg@'HњA;FN'ׇW_{}2ڤB!$?wuj?y?xU/-thj5BK,GE$*XaNl[<a8XࡰRey;Nz֛!%Kv}o@HHDEw.Gkm:3UE;~g~#^ Q4jH4sX0)4e甒,ʏ*-pg0bb4LF4GЎB:Og(;.hٔj~?Kwًևك;v2+!UM2d 4ќ8 B *&*a6(Rp4O/ْ mGzN?t<:So]5RT=碨fztڧ^]ϲ`tOm2x6˩ SD&iEA;O^3YqY(,Ɛ"0QcSEQ&:Icu)c7)Q@&O]h8aP½Gsهۇwm) Q!*ՏI . H~$<,Y71L]wE+Lު>wƣ"FFEyxLjb1A]W|HkCkOK#p46`}>TԆR(>2cUlEaԠJ.!oE'7?j@e%; 㬳(D'<ڞN)';QY#3Wb!RQh,8G<:t"3*`*P"%%DRp\^^)XxZ/W{y_}gsᶫ١ c ~sdtI1{S֛7RL`)jQV́ Vk6b]{*[ODC?:>3EPk~DlK\v90 iJXU 8EzETaX֭?so)Z@PV $wLPuTP^sn&-zaGbRڏ2 mĥ3w1`K?9&F;&*k[dv{P% jt^]]^us9W{fwvz307h bjYabJNJPUT gIFBr蛊4U@mzJUs=\w=yzp- YL&?+Ft. >-9"-h"e4Q~&?y"xίO」UTSq;g5m2 -%_${;׻a5HzS>dCFpst2VgNf*ivLO˸Qdu\^5wv{fuY:}%Sn'0d2d6-ie4y1BA kmuznn]䕷w.Ya)Y:ʱ'HT Gf&ُW7D.3cd7PȯM+wzK/~߼_xDI!|. $\WN!~}}}u;jgj.$oNvmdWsdLr @[]x{Fԯhv ]d f}n*BfX l B>rJ`SmvǣgB(V M ʨVBd]e[wuk_*hXwnn3- 9⒢:aјK0 )& M̜t.|iN2y.Y+Ͽ#L-aLŗPƲ8(Lu#m~uZB6hCK@1}$tgFE+QuNHo⃯h#vK mWj>LXq`cXQaı}:"uSC"Z\[3+ctjNu#Dtmwrz$d_k!"be)qcD=TuuV ߐI]fA H8n`@̃d[MP!ҲdlJJwuA KRA\S͔_3p]߳a R=VWӹ $!#`aGENQ?&U"Hf!D./{twۻuh0) : &B%4zᔀ`dפ5h U-! Nn5ynDkc>]/Ξ[oM9ߔoq*Us3~\i۷0Hvd%uqf`?BdB2oeODi LK]'~G@!5=gmޖ=І~$jYtriYhx9)z7) xv'!%\v$[T#/λUMuHv]u!ȁqdzԊ}!lA}4FcyjUM 1hY:'z'jojwD|pw??ޙ8 yZdJ^ @6o3d&T`fRo"SU9C yR]y|zV!$-!>nuo9髪EcZmʤ֪NV, MGi;IMqh#E99ofwDtȽ=z áux|uVT"D(bM p!ay A3c))tZ&-{ h?} nWkR-J)x-*Eًo篽vءTϣ.;c !cG̿ZjD$r0[>vdho~7~c_OM޷Hֶ,5]0Yl7۳MݬuWTl2zTp+t\(Y,ǏygyTHBgɓ~hԢ>ݬ98_ދ |P!E.Bzh&JdF5xJA8ܔ 9nwvhe;90Adlk5u9[?zrݻ`szx l[] iԹ[347S&\q=qfqc,aok~50)Aahf:OVQ^5g7_?Ї?zajyw,MSV֫սfS7meYJ0C`nA& DALP;JS̬j]vȲ/LKUl*bEY[?5>mZUQhN)(hq a.pD p0Zġ~;s~wCoHA]_DץC#b$].nXw=|ahF?X\BNSE!DJ"w!ͽ?שQ5HVdwCh-A[K_'z ^Eb?7G.,VU*V`ݭ)e.Oѩ4rUsVԺhU: sow\,ti#]خ]훃@J_צjՔ1H0cyByfb=ӏ%HxGIh&0 rhU:GԮvDs0jcb8uxZ]@WΞ9v;PCffI).Oو9mc]J6Hgۧu'8K SQKPow?07nO76WZH]1T׫:R`VXL<d%-wI@F^ЊS7 :̈́jD@GZT޽ӕG} u[ubCnNÅ(Z|6HW]cТ56qDL[QhgE˺bU+L#xJVAk٬mcUU*_}_;&dLuqސ wkC$/'9P 3?2JNSt&X09-nlsrZG>y'׭!oXnfR̝u9'ZTU x>#rEI=L@L-w,F]2ml?m2j%8N2r.ge޻xTh-vz!"I`#ΥII'-AAjso-lpJt]]׮TjR,IBIBiZ,i bUEЫC{W3<78:<-Jq-7Iv7v`-GDFQ?7Esv[h3~K5*1͍􏯮~h-"}}sfU˦vT6+<,| }P Q R=bRSQ%LjK{J>5QjC˶~4$yt.vlF5?za`8XExcXJ!4bd])eOOܙ<$j)pGA뺫Ku6oя?ƙIc3"R/j_.sbPW0Ò3Y=L'ueJƄT"%q.0%EӠ0s%8E(pwRWxJœj'*daMJySLѠ*0 !qqe5F2bfڴm0So( хCԸ?h[/. *xj%vp5IWXX儀@ f2 W6K,"‰#Ewl6mGz:^xaB۵ 301NB D1)Ĝz'@]c͌VfA W@Q={z}q-'ޫ'TwN'r}vcaRJ1C1 ˆ.UbIRO( jPv(ح"0 z3.LGG::^'3p%C H;3w6DN`Pݴ- #ٴoH5zyuyt@'(= p1轞s{s.a/C* d跺a8|ṁ;Zڢ{Bkd*:P.YUsRІ`JR|rg8St7$Pfiu̦`j_{r<1.Ϯ>{vέI}ºH- pVI%?iFH:/mIGyLUJԗ>Thϥ^Aއv[+wq3dʪ 9p/ߐq f[#UUQ(mYxcje;v\brp!FC$\C'GGsAb+$[6M.Ds LkaZ*CWbOHٶEm>t3Abn˜ ݈0 kaU(%>E8כGgO?شƒL99;۟>ܾvէO%>=٬hKP$"h*2S׿xSuVvXawrnle YU3t?*r4W239l$h£pW#"IIXOG/~8U_(AԴ f L􏨗A5sU D6vg@'tbWQsb^,~8}ZTd6ꨖ F4tNRO*7Eb7j.Ur."UϭFTuJD3$5ԁDDu3@d5ERL'Vw\^k?x<Ξ=xfyrviKM”K&A"ϒTI /^o>%I1wQԉ8A j%l1%TWՊ :h~?{|T@Ř5[{@ DBJ9Z! t@V*1#qw BN(\?W#ABWE|㧫u,!yӜ_]=z|o{I *_5RUՉв3S- sc ԝ9KSs35kͶ}l2Nn߿{4=yw"rr۶mn7BhEe[꽈Ꮆ ;99ì R[202Ps@;߁Gٍ /ϟ?/n_T?}شzRuH.?⭻dz:%PpEf溮IbVDCPwΥbDu޶`ŧGuŵ8 fӚ!_-zL'URԩ=_}4|kV2OdVOTqD39@Gr2a,T0_y[ca|tlBW߉j)c :g`Mz9I{IˠJH@$Vo_{~׳;955GZP^^f{]pPסX1Φ|IՌс 1*#7}ގnϟ;}tq*|Z3ȶؼS>>zG-96mQdR?|Ͽz,An82LkNTUC5Rϋ[JUq>*0 j1V-L0[l GGPs=O0VӺkN%4>D"d*[>ڴxWv8ӻwNo^nu}1a6o/~"NdNXM 1BPԈI|4z^Mdœ[mY.[C2P}>{5}ݓ)%DkROR~ca07?kQ ~:3GC;0dR1IPyhqޒu 3i `p:!:DdޮVz%`fI#,炇-PSz{o7!#951kEexxԕ]oKO/ٔyl-xC/C:|"PJc>Jv1'OAX(ޮO%v5;xN 5:oODǽ0p~f/=>^L 0` u m[j([ҼMirtrLxdPPg!Ec2"$EK!rVWֻ:9:y`ԑtX'uS5˹{Q+YK)_G:k)Z jDqՋf23&gL(xȦLIMqp81ێ=~u1AGz&߇grq,oI d"MmYkdqrK9;G &F-hs堛k[y6gG*U)b]5O4k!  S^Zv]hz܉栮n J Cy.7>8އOYѝ хfcD=8>< qdiۍ&^=uCQ )F5Ai8GNݴ;С4MaAHЧ<ǔ(h^ID wY-;o#/Nfu]D^U$ ѷjpUQr| -L|J0JWF&փ#B۪n_.(W5U ¶ي+?_Ǐ/@y23+8#W"m[Q@A7Z>RG;@1~g,54`qpU`@1jFTQ. n-sRz# md펥3JZ'T,d7W3ԎNrPCXl\)IIT NgShuvPUG"R3 6:26mK2Rb!+N(}X8_-Isqss n=IxgϾ.V7s29&*!L+ w#eX#2x*=YH;}>25nrmlMf&!9ýdTg= wM? ps0~Qݚw{q0q\#A})_(;RUZ &+N@&N"UeBT%a^ͫ\JvC\9&1LnE)ImZo6k&5+CGmmHAv(9G)R/S37sj jjVZm j˟\\~9xV\2 l=$܅5) Ä>,`?nw`3uILd=]`?nꇒ`S&r.vZ /t^4+`u!/;{ΡB=`W?kBzϾu1*WF"%TjS&HIM1K5,^y.\lԕۘ];;e@$-^\9{'~@&qbn^ls@S$V-%\ H,qSoS/R3{aBc%"1a6TO&ڣNsGw #P74fZ))@w,h:a$v`z!"yAFWGFt T%KK[Niզ4miZռCRD3t_7mEYJiVij͊=*f tx# w4`s4#s/nEliËo[ny b;#PЎ<|VlXMzF )FW]ԁJnxA숮:ϷۭԱU(n8Y}zc:0MnGH  xˎ2<;AD^= fG9!IhPÖmcALمRrh.LhQ<eLRBHͦ-;i L["DjKV5p V=K$ZtEKlAf5(jg~QټdI5SῩ;8:R:&8LGs?1{g1c0X g0|3Yb^SO3yÜsٟ" 8A}8Gb-b&[)>1S`LL]NݴZrAFS#RT;Ԭ3KlL2UX͌Yڵ ۶%fYH8NSØGRHEb2ij[mJj͇.l9qV@5̀(5"WY$B ξ(tt5Xtgw73-!{ƣ:oT7փ=8!)>((|W?urO;w~nDnaZ€z{c[nETм@Dil.d1t5.f,Ofh.i#ܖ\ lD=u DT @ ypl;?oy(b z?%^bql;];h{g.n蟆}x4Ԧ֗ht6s: f6L,fdN{w=ٝ nrt." ;"9` ]:0}s~󭟼8hۈ.;S|''sCGjXkZ2?}b}l6M)d0ApdNEӳ竲<˼MsE~"וJdCfhޖb"14M6v`*Zpq+a{у7?\#)A6@JDKpk+"[ .8< 0 첒%7x9@}mA3i/LIrDD􄍯){ {sV7WENh;m'ƈB6aГXb\?ËfN4LiQxѡV@݋ + mQ"Մw߿wI]eWÑ&-MmQ(V@D&5K"#*v:VMQ JhWGs*FM69_o}z}MAɊ*bĻ~]:B@&f#P"{;;(tfIp`sOƨq~]e.bޖ< L[ `[4Y|<vAZŮ 7eo?{PUIs'2,m c¨E]5$?H*N x>関4y4W|ET8xLR2u! *8{ŔH mUр 7v_}Gwl`ks9䜇SJA1f.6MuDwH7st_twt>2Gv 0@q2鲬Dq4BQ3~CsoL@Į_%Z%~\~?, А[i#WsupEJ)TOlLuL a HKFD&t1QTʹثI=T/Fwੂ5PpUuvaٶ~)%Y&j ޙsQ&hHf3&&·Ixw9:!a~k<nDJ9g-Zg;ŀ- \Ǫ^B uV5ai 4vP ^e|?|[/fB@)́g\bߦY# S'sxv;$ yZO1 pJIESUE3B*5bb -ˆ˷>8;d0bfĥ\wC k|#T''puE'!l 37%}0>Ax\\#4CC0.| pM JNUxTgf ej^sGrU5t~yYw>]PᶐP)%ܛj=Xnv  M#DV3;1 sat.>dvHCꁛ xIEl6[2b.oy 7V;gGc) qi!w 8vGz-#?74F>=a"\rR,mft0f!DDb6EjV/J,j9Id,)LD:<, \#})13܂dܘ?شo}?iDj:;<ˡG@oY :POuB@͘ʭ{S5v*?wV?HÍFv>ett42r?/E =DHE)ϗ8 ^-14oMPa۶,̩> i ԉTլřNZPPXtB'v׎PZ,%;4sv. icH^@=|TA.EUͭnI۹l W?1'ۅztia,kv'uLz?nE[IpZ2#q}y|Vp!tw6tX$.Ԉ)&wc ٴ_?#y63p4ud3=̓p^mqs(Ma\%U/xJ]0",D5+#y; J}E5:mQ6@O6'o~\YI<{rP'; 1eqG4vҒ'u{<mV7"TߥvahMczUDã6>qx AU 1GRCL{K{q"7Dޗ :^15([F|b1 `vrtskV@s.х +]@׉6R D08RFe$csrTd*]){߻^6,fd($4v&kCI"xw Bˆ]5s4H8jb[N*7J4W6 }<4#FM@RYqDdK 3#fB0!ff{'{i ~w~ ) 3gG%WJi Lc M0S -R,lKY7nۦML' +A>ʹ 3z:mZx'O~)P2@-xx6HPlh:8:h%X:#b'Qnj2AuP!RH#I2~A1y7̱T*hErg?=̕g켗sΪ&RE7!0SEUX5KޡC<ڟ~zM @ j!"5PP;T̊!ִHU 8#(tj'(iVͯ?}_2o &jy}UZ YUwZxڊ pv-EX ;` BjzѴ}ރ^,vpBAt^zB*ab֌ܭIcGǦ"L"MNꚙb ed>8ڱhBЛAA=Z >F`sP^l ZVKU3"J,cXT28  =;ڟ'O/UW"YaLaRWRjӺSr/%9g>PK"BH>ziY.qfbs!{vv}tKt3Yas-̆|أg~+#$ Ekeq  $Bsl r87&L\4E4 R'qxaqcaJgڇ񰃁9" qv@~isbmڲ]ohl] СN|s#T5!!i6㏮-!H,Ee`s646E ")kLJ)’4gmi|)ݶٳ34p.;QHA!lA_~n0eB,-Av#wU@- 4я?3HRޛڣ)v$CPCƎИ: a$&47qA;xof)h}YxNϙ=a0;zvۮ7j[6Z6L#&$L:9G7&@EA;G9 ˕-47Sf!=FɤJ1"`||yvnvm*ڔfD%m{Z_ߢ SR@Ky fE?]=tqݖ ?w-F?u4J션{}-Pf?FDPJ+e30# *Ho8Ka2;a7*sG#滨/ $J ]`kl֖mS.כ5n('jR 庪1aj'o% :L~/+Qo5( bD0%r@BtUmJ!ȉ*-/Y^mּ5Tbr]/ 7/_u6^Z,TKilW/>\oKfmad`3ȡ4cb6 *ݹW@Hu>Z3$Fff!.EDXwI՞xݽl+*igwol~;ϱ?3|\slvb#74`\>a+_=xIձ6[XnuYۜݠRY5P^Tj][T7}w"jQ[;H4|lmRKDh *#)3`_/7W˫byt]쓲97eXjٽYub>:md!p'OηKmJSH1QywKf՘i/M|kK{$9=2$CRs- hH]v@X蘅4<4 !ɾ+3郹{xxD,Af*+;r1rJB*bd70*wK[؏e0.| *E˞1gнJ^#MG:L7Y3Ҍ=L DpHH"@HƖمKW5@ʩmSW@ Qe G.KfhW^]o1J ׬/O|L$YDo%~~w1zzQyg1%!ku,~)ݧD%,~yJgJԏc)sՒqXMzlVcfu>AAz;;J $琛 &+s 0Y@b$g_>@e8cegߜl``%9<1#1I08) ^ߝog/d㣠Sd-^gIGd99ݿo v!LKꊐ|qʢ0}U69lXhOsË_/z/\`D:Ζs9!ןnsb 4bďk;f X)d /L|7kʤ '*hKKkWN뺜:en8\h6oXOc. ;&fG7@]_hSt!3FL{9{}[p} xZUg{?)CG^r`0Qά^bS%)r.P_Ǚfp|&d +wOGL1dU30K$O9sW=ϳp|/~|v.;>f49Ngտ\g|P7H\a+G\ "Za8ݡ9ՁXĹ&>Heɻ7m }uT6bݩz؄e\H;T-+emױiY}T)-uLx_Q+TŰe`!pDTgvqY~}ǿ\ީ:Iݶ Ë?}@!!3 fa&m׿)__?9pJ)ND z8ws2Yx緟߼g)9bF,咺(\Mځ])K7fPZ4[?7s=:+ɽh(ݒj"my֡GA +ଉ{},PZ(2pQrZ*ғuNn"?k^M],O" )Ԍ8 X0`{P/ CD~+oSyzIWcvH70qIh*:'[HeD j)`) ef0T?}Yf;DX l?fAWNdlRìtN9)L!0b|A sKJ)e'MrwmoW9 +KD$!Zb!6I5VcW<̒sa߬T؋ V`wv92ٜ stcٳ0@DRkod  AAֱyVL űc6ΡbܽӷeUR /z9[D$$nF23kQHs"XV"nV#cU3 tʊz͑˛ָώo׬A :p"@ VW!5yg8˵.YMm%V[.|qm.'V{K'I{U 5\ĚAI7w[WGpQ:wU x3?=}%eWSdLL~WzOr#7@̖%夔  `A]͔)%=_J4i̸j&2'I` !UM#$TrOjzIDATq;GLb\ ?X'1 T=$@*CȃӨ j1hWU.qa_˅ޡ,x\rT 1*Rw*3B6ɠCcg) "_~HEbEs^s9 JZT%3 DQ`뜓! (b1R`7*ޖ7 ]X38}lk0HF(ԡ7412DE(L8tړCPT{y\TvD{&%.[es&gEok]j/9urw 5̹Չrx *Z &?&-_ A͙/ I9enC8\D8f9UӜUL*_LEnBOu݂g how|ר/<Gr[AΏm{]Y'^i{aqaB-=2/ |ubԆ{ F;bs}Om.2ޣeP[U5ܐ= ;S o_ˏuR{MgERđDHsI 4s //Vmer5 }DfvLjl%͝3C?fm3okV#g+X Gm{֥QUZ"߾Z\[[vL(n\ .&xvwI-f[8[Ll%߿w;aVd7'11%;tȦt<+ԬmL$^ǪF?I[Ui+,ˡaj!$ Ͻ6dcAL}'PS hokԸ{P7SCqBa@M[RU%cģ1Z3yEfBqK{DdT$e\w(<7~-/ NLYXc~k/m<ȉ)eC.‘c&Szqѳ"͉@PBf1a#5T0Y^u1R8+&ZQctJ_vިz-g{R. a9lUlA3^=>\on1~7 J}KFY+".xFw{#URs[N~;-j]{7]\Y~o΢&9+LF%R2]}&~z{K0c|䉇4[=]PWV o{q]D}ۻ+c f awd?~xmӗ&xmbd,+A}/d 3 V ݹ܊[A @#eHZPNHXY-Ô`HݥɈȺǧVDCٳ?OH0%&($!R:NY8JT-QzM9fY)KcI68Kg Ymb5*m6g'k$>xӠ?t=aԿKN98W8MtdXxXخQnd{۹WJiTkpl-7 ~Qs9BѨ gL3WW{rN/ޠT l!LA]NܑVd GyԩePH r^+ :El-JT㭶_ 1_60W j3QnڒGV%j(Hs<و T̷"M-w|xdAo-'Dݎ$zPh\~#gCVO/s{G 1`AB!p1M@הЩѸO-NUcbr Ԋ ;)nYrl\{XKО٥v ʼn[%nM X ۴4 L=oZp3sjڻoݶ9ySխ[]-]4!ހH`fe01@A,MR#J_y )i+,a\h% Ƴ =6 表 s> ma=["]bd3-\RV9v};$rR(H,AC8>: ce 6bX߿B{OI8ĸ-hp]eh8"U7wOf4Ͳ0+r {xN>&ebq`AXp+s|'ڦUus&F}D)Qh"8k8jMQvxׂލ#(w.I*X>FrbGdG}<2@.&&OF,}"3OG:/>ydL^X0P5z!4{k[ +4{XRL l`3Rª6p!&h- _{,Xl8yCvAI]sCL5NvJpJw+ $9Sb.n^c(:($syj t``G`0$L,dj6MǏ(7sb !ӤYKպ -RN\jb8DvV!N(gSPJK6gs<`E&-3!L$LOne^Q]/8r%!liϭ{kzhe}P7Y_Ȱߦ/̱{t-yb+ UD8Jhs8M1("ƔU18L~.Ì8 1FU ip{Z T'$lNG& ƹG/aU&- ~,( 3Gss\a4 `H 7]~ Z3w}FGuf!J+,0 cb<,i[1|?=U+˞4,c#w&LQpx:LLzGVh?$tty}4L7󊹻݋z̦5ҪhL~:o>Jڝmn.#QPR̻AT:ҹƬ"| !q`P t9LGF9J8/\k% =N3~h|[eD `OIs9=}8TUXý[]{H)B%9`7}m^ϸ؄,3߿jĴR#,'iz7oAfیCǝ6+T`-@T1ܫ D9p8C7&bۿiީQ q"O掽aN\k,THnN'b>_/>[9իIQ8o E PkF]GU`a$5b.ˍbI]r͚!YOܺ >.?t}mtmaW-g65g|h;w՞0yL 9kH ߉1%>>|Qi{fPߪ_+Vn7auNx2r"-hivWof<|:"́jѮ5#R?UmRaBv?@ uQRb?춇mGQ8NYʾjs?||c!&SJwKz~sa_"rR7!Hv{յ=S8 ;LPֵ"Siʐ_ Pozg_ v~}mp%nS leScʮANnl@lOO~ħ$A$+J !v.aNj\A\__U.zh}lֳJE^T5';C vV O6f*,!=f|j&-75a٨<ű#\ҬsD0cu۶,La o BɈY* lԞs.u}j-h>j@uo"#f$ūl-5i}Bs'8I8bTP'S$xzf-r*m4 1p:D<_JČ8TF c5qlj!_vuzv⃟1xxG|Ex ~o{H'@jz38jmzgĀ$e5d{2(mhnu{o޹!B.*Uo-=5zmX-pKֲ^ ;u[(M7RhFrnp2_0%iɳjGe"Ewh;`%K{ cm%&)4늲`8h#X,h~3,_g#ߪZ:όCӡ8mn Ia!U1NQ5 jl:eLdٲ)bqeaZ(Ggb=5 1g&e7YAsQӺ]Y*/fTH:`rF͚ލaQ fue0HBtR/MobʄL+^mOS=BXL閨d"&JAKٜF5IENDB`file-validators-3.0.0/spec/fixtures/cute.jpg0000755000175000017510000003041614124557274017703 0ustar rmb571rmb571JFIF*ICC_PROFILE lcmsmntrRGB XYZ )9acspAPPL-lcms desc^cprt\ wtpthbkpt|rXYZgXYZbXYZrTRC@gTRC@bTRC@descc2textFBXYZ -XYZ 3XYZ o8XYZ bXYZ $curvck ?Q4!)2;FQw]kpz|i}0C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222@@" wWH~[PVQ0T%"l՝ek9O&ty5cDٓ]DpguIǮ6b)rhKV& :IPQJЫn]yJŜ(ZƹLJv<޾[}ǎ[tI(ٙ3J琗L˫+חTMZظ$=2_Vg=/YyDVwK og=n>b]?||zXirg˫#ӛ@l\o!j 5w9W[s`l=*ZEa HUh:@5e`4Pa(Lmkg^B5BAƎңZ3LRU˨'Pˇm0s璧z uf7En;\ssĔECVn<YPԑm,@'j*c ǬvڝMvU. IDԔFsnKfvT le]RHnwefN Ҵ6ӭcT=ym$G\@:)yIb2z58Z=LLY8g#O1c`-P2dCbXI$nb"SzYElK)JUh:T]u]ImRt/rleVpxqWRfJ]WW~+&},H6,BXR^M+Oj*͙;uC5g!h~Rؔ1uqWZ%\ɾrҥ0Q'1ߊ67hHEwcڴ'5]IԝYilO)՜ N-eZeU!%d.C8 B5V j*ְ)m"TET$>2%}Ǥd|\ug:c!(#NXT?ÑWBƆ|WcN 6rj9"IJ2eJY,8襗VǜuL]vG j%"4tԜĠD`"/3mcZ[}S$6(pfTf#,2XPRvd B'Tӎc!u}iƈJsja<]AE5 *cF#.EGuظ!#X;?}#~MNM&"PmQ %^zR42"gUM՜VgԊv%LDubjNn QtUոc?6v"DRfJ2tdvUd9G5%j6%{F(Ԝ#b?Y*B8O{#p92I"Yb w6(` E$f"酂y,SC}v2nMnE >70jsm:hP;npgOzM'0ьgL#74 .Y$<1'(F,D{TL$j1ij֋+lE )(TUVVٓNhys **ʪ}!꼮U^v!:鼨4RyوBb`<qBU*feK++˥[Sžlo˦)i1wc*>:js*m> q}N~IWmN`Vɦ]9UTLt8mI}De[mJq*&!1A Qaq0?!Y!zCOD-!쇱D.?_^ @| %L5XȅEaGuCϨ'>!ͧU! Ax%e^T$',gVv|,|máԈ$ (x^Q>Pllc܃kRD|9U;'}aD9o=xb}BUjucZ@!DŽ, H爍=xV[B5Š&)ʖVǧ0zg B0lBiA)n})B!8E *aD嶃Q7CbhwOQ-$!|샥[45TLA99_xz1CaHػcGsXMֵdv\C)#յXex3Cl#FQhhh)vD?L_N%$OԪ~iIUEư[9fBRp5Or^ѥbѦ{"H5҂J㱌9/B/S&<84gQf^pǡ(u0ђ$kLm US6аӣ3_(&Fԧ6AZr7ɹa0D 1i~6(KHbDd5 MEbMr@p،7N$h.$B*aȮ%xѴ.Kc^).; XМ"kcw^5dڡTPT8@Vzl?a)_H5$4JbJfbDDuM24A,I##+O)8 $"L5ĸђmC*ѷ So!ٔL $~#I#ŕk˸Ã$lGC26K!*v %PK7 vE F%mh-<1)x:Bƒhvpi?pN:b4)XMS {gPM6O0 HY%DmE踓= : KC 6РTF-eI_F CF ,E#",HHqXz16Аl /bRNE-c! ^}' !M qFqg0ɰz$o '"B2h'ĊM 96A m2,pF1"V19)l9 2I,yD7(e.zhQ$z5  )#&1$dc?tɯ  A:+`g7d0̞DLHpxX&4C#ٱGBҁ\>dL4z>jB"J솄K$l$$@Gk*%(KA$1ڂB=D 7I$^' 9&-:t2B6A~xk(rI4Ď~HuՍȆ=qG)hydE&bβa#f,htAc !WhDPI'yd:a̖B)Hu/d- I*I_p9JZ5+JS`Zt&2LC#d{©D7j9 p"#4LkLXNHV,vA? {&d!{$O BlNE* ض+hvEd{/b$/! !IHxED6elNHL5b[Qsd!쬎i @ ct@`X*!L@I4(h7p+P3# L bB ŋW$EgO̯e@M[Xt L D*k;k!AVH lP"(FV$qH!UQbp$9aPِ@l  I1 A FU t6 G9 SV`;1RD( B("`P00cDQ *%1J/T \BvT%601#)U)&!1A Qa?>dEAeb}O.nEY3><%.ͫj:=#clemD^ 2ۖ׏8ؓW!HBY'}x +:\9lVM/.}퀃X~@_ 3͉8.-cMݬ4tohr<ߧ?d('尅k,JvxM]#lZaf} Kd ¶|d#m$_|K 3a6y,ظ>i]_anJ5p[rxlG Kx  c+mL̇dᲧybvݳݞߓ,Uou?!1A Qaq?^_. BJո󉙾oNdz6s׻2X[]Dul]lt/c`=t6~>Le<1h!3'c}u5e{& r< JKy}ũ훳x#_ء͌|yd=]`\ǛF] ~Nvغ6KI 勬aƘ2P-p# ៕X`}?`}WIS~D/ ,0lpmJZQ?+mai&' j1A'-x:N V tHl'-'!1AQaq ?N(> _*?(~"ؤ?N U|AZ`w=4=>W_$^~kGxo`vi]W?q蹡^KL;֠e.expFр+jñb#LF0qbS_@|Ŷ~EZ.5O |zc7 1Liϥ@$b  >)؀{h>-.0%`m4B(ؙ%F2Z)#D:u 2YsKE#PTΈ{Ф!tAOȚ#qĭ@rA)`@?Ua (⮡R+N OC,zA37 L1탣pD-n!ɘe8b}CHXhzAM'^yAǢM =5o+03 tYǮ:DsAPXiMS7)1s +^'`[!T難*zEChݪ"\2-P)Wj#H2Sc)a@);KٸDV*G[*cÚe5eJ.j8Z*j&c,+j Eg/h9Xp'L@{CeR߈_Lw-ϙM# ~b:㙩oq,PԬ 8> MMޖӘe˳Xg_I'9K-[Xe/K-Wنx]!<+zzӚM@+B ?j}=GAmLbq $7߃ ,+&tuq 8%I\R۫e{Sp-j2Ck FT#1I]-m(^57pY#oIfNfb%LHRW*^h[Q0>fa3Y6P Pv՝-[5c8*8BZ3,hGhvKAPJf KK#7eFF @vRc08xaÀ亖]Ym4[bCC5,x.R;X[OS0P\jQ"xpY3"3 1uY*j89:GGSOAB8&*:L'âSc8FAs,[@EInaRq۷M<@̮ %8LnglCp-G0fs Ņnd05D фJϴM8%yYqi6nb j!(cp =p3MipܥP!\7,EƧ\!{9_:MTYXrlB`.<ƫpnX P-mQ*\N:F]MTF\ Pb\'mnǠa @ PF@ "XJnlz%Gs2$!GDGDny2tM} EC$LTL0D/f:e7S8-V!{BYNf0Ry]Y&W]u ]a"Bl3WQT0̳ DʊS.T7Q12dCq{e5T8ZJS0qV&d)k,t%dHCH:EJqn"&55q>&L!b&0wd0Ԇ ˅n KuL-A \ëf9LRydE.UPDM=!Kw8Qo8HQфr˘-KQI]y=E2Vnf`(ܪ֥aTH'CRWN6CWnèjM=y%Yị XlوxxG4"'PgM'bSP.P-nv0xD0rq pbfn)k`(u`^q\L lTYP10۹a9:oħ[!z{D9#r§ɃĽYx z24J3SōGNl.>1>7B\jq*;=Whr8aRu%ńS1At6 wnTe,L,v'-~bVq*zKr=_?ģb2AZ! (J۩Xs3#nG`(uzJ?X/Zj Z+~KLwg Q&K=.SG:"t@J,rM3 ůf1:]@B[g ʱQn-:^֫6 #UÃvb\36~%WgԳL# R s[7. eWG (Ai8 ǟr _̺Ư)yǼz2ȼ&e7x. C/TDS= +urJ]Z@ Wgp)3(L.^qTUJ\!@E`qx"mBgrLZ)ss+^*&PXU;75 3,eSRS]iV7|LKqs$229K 9SsGeHWQ~ KU9hցҏJ@fROrLט euK:_h#8IrջHr3iu X+3-Q]lf(^'=OBUA]]xDoU(-/ė^#Q}WYt˧rVm6h>LB Eá+*օuOX Eh,`\Uni{& :Photoshop 3.08BIM720141010< 020259-0202ICC_PROFILE lcmsmntrRGB XYZ )9acspAPPL-lcms desc^cprt\ wtpthbkpt|rXYZgXYZbXYZrTRC@gTRC@bTRC@descc2textFBXYZ -XYZ 3XYZ o8XYZ bXYZ $curvck ?Q4!)2;FQw]kpz|i}0C  % !###&)&")"#"C """""""""""""""""""""""""""""""""""""""""""""""""""*   4ef7),v粚ؒuKGе mMH ;qFk948CAg-ϜHyMYϢUr*FvoIP#E %`$*x8 Qh:}n`ZvY_*({pyY]yr%@Ep&Sr^HnE,coQzg_#ɭ1ٯdp",, Chv*H=kKxօ*8qm*ڤ@̬"d!D8ݨxB}+@Tg@{27ʮeȚ*elE,Zf!]'s[z:ZA+l$`Y+QSK7TEE/gz…V$>n( ;b) X=H*z]ѵyꅌ--WjrkˎV|\YQeAEt:ރOḘ Iu)qk:3L|r\cK6iƟ< jF(Dt:tq.3+ <!LwdӁ>~4M iz3ON)}#Ɩ̬[aKcX]fXuNJ~,5@<#Mi\Wb 6G7>FT sz `-"ZJPFi7fRdɯsW,% T0+/Gc$V RsE4MH=khm%<èHe38wim0RaSE %LmTiL a"ޘp|kIr)`B綆o=Yaƪ/;8tV-jbkIRhX. 5)R״nTjVyJ@<DE,ES ʪlZIƒͳ?TgM=UjB ۞p Y G$P/X P`TcI6B Tr3w Gh"C =>iNKuMaEn;nn`kl@ʵ*u+-mUkv cU+ -_yGsi9Ifc<`P +5>-F[nmuMTHnb^2Cܺƴ:jQ4]atU`p/B2=UɒqgkE\M(H+VKq=MRy^-"B! |㰶xQL$;HY~6$GXEX3`v2Ԧw &My:@ oP5A*]jf, 07f~w[\)ahMo5x6V9ܙms Pr-ILa#%.s,ny|ү#zeKE%\(V6Pg!WsN][dl/6F!nqХ Yy,bK9jkLj*J-sU'[ FMQ;ȀY@L&՗)`(}TSP (MR|i|4epZaDoWh΋A;t2z ٶwh\ٕ g~a v_Ut ЪqyJL\!>lAJev\fba ْהȮDC G/!h $3kׁw>kʻ/Ҭ-l4zs ,\ں×.yy4NmGEiCR+\dl YtR >p V2`A*[RTOȴ$䃚GM6x4*UStRD**Td@vrE'XS}itV}5G! cLK6ZE| ƊbsL4ph$F4LqϞlMNj0C+&,cur³R\g/W~C;.Se9Xrw00BTq" @qyso<*ڗYԟأs8N#c6^%^&^MWЊQz9cacƔ2 ~bs ( Zuia] FD9ZVSn>m?(fy_~&Cg+;lËKCtlX٘9J^]P$%G{j1{T}~G}$ֲw89lO]i8OE [V|?ϏVeɻ,`>FD'ؕzrZ#]-ܤF:jQV.Ij{mժt_j(TظD_wH2RhJ|NNNڸ1r;u|zGUhwy; kx5BhԬ.5Rw1VbMv~9vVqou2*ǘ>8F2t~PJ [!v+r||8lJs[j*`ȱn)ժD4жw;Kp}QR,ĩLWm3zP(Z֨~WgVP8*kr&=hXY/%+.OJGTe+-ܝܗ1ss%YP\ c~P|ޡm5A5°?)qӔe?WEGĪV[pvi:>5Or5 \2( r?vVO\kg62{=@u C3aص>;yeE]H p-*mwC"*a\eTR.k _Cg<} >ٽv䀕Κڰ1ׅK*m (vefm&pa55 fX ]˼KdA5,J|?fnL&o,{^OOU_ 5?t)>Mcr9o8Cw:;aYc-==+Q {ET3OO;&OPXjQ[*N+_[;78Z}5g)eeueg+ .M%Z5,N0Y:VcʚZٍ &6\cw3έE,"۶`MG)mr(N-Yَn1$"WMQE~BF צߤhyP/Y֚;Ş)3>CTp &ߒ=5@h'yVx3[q{Dh J씐ZΨ`[Yػ^lj[ޠ3bh rbʠW?'ӔI|MѱI;*9Kl|WFsXZYDy"j`؏~2+fYøݔmS"ퟯpY}#k_MϑwI¥ߎy00jXL]82?m"7%3e0mJ@'[gu vyg)NSWݬqQR j"BN{U6*M2 2W tMUŠsZ&.Caf[N"kR&'im%ّ;U(Ĕ"hCL| UC`>W]eUǦm4Z_~XΔ/~6H] ; 6nqE鹨ӺAG>rۆ8JQ.A?3& !01A"2@QC?5 g5Y^?Cؽ 7cDeGʨnV/_%'! _ _=8ش5x5(q%J5+墆vưFJ$$,viKZ!ӶF==KiF (XOR,2(S'5^dJn^QFDҁKFǜqhD5Sb}W+VtIQ'oC!jƇ=eaĊ.~^*+ tjJqv.' {W/:ފ(((9xգj21E1tI.5O›Ck ,̱<2 ٮlYe%b^;=4d>foO)Gr.7ٰQX=14m6epf l}27>)ЦiR5zdVń /tPeR+b:Gi|=QH! Eatk;XChmGԉnYXcC/26],>1 Q(s ɬ#ln/f=KCxV+7l\fF-%B^lhI 15K?,Xnf+Iyc\mfQi>51^MƸ^j1,<Vwvc$;h|/9$Jv7 F_BT{41ʅ6K"1y?脭abEq)W ,l#=4Uc2ecOk1 Y}f|1d=ΰZSܨ]bƤU+#_e'B7 +/fȊ6#R{I.ƻ>ny|%HSo>ie4<#YXI5}#E Ep|9҂E:54/,DTIXQ^>+#N"&kXtoM x?/:fD!X̣^݊&7-yX(>mDK,zlZ\͏9vV(G+DEQBlѧHl,[/EM-i m2Bgb,tbHolXۊ(,MpcdWXC蔰ՕB}OՉudS~z5c(YH6!?1臢X?fj.ɏ?( !10A"23#@?$<^5W X@e%f8b5x|$EsDb}F,G#ş;(;D=bE)ʇ6;=CxoG7Cel'yߖ7^P'dU."/,o  vx'crIvJǼM)"}d[7n7ddYeEE>,+ :&a.]aX[/ BU¸'B^nϼ}z,؞,Eaaa Qaaeᗊ+ o/ #Dz/ɦ9Z w2Lj. 4QY|,""$5dU<&OEf3K_Ɗ$,?.6bCV%%˱&7BfoGG h2Xxx_Yd.'D)qt[%km+҉mQ[¶(Ք! Fyo7}ZjD#1=]ڛȉ՛w[ӐRTm#x)И2y,o4FσϝCi}v5q+ 'ByGɞXDM=Cؽŋ,b*'y{]7#) 4&1,mem ˲.g˒ZCdq#COOh]pYcbLE63b^͈H[clזF|X*H$z!5(,EpH{=!;cuXi|vPݐ\Flng_1ĪbeIaF$xb[ڰݞ>̾C"b(jqȡ?B}jR~d6Ɵ IR'±e%[QL"Pwc#*7YʿROPrt*%,M5WVh-E7F(z4t|hQ*<<Ĭ&%XcN)# ŗL+TXz2&!Y+c?C&6?hU;\"oe\ݴ|܊3/4Jcʓ纆R[TSzׄ'k] rD(T_pĂsڕ2q%**t_>l3sn7 9{ODl $O0)KE:p6 Uw~EVL迻&ݐB1膽3i&̩`;'0st4[& 0UR SΜA^φw Lypr0@ϐGGUվ\ '8-[F˶Q:@y)U!b\tߎ ~nƎXɁ5]R[(7EB(ZN~hpK.P$.u-|kDtEU|JʶBֆR (p^J C?Z!jDEXlcNrSU*:9 g\Wɬ*`h{U: Go`I*Tt  yx@)}͂EudXn* 1U#V  de`Z櫺/bhe2QdUеbgVQi4h/W.kLƓBP!D"9z /z-Zrb+eOK4laOSDQ ʊϯudS Yi떍qDY.__&x*.N+znMlcQ^@P3p .mFPuKtlZUA0ᲓsU1ETOt_ۙ/8XeD1&qnoQ[6UWj&nn:2*:'Y+sN믉˧$H#ePEZ*#xuSAaѰGB.`I=wukikQQ8! Q~Otʨ wRMkFqG.B7tYըUes%M5u5HGs8)8V'*cZ}Jvƒndz"W(9 !S 蛥缄(a\+苗1QWWtDm*1n*7*ӌu`hL"16-b*S @-:}P d7C9Ujc`ƨ{Fo_ "|9C W^bx< NN*#)u쉌De_\?Q99&!1AQaq?!Ō,#JfǍ CZ8<#bZbgqu7+Ȥ^hb:=;.~A EDq]!Zus2s0E֔XLf9va eDr$DŚ*lj>H&hL7)@ ,hQ QMfQf9ޥY{\JQF1FP|:&*3&e]dD3+q2cB.tȴ6A!d!k,[hk$Xa>W#_ zDk {CyM0FEq!hc1KlSلnܺw&҈SXlK8%>Y݇L74K G0Jn^[υ> b9AJ9EԭT!\xEw.zAwʜK\],:Gs.W3 }JV_ˏ*)ߔA C$ue%j' E^afLNe3)f^~I-I\px2bl- q, KZxSu"o=vИs @%`qq~/eLef*f DUBQ0@VXw٘^fSɐ[Y/PEYI'` 7;yE(["u/0NœDrRDvAFLKVf1Lp0h`FO(-ܠx`cF9[*.TX fZK!V@+iܣ,$0Rx&`ˊ6B`LqDe6>jB(#-;ZKCUe2nγ+ccb,b,)?[{#T<0kB#*V`\Vk)m5p(s{=χ\W5`n q. +رX!MΆd f ԯޝA# X4FQiCrǹzPJp3s O"bY*ܺ},_@Qp< K$3؂/)bcIV1%Y*L'{7\׶5-u5cc˦{RHs8.vrU\K]j"Y䇨/VˡmbfK&1@kyfG.o:L,+4)eĮߎ2,ȋ KQQAVfEͤPJT&Knw/`UG`R?F>wMg}Af lFw5BrC_Pʸ} "`;r0fcU&DJ̬NE/ ǘzʙQ/'ĩM(O/0I]t\%h^`U-ɖ6;!ӂqH@.Mf<,5*)_"`CoӉJ_+g;&msfiS|KA RҷGDZN؟t@J],΃* `a"%%b*՛K)/@^ a ,mbY`V0˒衂+.=̹Bs#q fC0/XFdW%f &2c0{KR2Ԛ9HVZw+Z@ħHA_*o ,&6N'#5&"c< 0^c~~br#\=E%,OEql`GBd8>cơʸ,>KPLSVMţrz\0#6' bF31pPYȔ"ՏĶau BԻ$Q%ZĀ ;Elu:(Ħ҇U`Y^}L%sAXvaw;&s@E`J fڄ(&p*6S$L!)Xg>,"G j~Yjc-8cRD( !T*RMsM}"YM@YL}R+,ld2s%1<ډeIJB_}VlvS[  C2dܴb`h=YIqZ LjΧ=G!̶@IC2=oUH|k@j Xc +]fEPSo,lowpwl^ -R%2L1-PDrebeJ֣2x<4nSpu ucc,.Z V.BF|͗ +LD~p%tG3wN'p]RB^:/B8K 3}Q1f3~YoFPeSj1 NeHˎ&fb6fc;jÔ)oiϸ *2M8/"^ QUniVPtf &k Yh߿K(WN nB4pS)CLT)EfڥzX=g&Ԡ t9qnZ%R*bLܢa8Qn*N*&VKd5K|3|̄[IUڀg.Խ/JE1+zb$^YqlQi߉xVDfY,D@ 7L6uYx&^KY !{? *kmAKxEC96K. q$C*FNNґ[Q0Mhb Oc6 뭇{<-9T(]Y]TEt,;$?~`K!?}}axmdӼ5FQ}43`q l? V>RFwLd$k4b|Q'v2|Gj'9̝]/e_ wkpN{5KcHvOg^2e4 }"bF$ ͋ r[No M}mV}"l#O`Mf4Y-`jgJ3唗+bYf;4neN>SFR$= sաF|i:{&aA;ZE:i~ qdDqm-X;[i2(5vǜ:бc?9XS6_-J y֝nZGL^1cM1(ȱ0dZ+vA%-|Z պ[\C jNw_/x.[Z;1㋞2G1!|`E':ĢDS~ 2]Hs=f! !1A0Q@qa?%-A,ij! Z:_Iyb\?tr'X5WMc&zÐX>}d;Y٤bD |qK-ԱP5/l~\RЃ4>J`xZ*.mQ=S,6ͅ- ҤTVeܸjGغ&7C ^ฟA莋>0ȨJ kpF5*\7n [=+Zf ;:пp_SW;致4cf*66"mCBXVw>J(J-U`7̱o}hZR)̅K%Jbg2ccR:h|;ލF4cHM;X zn:?f*òM0fVQҳ+).bNK j)PZ*Y;͈lH/pPrW4%,,j/Sclw5#zCBFR(PPYL[p&A yp{#~ EB9Is!gPSQQi4 JmE\@}=K7#a; xbj˥oC 6bP-$ek0\c9[3?,C(!1 A0Qaq@?f7ŖQ&xAD^-'c?qZİ|8‹$u,ٶs^n+0b䍴|٫˃wa@/䘸>-Buu0aP~Auㅌ_%q˕XӲB I ."ɝ>Dά װ/"1%47Gu\hE: k0fj ªc6+L`I> l_`d0B.njՐx u}NE17z'vXao'y>bzk"JêVG. Q LNo.M_#H%pԂl? |b2xp2X-‹ k|x_QcD{ٶcaX՛xo ㅡ}BѢx >z!l_?j AAPG#apF\آuN0D&hAI M C?C}sxC yHhSF2.<{#%"Cz…Jr-Z>cGI%u=7Ԏ}87obc~ bpTvhD'Q[TA+,f^ fb1G49 hbۢXUȞ!|ul{DB7?7K4_MM4Еm8"1R ̐Y>A":$Ŋ_&!1AQaq?4Ш !Ә(4&!\u`eIrw ]mH(L]\BKgms,|.)Kb`Rʍͯ3@q )IWQU6JBDh*)+0}D]D2/j\ "TܒeV*eԥ,`lҌqawQ W^e)6TItCzԵ3-F7y `;;z%SlW| U0XOuQ 73r«oCQFHmԮAl q.S"" lY>ftyw)Vb6b[K^`!AwE.pD,3T_(S3岮qYYA`ۧ#jq,:z%Xq0 5IS$2k6{h/;+'"2&0cf`D;yxDL,z֢,!u:Mi"iyܠ,`-S ((-T>j5 H֨-TacDxT,PwSM5 URJ^RԻ*]1aW@+UBҧ kpT&Dw٘p,-&? pDM[ `ۍ.]25qu9`%R(8gQs yB"!V3ӉfFY&;$DT[QVr- Hڷ/XHxfIܪ0SE["- p9, &" .%ҍƾcs䱅[=nUQȕ8)3 0cdm%f ų;Į)h1 tƨ,XqrGf;`Y:n]b2^XԪCl &e%7AB U/ Qs/qZ<%&2"UX,%W/.*0J 0[P̴ctx`C5 JiC{" UA\)TQ0]gL!Y*c3 ] ac>g%DE;(p g#ja] UJ3ƒt 6 Y:E p Q::[x^&DR.:`1Zx\L-̶r隂0Q)iYޠ8!ŀDB@8tL M}ڰ1Y"td 6Da++)E }YXCue;Z:YlA< CW6{e ,e{_0P(9ԻE?#zcS9vV* LE$eP$BF3򗆾v?WGP5W, 5ui|ID0\KyEU\4%S V /K _Fh)4|XK.JZRžٸs1PQƁxNU 8`bO+m1([*8u {~ʷb 4{R lcӪAq; Zs9 bҺw2< ԷK OW3B9Tܱ`\Wo*Xa1 |@,pmQʋe6~e81 P+ș sdu9?s|E 7S0 M"_T #[5g <+"ơ(X_AՑVa*hԩ2:Yj'PTZ`1 \Ø@% f5Q` Ċ-28$&8W sԸSljiaü5.0|/F/i1uÜuDUPzuZuy%E.G9V^5QKDkLSG-c2)ؿ$:%,sp0`,W9%18IVƽAlTQaDAK_r; % xpjBƢVܲ<5߸uQB*[lϘ@ÎgQ}ŚfbՑE jr^" 0o5FżAP?AAS0ٞ{K4u- .TLOTIL²T`V ӏs+/naze]) ı?a_Z eim#H@2:l%W]3C1Xy2?KьbL{DU:(lP+S@?VfkCpz#3zf%6x?,^V+ KЈ/:aۈ%|KYr^q }rְ+ce/>ⴂ Dawc"3+3zоb`ܴ"Y75o`yR',~dX"h ,y.4Pu/ոhZl8ixo,3)T47XwRE`H6XPȕkQ)]$aKTUǏʋĘz`48^bUgcLSX  Ƈ%yXyr߃G "%PQ8><cxG&6ԱdT8ψbV˖<0%`Gy5w u0;DŴec n;~ Ʌp **%Ȭ@a*g!&f16Bn#}0U2-FWMJ Fв4֦n_dZŇS :f}MD{|BrϮXX9g톙TTDW0"6VשM2T[\5ԦDaR-f36iD4U\DWe 8l^@xlK׈^^_\۹BV&F#T` N!TYPͫv~D6L bS: ͔FM/3;%JRaBP"ıI4Af;q&!y|ʕ,cJ-ǟ[| ]ߙnT Ɵql2^F`/4#>5,DV"6d fcj V ?B =Ĺzln +bJ-y-*~s/F_l=h;HPu(8#4̡r]g7صgx`/fA J\Ň̯ڊl^s*ڸ+W`(OZ j!h į"a\*sK^TUyV@&)nq@?p3@==pZf5]ceaj'^4i:2τqPGP`@=ohfu`CdFO$1+!~H~ 2UQ*`sR */XLV_A/Pr X8/Wi9| :Y$U \*> { rʷWJzUpơ+/g/j`-O qr@,J%SM^QtX )Jc-n&M<3.DQ(}KDiĥ#@o'5 ,žqy+rՎ(9F zE*Vx`~]7Km3FH`x( WP8j&1p+y?1 ag1I7#KUR-,!n/o1{cX :W <dQ~hbv`EԽ+c]@j_P$ U pZ]a7`?}iBoL\R$00e9LL9ZV4G߉Vg8t.qm@Ue*QFpcBLCN_O*P3^s[W1+L:sܻ{%}YS:j-QTT[J 3j6";秄l6ô^}^G\ wtT=5F3qeZ'eQ@hTJ(<@ H-GR"vQWE*u`,P57vK;@ j-ن"T01d2ZR Ky., t`_L@oc4ͫ !e-oTrWjn qrxr^"ǜ|s!Jɚ5_LҴB EXM7Zi+5Z }@:! TҏlPXpD5sk]l>G M RZ$l8x{QwR"˒($(  E9mK _9Ӂ1Su,)R*.j5ew1Ghsşpv=sWs"am9[oox GDU&JIɨn"&" U[,Mr[_BYvB(#LcA?AcbcR,AC]F⥠$b]KS]file-validators-3.0.0/spec/fixtures/sample.txt0000755000175000017510000000001514124557274020253 0ustar rmb571rmb571a sample textfile-validators-3.0.0/spec/fixtures/spoofed.jpg0000755000175000017510000000001514124557274020372 0ustar rmb571rmb571a sample textfile-validators-3.0.0/spec/integration/0000755000175000017510000000000014124557274016704 5ustar rmb571rmb571file-validators-3.0.0/spec/integration/combined_validators_integration_spec.rb0000644000175000017510000000536514124557274026667 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' require 'rack/test/uploaded_file' describe 'Combined File Validators integration with ActiveModel' do class Person include ActiveModel::Validations attr_accessor :avatar end before :all do @cute_path = File.join(File.dirname(__FILE__), '../fixtures/cute.jpg') @chubby_bubble_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_bubble.jpg') @chubby_cute_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_cute.png') @sample_text_path = File.join(File.dirname(__FILE__), '../fixtures/sample.txt') end context 'without helpers' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { less_than: 20.kilobytes }, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } it { is_expected.to be_valid } end context 'with a disallowed type' do it 'invalidates jpeg image file having size bigger than the allowed size' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path, 'image/jpeg') expect(subject).not_to be_valid end it 'invalidates png image file' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') expect(subject).not_to be_valid end it 'invalidates text file' do subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') expect(subject).not_to be_valid end end end context 'with helpers' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates_file_size :avatar, less_than: 20.kilobytes validates_file_content_type :avatar, allow: 'image/jpeg' end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } it { is_expected.to be_valid } end context 'with a disallowed type' do it 'invalidates jpeg image file having size bigger than the allowed size' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path, 'image/jpeg') expect(subject).not_to be_valid end it 'invalidates png image file' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') expect(subject).not_to be_valid end it 'invalidates text file' do subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') expect(subject).not_to be_valid end end end end file-validators-3.0.0/spec/integration/file_content_type_validation_integration_spec.rb0000644000175000017510000003356414124557274030605 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' require 'rack/test/uploaded_file' describe 'File Content Type integration with ActiveModel' do class Person include ActiveModel::Validations attr_accessor :avatar end before :all do @cute_path = File.join(File.dirname(__FILE__), '../fixtures/cute.jpg') @chubby_bubble_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_bubble.jpg') @chubby_cute_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_cute.png') @sample_text_path = File.join(File.dirname(__FILE__), '../fixtures/sample.txt') @spoofed_file_path = File.join(File.dirname(__FILE__), '../fixtures/spoofed.jpg') end context ':allow option' do context 'a string' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } it { is_expected.to be_valid } end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') } it { is_expected.not_to be_valid } end end context 'as a regex' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: /^image\/.*/, mode: :strict } end end subject { Person.new } context 'with an allowed types' do it 'validates jpeg image file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end it 'validates png image file' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') expect(subject).to be_valid end end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') } it { is_expected.not_to be_valid } end end context 'as a list' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: ['image/jpeg', 'text/plain'], mode: :strict } end end subject { Person.new } context 'with allowed types' do it 'validates jpeg' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end it 'validates text file' do subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') expect(subject).to be_valid end end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') } it { is_expected.not_to be_valid } end end context 'as a proc' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: ->(_record) { ['image/jpeg', 'text/plain'] }, mode: :strict } end end subject { Person.new } context 'with allowed types' do it 'validates jpeg' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end it 'validates text file' do subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') expect(subject).to be_valid end end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') } it { is_expected.not_to be_valid } end end end context ':exclude option' do context 'a string' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { exclude: 'image/jpeg', mode: :strict } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') } it { is_expected.to be_valid } end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } it { is_expected.not_to be_valid } end end context 'as a regex' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { exclude: /^image\/.*/, mode: :strict } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') } it { is_expected.to be_valid } end context 'with a disallowed types' do it 'invalidates jpeg image file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).not_to be_valid end it 'invalidates png image file' do subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') expect(subject).not_to be_valid end end end context 'as a list' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { exclude: ['image/jpeg', 'text/plain'], mode: :strict } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') } it { is_expected.to be_valid } end context 'with a disallowed types' do it 'invalidates jpeg' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).not_to be_valid end it 'invalidates text file' do subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') expect(subject).not_to be_valid end end end context 'as a proc' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { exclude: ->(_record) { /^image\/.*/ }, mode: :strict } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') } it { is_expected.to be_valid } end context 'with a disallowed types' do it 'invalidates jpeg image file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).not_to be_valid end end end end context ':allow and :exclude combined' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: 'image/png', mode: :strict } end end subject { Person.new } context 'with an allowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') } it { is_expected.to be_valid } end context 'with a disallowed type' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path, 'image/png') } it { is_expected.not_to be_valid } end end context ':tool option' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg', tool: :marcel } end end subject { Person.new } context 'with valid file' do it 'validates the file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end end context 'with spoofed file' do it 'invalidates the file' do subject.avatar = Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') expect(subject).not_to be_valid end end end context ':mode option' do context 'strict mode' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :strict } end end subject { Person.new } context 'with valid file' do it 'validates the file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end end context 'with spoofed file' do it 'invalidates the file' do subject.avatar = Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') expect(subject).not_to be_valid end end end context 'relaxed mode' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :relaxed } end end subject { Person.new } context 'with valid file' do it 'validates the file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end end context 'with spoofed file' do it 'validates the file' do subject.avatar = Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') expect(subject).to be_valid end end end context 'default mode' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'with valid file' do it 'validates the file' do subject.avatar = Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') expect(subject).to be_valid end end context 'with spoofed file' do it 'invalidates the file' do subject.avatar = Rack::Test::UploadedFile.new(@spoofed_file_path, 'image/jpeg') expect(subject).to be_valid end end end end context 'image data as json string' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'for invalid content type' do before do subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":13150}' end it { is_expected.not_to be_valid } end context 'for valid content type' do before do subject.avatar = '{"filename":"img140910_88338.jpg","content_type":"image/jpeg","size":13150}' end it { is_expected.to be_valid } end context 'empty json string' do before { subject.avatar = '{}' } it { is_expected.to be_valid } end context 'empty string' do before { subject.avatar = '' } it { is_expected.to be_valid } end context 'invalid json string' do before { subject.avatar = '{filename":"img140910_88338.jpg","content_type":"image/jpeg","size":13150}' } it { is_expected.not_to be_valid } end end context 'image data as hash' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'for invalid content type' do before do subject.avatar = { 'filename' => 'img140910_88338.GIF', 'content_type' => 'image/gif', 'size' => 13_150 } end it { is_expected.not_to be_valid } end context 'for valid content type' do before do subject.avatar = { 'filename' => 'img140910_88338.jpg', 'content_type' => 'image/jpeg', 'size' => 13_150 } end it { is_expected.to be_valid } end context 'empty hash' do before { subject.avatar = {} } it { is_expected.to be_valid } end end context 'image data as array' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_content_type: { allow: 'image/jpeg' } end end subject { Person.new } context 'for one invalid content type' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain'), Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') ] end it { is_expected.not_to be_valid } end context 'for two invalid content types' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain'), Rack::Test::UploadedFile.new(@sample_text_path, 'text/plain') ] end it 'is invalid and adds just one error' do expect(subject).not_to be_valid expect(subject.errors.count).to eq 1 end end context 'for valid content type' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg'), Rack::Test::UploadedFile.new(@cute_path, 'image/jpeg') ] end it { is_expected.to be_valid } end context 'empty array' do before { subject.avatar = [] } it { is_expected.to be_valid } end end end file-validators-3.0.0/spec/integration/file_size_validator_integration_spec.rb0000644000175000017510000002423314124557274026670 0ustar rmb571rmb571# frozen_string_literal: true require 'spec_helper' require 'rack/test/uploaded_file' describe 'File Size Validator integration with ActiveModel' do class Person include ActiveModel::Validations attr_accessor :avatar end before :all do @cute_path = File.join(File.dirname(__FILE__), '../fixtures/cute.jpg') @chubby_bubble_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_bubble.jpg') @chubby_cute_path = File.join(File.dirname(__FILE__), '../fixtures/chubby_cute.png') end context ':in option' do context 'as a range' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { in: 20.kilobytes..40.kilobytes } end end subject { Person.new } context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path) } it { is_expected.not_to be_valid } end context 'when file size within range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end context 'as a proc' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { in: ->(_record) { 20.kilobytes..40.kilobytes } } end end subject { Person.new } context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path) } it { is_expected.not_to be_valid } end context 'when file size within range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end end context ':greater_than and :less_than option' do context 'as numbers' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: 20.kilobytes, less_than: 40.kilobytes } end end subject { Person.new } context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path) } it { is_expected.not_to be_valid } end context 'when file size within range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end context 'as procs' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: ->(_record) { 20.kilobytes }, less_than: ->(_record) { 40.kilobytes } } end end subject { Person.new } context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size is out of range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_cute_path) } it { is_expected.not_to be_valid } end context 'when file size within range' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end end context ':less_than_or_equal_to option' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { less_than_or_equal_to: 20.kilobytes } end end subject { Person.new } context 'when file size is greater than the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.to be_valid } end end context ':greater_than_or_equal_to option' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than_or_equal_to: 20.kilobytes } end end subject { Person.new } context 'when file size is less than the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end context ':less_than option' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { less_than: 20.kilobytes } end end subject { Person.new } context 'when file size is greater than the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.to be_valid } end end context ':greater_than option' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: 20.kilobytes } end end subject { Person.new } context 'when file size is less than the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@cute_path) } it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before { subject.avatar = Rack::Test::UploadedFile.new(@chubby_bubble_path) } it { is_expected.to be_valid } end end context 'image data as json string' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: 20.kilobytes } end end subject { Person.new } context 'when file size is less than the specified size' do before do subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":13150}' end it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before do subject.avatar = '{"filename":"img140910_88338.GIF","content_type":"image/gif","size":33150}' end it { is_expected.to be_valid } end context 'empty json string' do before { subject.avatar = '{}' } it { is_expected.to be_valid } end context 'empty string' do before { subject.avatar = '' } it { is_expected.to be_valid } end context 'invalid json string' do before { subject.avatar = '{filename":"img140910_88338.GIF","content_type":"image/gif","size":33150}' } it { is_expected.not_to be_valid } end end context 'image data as hash' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: 20.kilobytes } end end subject { Person.new } context 'when file size is less than the specified size' do before do subject.avatar = { 'filename' => 'img140910_88338.GIF', 'content_type' => 'image/gif', 'size' => 13_150 } end it { is_expected.not_to be_valid } end context 'when file size within the specified size' do before do subject.avatar = { 'filename' => 'img140910_88338.GIF', 'content_type' => 'image/gif', 'size' => 33_150 } end it { is_expected.to be_valid } end context 'empty hash' do before { subject.avatar = {} } it { is_expected.to be_valid } end end context 'image data as array' do before :all do Person.class_eval do Person.reset_callbacks(:validate) validates :avatar, file_size: { greater_than: 20.kilobytes } end end subject { Person.new } context 'when size of one file is less than the specified size' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@cute_path), Rack::Test::UploadedFile.new(@chubby_bubble_path) ] end it { is_expected.not_to be_valid } end context 'when size of all files is within the specified size' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@cute_path), Rack::Test::UploadedFile.new(@cute_path) ] end it 'is invalid and adds just one error' do expect(subject).not_to be_valid expect(subject.errors.count).to eq 1 end end context 'when size of all files is less than the specified size' do before do subject.avatar = [ Rack::Test::UploadedFile.new(@chubby_bubble_path), Rack::Test::UploadedFile.new(@chubby_bubble_path) ] end it { is_expected.to be_valid } end context 'one file' do context 'when file size is out of range' do before { subject.avatar = [Rack::Test::UploadedFile.new(@cute_path)] } it { is_expected.not_to be_valid } end context 'when file size within range' do before { subject.avatar = [Rack::Test::UploadedFile.new(@chubby_bubble_path)] } it { is_expected.to be_valid } end end context 'empty array' do before { subject.avatar = [] } it { is_expected.to be_valid } end end end file-validators-3.0.0/.rubocop.yml0000644000175000017510000000064514124557274015706 0ustar rmb571rmb571Bundler/OrderedGems: Enabled: false Style/Documentation: Enabled: false Style/MissingRespondToMissing: Enabled: false Style/CaseEquality: Enabled: false Style/GuardClause: Enabled: false Style/RegexpLiteral: Enabled: false Style/Next: Enabled: false Metrics/LineLength: Max: 110 Metrics/ModuleLength: Enabled: false Metrics/BlockLength: Enabled: false Metrics/MethodLength: Enabled: false file-validators-3.0.0/lib/0000755000175000017510000000000014124557274014175 5ustar rmb571rmb571file-validators-3.0.0/lib/file_validators/0000755000175000017510000000000014124557274017344 5ustar rmb571rmb571file-validators-3.0.0/lib/file_validators/validators/0000755000175000017510000000000014124557274021514 5ustar rmb571rmb571file-validators-3.0.0/lib/file_validators/validators/file_content_type_validator.rb0000644000175000017510000001136014124557274027621 0ustar rmb571rmb571# frozen_string_literal: true module ActiveModel module Validations class FileContentTypeValidator < ActiveModel::EachValidator CHECKS = %i[allow exclude].freeze SUPPORTED_MODES = { relaxed: :mime_types, strict: :file }.freeze def self.helper_method_name :validates_file_content_type end def validate_each(record, attribute, value) begin values = parse_values(value) rescue JSON::ParserError record.errors.add attribute, :invalid return end return if values.empty? mode = option_value(record, :mode) tool = option_value(record, :tool) || SUPPORTED_MODES[mode] allowed_types = option_content_types(record, :allow) forbidden_types = option_content_types(record, :exclude) values.each do |val| content_type = get_content_type(val, tool) validate_whitelist(record, attribute, content_type, allowed_types) validate_blacklist(record, attribute, content_type, forbidden_types) end end def check_validity! unless (CHECKS & options.keys).present? raise ArgumentError, 'You must at least pass in :allow or :exclude option' end options.slice(*CHECKS).each do |option, value| unless value.is_a?(String) || value.is_a?(Array) || value.is_a?(Regexp) || value.is_a?(Proc) raise ArgumentError, ":#{option} must be a string, an array, a regex or a proc" end end end private def parse_values(value) return [] unless value.present? value = JSON.parse(value) if value.is_a?(String) Array.wrap(value).reject(&:blank?) end def get_content_type(value, tool) if tool.present? FileValidators::MimeTypeAnalyzer.new(tool).call(value) else value = OpenStruct.new(value) if value.is_a?(Hash) value.content_type end end def option_content_types(record, key) [option_value(record, key)].flatten.compact end def option_value(record, key) options[key].is_a?(Proc) ? options[key].call(record) : options[key] end def validate_whitelist(record, attribute, content_type, allowed_types) if allowed_types.present? && allowed_types.none? { |type| type === content_type } mark_invalid record, attribute, :allowed_file_content_types, allowed_types end end def validate_blacklist(record, attribute, content_type, forbidden_types) if forbidden_types.any? { |type| type === content_type } mark_invalid record, attribute, :excluded_file_content_types, forbidden_types end end def mark_invalid(record, attribute, error, option_types) error_options = options.merge(types: option_types.join(', ')) unless record.errors.added?(attribute, error, error_options) record.errors.add attribute, error, **error_options end end end module HelperMethods # Places ActiveModel validations on the content type of the file # assigned. The possible options are: # * +allow+: Allowed content types. Can be a single content type # or an array. Each type can be a String or a Regexp. It can also # be a proc/lambda. It should be noted that Internet Explorer uploads # files with content_types that you may not expect. For example, # JPEG images are given image/pjpeg and PNGs are image/x-png, so keep # that in mind when determining how you match. # Allows all by default. # * +exclude+: Forbidden content types. # * +message+: The message to display when the uploaded file has an invalid # content type. # * +mode+: :strict or :relaxed. # :strict mode validates the content type based on the actual contents # of the files. Thus it can detect media type spoofing. # :relaxed validates the content type based on the file name using # the mime-types gem. It's only for sanity check. # If mode is not set then it uses form supplied content type. # * +tool+: :file, :fastimage, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime # You can choose a different built-in MIME type analyzer # By default supplied content type is used to determine the MIME type # This option have precedence over mode option # * +if+: A lambda or name of an instance method. Validation will only # be run is this lambda or method returns true. # * +unless+: Same as +if+ but validates if lambda or method returns false. def validates_file_content_type(*attr_names) validates_with FileContentTypeValidator, _merge_attributes(attr_names) end end end end file-validators-3.0.0/lib/file_validators/validators/file_size_validator.rb0000644000175000017510000001114714124557274026063 0ustar rmb571rmb571# frozen_string_literal: true module ActiveModel module Validations class FileSizeValidator < ActiveModel::EachValidator CHECKS = { in: :===, less_than: :<, less_than_or_equal_to: :<=, greater_than: :>, greater_than_or_equal_to: :>= }.freeze def self.helper_method_name :validates_file_size end def validate_each(record, attribute, value) begin values = parse_values(value) rescue JSON::ParserError record.errors.add attribute, :invalid return end return if values.empty? options.slice(*CHECKS.keys).each do |option, option_value| check_errors(record, attribute, values, option, option_value) end end def check_validity! unless (CHECKS.keys & options.keys).present? raise ArgumentError, 'You must at least pass in one of these options' \ ' - :in, :less_than, :less_than_or_equal_to,' \ ' :greater_than and :greater_than_or_equal_to' end check_options(Numeric, options.slice(*(CHECKS.keys - [:in]))) check_options(Range, options.slice(:in)) end private def parse_values(value) return [] unless value.present? value = JSON.parse(value) if value.is_a?(String) return [] unless value.present? value = OpenStruct.new(value) if value.is_a?(Hash) Array.wrap(value).reject(&:blank?) end def check_options(klass, options) options.each do |option, value| unless value.is_a?(klass) || value.is_a?(Proc) raise ArgumentError, ":#{option} must be a #{klass.name.to_s.downcase} or a proc" end end end def check_errors(record, attribute, values, option, option_value) option_value = option_value.call(record) if option_value.is_a?(Proc) has_invalid_size = values.any? { |v| !valid_size?(value_byte_size(v), option, option_value) } if has_invalid_size record.errors.add( attribute, "file_size_is_#{option}".to_sym, **filtered_options(values).merge!(detect_error_options(option_value)) ) end end def value_byte_size(value) if value.respond_to?(:byte_size) value.byte_size else value.size end end def valid_size?(size, option, option_value) return false if size.nil? if option_value.is_a?(Range) option_value.send(CHECKS[option], size) else size.send(CHECKS[option], option_value) end end def filtered_options(value) filtered = options.except(*CHECKS.keys) filtered[:value] = value filtered end def detect_error_options(option_value) if option_value.is_a?(Range) { min: human_size(option_value.min), max: human_size(option_value.max) } else { count: human_size(option_value) } end end def human_size(size) if defined?(ActiveSupport::NumberHelper) # Rails 4.0+ ActiveSupport::NumberHelper.number_to_human_size(size) else storage_units_format = I18n.translate( :'number.human.storage_units.format', locale: options[:locale], raise: true ) unit = I18n.translate( :'number.human.storage_units.units.byte', locale: options[:locale], count: size.to_i, raise: true ) storage_units_format.gsub(/%n/, size.to_i.to_s).gsub(/%u/, unit).html_safe end end end module HelperMethods # Places ActiveModel validations on the size of the file assigned. The # possible options are: # * +in+: a Range of bytes (i.e. +1..1.megabyte+), # * +less_than_or_equal_to+: equivalent to :in => 0..options[:less_than_or_equal_to] # * +greater_than_or_equal_to+: equivalent to :in => options[:greater_than_or_equal_to]..Infinity # * +less_than+: less than a number in bytes # * +greater_than+: greater than a number in bytes # * +message+: error message to display, use :min and :max as replacements # * +if+: A lambda or name of an instance method. Validation will only # be run if this lambda or method returns true. # * +unless+: Same as +if+ but validates if lambda or method returns false. def validates_file_size(*attr_names) validates_with FileSizeValidator, _merge_attributes(attr_names) end end end end file-validators-3.0.0/lib/file_validators/version.rb0000644000175000017510000000011514124557274021353 0ustar rmb571rmb571# frozen_string_literal: true module FileValidators VERSION = '3.0.0' end file-validators-3.0.0/lib/file_validators/mime_type_analyzer.rb0000644000175000017510000000514714124557274023575 0ustar rmb571rmb571# frozen_string_literal: true # Extracted from shrine/plugins/determine_mime_type.rb module FileValidators class MimeTypeAnalyzer SUPPORTED_TOOLS = %i[fastimage file filemagic mimemagic marcel mime_types mini_mime].freeze MAGIC_NUMBER = 256 * 1024 def initialize(tool) raise Error, "unknown mime type analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(',')}" unless SUPPORTED_TOOLS.include?(tool) @tool = tool end def call(io) mime_type = send(:"extract_with_#{@tool}", io) io.rewind mime_type end private def extract_with_file(io) require 'open3' return nil if io.eof? # file command returns "application/x-empty" for empty files Open3.popen3(*%W[file --mime-type --brief -]) do |stdin, stdout, stderr, thread| begin IO.copy_stream(io, stdin.binmode) rescue Errno::EPIPE end stdin.close status = thread.value raise Error, "file command failed to spawn: #{stderr.read}" if status.nil? raise Error, "file command failed: #{stderr.read}" unless status.success? $stderr.print(stderr.read) stdout.read.strip end rescue Errno::ENOENT raise Error, 'file command-line tool is not installed' end def extract_with_fastimage(io) require 'fastimage' type = FastImage.type(io) "image/#{type}" if type end def extract_with_filemagic(io) require 'filemagic' return nil if io.eof? # FileMagic returns "application/x-empty" for empty files FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic| filemagic.buffer(io.read(MAGIC_NUMBER)) end end def extract_with_mimemagic(io) require 'mimemagic' mime = MimeMagic.by_magic(io) mime.type if mime end def extract_with_marcel(io) require 'marcel' return nil if io.eof? # marcel returns "application/octet-stream" for empty files Marcel::MimeType.for(io) end def extract_with_mime_types(io) require 'mime/types' if filename = extract_filename(io) mime_type = MIME::Types.of(filename).first mime_type.content_type if mime_type end end def extract_with_mini_mime(io) require 'mini_mime' if filename = extract_filename(io) info = MiniMime.lookup_by_filename(filename) info.content_type if info end end def extract_filename(io) if io.respond_to?(:original_filename) io.original_filename elsif io.respond_to?(:path) File.basename(io.path) end end end end file-validators-3.0.0/lib/file_validators/locale/0000755000175000017510000000000014124557274020603 5ustar rmb571rmb571file-validators-3.0.0/lib/file_validators/locale/en.yml0000755000175000017510000000110014124557274021723 0ustar rmb571rmb571en: errors: messages: file_size_is_in: ! 'file size must be between %{min} and %{max}' file_size_is_less_than: ! 'file size must be less than %{count}' file_size_is_less_than_or_equal_to: ! 'file size must be less than or equal to %{count}' file_size_is_greater_than: ! 'file size must be greater than %{count}' file_size_is_greater_than_or_equal_to: ! 'file size must be greater than or equal to %{count}' allowed_file_content_types: ! 'file should be one of %{types}' excluded_file_content_types: ! 'file cannot be %{types}' file-validators-3.0.0/lib/file_validators/error.rb0000644000175000017510000000013514124557274021021 0ustar rmb571rmb571# frozen_string_literal: true module FileValidators class Error < StandardError end end file-validators-3.0.0/lib/file_validators.rb0000644000175000017510000000065614124557274017700 0ustar rmb571rmb571# frozen_string_literal: true require 'active_model' require 'ostruct' module FileValidators extend ActiveSupport::Autoload autoload :Error autoload :MimeTypeAnalyzer end Dir[File.dirname(__FILE__) + '/file_validators/validators/*.rb'].each { |file| require file } locale_path = Dir.glob(File.dirname(__FILE__) + '/file_validators/locale/*.yml') I18n.load_path += locale_path unless I18n.load_path.include?(locale_path) file-validators-3.0.0/.gitignore0000755000175000017510000000032714124557274015424 0ustar rmb571rmb571.bundle/ log/*.log pkg/ spec/dummy/db/*.sqlite3 spec/dummy/db/*.sqlite3-journal spec/dummy/log/*.log spec/dummy/tmp/ spec/dummy/.sass-cache Gemfile.lock gemfiles/*.lock coverage/ /.idea .ruby-version .agignore tags file-validators-3.0.0/README.rdoc0000755000175000017510000000007214124557274015237 0ustar rmb571rmb571= FileValidators This project rocks and uses MIT-LICENSE.file-validators-3.0.0/Appraisals0000644000175000017510000000061614124557274015454 0ustar rmb571rmb571# frozen_string_literal: true appraise 'activemodel-3.2' do gem 'activemodel', '3.2.22.5' gem 'rack', '1.6.5' end appraise 'activemodel-4.0' do gem 'activemodel', '4.0.13' gem 'rack', '1.6.5' end appraise 'activemodel-5.0' do gem 'activemodel', '5.0.1' end appraise 'activemodel-6.0' do gem 'activemodel', '6.0.3' end appraise 'activemodel-6.1' do gem 'activemodel', '6.1.0' end file-validators-3.0.0/CHANGELOG.md0000644000175000017510000000232314124557274015240 0ustar rmb571rmb571# 3.0.0 * [#32](https://github.com/musaffa/file_validators/pull/32) Removed cocaine/terrapin. Added options for choosing MIME type analyzers with `:tool` option. * [#40](https://github.com/musaffa/file_validators/pull/40) Added Support for Ruby 3. * Rubocop style guide # 3.0.0.beta2 * [#32](https://github.com/musaffa/file_validators/pull/32) Removed terrapin. Added options for choosing MIME type analyzers with `:tool` option. # 3.0.0.beta1 * [#29](https://github.com/musaffa/file_validators/pull/29) Upgrade cocaine to terrapin * Rubocop style guide # 2.3.0 * [#19](https://github.com/musaffa/file_validators/pull/19) Return false with blank size * [#27](https://github.com/musaffa/file_validators/pull/27) Fix file size validator for ActiveStorage # 2.2.0-beta.1 * [#17](https://github.com/musaffa/file_validators/pull/17) Now Supports multiple file uploads * As activemodel 3.0 and 3.1 doesn't support `added?` method on the Errors class, the support for both of them have been deprecated in this release. # 2.1.0 * Use autoload for lazy loading of libraries. * Media type spoof valiation is moved to content type detector. * `spoofed_file_media_type` message isn't needed anymore. * Logger info and warning is added. file-validators-3.0.0/gemfiles/0000755000175000017510000000000014124557274015222 5ustar rmb571rmb571file-validators-3.0.0/gemfiles/activemodel_6.1.gemfile0000644000175000017510000000021214124557274021427 0ustar rmb571rmb571# This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "activemodel", "6.1.0" gemspec :path => "../" file-validators-3.0.0/gemfiles/activemodel_3.2.gemfile0000644000175000017510000000024114124557274021427 0ustar rmb571rmb571# This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "activemodel", "3.2.22.5" gem "rack", "1.6.5" gemspec :path => "../" file-validators-3.0.0/gemfiles/activemodel_4.0.gemfile0000644000175000017510000000023714124557274021433 0ustar rmb571rmb571# This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "activemodel", "4.0.13" gem "rack", "1.6.5" gemspec :path => "../" file-validators-3.0.0/gemfiles/activemodel_5.0.gemfile0000644000175000017510000000021214124557274021425 0ustar rmb571rmb571# This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "activemodel", "5.0.1" gemspec :path => "../" file-validators-3.0.0/gemfiles/activemodel_6.0.gemfile0000644000175000017510000000021214124557274021426 0ustar rmb571rmb571# This file was generated by Appraisal source "https://rubygems.org" gem "appraisal" gem "activemodel", "6.0.3" gemspec :path => "../" file-validators-3.0.0/Rakefile0000644000175000017510000000135514124557274015100 0ustar rmb571rmb571# frozen_string_literal: true begin require 'bundler/setup' rescue LoadError puts 'You must `gem install bundler` and `bundle install` to run rake tasks' end require 'rspec/core/rake_task' namespace :test do RSpec::Core::RakeTask.new(:unit) do |t| t.pattern = ['spec/lib/**/*_spec.rb'] end RSpec::Core::RakeTask.new(:integration) do |t| t.pattern = ['spec/integration/**/*_spec.rb'] end end task default: ['test:unit', 'test:integration'] # require 'rdoc/task' # RDoc::Task.new(:rdoc) do |rdoc| # rdoc.rdoc_dir = 'rdoc' # rdoc.title = 'FileValidators' # rdoc.options << '--line-numbers' # rdoc.rdoc_files.include('README.rdoc') # rdoc.rdoc_files.include('lib/**/*.rb') # end Bundler::GemHelper.install_tasks file-validators-3.0.0/file_validators.gemspec0000644000175000017510000000234614124557274020150 0ustar rmb571rmb571# frozen_string_literal: true $LOAD_PATH.push File.expand_path('lib', __dir__) require 'file_validators/version' Gem::Specification.new do |s| s.name = 'file_validators' s.version = FileValidators::VERSION s.authors = ['Ahmad Musaffa'] s.email = ['musaffa_csemm@yahoo.com'] s.summary = 'ActiveModel file validators' s.description = 'Adds file validators to ActiveModel' s.homepage = 'https://github.com/musaffa/file_validators' s.license = 'MIT' s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } s.test_files = s.files.grep(%r{^spec/}) s.require_paths = ['lib'] s.add_dependency 'activemodel', '>= 3.2' s.add_dependency 'mime-types', '>= 1.0' s.add_development_dependency 'coveralls' s.add_development_dependency 'fastimage' s.add_development_dependency 'marcel', '~> 0.3' if RUBY_VERSION >= '2.2.0' s.add_development_dependency 'mimemagic', '>= 0.3.2' s.add_development_dependency 'mini_mime', '~> 1.0' s.add_development_dependency 'rack-test' s.add_development_dependency 'rake' s.add_development_dependency 'rspec', '~> 3.5.0' s.add_development_dependency 'rubocop', '~> 0.58.2' end file-validators-3.0.0/.tool-versions0000644000175000017510000000001314124557274016245 0ustar rmb571rmb571ruby 2.3.1 file-validators-3.0.0/Gemfile0000644000175000017510000000111314124557274014716 0ustar rmb571rmb571# frozen_string_literal: true source 'https://rubygems.org' # Declare your gem's dependencies in file_validators.gemspec. # Bundler will treat runtime dependencies like base dependencies, and # development dependencies will be added by default to the :development group. gemspec # Declare any dependencies that are still in development here instead of in # your gemspec. These might include edge Rails or gems from your path or # Git. Remember to move these dependencies to your gemspec before releasing # your gem to rubygems.org. # To use debugger # gem 'debugger' gem 'appraisal' file-validators-3.0.0/README.md0000644000175000017510000002612714124557274014716 0ustar rmb571rmb571# File Validators [![Gem Version](https://badge.fury.io/rb/file_validators.svg)](http://badge.fury.io/rb/file_validators) [![Build Status](https://travis-ci.org/musaffa/file_validators.svg)](https://travis-ci.org/musaffa/file_validators) [![Coverage Status](https://coveralls.io/repos/musaffa/file_validators/badge.png)](https://coveralls.io/r/musaffa/file_validators) [![Code Climate](https://codeclimate.com/github/musaffa/file_validators/badges/gpa.svg)](https://codeclimate.com/github/musaffa/file_validators) [![Inline docs](http://inch-ci.org/github/musaffa/file_validators.svg)](http://inch-ci.org/github/musaffa/file_validators) File Validators gem adds file size and content type validations to ActiveModel. Any module that uses ActiveModel, for example ActiveRecord, can use these file validators. ## Support * ActiveModel versions: 3.2, 4, 5 and 6. * Rails versions: 3.2, 4, 5 and 6. As of version `2.2`, activemodel 3.0 and 3.1 will no longer be supported. For activemodel 3.0 and 3.1, please use file_validators version `<= 2.1`. It has been tested to work with Carrierwave, Paperclip, Dragonfly, Refile etc file uploading solutions. Validations works both before and after uploads. ## Installation Add the following to your Gemfile: ```ruby gem 'file_validators' ``` ## Examples ActiveModel example: ```ruby class Profile include ActiveModel::Validations attr_accessor :avatar validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes }, file_content_type: { allow: ['image/jpeg', 'image/png'] } end ``` ActiveRecord example: ```ruby class Profile < ActiveRecord::Base validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes }, file_content_type: { allow: ['image/jpeg', 'image/png'] } end ``` You can also use `:validates_file_size` and `:validates_file_content_type` idioms. ## API ### File Size Validator: * `in`: A range of bytes or a proc that returns a range ```ruby validates :avatar, file_size: { in: 100.kilobytes..1.megabyte } ``` * `less_than`: Less than a number in bytes or a proc that returns a number ```ruby validates :avatar, file_size: { less_than: 2.gigabytes } ``` * `less_than_or_equal_to`: Less than or equal to a number in bytes or a proc that returns a number ```ruby validates :avatar, file_size: { less_than_or_equal_to: 50.bytes } ``` * `greater_than`: greater than a number in bytes or a proc that returns a number ```ruby validates :avatar, file_size: { greater_than: 1.byte } ``` * `greater_than_or_equal_to`: Greater than or equal to a number in bytes or a proc that returns a number ```ruby validates :avatar, file_size: { greater_than_or_equal_to: 50.bytes } ``` * `message`: Error message to display. With all the options above except `:in`, you will get `count` as a replacement. With `:in` you will get `min` and `max` as replacements. `count`, `min` and `max` each will have its value and unit together. You can write error messages without using any replacement. ```ruby validates :avatar, file_size: { less_than: 100.kilobytes, message: 'avatar should be less than %{count}' } ``` ```ruby validates :document, file_size: { in: 1.kilobyte..1.megabyte, message: 'must be within %{min} and %{max}' } ``` * `if`: A lambda or name of an instance method. Validation will only be run if this lambda or method returns true. * `unless`: Same as `if` but validates if lambda or method returns false. You can combine different options. ```ruby validates :avatar, file_size: { less_than: 1.megabyte, greater_than_or_equal_to: 20.kilobytes } ``` The following two examples are equivalent: ```ruby validates :avatar, file_size: { greater_than_or_equal_to: 500.kilobytes, less_than_or_equal_to: 3.megabytes } ``` ```ruby validates :avatar, file_size: { in: 500.kilobytes..3.megabytes } ``` Options can also take `Proc`/`lambda`: ```ruby validates :avatar, file_size: { less_than: lambda { |record| record.size_in_bytes } } ``` ### File Content Type Validator * `allow`: Allowed content types. Can be a single content type or an array. Each type can be a String or a Regexp. It also accepts `proc`. Allows all by default. ```ruby # string validates :avatar, file_content_type: { allow: 'image/jpeg' } ``` ```ruby # array of strings validates :attachment, file_content_type: { allow: ['image/jpeg', 'text/plain'] } ``` ```ruby # regexp validates :avatar, file_content_type: { allow: /^image\/.*/ } ``` ```ruby # array of regexps validates :attachment, file_content_type: { allow: [/^image\/.*/, /^text\/.*/] } ``` ```ruby # array of regexps and strings validates :attachment, file_content_type: { allow: [/^image\/.*/, 'video/mp4'] } ``` ```ruby # proc/lambda example validates :video, file_content_type: { allow: lambda { |record| record.content_types } } ``` * `exclude`: Forbidden content types. Can be a single content type or an array. Each type can be a String or a Regexp. It also accepts `proc`. See `:allow` options examples. * `mode`: `:strict` or `:relaxed`. `:strict` mode can detect content type based on the contents of the files. It also detects media type spoofing (see more in [security](#security)). `:file` analyzer is used in `:strict` mode. `:relaxed` mode uses file name to detect the content type. `mime_types` analyzer is used in `relaxed` mode. If mode option is not set then the validator uses form supplied content type. * `tool`: `:file`, `:fastimage`, `:filemagic`, `:mimemagic`, `:marcel`, `:mime_types`, `:mini_mime`. You can choose one of these built-in MIME type analyzers. You have to install the analyzer gem you choose. By default supplied content type is used to determine the MIME type. This option takes precedence over `mode` option. ```ruby validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :strict } validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :relaxed } ``` * `message`: The message to display when the uploaded file has an invalid content type. You will get `types` as a replacement. You can write error messages without using any replacement. ```ruby validates :avatar, file_content_type: { allow: ['image/jpeg', 'image/gif'], message: 'only %{types} are allowed' } ``` ```ruby validates :avatar, file_content_type: { allow: ['image/jpeg', 'image/gif'], message: 'Avatar only allows jpeg and gif' } ``` * `if`: A lambda or name of an instance method. Validation will only be run is this lambda or method returns true. * `unless`: Same as `if` but validates if lambda or method returns false. You can combine `:allow` and `:exclude`: ```ruby # this will allow all the image types except png and gif validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: ['image/png', 'image/gif'] } ``` ## Security This gem can use Unix file command to get the content type based on the content of the file rather than the extension. This prevents fake content types inserted in the request header. It also prevents file media type spoofing. For example, user may upload a .html document as a part of the EXIF header of a valid JPEG file. Content type validator will identify its content type as `image/jpeg` and, without spoof detection, it may pass the validation and be saved as .html document thus exposing your application to a security vulnerability. Media type spoof detector wont let that happen. It will not allow a file having `image/jpeg` content type to be saved as `text/plain`. It checks only media type mismatch, for example `text` of `text/plain` and `image` of `image/jpeg`. So it will not prevent `image/jpeg` from saving as `image/png` as both have the same `image` media type. **note**: This security feature is disabled by default. To enable it, add `mode: :strict` option in [content type validations](#file-content-type-validator). `:strict` mode may not work in direct file uploading systems as the file is not passed along with the form. ## i18n Translations File Size Errors * `file_size_is_in`: takes `min` and `max` as replacements * `file_size_is_less_than`: takes `count` as replacement * `file_size_is_less_than_or_equal_to`: takes `count` as replacement * `file_size_is_greater_than`: takes `count` as replacement * `file_size_is_greater_than_or_equal_to`: takes `count` as replacement Content Type Errors * `allowed_file_content_types`: generated when you have specified allowed types but the content type of the file doesn't match. takes `types` as replacement. * `excluded_file_content_types`: generated when you have specified excluded types and the content type of the file matches anyone of them. takes `types` as replacement. This gem provides `en` translations for this errors under `errors.messages` namespace. If you want to override and/or create other locales, you can check [this](https://github.com/musaffa/file_validators/blob/master/lib/file_validators/locale/en.yml) out to see how translations are done. You can override all of them with the `:message` option. For unit format, it will use `number.human.storage_units.format` from your locale. For unit translation, `number.human.storage_units` is used. Rails applications already have these translations either in ActiveSupport's locale (Rails 4) or in ActionView's locale (Rails 3). In case your setup doesn't have the translations, here's an example for `en`: ```yml en: number: human: storage_units: format: "%n %u" units: byte: one: "Byte" other: "Bytes" kb: "KB" mb: "MB" gb: "GB" tb: "TB" ``` ## Further Instructions If you are using `:strict` or `:relaxed` mode, for content types which are not supported by mime-types gem, you need to register those content types. For example, you can register `.docx` in the initializer: ```Ruby # config/initializers/mime_types.rb Mime::Type.register "application/vnd.openxmlformats-officedocument.wordprocessingml.document", :docx ``` If you want to see what content type `:strict` mode returns, run this command in the shell: ```Shell $ file -b --mime-type your-file.xxx ``` ## Issues **Carrierwave** - You are adding file validators to a model, then you are recommended to keep extension_white_list &/ extension_black_list in the uploaders (in case you don't have, add that method). As of this writing (see [issue](https://github.com/carrierwaveuploader/carrierwave/issues/361)), Carrierwave uploaders start processing a file immediately after its assignment (even before the validators are called). ## Tests ```Shell $ rake $ rake test:unit $ rake test:integration $ rubocop # test different active model versions $ bundle exec appraisal install $ bundle exec appraisal rake ``` ## Problems Please use GitHub's [issue tracker](http://github.com/musaffa/file_validations/issues). ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Added some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request ## Inspirations * [PaperClip](https://github.com/thoughtbot/paperclip) ## License This project rocks and uses MIT-LICENSE. file-validators-3.0.0/MIT-LICENSE0000755000175000017510000000203014124557274015061 0ustar rmb571rmb571Copyright 2014 YOURNAME Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. file-validators-3.0.0/.rspec0000755000175000017510000000001014124557274014536 0ustar rmb571rmb571--color