lib/sup/util.rb (16441B) - raw
1 # encoding: utf-8
2
3 require 'thread'
4 require 'lockfile'
5 require 'mime/types'
6 require 'pathname'
7 require 'rmail'
8 require 'set'
9 require 'enumerator'
10 require 'benchmark'
11 require 'unicode'
12 require 'unicode/display_width'
13 require 'fileutils'
14 require 'string-scrub' if /^2\.0\./ =~ RUBY_VERSION
15
16 module ExtendedLockfile
17 def gen_lock_id
18 Hash[
19 'host' => "#{ Socket.gethostname }",
20 'pid' => "#{ Process.pid }",
21 'ppid' => "#{ Process.ppid }",
22 'time' => timestamp,
23 'pname' => $0,
24 'user' => ENV["USER"]
25 ]
26 end
27
28 def dump_lock_id lock_id = @lock_id
29 "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
30 lock_id.values_at('host','pid','ppid','time','user', 'pname')
31 end
32
33 def lockinfo_on_disk
34 h = load_lock_id IO.read(path)
35 h['mtime'] = File.mtime path
36 h['path'] = path
37 h
38 end
39
40 def touch_yourself; touch path end
41 end
42 Lockfile.send :prepend, ExtendedLockfile
43
44 class File
45 # platform safe file.link which attempts a copy if hard-linking fails
46 def self.safe_link src, dest
47 begin
48 File.link src, dest
49 rescue
50 FileUtils.copy src, dest
51 end
52 end
53 end
54
55 class Pathname
56 def human_size
57 s =
58 begin
59 size
60 rescue SystemCallError
61 return "?"
62 end
63 s.to_human_size
64 end
65
66 def human_time
67 begin
68 ctime.strftime("%Y-%m-%d %H:%M")
69 rescue SystemCallError
70 "?"
71 end
72 end
73 end
74
75 ## more monkeypatching!
76 module RMail
77 class EncodingUnsupportedError < StandardError; end
78
79 class Message
80 def self.make_file_attachment fn
81 bfn = File.basename fn
82 t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
83 payload = IO.read fn
84 ## Need to encode as base64 or quoted-printable if any lines are longer than 998 chars.
85 encoding = if t.encoding != t.default_encoding and payload.each_line.any? { |l| l.length > 998 }
86 t.default_encoding
87 else
88 t.encoding
89 end
90 make_attachment payload, t.content_type, encoding, bfn.to_s
91 end
92
93 def charset
94 if header.field?("content-type") && header.fetch("content-type") =~ /charset\s*=\s*"?(.*?)"?(;|$)/i
95 $1
96 end
97 end
98
99 def self.make_attachment payload, mime_type, encoding, filename
100 a = Message.new
101 a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
102 a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
103 a.header.add "Content-Transfer-Encoding", encoding if encoding
104 a.body =
105 case encoding
106 when "base64"
107 [payload].pack "m"
108 when "quoted-printable"
109 [payload].pack "M"
110 when "7bit", "8bit", nil
111 payload
112 else
113 raise EncodingUnsupportedError, encoding.inspect
114 end
115 a
116 end
117 end
118
119 module CustomizedSerialize
120 ## Don't add MIME-Version headers on serialization. Sup sometimes want's to serialize
121 ## message parts where these headers are not needed and messing with the message on
122 ## serialization breaks gpg signatures. The commented section shows the original RMail
123 ## code.
124 def calculate_boundaries(message)
125 calculate_boundaries_low(message, [])
126 # unless message.header['MIME-Version']
127 # message.header['MIME-Version'] = "1.0"
128 # end
129 end
130 end
131 Serialize.send :prepend, CustomizedSerialize
132 end
133
134 class Module
135 def bool_reader *args
136 args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
137 end
138 def bool_writer *args; attr_writer(*args); end
139 def bool_accessor *args
140 bool_reader(*args)
141 bool_writer(*args)
142 end
143
144 def defer_all_other_method_calls_to obj
145 class_eval %{
146 def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
147 def respond_to?(m, include_private = false)
148 @#{obj}.respond_to?(m, include_private)
149 end
150 }
151 end
152 end
153
154 class Object
155 ## "k combinator"
156 def returning x; yield x; x; end
157
158 unless method_defined? :tap
159 def tap; yield self; self; end
160 end
161
162 ## clone of java-style whole-method synchronization
163 ## assumes a @mutex variable
164 ## TODO: clean up, try harder to avoid namespace collisions
165 def synchronized *methods
166 methods.each do |meth|
167 class_eval <<-EOF
168 alias unsynchronized_#{meth} #{meth}
169 def #{meth}(*a, &b)
170 @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
171 end
172 EOF
173 end
174 end
175
176 def ignore_concurrent_calls *methods
177 methods.each do |meth|
178 mutex = "@__concurrent_protector_#{meth}"
179 flag = "@__concurrent_flag_#{meth}"
180 oldmeth = "__unprotected_#{meth}"
181 class_eval <<-EOF
182 alias #{oldmeth} #{meth}
183 def #{meth}(*a, &b)
184 #{mutex} = Mutex.new unless defined? #{mutex}
185 #{flag} = true unless defined? #{flag}
186 run = #{mutex}.synchronize do
187 if #{flag}
188 #{flag} = false
189 true
190 end
191 end
192 if run
193 ret = #{oldmeth}(*a, &b)
194 #{mutex}.synchronize { #{flag} = true }
195 ret
196 end
197 end
198 EOF
199 end
200 end
201
202 def benchmark s, &b
203 ret = nil
204 times = Benchmark.measure { ret = b.call }
205 debug "benchmark #{s}: #{times}"
206 ret
207 end
208 end
209
210 class String
211 def display_length
212 @display_length ||= Unicode::DisplayWidth.of(self)
213 end
214
215 def slice_by_display_length len
216 each_char.each_with_object (+"") do |c, buffer|
217 len -= Unicode::DisplayWidth.of(c)
218 return buffer if len < 0
219 buffer << c
220 end
221 end
222
223 def camel_to_hyphy
224 self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
225 end
226
227 def find_all_positions x
228 ret = []
229 start = 0
230 while start < length
231 pos = index x, start
232 break if pos.nil?
233 ret << pos
234 start = pos + 1
235 end
236 ret
237 end
238
239 ## a very complicated regex found on teh internets to split on
240 ## commas, unless they occurr within double quotes.
241 def split_on_commas
242 normalize_whitespace().split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
243 end
244
245 ## ok, here we do it the hard way. got to have a remainder for purposes of
246 ## tab-completing full email addresses
247 def split_on_commas_with_remainder
248 ret = []
249 state = :outstring
250 pos = 0
251 region_start = 0
252 while pos <= length
253 newpos = case state
254 when :escaped_instring, :escaped_outstring then pos
255 else index(/[,"\\]/, pos)
256 end
257
258 if newpos
259 char = self[newpos]
260 else
261 char = nil
262 newpos = length
263 end
264
265 case char
266 when ?"
267 state = case state
268 when :outstring then :instring
269 when :instring then :outstring
270 when :escaped_instring then :instring
271 when :escaped_outstring then :outstring
272 end
273 when ?,, nil
274 state = case state
275 when :outstring, :escaped_outstring then
276 ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
277 region_start = newpos + 1
278 :outstring
279 when :instring then :instring
280 when :escaped_instring then :instring
281 end
282 when ?\\
283 state = case state
284 when :instring then :escaped_instring
285 when :outstring then :escaped_outstring
286 when :escaped_instring then :instring
287 when :escaped_outstring then :outstring
288 end
289 end
290 pos = newpos + 1
291 end
292
293 remainder = case state
294 when :instring
295 self[region_start .. -1].gsub(/^\s+/, "")
296 else
297 nil
298 end
299
300 [ret, remainder]
301 end
302
303 def wrap len
304 ret = []
305 s = self
306 while s.display_length > len
307 slice = s.slice_by_display_length(len)
308 cut = slice.rindex(/\s/)
309 if cut
310 ret << s[0 ... cut]
311 s = s[(cut + 1) .. -1]
312 else
313 ret << slice
314 s = s[slice.length .. -1]
315 end
316 end
317 ret << s
318 end
319
320 # Fix the damn string! make sure it is valid utf-8, then convert to
321 # user encoding.
322 def fix_encoding!
323 # first try to encode to utf-8 from whatever current encoding
324 encode!('UTF-8', :invalid => :replace, :undef => :replace)
325
326 # ensure invalid chars are replaced
327 scrub!
328
329 fail "Could not create valid UTF-8 string out of: '#{self.to_s}'." unless valid_encoding?
330
331 # now convert to $encoding
332 encode!($encoding, :invalid => :replace, :undef => :replace)
333
334 fail "Could not create valid #{$encoding.inspect} string out of: '#{self.to_s}'." unless valid_encoding?
335
336 self
337 end
338
339 # transcode the string if original encoding is know
340 # fix if broken.
341 def transcode to_encoding, from_encoding
342 begin
343 encode!(to_encoding, from_encoding, :invalid => :replace, :undef => :replace)
344
345 unless valid_encoding?
346 # fix encoding (through UTF-8)
347 encode!('UTF-16', from_encoding, :invalid => :replace, :undef => :replace)
348 encode!(to_encoding, 'UTF-16', :invalid => :replace, :undef => :replace)
349 end
350
351 rescue Encoding::ConverterNotFoundError
352 debug "Encoding converter not found for #{from_encoding.inspect} or #{to_encoding.inspect}, fixing string: '#{self.to_s}', but expect weird characters."
353 fix_encoding!
354 end
355
356 fail "Could not create valid #{to_encoding.inspect} string out of: '#{self.to_s}'." unless valid_encoding?
357
358 self
359 end
360
361 ## Decodes UTF-7 and returns the resulting decoded string as UTF-8.
362 ##
363 ## Ruby doesn't supply a UTF-7 encoding natively. There is
364 ## Net::IMAP::decode_utf7 which only handles the IMAP "modified UTF-7"
365 ## encoding. This implementation is inspired by that one but handles
366 ## standard UTF-7 shift characters and not the IMAP-specific variation.
367 def decode_utf7
368 gsub(/\+([^-]+)?-/) {
369 if $1
370 ($1 + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE)
371 else
372 "+"
373 end
374 }
375 end
376
377 def normalize_whitespace
378 gsub(/\t/, " ").gsub(/\r/, "")
379 end
380
381 unless method_defined? :ord
382 def ord
383 self[0]
384 end
385 end
386
387 unless method_defined? :each
388 def each &b
389 each_line(&b)
390 end
391 end
392
393 ## takes a list of words, and returns an array of symbols. typically used in
394 ## Sup for translating Xapian's representation of a list of labels (a string)
395 ## to an array of label symbols.
396 ##
397 ## split_on will be passed to String#split, so you can leave this nil for space.
398 def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
399
400 class CheckError < ArgumentError; end
401 def check
402 begin
403 fail "unexpected encoding #{encoding}" if respond_to?(:encoding) && !(encoding == Encoding::UTF_8 || encoding == Encoding::ASCII)
404 fail "invalid encoding" if respond_to?(:valid_encoding?) && !valid_encoding?
405 rescue
406 raise CheckError.new($!.message)
407 end
408 end
409
410 def ascii
411 out = ""
412 each_byte do |b|
413 if (b & 128) != 0
414 out << "\\x#{b.to_s 16}"
415 else
416 out << b.chr
417 end
418 end
419 out = out.fix_encoding! # this should now be an utf-8 string of ascii
420 # compat chars.
421 end
422 end
423
424 class Numeric
425 def clamp min, max
426 if self < min
427 min
428 elsif self > max
429 max
430 else
431 self
432 end
433 end
434
435 def in? range; range.member? self; end
436
437 def to_human_size
438 if self < 1024
439 to_s + "B"
440 elsif self < (1024 * 1024)
441 (self / 1024).to_s + "KiB"
442 elsif self < (1024 * 1024 * 1024)
443 (self / 1024 / 1024).to_s + "MiB"
444 else
445 (self / 1024 / 1024 / 1024).to_s + "GiB"
446 end
447 end
448 end
449
450 class Integer
451 def to_character
452 if self < 128 && self >= 0
453 chr
454 else
455 "<#{self}>"
456 end
457 end
458
459 ## hacking the english language
460 def pluralize s
461 to_s + " " +
462 if self == 1
463 s
464 else
465 if s =~ /(.*)y$/
466 $1 + "ies"
467 else
468 s + "s"
469 end
470 end
471 end
472 end
473
474 class Hash
475 def - o
476 Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
477 end
478
479 def select_by_value v=true
480 select { |k, vv| vv == v }.map { |x| x.first }
481 end
482 end
483
484 module Enumerable
485 def map_with_index
486 ret = []
487 each_with_index { |x, i| ret << yield(x, i) }
488 ret
489 end
490
491 if not method_defined? :sum
492 def sum; inject(0) { |x, y| x + y }; end
493 end
494
495 def map_to_hash
496 ret = {}
497 each { |x| ret[x] = yield(x) }
498 ret
499 end
500
501 # like find, except returns the value of the block rather than the
502 # element itself.
503 def argfind
504 ret = nil
505 find { |e| ret ||= yield(e) }
506 ret || nil # force
507 end
508
509 def argmin
510 best, bestval = nil, nil
511 each do |e|
512 val = yield e
513 if bestval.nil? || val < bestval
514 best, bestval = e, val
515 end
516 end
517 best
518 end
519
520 ## returns the maximum shared prefix of an array of strings
521 ## optinally excluding a prefix
522 def shared_prefix caseless=false, exclude=""
523 return "" if empty?
524 prefix = ""
525 (0 ... first.length).each do |i|
526 c = (caseless ? first.downcase : first)[i]
527 break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
528 next if exclude[i] == c
529 prefix += first[i].chr
530 end
531 prefix
532 end
533
534 def max_of
535 map { |e| yield e }.max
536 end
537
538 ## returns all the entries which are equal to startline up to endline
539 def between startline, endline
540 select { |l| true if l == startline .. l == endline }
541 end
542 end
543
544 class Array
545 def flatten_one_level
546 inject([]) { |a, e| a + e }
547 end
548
549 if not method_defined? :to_h
550 def to_h; Hash[*flatten_one_level]; end
551 end
552 def rest; self[1..-1]; end
553
554 def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
555
556 def last= e; self[-1] = e end
557 def nonempty?; !empty? end
558 end
559
560 ## simple singleton module. far less complete and insane than the ruby standard
561 ## library one, but it automatically forwards methods calls and allows for
562 ## constructors that take arguments.
563 ##
564 ## classes that inherit this can define initialize. however, you cannot call
565 ## .new on the class. To get the instance of the class, call .instance;
566 ## to create the instance, call init.
567 module Redwood
568 module Singleton
569 module ClassMethods
570 def instance; @instance; end
571 def instantiated?; defined?(@instance) && !@instance.nil?; end
572 def deinstantiate!; @instance = nil; end
573 def method_missing meth, *a, &b
574 raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
575
576 ## if we've been deinstantiated, just drop all calls. this is
577 ## useful because threads that might be active during the
578 ## cleanup process (e.g. polling) would otherwise have to
579 ## special-case every call to a Singleton object
580 return nil if @instance.nil?
581
582 # Speed up further calls by defining a shortcut around method_missing
583 if meth.to_s[-1,1] == '='
584 # Argh! Inconsistency! Setters do not work like all the other methods.
585 class_eval "def self.#{meth}(a); @instance.send :#{meth}, a; end"
586 else
587 class_eval "def self.#{meth}(*a, &b); @instance.send :#{meth}, *a, &b; end"
588 end
589
590 @instance.send meth, *a, &b
591 end
592 def init *args
593 raise "there can be only one! (instance)" if instantiated?
594 @instance = new(*args)
595 end
596 end
597
598 def self.included klass
599 klass.private_class_method :allocate, :new
600 klass.extend ClassMethods
601 end
602 end
603 end
604
605 ## acts like a hash with an initialization block, but saves any
606 ## newly-created value even upon lookup.
607 ##
608 ## for example:
609 ##
610 ## class C
611 ## attr_accessor :val
612 ## def initialize; @val = 0 end
613 ## end
614 ##
615 ## h = Hash.new { C.new }
616 ## h[:a].val # => 0
617 ## h[:a].val = 1
618 ## h[:a].val # => 0
619 ##
620 ## h2 = SavingHash.new { C.new }
621 ## h2[:a].val # => 0
622 ## h2[:a].val = 1
623 ## h2[:a].val # => 1
624 ##
625 ## important note: you REALLY want to use #member? to test existence,
626 ## because just checking h[anything] will always evaluate to true
627 ## (except for degenerate constructor blocks that return nil or false)
628 class SavingHash
629 def initialize &b
630 @constructor = b
631 @hash = Hash.new
632 end
633
634 def [] k
635 @hash[k] ||= @constructor.call(k)
636 end
637
638 defer_all_other_method_calls_to :hash
639 end
640
641 ## easy thread-safe class for determining who's the "winner" in a race (i.e.
642 ## first person to hit the finish line
643 class FinishLine
644 def initialize
645 @m = Mutex.new
646 @over = false
647 end
648
649 def winner?
650 @m.synchronize { !@over && @over = true }
651 end
652 end
653