sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/

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