ion1-mischacks 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ === 0.0.4 / 2009-06-28
2
+
3
+ * Implement overwrite, a method to safely replace a file.
4
+ * Update README.
5
+ * Move exception printing from sh to catching_exit.
6
+
1
7
  === 0.0.3 / 2009-06-28
2
8
 
3
9
  * Rename checking_exit_status as fork_and_check to make it clear it forks.
data/README.txt CHANGED
@@ -4,7 +4,30 @@
4
4
 
5
5
  == DESCRIPTION:
6
6
 
7
- Safely pass untrusted parameters to sh scripts
7
+ Miscellaneous methods that may or may not be useful.
8
+
9
+ sh:: Safely pass untrusted parameters to sh scripts.
10
+
11
+ fork_and_check:: Run a block in a forked process and raise an exception if the
12
+ process returns a non-zero value.
13
+
14
+ do_and_exit, do_and_exit!:: Run a block. If the block does not run exit!, a
15
+ successful exec or equivalent, run exit(1) or exit!(1) ourselves. Useful to
16
+ make sure a forked block either runs a successful exec or dies.
17
+
18
+ Any exceptions from the block are printed to standard error.
19
+
20
+ overwrite:: Safely replace a file. Writes to a temporary file and then moves it
21
+ over the old file.
22
+
23
+ tempname_for:: Generates an unique temporary path based on a filename. The
24
+ generated filename resides in the same directory as the original one.
25
+
26
+ try_n_times:: Retries a block of code until it succeeds or a maximum number of
27
+ attempts (default 10) is exceeded.
28
+
29
+ Exception#to_formatted_string:: Returns a string that looks like how Ruby would
30
+ dump an uncaught exception.
8
31
 
9
32
  == FEATURES/PROBLEMS:
10
33
 
@@ -14,6 +37,8 @@ ever use an untrusted variable as a command.
14
37
 
15
38
  == SYNOPSIS:
16
39
 
40
+ # sh
41
+
17
42
  MiscHacks.sh %q{
18
43
  diff -u "$1" "$2" | tr a-z A-Z >"$output"
19
44
  }, '/dev/null', '/etc/motd', :output => 'foo'
@@ -21,6 +46,42 @@ ever use an untrusted variable as a command.
21
46
  unsafe_str = %q{" 'foo' $(bar) `baz` "}
22
47
  MiscHacks.sh 'printf "%s\n" "$1"', unsafe_str
23
48
 
49
+ # fork_and_check
50
+
51
+ # These examples raise MiscHacks::ChildError.
52
+ MiscHacks.fork_and_check do exit! 42 end
53
+ MiscHacks.fork_and_check do exec 'sh', '-c', 'exit 42' end
54
+ MiscHacks.fork_and_check do exec 'failure' end
55
+
56
+ # Does not raise an error.
57
+ MiscHacks.fork_and_check do exit! 0 end
58
+
59
+ # do_and_exit
60
+
61
+ # Prints foo if there are no failures. If anything fails, raises an
62
+ # exception.
63
+ MiscHacks.fork_and_check do
64
+ MiscHacks.do_and_exit! do
65
+ exec 'sh', '-c', 'echo foo'
66
+ end
67
+ end
68
+
69
+ # overwrite
70
+
71
+ MiscHacks.overwrite 'myconfig' do |io|
72
+ io << config.to_yaml
73
+ end
74
+
75
+ # tempname_for
76
+
77
+ MiscHacks.tempname_for '/foo/bar/baz' # => '/foo/bar/.baz.klyce3f517qkh9l'
78
+
79
+ # try_n_times
80
+
81
+ io = MiscHacks.try_n_times do
82
+ File.open path, File::RDWR|File::CREAT|File::EXCL
83
+ end
84
+
24
85
  == REQUIREMENTS:
25
86
 
26
87
  * POSIX sh
data/lib/mischacks.rb CHANGED
@@ -14,7 +14,7 @@
14
14
  # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
15
 
16
16
  module MiscHacks
17
- VERSION = '0.0.3'
17
+ VERSION = '0.0.4'
18
18
 
19
19
  class ChildError < RuntimeError
20
20
  attr_reader :status
@@ -25,6 +25,19 @@ module MiscHacks
25
25
  end
26
26
  end
27
27
 
28
+ def self.sh cmd, *args
29
+ env = if args.last.is_a? Hash then args.pop else {} end
30
+
31
+ fork_and_check do
32
+ do_and_exit! do
33
+ env.each_pair do |k, v| ENV[k.to_s] = v.to_s end
34
+ exec *(%W{sh -e -c #{cmd} sh} + args.map {|a| a.to_s })
35
+ end
36
+ end
37
+
38
+ nil
39
+ end
40
+
28
41
  def self.fork_and_check
29
42
  fork do
30
43
  yield
@@ -43,6 +56,8 @@ module MiscHacks
43
56
  yield
44
57
  rescue SystemExit => e
45
58
  status = e.status
59
+ rescue Exception => e
60
+ warn e.to_formatted_string
46
61
  ensure
47
62
  final_proc.call status
48
63
  end
@@ -58,21 +73,66 @@ module MiscHacks
58
73
  catching_exit method(:exit!), status, &block
59
74
  end
60
75
 
61
- def self.sh cmd, *args
62
- env = if args.last.is_a? Hash then args.pop else {} end
76
+ def self.overwrite path, mode=nil
77
+ begin
78
+ stat = File.stat path
63
79
 
64
- fork_and_check do
65
- do_and_exit! do
66
- begin
67
- env.each_pair do |k, v| ENV[k.to_s] = v.to_s end
68
- exec *(%W{sh -e -c #{cmd} sh} + args.map {|a| a.to_s })
69
- rescue Exception => e
70
- warn e.to_formatted_string
71
- end
72
- end
80
+ raise ArgumentError, "Not a file: #{path}", caller unless stat.file?
81
+ raise ArgumentError, "Not writable: #{path}", caller unless stat.writable?
82
+
83
+ mode ||= stat.mode & 0777
84
+ rescue Errno::ENOENT
85
+ stat = nil
73
86
  end
74
87
 
75
- nil
88
+ temppath, io = try_n_times do
89
+ t = tempname_for path
90
+ io = File.open t, File::RDWR|File::CREAT|File::EXCL
91
+ [t, io]
92
+ end
93
+
94
+ begin
95
+ ret = yield io
96
+
97
+ File.chmod mode, temppath if mode
98
+
99
+ File.rename temppath, path
100
+
101
+ rescue
102
+ File.unlink temppath
103
+ raise
104
+
105
+ ensure
106
+ io.close
107
+ end
108
+
109
+ ret
110
+ end
111
+
112
+ def self.tempname_for path
113
+ dirname = File.dirname path
114
+ basename = File.basename path
115
+
116
+ '%s/.%s.%s%s%s' % [
117
+ dirname,
118
+ basename,
119
+ Time.now.to_i.to_s(36),
120
+ $$.to_s(36),
121
+ rand(1<<32).to_s(36)
122
+ ]
123
+ end
124
+
125
+ def self.try_n_times n=10
126
+ i = 0
127
+ begin
128
+ ret = yield
129
+ rescue
130
+ i += 1
131
+ retry if i < n
132
+ raise
133
+ end
134
+
135
+ ret
76
136
  end
77
137
  end
78
138
 
data/mischacks.gemspec CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{mischacks}
5
- s.version = "0.0.3"
5
+ s.version = "0.0.4"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Johan Kiviniemi"]
9
9
  s.date = %q{2009-06-28}
10
- s.description = %q{Safely pass untrusted parameters to sh scripts}
10
+ s.description = %q{Miscellaneous methods that may or may not be useful. sh:: Safely pass untrusted parameters to sh scripts. fork_and_check:: Run a block in a forked process and raise an exception if the process returns a non-zero value. do_and_exit, do_and_exit!:: Run a block. If the block does not run exit!, a successful exec or equivalent, run exit(1) or exit!(1) ourselves. Useful to make sure a forked block either runs a successful exec or dies. Any exceptions from the block are printed to standard error. overwrite:: Safely replace a file. Writes to a temporary file and then moves it over the old file. tempname_for:: Generates an unique temporary path based on a filename. The generated filename resides in the same directory as the original one. try_n_times:: Retries a block of code until it succeeds or a maximum number of attempts (default 10) is exceeded. Exception#to_formatted_string:: Returns a string that looks like how Ruby would dump an uncaught exception.}
11
11
  s.email = ["devel@johan.kiviniemi.name"]
12
12
  s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
13
13
  s.files = ["COPYING", "History.txt", "Manifest.txt", "README.txt", "Rakefile", "lib/mischacks.rb", "mischacks.gemspec", "spec/mischacks_spec.rb", "spec/spec_helper.rb"]
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.require_paths = ["lib"]
18
18
  s.rubyforge_project = %q{mischacks}
19
19
  s.rubygems_version = %q{1.3.1}
20
- s.summary = %q{Safely pass untrusted parameters to sh scripts}
20
+ s.summary = %q{Miscellaneous methods that may or may not be useful}
21
21
 
22
22
  if s.respond_to? :specification_version then
23
23
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -17,11 +17,64 @@ require File.expand_path(File.dirname(__FILE__)+'/spec_helper.rb')
17
17
  require 'mischacks'
18
18
 
19
19
  require 'digest/sha1'
20
+ require 'fileutils'
21
+ require 'set'
22
+ require 'tmpdir'
20
23
 
21
24
  mh = MiscHacks
22
25
  ce = MiscHacks::ChildError
23
26
 
24
27
  describe mh do
28
+ describe 'sh' do
29
+ unsafe_str = %q{" 'foo' $(bar) `baz` "}
30
+ checksum = Digest::SHA1.hexdigest unsafe_str
31
+
32
+ it "should raise #{ce} on error" do
33
+ good = ['true', '', 'printf ""']
34
+ bad = ['false', 'exit 2', 'return 2']
35
+
36
+ bad.each do |c|
37
+ lambda do mh.sh c end.should raise_error ce
38
+ end
39
+
40
+ good.each do |c|
41
+ lambda do mh.sh c end.should_not raise_error
42
+ end
43
+ end
44
+
45
+ it 'should call sh with -e' do
46
+ lambda do mh.sh 'false; true' end.should raise_error ce
47
+ end
48
+
49
+ it 'should pass normal parameters safely' do
50
+ test = lambda do |str, sha|
51
+ mh.sh %q{
52
+ temp="$(mktemp -t mischacks.XXXXXXXXXX)"
53
+ trap 'rm -f "$temp"' 0 1 2 13 15
54
+ printf "%s" "$1" >"$temp"
55
+ printf "%s *%s\n" "$2" "$temp" | sha1sum -c >/dev/null 2>&1
56
+ }, str, sha
57
+ end
58
+
59
+ lambda do test.call unsafe_str, checksum end.should_not raise_error
60
+ lambda do test.call 'foo', checksum end.should raise_error ce
61
+ end
62
+
63
+ it 'should pass environment variables safely' do
64
+ test = lambda do |str, sha|
65
+ mh.sh %q{
66
+ temp="$(mktemp -t mischacks.XXXXXXXXXX)"
67
+ trap 'rm -f "$temp"' 0 1 2 13 15
68
+ printf "%s" "$string" >"$temp"
69
+ printf "%s *%s\n" "$checksum" "$temp" | sha1sum -c >/dev/null 2>&1
70
+ }, :string => str, :checksum => sha
71
+ end
72
+
73
+ lambda do test.call unsafe_str, checksum end.should_not raise_error
74
+ lambda do test.call 'foo', checksum end.should raise_error ce
75
+ end
76
+ end
77
+
25
78
  describe 'fork_and_check' do
26
79
  it 'should raise an error when child fails' do
27
80
  lambda do mh.fork_and_check do exit 1 end end.should raise_error ce
@@ -127,53 +180,90 @@ describe mh do
127
180
  end
128
181
  end
129
182
 
130
- describe 'sh' do
131
- unsafe_str = %q{" 'foo' $(bar) `baz` "}
132
- checksum = Digest::SHA1.hexdigest unsafe_str
183
+ describe 'overwrite' do
184
+ before :all do
185
+ @dir = Dir.mktmpdir
186
+ @file = "#{@dir}/foo"
187
+ @content0 = "bar\n"
188
+ @content1 = "baz\n"
189
+ @mode0 = 0714
190
+ @mode1 = 0755
191
+ end
133
192
 
134
- it "should raise #{ce} on error" do
135
- good = ['true', '', 'printf ""']
136
- bad = ['false', 'exit 2', 'return 2']
193
+ after :all do
194
+ FileUtils.rm_rf @dir
195
+ end
137
196
 
138
- bad.each do |c|
139
- lambda do mh.sh c end.should raise_error ce
197
+ it 'should create a fresh file' do
198
+ mh.overwrite @file do |io|
199
+ io << @content0
200
+ File.exists?(@file).should == false
140
201
  end
202
+ File.read(@file).should == @content0
203
+ end
141
204
 
142
- good.each do |c|
143
- lambda do mh.sh c end.should_not raise_error
205
+ it 'should overwrite a file' do
206
+ mh.overwrite @file do |io|
207
+ io << @content1
208
+ File.read(@file).should == @content0
144
209
  end
210
+ File.read(@file).should == @content1
145
211
  end
146
212
 
147
- it 'should call sh with -e' do
148
- lambda do mh.sh 'false; true' end.should raise_error ce
213
+ it 'should retain the mode' do
214
+ File.chmod @mode0, @file
215
+ mh.overwrite @file do end
216
+ (File.stat(@file).mode & 07777).should == @mode0
149
217
  end
150
218
 
151
- it 'should pass normal parameters safely' do
152
- test = lambda do |str, sha|
153
- mh.sh %q{
154
- temp="$(mktemp -t mischacks.XXXXXXXXXX)"
155
- trap 'rm -f "$temp"' 0 1 2 13 15
156
- printf "%s" "$1" >"$temp"
157
- printf "%s *%s\n" "$2" "$temp" | sha1sum -c >/dev/null 2>&1
158
- }, str, sha
159
- end
160
-
161
- lambda do test.call unsafe_str, checksum end.should_not raise_error
162
- lambda do test.call 'foo', checksum end.should raise_error ce
219
+ it 'should set the mode' do
220
+ mh.overwrite @file, @mode1 do end
221
+ (File.stat(@file).mode & 07777).should == @mode1
163
222
  end
223
+ end
164
224
 
165
- it 'should pass environment variables safely' do
166
- test = lambda do |str, sha|
167
- mh.sh %q{
168
- temp="$(mktemp -t mischacks.XXXXXXXXXX)"
169
- trap 'rm -f "$temp"' 0 1 2 13 15
170
- printf "%s" "$string" >"$temp"
171
- printf "%s *%s\n" "$checksum" "$temp" | sha1sum -c >/dev/null 2>&1
172
- }, :string => str, :checksum => sha
225
+ describe 'tempname_for' do
226
+ it 'should generate an unique temporary path' do
227
+ path = '/foo/bar/baz'
228
+ tempnames = (0...10).map { mh.tempname_for(path) }.to_set
229
+
230
+ # It is very, very unlikely there are duplicates.
231
+ tempnames.length.should > 8
232
+
233
+ tempnames.each do |n|
234
+ n.should =~ %r{\A/foo/bar/.baz.[a-z0-9]+\z}
173
235
  end
236
+ end
237
+ end
174
238
 
175
- lambda do test.call unsafe_str, checksum end.should_not raise_error
176
- lambda do test.call 'foo', checksum end.should raise_error ce
239
+ describe 'try_n_times' do
240
+ it 'should try 10 times' do
241
+ e = Class.new RuntimeError
242
+
243
+ i = 0
244
+ mh.try_n_times do
245
+ i += 1
246
+ 42
247
+ end.should == 42
248
+ i.should == 1
249
+
250
+ i = 0
251
+ mh.try_n_times do
252
+ i += 1
253
+ raise e if i < 10
254
+ 42
255
+ end.should == 42
256
+ i.should == 10
257
+
258
+ i = 0
259
+ lambda do
260
+ mh.try_n_times do
261
+ i += 1
262
+ raise e if i < 11
263
+ 42
264
+ end
265
+ end.should raise_error e
266
+ i.should == 10
177
267
  end
178
268
  end
179
269
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ion1-mischacks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johan Kiviniemi
@@ -22,7 +22,7 @@ dependencies:
22
22
  - !ruby/object:Gem::Version
23
23
  version: 2.3.1
24
24
  version:
25
- description: Safely pass untrusted parameters to sh scripts
25
+ description: "Miscellaneous methods that may or may not be useful. sh:: Safely pass untrusted parameters to sh scripts. fork_and_check:: Run a block in a forked process and raise an exception if the process returns a non-zero value. do_and_exit, do_and_exit!:: Run a block. If the block does not run exit!, a successful exec or equivalent, run exit(1) or exit!(1) ourselves. Useful to make sure a forked block either runs a successful exec or dies. Any exceptions from the block are printed to standard error. overwrite:: Safely replace a file. Writes to a temporary file and then moves it over the old file. tempname_for:: Generates an unique temporary path based on a filename. The generated filename resides in the same directory as the original one. try_n_times:: Retries a block of code until it succeeds or a maximum number of attempts (default 10) is exceeded. Exception#to_formatted_string:: Returns a string that looks like how Ruby would dump an uncaught exception."
26
26
  email:
27
27
  - devel@johan.kiviniemi.name
28
28
  executables: []
@@ -69,6 +69,6 @@ rubyforge_project: mischacks
69
69
  rubygems_version: 1.2.0
70
70
  signing_key:
71
71
  specification_version: 2
72
- summary: Safely pass untrusted parameters to sh scripts
72
+ summary: Miscellaneous methods that may or may not be useful
73
73
  test_files: []
74
74