yugui-chkbuild 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. data/README.ja.rd +191 -0
  2. data/Rakefile +56 -0
  3. data/VERSION +1 -0
  4. data/bin/last-build +28 -0
  5. data/bin/start-build +37 -0
  6. data/chkbuild.gemspec +107 -0
  7. data/core_ext/io.rb +17 -0
  8. data/core_ext/string.rb +10 -0
  9. data/lib/chkbuild.rb +45 -0
  10. data/lib/chkbuild/build.rb +718 -0
  11. data/lib/chkbuild/lock.rb +57 -0
  12. data/lib/chkbuild/logfile.rb +230 -0
  13. data/lib/chkbuild/main.rb +138 -0
  14. data/lib/chkbuild/options.rb +62 -0
  15. data/lib/chkbuild/scm/cvs.rb +132 -0
  16. data/lib/chkbuild/scm/git.rb +223 -0
  17. data/lib/chkbuild/scm/svn.rb +215 -0
  18. data/lib/chkbuild/scm/xforge.rb +33 -0
  19. data/lib/chkbuild/target.rb +180 -0
  20. data/lib/chkbuild/targets/gcc.rb +94 -0
  21. data/lib/chkbuild/targets/ruby.rb +456 -0
  22. data/lib/chkbuild/title.rb +107 -0
  23. data/lib/chkbuild/upload.rb +66 -0
  24. data/lib/misc/escape.rb +535 -0
  25. data/lib/misc/gdb.rb +74 -0
  26. data/lib/misc/timeoutcom.rb +174 -0
  27. data/lib/misc/udiff.rb +244 -0
  28. data/lib/misc/util.rb +232 -0
  29. data/sample/build-autoconf-ruby +69 -0
  30. data/sample/build-gcc-ruby +43 -0
  31. data/sample/build-ruby +37 -0
  32. data/sample/build-ruby2 +36 -0
  33. data/sample/build-svn +55 -0
  34. data/sample/build-yarv +35 -0
  35. data/sample/test-apr +12 -0
  36. data/sample/test-catcherr +23 -0
  37. data/sample/test-combfail +21 -0
  38. data/sample/test-core +14 -0
  39. data/sample/test-core2 +19 -0
  40. data/sample/test-date +9 -0
  41. data/sample/test-dep +17 -0
  42. data/sample/test-depver +14 -0
  43. data/sample/test-echo +9 -0
  44. data/sample/test-env +9 -0
  45. data/sample/test-error +9 -0
  46. data/sample/test-fail +18 -0
  47. data/sample/test-fmesg +16 -0
  48. data/sample/test-gcc-v +15 -0
  49. data/sample/test-git +11 -0
  50. data/sample/test-leave-proc +9 -0
  51. data/sample/test-limit +9 -0
  52. data/sample/test-make +9 -0
  53. data/sample/test-neterr +16 -0
  54. data/sample/test-savannah +14 -0
  55. data/sample/test-sleep +9 -0
  56. data/sample/test-timeout +9 -0
  57. data/sample/test-timeout2 +10 -0
  58. data/sample/test-timeout3 +9 -0
  59. data/sample/test-upload +13 -0
  60. data/sample/test-warn +13 -0
  61. data/setup/upload-rsync-ssh +572 -0
  62. data/test/misc/test-escape.rb +17 -0
  63. data/test/misc/test-logfile.rb +108 -0
  64. data/test/misc/test-timeoutcom.rb +23 -0
  65. data/test/test_helper.rb +9 -0
  66. metadata +123 -0
@@ -0,0 +1,107 @@
1
+ # Copyright (C) 2006,2009 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ require 'util'
26
+
27
+ class ChkBuild::Title
28
+ def initialize(target, logfile)
29
+ @target = target
30
+ @logfile = logfile
31
+ @title = {}
32
+ @title[:version] = @logfile.suffixed_name
33
+ @title[:dep_versions] = []
34
+ @title[:hostname] = "(#{Util.simple_hostname})"
35
+ @title_order = [:version, :dep_versions, :hostname, :warn, :mark, :status]
36
+ @logfile.each_secname {|secname|
37
+ log = @logfile.get_section(secname)
38
+ lastline = log.chomp("").lastline
39
+ if /\Afailed\(.*\)\z/ =~ lastline
40
+ sym = "failure_#{secname}".intern
41
+ @title_order << sym
42
+ @title[sym] = lastline
43
+ end
44
+ }
45
+ end
46
+ attr_reader :logfile
47
+
48
+ def version
49
+ return @title[:version]
50
+ end
51
+
52
+ def depsuffixed_name() @logfile.depsuffixed_name end
53
+ def suffixed_name() @logfile.suffixed_name end
54
+ def target_name() @logfile.target_name end
55
+ def suffixes() @logfile.suffixes end
56
+
57
+ def run_hooks
58
+ run_title_hooks
59
+ run_failure_hooks
60
+ end
61
+
62
+ def run_title_hooks
63
+ @target.each_title_hook {|secname, block|
64
+ if secname == nil
65
+ block.call self, @logfile.get_all_log
66
+ elsif log = @logfile.get_section(secname)
67
+ block.call self, log
68
+ end
69
+ }
70
+ end
71
+
72
+ def run_failure_hooks
73
+ @target.each_failure_hook {|secname, block|
74
+ if log = @logfile.get_section(secname)
75
+ lastline = log.chomp("").lastline
76
+ if /\Afailed\(.*\)\z/ =~ lastline
77
+ sym = "failure_#{secname}".intern
78
+ if newval = block.call(log)
79
+ @title[sym] = newval
80
+ end
81
+ end
82
+ end
83
+ }
84
+ end
85
+
86
+ def update_title(key, val=nil)
87
+ if val == nil && block_given?
88
+ val = yield @title[key]
89
+ return if !val
90
+ end
91
+ @title[key] = val
92
+ unless @title_order.include? key
93
+ @title_order[-1,0] = [key]
94
+ end
95
+ end
96
+
97
+ def make_title
98
+ title_hash = @title
99
+ @title_order.map {|key|
100
+ title_hash[key]
101
+ }.flatten.join(' ').gsub(/\s+/, ' ').strip
102
+ end
103
+
104
+ def [](key)
105
+ @title[key]
106
+ end
107
+ end
@@ -0,0 +1,66 @@
1
+ # Copyright (C) 2006 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ module ChkBuild
26
+ @upload_hook = []
27
+
28
+ def self.add_upload_hook(&block)
29
+ @upload_hook << block
30
+ end
31
+
32
+ def self.run_upload_hooks(suffixed_name)
33
+ @upload_hook.reverse_each {|block|
34
+ begin
35
+ block.call suffixed_name
36
+ rescue Exception
37
+ p $!
38
+ end
39
+ }
40
+ end
41
+
42
+ # rsync/ssh
43
+
44
+ def self.rsync_ssh_upload_target(rsync_target, private_key=nil)
45
+ self.add_upload_hook {|name|
46
+ self.do_upload_rsync_ssh(rsync_target, private_key, name)
47
+ }
48
+ end
49
+
50
+ def self.do_upload_rsync_ssh(rsync_target, private_key, name)
51
+ if %r{\A(?:([^@:]+)@)([^:]+)::(.*)\z} !~ rsync_target
52
+ raise "invalid rsync target: #{rsync_target.inspect}"
53
+ end
54
+ remote_user = $1 || ENV['USER'] || Etc.getpwuid.name
55
+ remote_host = $2
56
+ remote_path = $3
57
+ local_host = Socket.gethostname
58
+ private_key ||= "#{ENV['HOME']}/.ssh/chkbuild-#{local_host}-#{remote_host}"
59
+
60
+ pid = fork {
61
+ ENV.delete 'SSH_AUTH_SOCK'
62
+ exec "rsync", "--delete", "-rte", "ssh -akxi #{private_key}", "#{ChkBuild.public_top}/#{name}", "#{rsync_target}"
63
+ }
64
+ Process.wait pid
65
+ end
66
+ end
@@ -0,0 +1,535 @@
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
+ # * MIME parameter
32
+ module Escape
33
+ module_function
34
+
35
+ class StringWrapper
36
+ class << self
37
+ alias new_no_dup new
38
+ def new(str)
39
+ new_no_dup(str.dup)
40
+ end
41
+ end
42
+
43
+ def initialize(str)
44
+ @str = str
45
+ end
46
+
47
+ def escaped_string
48
+ @str.dup
49
+ end
50
+
51
+ alias to_s escaped_string
52
+
53
+ def inspect
54
+ "\#<#{self.class}: #{@str}>"
55
+ end
56
+
57
+ def ==(other)
58
+ other.class == self.class && @str == other.instance_variable_get(:@str)
59
+ end
60
+ alias eql? ==
61
+
62
+ def hash
63
+ @str.hash
64
+ end
65
+ end
66
+
67
+ class ShellEscaped < StringWrapper
68
+ end
69
+
70
+ # Escape.shell_command composes
71
+ # a sequence of words to
72
+ # a single shell command line.
73
+ # All shell meta characters are quoted and
74
+ # the words are concatenated with interleaving space.
75
+ # It returns an instance of ShellEscaped.
76
+ #
77
+ # Escape.shell_command(["ls", "/"]) #=> #<Escape::ShellEscaped: ls />
78
+ # Escape.shell_command(["echo", "*"]) #=> #<Escape::ShellEscaped: echo '*'>
79
+ #
80
+ # Note that system(*command) and
81
+ # system(Escape.shell_command(command).to_s) is roughly same.
82
+ # There are two exception as follows.
83
+ # * The first is that the later may invokes /bin/sh.
84
+ # * The second is an interpretation of an array with only one element:
85
+ # the element is parsed by the shell with the former but
86
+ # it is recognized as single word with the later.
87
+ # For example, system(*["echo foo"]) invokes echo command with an argument "foo".
88
+ # But system(Escape.shell_command(["echo foo"]).to_s) invokes "echo foo" command
89
+ # without arguments (and it probably fails).
90
+ def shell_command(command)
91
+ s = command.map {|word| shell_single_word(word) }.join(' ')
92
+ ShellEscaped.new_no_dup(s)
93
+ end
94
+
95
+ # Escape.shell_single_word quotes shell meta characters.
96
+ # It returns an instance of ShellEscaped.
97
+ #
98
+ # The result string is always single shell word, even if
99
+ # the argument is "".
100
+ # Escape.shell_single_word("") returns #<Escape::ShellEscaped: ''>.
101
+ #
102
+ # Escape.shell_single_word("") #=> #<Escape::ShellEscaped: ''>
103
+ # Escape.shell_single_word("foo") #=> #<Escape::ShellEscaped: foo>
104
+ # Escape.shell_single_word("*") #=> #<Escape::ShellEscaped: '*'>
105
+ def shell_single_word(str)
106
+ if str.empty?
107
+ ShellEscaped.new_no_dup("''")
108
+ elsif %r{\A[0-9A-Za-z+,./:=@_-]+\z} =~ str
109
+ ShellEscaped.new(str)
110
+ else
111
+ result = ''
112
+ str.scan(/('+)|[^']+/) {
113
+ if $1
114
+ result << %q{\'} * $1.length
115
+ else
116
+ result << "'#{$&}'"
117
+ end
118
+ }
119
+ ShellEscaped.new_no_dup(result)
120
+ end
121
+ end
122
+
123
+ class InvalidHTMLForm < StandardError
124
+ end
125
+ class PercentEncoded < StringWrapper
126
+ # Escape::PercentEncoded#split_html_form decodes
127
+ # percent-encoded string as
128
+ # application/x-www-form-urlencoded
129
+ # defined by HTML specification.
130
+ #
131
+ # It recognizes "&" and ";" as a separator of key-value pairs.
132
+ #
133
+ # If it find is not valid as
134
+ # application/x-www-form-urlencoded,
135
+ # Escape::InvalidHTMLForm exception is raised.
136
+ #
137
+ # Escape::PercentEncoded.new("a=b&c=d")
138
+ # #=> [[#<Escape::PercentEncoded: a>, #<Escape::PercentEncoded: b>],
139
+ # [#<Escape::PercentEncoded: c>, #<Escape::PercentEncoded: d>]]
140
+ #
141
+ # Escape::PercentEncoded.new("a=b;c=d").split_html_form
142
+ # #=> [[#<Escape::PercentEncoded: a>, #<Escape::PercentEncoded: b>],
143
+ # [#<Escape::PercentEncoded: c>, #<Escape::PercentEncoded: d>]]
144
+ #
145
+ # Escape::PercentEncoded.new("%3D=%3F").split_html_form
146
+ # #=> [[#<Escape::PercentEncoded: %3D>, #<Escape::PercentEncoded: %3F>]]
147
+ #
148
+ def split_html_form
149
+ assoc = []
150
+ @str.split(/[&;]/, -1).each {|s|
151
+ raise InvalidHTMLForm, "invalid: #{@str}" unless /=/ =~ s
152
+ assoc << [PercentEncoded.new_no_dup($`), PercentEncoded.new_no_dup($')]
153
+ }
154
+ assoc
155
+ end
156
+ end
157
+
158
+ # Escape.percent_encoding escapes URI non-unreserved characters using percent-encoding.
159
+ # It returns an instance of PercentEncoded.
160
+ #
161
+ # The unreserved characters are alphabet, digit, hyphen, dot, underscore and tilde.
162
+ # [RFC 3986]
163
+ #
164
+ # Escape.percent_encoding("foo") #=> #<Escape::PercentEncoded: foo>
165
+ #
166
+ # Escape.percent_encoding(' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~')
167
+ # #=> #<Escape::PercentEncoded: %20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~>
168
+ def percent_encoding(str)
169
+ s = str.gsub(%r{[^a-zA-Za-z0-9\-._~]}n) {
170
+ '%' + $&.unpack("H2")[0].upcase
171
+ }
172
+ PercentEncoded.new_no_dup(s)
173
+ end
174
+
175
+ # Escape.uri_segment escapes URI segment using percent-encoding.
176
+ # It returns an instance of PercentEncoded.
177
+ #
178
+ # Escape.uri_segment("a/b") #=> #<Escape::PercentEncoded: a%2Fb>
179
+ #
180
+ # The segment is "/"-splitted element after authority before query in URI, as follows.
181
+ #
182
+ # scheme://authority/segment1/segment2/.../segmentN?query#fragment
183
+ #
184
+ # See RFC 3986 for details of URI.
185
+ def uri_segment(str)
186
+ # pchar - pct-encoded = unreserved / sub-delims / ":" / "@"
187
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
188
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
189
+ s = str.gsub(%r{[^A-Za-z0-9\-._~!$&'()*+,;=:@]}n) {
190
+ '%' + $&.unpack("H2")[0].upcase
191
+ }
192
+ PercentEncoded.new_no_dup(s)
193
+ end
194
+
195
+ # Escape.uri_path escapes URI path using percent-encoding.
196
+ #
197
+ # The given path should be one of follows.
198
+ # * a sequence of (non-escaped) segments separated by "/". (The segments cannot contains "/".)
199
+ # * an array containing (non-escaped) segments. (The segments may contains "/".)
200
+ #
201
+ # It returns an instance of PercentEncoded.
202
+ #
203
+ # Escape.uri_path("a/b/c") #=> #<Escape::PercentEncoded: a/b/c>
204
+ # Escape.uri_path("a?b/c?d/e?f") #=> #<Escape::PercentEncoded: a%3Fb/c%3Fd/e%3Ff>
205
+ # Escape.uri_path(%w[/d f]) #=> "%2Fd/f"
206
+ #
207
+ # The path is the part after authority before query in URI, as follows.
208
+ #
209
+ # scheme://authority/path#fragment
210
+ #
211
+ # See RFC 3986 for details of URI.
212
+ #
213
+ # Note that this function is not appropriate to convert OS path to URI.
214
+ def uri_path(arg)
215
+ if arg.respond_to? :to_ary
216
+ s = arg.map {|elt| uri_segment(elt) }.join('/')
217
+ else
218
+ s = arg.gsub(%r{[^/]+}n) { uri_segment($&) }
219
+ end
220
+ PercentEncoded.new_no_dup(s)
221
+ end
222
+
223
+ # :stopdoc:
224
+ def html_form_fast(pairs, sep='&')
225
+ s = pairs.map {|k, v|
226
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
227
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
228
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
229
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
230
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
231
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
232
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
233
+ k = k.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
234
+ '%' + $&.unpack("H2")[0].upcase
235
+ }
236
+ v = v.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
237
+ '%' + $&.unpack("H2")[0].upcase
238
+ }
239
+ "#{k}=#{v}"
240
+ }.join(sep)
241
+ PercentEncoded.new_no_dup(s)
242
+ end
243
+ # :startdoc:
244
+
245
+ # Escape.html_form composes HTML form key-value pairs as a x-www-form-urlencoded encoded string.
246
+ # It returns an instance of PercentEncoded.
247
+ #
248
+ # Escape.html_form takes an array of pair of strings or
249
+ # an hash from string to string.
250
+ #
251
+ # Escape.html_form([["a","b"], ["c","d"]]) #=> #<Escape::PercentEncoded: a=b&c=d>
252
+ # Escape.html_form({"a"=>"b", "c"=>"d"}) #=> #<Escape::PercentEncoded: a=b&c=d>
253
+ #
254
+ # In the array form, it is possible to use same key more than once.
255
+ # (It is required for a HTML form which contains
256
+ # checkboxes and select element with multiple attribute.)
257
+ #
258
+ # Escape.html_form([["k","1"], ["k","2"]]) #=> #<Escape::PercentEncoded: k=1&k=2>
259
+ #
260
+ # If the strings contains characters which must be escaped in x-www-form-urlencoded,
261
+ # they are escaped using %-encoding.
262
+ #
263
+ # Escape.html_form([["k=","&;="]]) #=> #<Escape::PercentEncoded: k%3D=%26%3B%3D>
264
+ #
265
+ # The separator can be specified by the optional second argument.
266
+ #
267
+ # Escape.html_form([["a","b"], ["c","d"]], ";") #=> #<Escape::PercentEncoded: a=b;c=d>
268
+ #
269
+ # See HTML 4.01 for details.
270
+ def html_form(pairs, sep='&')
271
+ r = ''
272
+ first = true
273
+ pairs.each {|k, v|
274
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
275
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
276
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
277
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
278
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
279
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
280
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
281
+ r << sep if !first
282
+ first = false
283
+ k.each_byte {|byte|
284
+ ch = byte.chr
285
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
286
+ r << "%" << ch.unpack("H2")[0].upcase
287
+ else
288
+ r << ch
289
+ end
290
+ }
291
+ r << '='
292
+ v.each_byte {|byte|
293
+ ch = byte.chr
294
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
295
+ r << "%" << ch.unpack("H2")[0].upcase
296
+ else
297
+ r << ch
298
+ end
299
+ }
300
+ }
301
+ PercentEncoded.new_no_dup(r)
302
+ end
303
+
304
+ class HTMLEscaped < StringWrapper
305
+ end
306
+
307
+ # :stopdoc:
308
+ HTML_TEXT_ESCAPE_HASH = {
309
+ '&' => '&amp;',
310
+ '<' => '&lt;',
311
+ '>' => '&gt;',
312
+ }
313
+ # :startdoc:
314
+
315
+ # Escape.html_text escapes a string appropriate for HTML text using character references.
316
+ # It returns an instance of HTMLEscaped.
317
+ #
318
+ # It escapes 3 characters:
319
+ # * '&' to '&amp;'
320
+ # * '<' to '&lt;'
321
+ # * '>' to '&gt;'
322
+ #
323
+ # Escape.html_text("abc") #=> #<Escape::HTMLEscaped: abc>
324
+ # Escape.html_text("a & b < c > d") #=> #<Escape::HTMLEscaped: a &amp; b &lt; c &gt; d>
325
+ #
326
+ # This function is not appropriate for escaping HTML element attribute
327
+ # because quotes are not escaped.
328
+ def html_text(str)
329
+ s = str.gsub(/[&<>]/) {|ch| HTML_TEXT_ESCAPE_HASH[ch] }
330
+ HTMLEscaped.new_no_dup(s)
331
+ end
332
+
333
+ # :stopdoc:
334
+ HTML_ATTR_ESCAPE_HASH = {
335
+ '&' => '&amp;',
336
+ '<' => '&lt;',
337
+ '>' => '&gt;',
338
+ '"' => '&quot;',
339
+ }
340
+ # :startdoc:
341
+
342
+ class HTMLAttrValue < StringWrapper
343
+ end
344
+
345
+ # Escape.html_attr_value encodes a string as a double-quoted HTML attribute using character references.
346
+ # It returns an instance of HTMLAttrValue.
347
+ #
348
+ # Escape.html_attr_value("abc") #=> #<Escape::HTMLAttrValue: "abc">
349
+ # Escape.html_attr_value("a&b") #=> #<Escape::HTMLAttrValue: "a&amp;b">
350
+ # Escape.html_attr_value("ab&<>\"c") #=> #<Escape::HTMLAttrValue: "ab&amp;&lt;&gt;&quot;c">
351
+ # Escape.html_attr_value("a'c") #=> #<Escape::HTMLAttrValue: "a'c">
352
+ #
353
+ # It escapes 4 characters:
354
+ # * '&' to '&amp;'
355
+ # * '<' to '&lt;'
356
+ # * '>' to '&gt;'
357
+ # * '"' to '&quot;'
358
+ #
359
+ def html_attr_value(str)
360
+ s = '"' + str.gsub(/[&<>"]/) {|ch| HTML_ATTR_ESCAPE_HASH[ch] } + '"'
361
+ HTMLAttrValue.new_no_dup(s)
362
+ end
363
+
364
+ # MIMEParameter represents parameter, token, quoted-string in MIME.
365
+ # parameter and token is defined in RFC 2045.
366
+ # quoted-string is defined in RFC 822.
367
+ class MIMEParameter < StringWrapper
368
+ end
369
+
370
+ # predicate for MIME token.
371
+ #
372
+ # token is a sequence of any (US-ASCII) CHAR except SPACE, CTLs, or tspecials.
373
+ def mime_token?(str)
374
+ /\A[!\#-'*+\-.0-9A-Z^-~]+\z/ =~ str ? true : false
375
+ end
376
+
377
+ # :stopdoc:
378
+ RFC2822_FWS = /(?:[ \t]*\r?\n)?[ \t]+/
379
+ # :startdoc:
380
+
381
+ # Escape.rfc2822_quoted_string escapes a string as quoted-string defined in RFC 2822.
382
+ # It returns an instance of MIMEParameter.
383
+ #
384
+ # The obsolete syntax in quoted-string is not permitted.
385
+ # For example, NUL causes ArgumentError.
386
+ #
387
+ # The given string may contain carriage returns ("\r") and line feeds ("\n").
388
+ # However they must be part of folding white space: /\r\n[ \t]/ or /\n[ \t]/.
389
+ # Escape.rfc2822_quoted_string assumes that newlines are represented as
390
+ # "\n" or "\r\n".
391
+ #
392
+ # Escape.rfc2822_quoted_string does not permit consecutive sequence of
393
+ # folding white spaces such as "\n \n ", according to RFC 2822 syntax.
394
+ def rfc2822_quoted_string(str)
395
+ if /\A(?:#{RFC2822_FWS}?[\x01-\x09\x0b\x0c\x0e-\x7f])*#{RFC2822_FWS}?\z/o !~ str
396
+ raise ArgumentError, "not representable in quoted-string of RFC 2822: #{str.inspect}"
397
+ end
398
+ s = '"' + str.gsub(/["\\]/, '\\\\\&') + '"'
399
+ MIMEParameter.new_no_dup(s)
400
+ end
401
+
402
+ # Escape.mime_parameter_value escapes a string as MIME parameter value in RFC 2045.
403
+ # It returns an instance of MIMEParameter.
404
+ #
405
+ # MIME parameter value is token or quoted-string.
406
+ # token is used if possible.
407
+ def mime_parameter_value(str)
408
+ if mime_token?(str)
409
+ MIMEParameter.new(str)
410
+ else
411
+ rfc2822_quoted_string(str)
412
+ end
413
+ end
414
+
415
+ # Escape.mime_parameter encodes attribute and value as MIME parameter in RFC 2045.
416
+ # It returns an instance of MIMEParameter.
417
+ #
418
+ # ArgumentError is raised if attribute is not MIME token.
419
+ #
420
+ # ArgumentError is raised if value contains CR, LF or NUL.
421
+ #
422
+ # Escape.mime_parameter("n", "v") #=> #<Escape::MIMEParameter: n=v>
423
+ # Escape.mime_parameter("charset", "us-ascii") #=> #<Escape::MIMEParameter: charset=us-ascii>
424
+ # Escape.mime_parameter("boundary", "gc0pJq0M:08jU534c0p") #=> #<Escape::MIMEParameter: boundary="gc0pJq0M:08jU534c0p">
425
+ # Escape.mime_parameter("boundary", "simple boundary") #=> #<Escape::MIMEParameter: boundary="simple boundary">
426
+ def mime_parameter(attribute, value)
427
+ unless mime_token?(attribute)
428
+ raise ArgumentError, "not MIME token: #{attribute.inspect}"
429
+ end
430
+ MIMEParameter.new("#{attribute}=#{mime_parameter_value(value)}")
431
+ end
432
+
433
+ # predicate for MIME token.
434
+ #
435
+ # token is a sequence of any CHAR except CTLs or separators
436
+ def http_token?(str)
437
+ /\A[!\#-'*+\-.0-9A-Z^-z|~]+\z/ =~ str ? true : false
438
+ end
439
+
440
+ # Escape.http_quoted_string escapes a string as quoted-string defined in RFC 2616.
441
+ # It returns an instance of MIMEParameter.
442
+ #
443
+ # The given string may contain carriage returns ("\r") and line feeds ("\n").
444
+ # However they must be part of folding white space: /\r\n[ \t]/ or /\n[ \t]/.
445
+ # Escape.http_quoted_string assumes that newlines are represented as
446
+ # "\n" or "\r\n".
447
+ def http_quoted_string(str)
448
+ if /\A(?:[\0-\x09\x0b\x0c\x0e-\xff]|\r?\n[ \t])*\z/ !~ str
449
+ raise ArgumentError, "CR or LF not part of folding white space exists: #{str.inspect}"
450
+ end
451
+ s = '"' + str.gsub(/["\\]/, '\\\\\&') + '"'
452
+ MIMEParameter.new_no_dup(s)
453
+ end
454
+
455
+ # Escape.http_parameter_value escapes a string as HTTP parameter value in RFC 2616.
456
+ # It returns an instance of MIMEParameter.
457
+ #
458
+ # HTTP parameter value is token or quoted-string.
459
+ # token is used if possible.
460
+ def http_parameter_value(str)
461
+ if http_token?(str)
462
+ MIMEParameter.new(str)
463
+ else
464
+ http_quoted_string(str)
465
+ end
466
+ end
467
+
468
+ # Escape.http_parameter encodes attribute and value as HTTP parameter in RFC 2616.
469
+ # It returns an instance of MIMEParameter.
470
+ #
471
+ # ArgumentError is raised if attribute is not HTTP token.
472
+ #
473
+ # ArgumentError is raised if value is not representable in quoted-string.
474
+ #
475
+ # Escape.http_parameter("n", "v") #=> #<Escape::MIMEParameter: n=v>
476
+ # Escape.http_parameter("charset", "us-ascii") #=> #<Escape::MIMEParameter: charset=us-ascii>
477
+ # Escape.http_parameter("q", "0.2") #=> #<Escape::MIMEParameter: q=0.2>
478
+ def http_parameter(attribute, value)
479
+ unless http_token?(attribute)
480
+ raise ArgumentError, "not HTTP token: #{attribute.inspect}"
481
+ end
482
+ MIMEParameter.new("#{attribute}=#{http_parameter_value(value)}")
483
+ end
484
+
485
+ # :stopdoc:
486
+ def _parse_http_params_args(args)
487
+ pairs = []
488
+ until args.empty?
489
+ if args[0].respond_to?(:to_str) && args[1].respond_to?(:to_str)
490
+ pairs << [args.shift, args.shift]
491
+ else
492
+ raise ArgumentError, "unexpected argument: #{args.inspect}"
493
+ end
494
+ end
495
+ pairs
496
+ end
497
+ # :startdoc:
498
+
499
+ # Escape.http_params_with_sep encodes parameters and joins with sep.
500
+ #
501
+ # Escape.http_params_with_sep("; ", "foo", "bar")
502
+ # #=> #<Escape::MIMEParameter: foo=bar>
503
+ #
504
+ # Escape.http_params_with_sep("; ", "foo", "bar", "hoge", "fuga")
505
+ # #=> #<Escape::MIMEParameter: foo=bar; hoge=fuga>
506
+ #
507
+ # If args are empty, empty MIMEParameter is returned.
508
+ #
509
+ # Escape.http_params_with_sep("; ") #=> #<Escape::MIMEParameter: >
510
+ #
511
+ def http_params_with_sep(sep, *args)
512
+ pairs = _parse_http_params_args(args)
513
+ s = pairs.map {|attribute, value| http_parameter(attribute, value) }.join(sep)
514
+ MIMEParameter.new_no_dup(s)
515
+ end
516
+
517
+ # Escape.http_params_with_pre encodes parameters and joins with given prefix.
518
+ #
519
+ # Escape.http_params_with_pre("; ", "foo", "bar")
520
+ # #=> #<Escape::MIMEParameter: ; foo=bar>
521
+ #
522
+ # Escape.http_params_with_pre("; ", "foo", "bar", "hoge", "fuga")
523
+ # #=> #<Escape::MIMEParameter: ; foo=bar; hoge=fuga>
524
+ #
525
+ # If args are empty, empty MIMEParameter is returned.
526
+ #
527
+ # Escape.http_params_with_pre("; ") #=> #<Escape::MIMEParameter: >
528
+ #
529
+ def http_params_with_pre(pre, *args)
530
+ pairs = _parse_http_params_args(args)
531
+ s = pairs.map {|attribute, value| pre + http_parameter(attribute, value).to_s }.join('')
532
+ MIMEParameter.new_no_dup(s)
533
+ end
534
+
535
+ end