diff --git a/.gitignore b/.gitignore index 9465b60..00c45ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /faraday-0.9.0.gem /faraday-0.15.4-tests.tgz /faraday-0.15.4.gem +/faraday-1.0.1.gem diff --git a/parser.rb b/parser.rb new file mode 100644 index 0000000..dfb1b05 --- /dev/null +++ b/parser.rb @@ -0,0 +1,246 @@ +module MultipartParser + # A low level parser for multipart messages, + # based on the node-formidable parser. + class Parser + + def initialize + @boundary = nil + @boundary_chars = nil + @lookbehind = nil + @state = :parser_uninitialized + @index = 0 # Index into boundary or header + @flags = {} + @marks = {} # Keep track of different parts + @callbacks = {} + end + + # Initializes the parser, using the given boundary + def init_with_boundary(boundary) + @boundary = "\r\n--" + boundary + @lookbehind = "\0"*(@boundary.length + 8) + @state = :start + + @boundary_chars = {} + @boundary.each_byte do |b| + @boundary_chars[b.chr] = true + end + end + + # Registers a callback to be called when the + # given event occurs. Each callback is expected to + # take three parameters: buffer, start_index, and end_index. + # All of these parameters may be null, depending on the callback. + # Valid callbacks are: + # :end + # :header_field + # :header_value + # :header_end + # :headers_end + # :part_begin + # :part_data + # :part_end + def on(event, &callback) + @callbacks[event] = callback + end + + # Writes data to the parser. + # Returns the number of bytes parsed. + # In practise, this means that if the return value + # is less than the buffer length, a parse error occured. + def write(buffer) + i = 0 + buffer_length = buffer.length + index = @index + flags = @flags.dup + state = @state + lookbehind = @lookbehind + boundary = @boundary + boundary_chars = @boundary_chars + boundary_length = @boundary.length + boundary_end = boundary_length - 1 + + while i < buffer_length + c = buffer[i, 1] + case state + when :parser_uninitialized + return i; + when :start + index = 0; + state = :start_boundary + when :start_boundary # Differs in that it has no preceeding \r\n + if index == boundary_length - 2 + return i unless c == "\r" + index += 1 + elsif index - 1 == boundary_length - 2 + return i unless c == "\n" + # Boundary read successfully, begin next part + callback(:part_begin) + state = :header_field_start + else + return i unless c == boundary[index+2, 1] # Unexpected character + index += 1 + end + i += 1 + when :header_field_start + state = :header_field + @marks[:header_field] = i + index = 0 + when :header_field + if c == "\r" + @marks.delete :header_field + state = :headers_almost_done + else + index += 1 + unless c == "-" # Skip hyphens + if c == ":" + return i if index == 1 # Empty header field + data_callback(:header_field, buffer, i, :clear => true) + state = :header_value_start + else + cl = c.downcase + return i if cl < "a" || cl > "z" + end + end + end + i += 1 + when :header_value_start + if c == " " # Skip spaces + i += 1 + else + @marks[:header_value] = i + state = :header_value + end + when :header_value + if c == "\r" + data_callback(:header_value, buffer, i, :clear => true) + callback(:header_end) + state = :header_value_almost_done + end + i += 1 + when :header_value_almost_done + return i unless c == "\n" + state = :header_field_start + i += 1 + when :headers_almost_done + return i unless c == "\n" + callback(:headers_end) + state = :part_data_start + i += 1 + when :part_data_start + state = :part_data + @marks[:part_data] = i + when :part_data + prev_index = index + + if index == 0 + # Boyer-Moore derived algorithm to safely skip non-boundary data + # See http://debuggable.com/posts/parsing-file-uploads-at-500- + # mb-s-with-node-js:4c03862e-351c-4faa-bb67-4365cbdd56cb + while i + boundary_length <= buffer_length + break if boundary_chars.has_key? buffer[i + boundary_end].chr + i += boundary_length + end + c = buffer[i, 1] + end + + if index < boundary_length + if boundary[index, 1] == c + if index == 0 + data_callback(:part_data, buffer, i, :clear => true) + end + index += 1 + else # It was not the boundary we found, after all + index = 0 + end + elsif index == boundary_length + index += 1 + if c == "\r" + flags[:part_boundary] = true + elsif c == "-" + flags[:last_boundary] = true + else # We did not find a boundary after all + index = 0 + end + elsif index - 1 == boundary_length + if flags[:part_boundary] + index = 0 + if c == "\n" + flags.delete :part_boundary + callback(:part_end) + callback(:part_begin) + state = :header_field_start + i += 1 + next # Ugly way to break out of the case statement + end + elsif flags[:last_boundary] + if c == "-" + callback(:part_end) + callback(:end) + state = :end + else + index = 0 # False alarm + end + else + index = 0 + end + end + + if index > 0 + # When matching a possible boundary, keep a lookbehind + # reference in case it turns out to be a false lead + lookbehind[index-1] = c + elsif prev_index > 0 + # If our boundary turns out to be rubbish, + # the captured lookbehind belongs to part_data + callback(:part_data, lookbehind, 0, prev_index) + @marks[:part_data] = i + + # Reconsider the current character as it might be the + # beginning of a new sequence. + i -= 1 + end + + i += 1 + when :end + i += 1 + else + return i; + end + end + + data_callback(:header_field, buffer, buffer_length) + data_callback(:header_value, buffer, buffer_length) + data_callback(:part_data, buffer, buffer_length) + + @index = index + @state = state + @flags = flags + + return buffer_length + end + + private + + # Issues a callback. + def callback(event, buffer = nil, start = nil, the_end = nil) + return if !start.nil? && start == the_end + if @callbacks.has_key? event + @callbacks[event].call(buffer, start, the_end) + end + end + + # Issues a data callback, + # The only valid options is :clear, + # which, if true, will reset the appropriate mark to 0, + # If not specified, the mark will be removed. + def data_callback(data_type, buffer, the_end, options = {}) + return unless @marks.has_key? data_type + callback(data_type, buffer, @marks[data_type], the_end) + unless options[:clear] + @marks[data_type] = 0 + else + @marks.delete data_type + end + end + end +end diff --git a/reader.rb b/reader.rb new file mode 100644 index 0000000..d63562c --- /dev/null +++ b/reader.rb @@ -0,0 +1,152 @@ +require 'multipart_parser/parser' + +module MultipartParser + class NotMultipartError < StandardError; end; + + # A more high level interface to MultipartParser. + class Reader + + # Initializes a MultipartReader, that will + # read a request with the given boundary value. + def initialize(boundary) + @parser = Parser.new + @parser.init_with_boundary(boundary) + @header_field = '' + @header_value = '' + @part = nil + @ended = false + @on_error = nil + @on_part = nil + + init_parser_callbacks + end + + # Returns true if the parser has finished parsing + def ended? + @ended + end + + # Sets to a code block to call + # when part headers have been parsed. + def on_part(&callback) + @on_part = callback + end + + # Sets a code block to call when + # a parser error occurs. + def on_error(&callback) + @on_error = callback + end + + # Write data from the given buffer (String) + # into the reader. + def write(buffer) + bytes_parsed = @parser.write(buffer) + if bytes_parsed != buffer.size + msg = "Parser error, #{bytes_parsed} of #{buffer.length} bytes parsed" + @on_error.call(msg) unless @on_error.nil? + end + end + + # Extracts a boundary value from a Content-Type header. + # Note that it is the header value you provide here. + # Raises NotMultipartError if content_type is invalid. + def self.extract_boundary_value(content_type) + if content_type =~ /multipart/i + if match = (content_type =~ /boundary=(?:"([^"]+)"|([^;]+))/i) + $1 || $2 + else + raise NotMultipartError.new("No multipart boundary") + end + else + raise NotMultipartError.new("Not a multipart content type!") + end + end + + class Part + attr_accessor :filename, :headers, :name, :mime + + def initialize + @headers = {} + @data_callback = nil + @end_callback = nil + end + + # Calls the data callback with the given data + def emit_data(data) + @data_callback.call(data) unless @data_callback.nil? + end + + # Calls the end callback + def emit_end + @end_callback.call unless @end_callback.nil? + end + + # Sets a block to be called when part data + # is read. The block should take one parameter, + # namely the read data. + def on_data(&callback) + @data_callback = callback + end + + # Sets a block to be called when all data + # for the part has been read. + def on_end(&callback) + @end_callback = callback + end + end + + private + + def init_parser_callbacks + @parser.on(:part_begin) do + @part = Part.new + @header_field = '' + @header_value = '' + end + + @parser.on(:header_field) do |b, start, the_end| + @header_field << b[start...the_end] + end + + @parser.on(:header_value) do |b, start, the_end| + @header_value << b[start...the_end] + end + + @parser.on(:header_end) do + @header_field.downcase! + @part.headers[@header_field] = @header_value + if @header_field == 'content-disposition' + if @header_value =~ /name="([^"]+)"/i + @part.name = $1 + end + if @header_value =~ /filename="([^;]+)"/i + match = $1 + start = (match.rindex("\\") || -1)+1 + @part.filename = match[start...(match.length)] + end + elsif @header_field == 'content-type' + @part.mime = @header_value + end + @header_field = '' + @header_value = '' + end + + @parser.on(:headers_end) do + @on_part.call(@part) unless @on_part.nil? + end + + @parser.on(:part_data) do |b, start, the_end| + @part.emit_data b[start...the_end] + end + + @parser.on(:part_end) do + @part.emit_end + end + + @parser.on(:end) do + @ended = true + end + end + end +end diff --git a/rubygem-faraday-1.0.1-Properly-fix-test-failure-with-Rack-2.1.patch b/rubygem-faraday-1.0.1-Properly-fix-test-failure-with-Rack-2.1.patch new file mode 100644 index 0000000..8816a50 --- /dev/null +++ b/rubygem-faraday-1.0.1-Properly-fix-test-failure-with-Rack-2.1.patch @@ -0,0 +1,27 @@ +From bf03db0979954ef4dd8646c53b73a003af70a953 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?V=C3=ADt=20Ondruch?= +Date: Fri, 24 Jul 2020 20:39:25 +0200 +Subject: [PATCH] Properly fix test failure with Rack 2.1+. + +Rack is not to blame, just naive test case which was enough so far. + +Fixes #1119 +--- + spec/support/shared_examples/request_method.rb | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/spec/support/shared_examples/request_method.rb b/spec/support/shared_examples/request_method.rb +index 8e2828a2..917e48ca 100644 +--- a/spec/support/shared_examples/request_method.rb ++++ b/spec/support/shared_examples/request_method.rb +@@ -13,8 +13,8 @@ + end + + it 'handles headers with multiple values' do +- request_stub.to_return(headers: { 'Set-Cookie' => 'one, two' }) +- expect(response.headers['set-cookie']).to eq('one, two') ++ request_stub.to_return(headers: { 'Set-Cookie' => 'name=value' }) ++ expect(response.headers['set-cookie']).to eq('name=value') + end + + it 'retrieves the response headers' do diff --git a/rubygem-faraday.spec b/rubygem-faraday.spec index 6f2b163..4c9bc02 100644 --- a/rubygem-faraday.spec +++ b/rubygem-faraday.spec @@ -2,22 +2,28 @@ %global gem_name faraday Name: rubygem-%{gem_name} -Version: 0.15.4 -Release: 3%{?dist} +Version: 1.0.1 +Release: 1%{?dist} Summary: HTTP/REST API client library License: MIT -URL: https://github.com/lostisland/faraday +URL: https://lostisland.github.io/faraday Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem -# git clone https://github.com/lostisland/faraday.git && cd faraday -# git checkout v0.15.4 && tar czvf faraday-0.15.4-tests.tgz test/ script/ -Source1: %{gem_name}-%{version}-tests.tgz +# Since we don't have multipart-parser in Fedora, include the essential part +# just for testing purposes. +# https://github.com/danabr/multipart-parser/blob/master/lib/multipart_parser/parser.rb +Source1: https://raw.githubusercontent.com/danabr/multipart-parser/master/lib/multipart_parser/parser.rb +# https://github.com/danabr/multipart-parser/blob/master/lib/multipart_parser/reader.rb +Source2: https://raw.githubusercontent.com/danabr/multipart-parser/master/lib/multipart_parser/reader.rb +# Fix Rack 2.1+ test compatibility. +# https://github.com/lostisland/faraday/pull/1171 +Patch0: rubygem-faraday-1.0.1-Properly-fix-test-failure-with-Rack-2.1.patch BuildRequires: ruby(release) BuildRequires: rubygems-devel -BuildRequires: ruby >= 1.9 -BuildRequires: %{_bindir}/lsof -BuildRequires: rubygem(minitest) +BuildRequires: ruby >= 2.3 BuildRequires: rubygem(multipart-post) -BuildRequires: rubygem(sinatra) +BuildRequires: rubygem(rack) +BuildRequires: rubygem(rspec) +BuildRequires: rubygem(webmock) # Adapter test dependencies, might be optionally disabled. BuildRequires: rubygem(em-http-request) BuildRequires: rubygem(excon) @@ -40,7 +46,12 @@ BuildArch: noarch Documentation for %{name}. %prep -%setup -q -n %{gem_name}-%{version} -b 1 +mkdir -p multipart_parser/multipart_parser +cp %{SOURCE1} %{SOURCE2} multipart_parser/multipart_parser + +%setup -q -n %{gem_name}-%{version} + +%patch0 -p1 %build # Create the gem as gem install only works on a gem file @@ -59,29 +70,23 @@ cp -a .%{gem_dir}/* \ %check pushd .%{gem_instdir} -ln -s %{_builddir}/test test -ln -s %{_builddir}/script script - -# Avoid Bundler. -sed -i '/bundler\/setup/,/^fi$/ s/^/#/' script/test - -# Follow symlinks. -sed -i "s/find /find -L /" script/test - # We don't care about code coverage. -sed -i "/simplecov/,/^end$/ s/^/#/" test/helper.rb +sed -i "/simplecov/ s/^/#/" spec/spec_helper.rb +sed -i "/coveralls/ s/^/#/" spec/spec_helper.rb +sed -i "/SimpleCov/,/^end$/ s/^/#/" spec/spec_helper.rb + +# We don't need Pry. +sed -i "/pry/ s/^/#/" spec/spec_helper.rb # We don't have {patron,em-synchrony} available in Fedora. -mv test/adapters/em_synchrony_test.rb{,.disabled} -mv test/adapters/patron_test.rb{,.disabled} +mv spec/faraday/adapter/em_synchrony_spec.rb{,.disabled} +mv spec/faraday/adapter/patron_spec.rb{,.disabled} -# Rrequirs internet access. -sed -i "/def test_dynamic_no_proxy/a\ skip" test/connection_test.rb +# This needs http-net-persistent 3.0+. +sed -i '/allows to set min_version in SSL settings/a\ skip' \ + spec/faraday/adapter/net_http_persistent_spec.rb -# Fails with Typhoeus 1.0.2 -sed -i "/def test_custom_adapter_config/a\ skip" test/adapters/typhoeus_test.rb - -RUBYOPT="-Ilib -ryaml" script/test +rspec -I%{_builddir}/multipart_parser -rspec_helper -r%{SOURCE1} spec -f d popd %files @@ -93,9 +98,17 @@ popd %files doc %doc %{gem_docdir} +%doc %{gem_instdir}/CHANGELOG.md %doc %{gem_instdir}/README.md +%{gem_instdir}/Rakefile +%{gem_instdir}/examples +%{gem_instdir}/spec %changelog +* Thu Jul 23 2020 Vít Ondruch - 1.0.1-1 +- Update to Faraday 1.0.1. + Resolves: rhbz#1756449 + * Thu Jan 30 2020 Fedora Release Engineering - 0.15.4-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild diff --git a/sources b/sources index 9c04dc1..3dacc06 100644 --- a/sources +++ b/sources @@ -1,2 +1 @@ -SHA512 (faraday-0.15.4-tests.tgz) = a67dec047bdf38b4fdf16497b62a8bee05cef38652b44a95252dcbe70af09d23ed43f7490c4490e887acf8d503d8bdcc7b41c4f96879d16099ef626477485cc1 -SHA512 (faraday-0.15.4.gem) = e63bf8a84dfd6a945c7172fe48197f9b022f4d9d8aa63712e249475202eb8e6e070683fe8f0816b1f72d945f3280fed4104b4c6956e2853dc4d13e718be1f23b +SHA512 (faraday-1.0.1.gem) = 0374cf32669e1727f435b765d959c5cefd774a451073e88c81c3b49d73885798803b53580f591f1e8862813baf113be7efb7abdc4d526719002f899a7c3b5c82