class Glima::Zip

Attributes

password[RW]

Public Class Methods

new(zip_string, password = "") click to toggle source
# File lib/glima/zip.rb, line 12
def initialize(zip_string, password = "")
  @zip_string = zip_string
  @password = password
end
read(zip_filename, password = "") click to toggle source
# File lib/glima/zip.rb, line 8
def self.read(zip_filename, password = "")
  new(File.open(File.expand_path(zip_filename)).read, password)
end

Public Instance Methods

correct_password?(password) click to toggle source
# File lib/glima/zip.rb, line 17
def correct_password?(password)
  with_input_stream(password) do |zip|
    begin
      # Looking the first entry is not enough, because
      # some zip files have directory entry which size is zero
      # and no error is emitted even with wrong password.
      while entry = zip.get_next_entry
        size = zip.read.size # Exception if invalid password
        return false if size != entry.size
        return true  if size > 0 # short cut
      end
    rescue Zlib::DataError => e
      puts "*** #{e} ***" if $DEBUG
      return false
    end

    # False-positive if all files are emtpy.
    return true
  end
end
encrypted?() click to toggle source
# File lib/glima/zip.rb, line 55
def encrypted?
  correct_password?("")
end
to_base64() click to toggle source
# File lib/glima/zip.rb, line 71
def to_base64
  Base64.encode64(@zip_string)
end
to_decrypted_unicode_zip() click to toggle source
# File lib/glima/zip.rb, line 75
def to_decrypted_unicode_zip()
  ::Zip.unicode_names = true

  out = ::Zip::OutputStream.write_buffer(StringIO.new) do |zos|
    with_input_stream(@password) do |zis|
      while entry = zis.get_next_entry
        name = cp932_path_to_utf8_path(entry.name)

        # Two types of Exception will occur on encrypted zip:
        #  1) "invalid block type (Zlib::DataError)" if password is not specified.
        #  2) "invalid stored block lengths (Zlib::DataError)" if password is wrong.
        content = zis.read
        raise Zlib::DataError if content.size != entry.size

        zos.put_next_entry(name)
        zos.write(content)
      end
    end
  end
  Zip.new(out.string)
end
to_s() click to toggle source
# File lib/glima/zip.rb, line 67
def to_s
  @zip_string
end
unlock_password!(password_candidates, logger = nil) click to toggle source
# File lib/glima/zip.rb, line 38
def unlock_password!(password_candidates, logger = nil)
  list = sort_by_password_strength(password_candidates.uniq).unshift("")

  list.each_with_index do |password, i|
    msg = "Try password(#{i}):'#{password}' (#{password_strength(password)})..."

    if correct_password?(password)
      logger.info(msg + " OK.") if logger
      @password = password
      return password # Found password
    else
      logger.info(msg + " NG.") if logger
    end
  end
  return nil # No luck
end
write_to_file(file) click to toggle source
# File lib/glima/zip.rb, line 59
def write_to_file(file)
  return file.write(@zip_string) if file.respond_to?(:write)

  File.open(file, "w") do |f|
    f.write(@zip_string)
  end
end

Private Instance Methods

cp932_path_to_utf8_path(cp932_path_string) click to toggle source

1) Convert CP932 (SJIS) to UTF8. 2) Replace path-separators from backslash () to slash (/).

Example:

path = io.get_next_entry.name # rubyzip returns ASCII-8BIT string as name.
path is:
 + ASCII-8BIT
 + Every backslash is replaced to '/' even in second-byte of CP932.

See also:

https://github.com/rubyzip/rubyzip/blob/master/lib/zip/entry.rb#L223
Zip::Entry#read_local_entry does gsub('\\', '/')
# File lib/glima/zip.rb, line 148
def cp932_path_to_utf8_path(cp932_path_string)
  # Replace-back all '/' to '\'
  name = cp932_path_string.force_encoding("BINARY").gsub('/', '\\')

  # Change endoding to CP932 (SJIS) and replace all '\' to '/'
  # In this replacement, '\' in second-byte of CP932 will be preserved.
  name = name.force_encoding("CP932").gsub('\\', '/')

  # Convert CP932 to UTF-8
  return name.encode("utf-8", "CP932",
                     :invalid => :replace, :undef => :replace)
end
decrypter(password = "") click to toggle source
# File lib/glima/zip.rb, line 127
def decrypter(password = "")
  if password.empty?
    nil # return empty decrypter
  else
    ::Zip::TraditionalDecrypter.new(password)
  end
end
password_strength(password) click to toggle source
# File lib/glima/zip.rb, line 99
def password_strength(password)
  password = password.to_s
  score = Math.log2(password.length + 1)

  password.scan(/[A-Z]?[a-z]+|[A-Z]+|[-+\d]+|[!"#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]+/) do |s|
    score += 1.0
  end
  return score
end
sort_by_password_strength(password_array) click to toggle source
# File lib/glima/zip.rb, line 109
def sort_by_password_strength(password_array)
  password_array.sort{|a,b|
    password_strength(b) <=> password_strength(a)
  }
end
with_input_stream(password = "") { |zis| ... } click to toggle source
# File lib/glima/zip.rb, line 115
def with_input_stream(password = "", &block)
  # I have to read entire content of @zip_string to avoid the problems on
  # `general purpose flag Bit 3 is set`
  #
  zip_stringio = ::Zip::File.open_buffer(@zip_string).write_buffer
  zip_stringio.rewind

  ::Zip::InputStream.open(zip_stringio, 0, decrypter(password)) do |zis|
    yield zis
  end
end