Module: HTTPTools::Encoding

Defined in:
lib/http_tools/encoding.rb

Overview

HTTPTools::Encoding provides methods to deal with several HTTP related encodings including url, www-form, and chunked transfer encoding. It can be used as a mixin or class methods on HTTPTools::Encoding.

Constant Summary collapse

HEX_BIG_ENDIAN_2_BYTES =

:stopdoc:

"H2".freeze
HEX_BIG_ENDIAN_REPEATING =
"H*".freeze
PERCENT =
"%".freeze
PLUS =
"+".freeze
SPACE =
" ".freeze
AMPERSAND =
"&".freeze
EQUALS =
"=".freeze
CHUNK_FORMAT =
"%x\r\n%s\r\n".freeze

Class Method Summary collapse

Class Method Details

.transfer_encoding_chunked_decode(str, scanner = StringScanner.new(str)) ⇒ Object

:call-seq: Encoding.transfer_encoding_chunked_decode(encoded_string) -> array

Decoding a complete chunked response will return an array containing the decoded response and nil. Example:

encoded_string = "3\r\nfoo\r\n3\r\nbar\r\n0\r\n"
Encoding.transfer_encoding_chunked_decode(encoded_string)\

> [“foobar”, nil]

Decoding a partial response will return an array of the response decoded so far, and the remainder of the encoded string. Example

encoded_string = "3\r\nfoo\r\n3\r\nba"
Encoding.transfer_encoding_chunked_decode(encoded_string)\

> [“foo”, “3rnba”]

If the chunks are complete, but there is no empty terminating chunk, the second element in the array will be an empty string.

encoded_string = "3\r\nfoo\r\n3\r\nbar"
Encoding.transfer_encoding_chunked_decode(encoded_string)\

> [“foobar”, “”]

If nothing can be decoded the first element in the array will be nil and the second the remainder

encoded_string = "3\r\nfo"
Encoding.transfer_encoding_chunked_decode(encoded_string)\

> [nil, “3rnfo”]

Example use:

include Encoding
decoded = ""
remainder = ""
while remainder
  remainder << get_data
  chunk, remainder = transfer_encoding_chunked_decode(remainder)
  decoded << chunk if chunk
end


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/http_tools/encoding.rb', line 143

def transfer_encoding_chunked_decode(str, scanner=StringScanner.new(str))
  decoded = ""
  
  remainder = while true
    start_pos = scanner.pos
    hex_chunk_length = scanner.scan(/[0-9a-f]+ *\r?\n/i)
    break scanner.rest unless hex_chunk_length
    
    chunk_length = hex_chunk_length.to_i(16)
    break nil if chunk_length == 0
    
    begin
      chunk = scanner.rest.slice(0, chunk_length)
      scanner.pos += chunk_length
      if chunk && scanner.skip(/\r?\n/i)
        decoded << chunk
      else
        scanner.pos = start_pos
        break scanner.rest
      end
    rescue RangeError
      scanner.pos = start_pos
      break scanner.rest
    end
  end
  
  [(decoded if decoded.length > 0), remainder]
end

.transfer_encoding_chunked_encode(string) ⇒ Object

:call-seq: Encoding.transfer_encoding_chunked_encode(string) -> encoded_string

Returns string as a ‘chunked’ transfer encoding encoded string, suitable for a streaming response from a HTTP server, eg “foo” becomes “3rnfoorn”

chunked responses should be terminted with a empty chunk, eg “0rn”, passing an empty string or nil will generate the empty chunk.



96
97
98
99
100
101
102
# File 'lib/http_tools/encoding.rb', line 96

def transfer_encoding_chunked_encode(string)
  if string && string.length > 0
    sprintf(CHUNK_FORMAT, string.length, string)
  else
    "0\r\n"
  end
end

.url_decode(string) ⇒ Object

:call-seq: Encoding.url_decode(encoded_string) -> string

URL decode a string, eg “le+caf%c3%a9” becomes “le café”



38
39
40
41
42
43
# File 'lib/http_tools/encoding.rb', line 38

def url_decode(string)
  string.tr(PLUS, SPACE).gsub(/(%[0-9a-fA-F]{2})+/) do |match|
    r = [match.delete(PERCENT)].pack(HEX_BIG_ENDIAN_REPEATING)
    r.respond_to?(:force_encoding) ? r.force_encoding(string.encoding) : r
  end
end

.url_encode(string) ⇒ Object

:call-seq: Encoding.url_encode(string) -> encoded_string

URL encode a string, eg “le café” becomes “le+caf%c3%a9”



27
28
29
30
31
32
# File 'lib/http_tools/encoding.rb', line 27

def url_encode(string)
  string.gsub(/[^a-zA-Z0-9._~-]+/) do |match|
    length = match.respond_to?(:bytesize) ? match.bytesize : match.length
    PERCENT + match.unpack(HEX_BIG_ENDIAN_2_BYTES * length).join(PERCENT)
  end
end

.www_form_decode(string) ⇒ Object

:call-seq: Encoding.www_form_decode(string) -> hash

Takes a String resulting from a HTML form being submitted, and converts it to a hash, eg “lang=en&query=fish” becomes => “en”, “query” => “fish”

Multiple key value pairs with the same key will become a single key with an array value, eg “lang=en&lang=fr” becomes => [“en”, “fr”]



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/http_tools/encoding.rb', line 74

def www_form_decode(string)
  string.split(AMPERSAND).inject({}) do |memo, key_value|
    key, value = key_value.split(EQUALS)
    key, value = url_decode(key), url_decode(value)
    if memo.key?(key)
      memo.merge(key => [*memo[key]].push(value))
    else
      memo.merge(key => value)
    end
  end
end

.www_form_encode(hash) ⇒ Object

:call-seq: Encoding.www_form_encode(hash) -> string

Takes a Hash and converts it to a String as if it was a HTML form being submitted, eg => “fish”, “lang” => “en” becomes “query=fish&lang=en”

To get multiple key value pairs with the same key use an array as the value, eg => [“en”, “fr”] become “lang=en&lang=fr”



55
56
57
58
59
60
61
62
63
# File 'lib/http_tools/encoding.rb', line 55

def www_form_encode(hash)
  hash.map do |key, value|
    if value.respond_to?(:map) && !value.is_a?(String)
      value.map {|val| www_form_encode(key => val.to_s)}.join(AMPERSAND)
    else
      url_encode(key.to_s) << EQUALS << url_encode(value.to_s)
    end
  end.join(AMPERSAND)
end