stella 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/README.txt +135 -0
  2. data/Rakefile +100 -0
  3. data/bin/stella +12 -0
  4. data/lib/stella.rb +58 -0
  5. data/lib/stella/adapter/ab.rb +303 -0
  6. data/lib/stella/adapter/base.rb +87 -0
  7. data/lib/stella/adapter/httperf.rb +296 -0
  8. data/lib/stella/adapter/siege.rb +321 -0
  9. data/lib/stella/cli.rb +291 -0
  10. data/lib/stella/cli/agents.rb +73 -0
  11. data/lib/stella/cli/base.rb +19 -0
  12. data/lib/stella/cli/language.rb +18 -0
  13. data/lib/stella/cli/localtest.rb +80 -0
  14. data/lib/stella/command/base.rb +111 -0
  15. data/lib/stella/command/localtest.rb +339 -0
  16. data/lib/stella/logger.rb +63 -0
  17. data/lib/stella/response.rb +82 -0
  18. data/lib/stella/storable.rb +116 -0
  19. data/lib/stella/support.rb +106 -0
  20. data/lib/stella/test/base.rb +34 -0
  21. data/lib/stella/test/definition.rb +79 -0
  22. data/lib/stella/test/run/summary.rb +50 -0
  23. data/lib/stella/test/summary.rb +82 -0
  24. data/lib/stella/text.rb +64 -0
  25. data/lib/stella/text/resource.rb +39 -0
  26. data/lib/utils/crypto-key.rb +84 -0
  27. data/lib/utils/escape.rb +302 -0
  28. data/lib/utils/fileutil.rb +59 -0
  29. data/lib/utils/httputil.rb +210 -0
  30. data/lib/utils/mathutil.rb +78 -0
  31. data/lib/utils/textgraph.rb +267 -0
  32. data/lib/utils/timerutil.rb +58 -0
  33. data/support/text/en.yaml +54 -0
  34. data/support/text/nl.yaml +1 -0
  35. data/support/useragents.txt +75 -0
  36. data/vendor/useragent/MIT-LICENSE +20 -0
  37. data/vendor/useragent/README +21 -0
  38. data/vendor/useragent/init.rb +1 -0
  39. data/vendor/useragent/lib/user_agent.rb +83 -0
  40. data/vendor/useragent/lib/user_agent/browsers.rb +24 -0
  41. data/vendor/useragent/lib/user_agent/browsers/all.rb +69 -0
  42. data/vendor/useragent/lib/user_agent/browsers/gecko.rb +43 -0
  43. data/vendor/useragent/lib/user_agent/browsers/internet_explorer.rb +40 -0
  44. data/vendor/useragent/lib/user_agent/browsers/opera.rb +49 -0
  45. data/vendor/useragent/lib/user_agent/browsers/webkit.rb +94 -0
  46. data/vendor/useragent/lib/user_agent/comparable.rb +25 -0
  47. data/vendor/useragent/lib/user_agent/operating_systems.rb +19 -0
  48. data/vendor/useragent/spec/browsers/gecko_user_agent_spec.rb +209 -0
  49. data/vendor/useragent/spec/browsers/internet_explorer_user_agent_spec.rb +99 -0
  50. data/vendor/useragent/spec/browsers/opera_user_agent_spec.rb +59 -0
  51. data/vendor/useragent/spec/browsers/other_user_agent_spec.rb +19 -0
  52. data/vendor/useragent/spec/browsers/webkit_user_agent_spec.rb +373 -0
  53. data/vendor/useragent/spec/spec_helper.rb +1 -0
  54. data/vendor/useragent/spec/user_agent_spec.rb +331 -0
  55. data/vendor/useragent/useragent.gemspec +12 -0
  56. metadata +139 -0
@@ -0,0 +1,50 @@
1
+
2
+
3
+
4
+ module Stella::Test::Run
5
+
6
+ class Summary < Stella::Storable
7
+
8
+ attr_accessor :note
9
+ attr_accessor :tool, :version
10
+ attr_accessor :test, :transactions, :headers_transferred
11
+ attr_accessor :elapsed_time, :data_transferred, :response_time
12
+ attr_accessor :successful, :failed, :transaction_rate, :vusers, :raw
13
+
14
+ def initialize(note="")
15
+ @note = note
16
+ @transactions = 0
17
+ @headers_transferred = 0
18
+ @elapsed_time = 0
19
+ @data_transferred = 0
20
+ @response_time = 0
21
+ @successful = 0
22
+ @failed = 0
23
+ @transaction_rate = 0
24
+ @vusers = 0
25
+ end
26
+
27
+ def availability
28
+ return 0 if @successful == 0
29
+ (@transactions / @successful).to_f * 100
30
+ end
31
+
32
+ # We calculate the throughput because Apache Bench does not provide this
33
+ # value in the output.
34
+ def throughput
35
+ return 0 if !@elapsed_time || @elapsed_time == 0
36
+ (@data_transferred / @elapsed_time).to_f
37
+ end
38
+
39
+ def field_names
40
+ [
41
+ :availability, :transactions, :elapsed_time, :data_transferred,
42
+ :headers_transferred, :response_time, :transaction_rate, :throughput,
43
+ :vusers, :successful, :failed, :note
44
+ ]
45
+ end
46
+
47
+ end
48
+
49
+
50
+ end
@@ -0,0 +1,82 @@
1
+
2
+ require 'stella/test/base'
3
+
4
+ module Stella::Test
5
+
6
+ # Stella::Test::Summary
7
+ class Summary < Stella::Storable
8
+ include Base
9
+
10
+ attr_reader :runs
11
+
12
+ def initialize(msg="")
13
+ @message = msg
14
+ @runs = []
15
+ end
16
+
17
+ # Add a TestRun object to the list
18
+ def add_run(run)
19
+ raise "I got a #{run.class} but I wanted a Run::Summary" unless run.is_a?(Run::Summary)
20
+ @runs << run
21
+ calculate
22
+ end
23
+
24
+ private
25
+ def calculate
26
+ # We simply keep a running tally of these
27
+ @transactions_total = 0
28
+ @headers_transferred_total = 0
29
+ @data_transferred_total = 0
30
+ @elapsed_time_total = 0
31
+ @successful_total = 0
32
+ @failed_total = 0
33
+
34
+ # We keep a list of the values for averaging and std dev
35
+ elapsed_times = []
36
+ transaction_rates = []
37
+ vusers_list = []
38
+ response_times = []
39
+ response_time = []
40
+ transaction_rate = []
41
+ throughput = []
42
+ vusers = []
43
+
44
+ # Each run is the summary of a single run (i.e. run01/SUMMARY.csv)
45
+ runs.each do |run|
46
+ # These are totaled
47
+ @transactions_total += run.transactions
48
+ @headers_transferred_total += run.headers_transferred
49
+ @data_transferred_total += run.data_transferred
50
+ @successful_total += run.successful
51
+ @failed_total += run.failed
52
+ @elapsed_time_total += run.elapsed_time
53
+
54
+ # These are used for standard deviation
55
+ elapsed_times << run.elapsed_time
56
+ transaction_rates << run.transaction_rate
57
+ vusers_list << run.vusers
58
+ response_times << run.response_time
59
+ throughput << run.throughput
60
+ response_time << run.response_time
61
+ transaction_rate << run.transaction_rate
62
+ vusers << run.vusers
63
+ end
64
+
65
+ # Calculate Averages
66
+ @elapsed_time_avg = elapsed_times.average
67
+ @throughput_avg = throughput.average
68
+ @response_time_avg = response_time.average
69
+ @transaction_rate_avg = transaction_rate.average
70
+ @vusers_avg = vusers.average
71
+
72
+ # Calculate Standard Deviations
73
+ @elapsed_time_sdev = elapsed_times.standard_deviation
74
+ @throughput_sdev= throughput.standard_deviation
75
+ @transaction_rate_sdev = transaction_rates.standard_deviation
76
+ @vusers_sdev = vusers_list.standard_deviation
77
+ @response_time_sdev = response_times.standard_deviation
78
+
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,64 @@
1
+
2
+ module Stella
3
+
4
+ # Stella::Text
5
+ #
6
+ # This is the API for retrieving interface text in Stella. The intended use
7
+ # is to have a single instance of this class although there's nothing stopping
8
+ # you (or anyone else!) from having as many instances as you see fit.
9
+ # Currently only yaml files are supported.
10
+ class Text
11
+ require 'stella/text/resource'
12
+
13
+ DEFAULT_LANGUAGE = 'en'.freeze unless defined? LANGUAGE
14
+ MESSAGE_NOT_DEFINED = "The message %s is not defined"
15
+ RESOURCES_PATH = File.join(STELLA_HOME, "support", "text").freeze unless defined? RESOURCES_PATH
16
+
17
+ attr_reader :resource, :lang
18
+
19
+ def initialize(language=nil)
20
+ @lang = determine_language(language)
21
+ @resource = Resource.new(RESOURCES_PATH, @lang)
22
+ @available_languages = []
23
+ end
24
+
25
+ def determine_language(language)
26
+ return language if Text.supported_language?(language)
27
+ Stella::LOGGER.info("There's no translation for '#{language}' yet. Maybe you can help? stella@solutious.com") if language
28
+ language = (ENV['STELLA_LANG'] || ENV['LOCALE'] || '').split('_')[0]
29
+ Text.supported_language?(language) ? language : DEFAULT_LANGUAGE
30
+ end
31
+
32
+ def msg(txtsym, *vars)
33
+ return self.parse(MESSAGE_NOT_DEFINED, txtsym) unless @resource.message.has_key?(txtsym)
34
+ parse(@resource.message[txtsym], vars)
35
+ end
36
+
37
+ def err(txtsym, *vars)
38
+ return self.parse(MESSAGE_NOT_DEFINED, txtsym) unless @resource.error.has_key?(txtsym)
39
+ parse(@resource.error[txtsym], vars)
40
+ end
41
+
42
+ def parse(text, vars)
43
+ sprintf(text, *vars)
44
+ end
45
+
46
+ def self.supported_language?(language)
47
+ return File.exists?(File.join(RESOURCES_PATH, "#{language}.yaml"))
48
+ end
49
+
50
+ def available_languages
51
+ #return @available_languages unless @available_languages.empty?
52
+ translations = Dir.glob(File.join(RESOURCES_PATH, "*.yaml"))
53
+ translations.each do |path|
54
+ trans = YAML.load_file(path)
55
+ next if !trans || trans.empty? || !trans[:info] || !trans[:info][:enabled]
56
+ @available_languages << trans[:info]
57
+ end
58
+ @available_languages
59
+ end
60
+
61
+ end
62
+
63
+
64
+ end
@@ -0,0 +1,39 @@
1
+
2
+
3
+ module Stella
4
+
5
+ class Text
6
+ class Resource
7
+ require 'yaml'
8
+
9
+ attr_reader :lang, :country, :encoding
10
+ attr_reader :messages, :path
11
+
12
+ def initialize(path, lang)
13
+ @path = path
14
+ @lang = lang
15
+ @messages = {}
16
+ load_resource
17
+ end
18
+
19
+ def path
20
+ File.join(@path, "#{@lang}.yaml")
21
+ end
22
+
23
+ def load_resource
24
+ return @messages unless @messages.empty?
25
+ Stella::LOGGER.debug("LOADING #{path}")
26
+ raise UnsupportedLanguage unless File.exists?(path)
27
+ @messages = YAML.load_file(path)
28
+ end
29
+
30
+ def messages
31
+ @messages
32
+ end
33
+ alias :message :messages
34
+ alias :error :messages
35
+
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,84 @@
1
+ # based on: http://blog.leetsoft.com/2006/03/14/simple-encryption
2
+
3
+
4
+ require 'base64' # Added, to fix called to Base64
5
+
6
+ # IF NOT JRUBY
7
+ require 'openssl'
8
+ # ELSE
9
+ #module JRuby
10
+ # module OpenSSL
11
+ # GEM_ONLY = false unless defined?(GEM_ONLY)
12
+ # end
13
+ # end
14
+ #
15
+ # if JRuby::OpenSSL::GEM_ONLY
16
+ # require 'jruby/openssl/gem'
17
+ # else
18
+ # module OpenSSL
19
+ # class OpenSSLError < StandardError; end
20
+ # # These require the gem
21
+ # %w[
22
+ # ASN1
23
+ # BN
24
+ # Cipher
25
+ # Config
26
+ # Netscape
27
+ # PKCS7
28
+ # PKey
29
+ # Random
30
+ # SSL
31
+ # X509
32
+ # ].each {|c| autoload c, "jruby/openssl/gem"}
33
+ # end
34
+ # require "jruby/openssl/builtin"
35
+ # end
36
+ #end
37
+
38
+ module Crypto
39
+ VERSION = 1.0
40
+
41
+ def self.create_keys(bits = 2048)
42
+ pk = OpenSSL::PKey::RSA.new(bits)
43
+ end
44
+
45
+ @@digest = OpenSSL::Digest::Digest.new("sha1")
46
+ def self.sign(secret, string)
47
+ sig = OpenSSL::HMAC.hexdigest(@@digest, secret, string).strip
48
+ #sig.gsub(/\+/, "%2b")
49
+ end
50
+ def self.aws_sign(secret, string)
51
+ Base64.encode64(self.sign(secret, string)).strip
52
+ end
53
+
54
+ class Key
55
+ attr_reader :data, :key
56
+
57
+ def initialize(data)
58
+ @data = data
59
+ @public = (data =~ /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/).nil?
60
+ @key = OpenSSL::PKey::RSA.new(@data)
61
+ end
62
+
63
+
64
+ def self.from_file(filename)
65
+ self.new File.read( filename )
66
+ end
67
+
68
+ def encrypt(text)
69
+ Base64.encode64(@key.send("#{type}_encrypt", text))
70
+ end
71
+
72
+ def decrypt(text)
73
+ @key.send("#{type}_decrypt", Base64.decode64(text))
74
+ end
75
+
76
+ def private?() !@public; end # Added () and ;
77
+
78
+ def public?() @public; end # Added () and ;
79
+
80
+ def type
81
+ @public ? :public : :private
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,302 @@
1
+ # escape.rb - escape/unescape library for several formats
2
+ #
3
+ # Copyright (C) 2006,2007 Tanaka Akira <akr@fsij.org>
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote products
14
+ # derived from this software without specific prior written permission.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
21
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
25
+ # OF SUCH DAMAGE.
26
+
27
+ # Escape module provides several escape functions.
28
+ # * URI
29
+ # * HTML
30
+ # * shell command
31
+ module EscapeUtil
32
+ module_function
33
+
34
+ class StringWrapper
35
+ class << self
36
+ alias new_no_dup new
37
+ def new(str)
38
+ new_no_dup(str.dup)
39
+ end
40
+ end
41
+
42
+ def initialize(str)
43
+ @str = str
44
+ end
45
+
46
+ def to_s
47
+ @str.dup
48
+ end
49
+
50
+ def inspect
51
+ "\#<#{self.class}: #{@str}>"
52
+ end
53
+
54
+ def ==(other)
55
+ other.class == self.class && @str == other.instance_variable_get(:@str)
56
+ end
57
+ alias eql? ==
58
+
59
+ def hash
60
+ @str.hash
61
+ end
62
+ end
63
+
64
+ class ShellEscaped < StringWrapper
65
+ end
66
+
67
+ # Escape.shell_command composes
68
+ # a sequence of words to
69
+ # a single shell command line.
70
+ # All shell meta characters are quoted and
71
+ # the words are concatenated with interleaving space.
72
+ # It returns an instance of ShellEscaped.
73
+ #
74
+ # Escape.shell_command(["ls", "/"]) #=> #<Escape::ShellEscaped: ls />
75
+ # Escape.shell_command(["echo", "*"]) #=> #<Escape::ShellEscaped: echo '*'>
76
+ #
77
+ # Note that system(*command) and
78
+ # system(Escape.shell_command(command)) is roughly same.
79
+ # There are two exception as follows.
80
+ # * The first is that the later may invokes /bin/sh.
81
+ # * The second is an interpretation of an array with only one element:
82
+ # the element is parsed by the shell with the former but
83
+ # it is recognized as single word with the later.
84
+ # For example, system(*["echo foo"]) invokes echo command with an argument "foo".
85
+ # But system(Escape.shell_command(["echo foo"])) invokes "echo foo" command without arguments (and it probably fails).
86
+ def shell_command(command)
87
+ s = command.map {|word| shell_single_word(word) }.join(' ')
88
+ ShellEscaped.new_no_dup(s)
89
+ end
90
+
91
+ # Escape.shell_single_word quotes shell meta characters.
92
+ # It returns an instance of ShellEscaped.
93
+ #
94
+ # The result string is always single shell word, even if
95
+ # the argument is "".
96
+ # Escape.shell_single_word("") returns #<Escape::ShellEscaped: ''>.
97
+ #
98
+ # Escape.shell_single_word("") #=> #<Escape::ShellEscaped: ''>
99
+ # Escape.shell_single_word("foo") #=> #<Escape::ShellEscaped: foo>
100
+ # Escape.shell_single_word("*") #=> #<Escape::ShellEscaped: '*'>
101
+ def shell_single_word(str)
102
+ if str && str.empty?
103
+ ShellEscaped.new_no_dup("''")
104
+ elsif %r{\A[0-9A-Za-z+,./:=@_-]+\z} =~ str
105
+ ShellEscaped.new(str)
106
+ else
107
+ result = ''
108
+ str.scan(/('+)|[^']+/) {
109
+ if $1
110
+ result << %q{\'} * $1.length
111
+ else
112
+ result << "'#{$&}'"
113
+ end
114
+ }
115
+ ShellEscaped.new_no_dup(result)
116
+ end
117
+ end
118
+
119
+ class PercentEncoded < StringWrapper
120
+ end
121
+
122
+ # Escape.uri_segment escapes URI segment using percent-encoding.
123
+ # It returns an instance of PercentEncoded.
124
+ #
125
+ # Escape.uri_segment("a/b") #=> #<Escape::PercentEncoded: a%2Fb>
126
+ #
127
+ # The segment is "/"-splitted element after authority before query in URI, as follows.
128
+ #
129
+ # scheme://authority/segment1/segment2/.../segmentN?query#fragment
130
+ #
131
+ # See RFC 3986 for details of URI.
132
+ def uri_segment(str)
133
+ # pchar - pct-encoded = unreserved / sub-delims / ":" / "@"
134
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
135
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
136
+ s = str.gsub(%r{[^A-Za-z0-9\-._~!$&'()*+,;=:@]}n) {
137
+ '%' + $&.unpack("H2")[0].upcase
138
+ }
139
+ PercentEncoded.new_no_dup(s)
140
+ end
141
+
142
+ # Escape.uri_path escapes URI path using percent-encoding.
143
+ # The given path should be a sequence of (non-escaped) segments separated by "/".
144
+ # The segments cannot contains "/".
145
+ # It returns an instance of PercentEncoded.
146
+ #
147
+ # Escape.uri_path("a/b/c") #=> #<Escape::PercentEncoded: a/b/c>
148
+ # Escape.uri_path("a?b/c?d/e?f") #=> #<Escape::PercentEncoded: a%3Fb/c%3Fd/e%3Ff>
149
+ #
150
+ # The path is the part after authority before query in URI, as follows.
151
+ #
152
+ # scheme://authority/path#fragment
153
+ #
154
+ # See RFC 3986 for details of URI.
155
+ #
156
+ # Note that this function is not appropriate to convert OS path to URI.
157
+ def uri_path(str)
158
+ s = str.gsub(%r{[^/]+}n) { uri_segment($&) }
159
+ PercentEncoded.new_no_dup(s)
160
+ end
161
+
162
+ # :stopdoc:
163
+ def html_form_fast(pairs, sep='&')
164
+ s = pairs.map {|k, v|
165
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
166
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
167
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
168
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
169
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
170
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
171
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
172
+ k = k.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
173
+ '%' + $&.unpack("H2")[0].upcase
174
+ }
175
+ v = v.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
176
+ '%' + $&.unpack("H2")[0].upcase
177
+ }
178
+ "#{k}=#{v}"
179
+ }.join(sep)
180
+ PercentEncoded.new_no_dup(s)
181
+ end
182
+ # :startdoc:
183
+
184
+ # Escape.html_form composes HTML form key-value pairs as a x-www-form-urlencoded encoded string.
185
+ # It returns an instance of PercentEncoded.
186
+ #
187
+ # Escape.html_form takes an array of pair of strings or
188
+ # an hash from string to string.
189
+ #
190
+ # Escape.html_form([["a","b"], ["c","d"]]) #=> #<Escape::PercentEncoded: a=b&c=d>
191
+ # Escape.html_form({"a"=>"b", "c"=>"d"}) #=> #<Escape::PercentEncoded: a=b&c=d>
192
+ #
193
+ # In the array form, it is possible to use same key more than once.
194
+ # (It is required for a HTML form which contains
195
+ # checkboxes and select element with multiple attribute.)
196
+ #
197
+ # Escape.html_form([["k","1"], ["k","2"]]) #=> #<Escape::PercentEncoded: k=1&k=2>
198
+ #
199
+ # If the strings contains characters which must be escaped in x-www-form-urlencoded,
200
+ # they are escaped using %-encoding.
201
+ #
202
+ # Escape.html_form([["k=","&;="]]) #=> #<Escape::PercentEncoded: k%3D=%26%3B%3D>
203
+ #
204
+ # The separator can be specified by the optional second argument.
205
+ #
206
+ # Escape.html_form([["a","b"], ["c","d"]], ";") #=> #<Escape::PercentEncoded: a=b;c=d>
207
+ #
208
+ # See HTML 4.01 for details.
209
+ def html_form(pairs, sep='&')
210
+ r = ''
211
+ first = true
212
+ pairs.each {|k, v|
213
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
214
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
215
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
216
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
217
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
218
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
219
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
220
+ r << sep if !first
221
+ first = false
222
+ k.each_byte {|byte|
223
+ ch = byte.chr
224
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
225
+ r << "%" << ch.unpack("H2")[0].upcase
226
+ else
227
+ r << ch
228
+ end
229
+ }
230
+ r << '='
231
+ v.each_byte {|byte|
232
+ ch = byte.chr
233
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
234
+ r << "%" << ch.unpack("H2")[0].upcase
235
+ else
236
+ r << ch
237
+ end
238
+ }
239
+ }
240
+ PercentEncoded.new_no_dup(r)
241
+ end
242
+
243
+ class HTMLEscaped < StringWrapper
244
+ end
245
+
246
+ # :stopdoc:
247
+ HTML_TEXT_ESCAPE_HASH = {
248
+ '&' => '&amp;',
249
+ '<' => '&lt;',
250
+ '>' => '&gt;',
251
+ }
252
+ # :startdoc:
253
+
254
+ # Escape.html_text escapes a string appropriate for HTML text using character references.
255
+ # It returns an instance of HTMLEscaped.
256
+ #
257
+ # It escapes 3 characters:
258
+ # * '&' to '&amp;'
259
+ # * '<' to '&lt;'
260
+ # * '>' to '&gt;'
261
+ #
262
+ # Escape.html_text("abc") #=> #<Escape::HTMLEscaped: abc>
263
+ # Escape.html_text("a & b < c > d") #=> #<Escape::HTMLEscaped: a &amp; b &lt; c &gt; d>
264
+ #
265
+ # This function is not appropriate for escaping HTML element attribute
266
+ # because quotes are not escaped.
267
+ def html_text(str)
268
+ s = str.gsub(/[&<>]/) {|ch| HTML_TEXT_ESCAPE_HASH[ch] }
269
+ HTMLEscaped.new_no_dup(s)
270
+ end
271
+
272
+ # :stopdoc:
273
+ HTML_ATTR_ESCAPE_HASH = {
274
+ '&' => '&amp;',
275
+ '<' => '&lt;',
276
+ '>' => '&gt;',
277
+ '"' => '&quot;',
278
+ }
279
+ # :startdoc:
280
+
281
+ class HTMLAttrValue < StringWrapper
282
+ end
283
+
284
+ # Escape.html_attr_value encodes a string as a double-quoted HTML attribute using character references.
285
+ # It returns an instance of HTMLAttrValue.
286
+ #
287
+ # Escape.html_attr_value("abc") #=> #<Escape::HTMLAttrValue: "abc">
288
+ # Escape.html_attr_value("a&b") #=> #<Escape::HTMLAttrValue: "a&amp;b">
289
+ # Escape.html_attr_value("ab&<>\"c") #=> #<Escape::HTMLAttrValue: "ab&amp;&lt;&gt;&quot;c">
290
+ # Escape.html_attr_value("a'c") #=> #<Escape::HTMLAttrValue: "a'c">
291
+ #
292
+ # It escapes 4 characters:
293
+ # * '&' to '&amp;'
294
+ # * '<' to '&lt;'
295
+ # * '>' to '&gt;'
296
+ # * '"' to '&quot;'
297
+ #
298
+ def html_attr_value(str)
299
+ s = '"' + str.gsub(/[&<>"]/) {|ch| HTML_ATTR_ESCAPE_HASH[ch] } + '"'
300
+ HTMLAttrValue.new_no_dup(s)
301
+ end
302
+ end