require 'zip' module Zip # The ZipFileSystem API provides an API for accessing entries in # a zip archive that is similar to ruby's builtin File and Dir # classes. # # Requiring 'zip/filesystem' includes this module in Zip::File # making the methods in this module available on Zip::File objects. # # Using this API the following example creates a new zip file # my.zip containing a normal entry with the name # first.txt, a directory entry named mydir # and finally another normal entry named second.txt # # require 'zip/filesystem' # # Zip::File.open("my.zip", Zip::File::CREATE) { # |zipfile| # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" } # zipfile.dir.mkdir("mydir") # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" } # } # # Reading is as easy as writing, as the following example shows. The # example writes the contents of first.txt from zip archive # my.zip to standard out. # # require 'zip/filesystem' # # Zip::File.open("my.zip") { # |zipfile| # puts zipfile.file.read("first.txt") # } module FileSystem def initialize # :nodoc: mapped_zip = ZipFileNameMapper.new(self) @zip_fs_dir = ZipFsDir.new(mapped_zip) @zip_fs_file = ZipFsFile.new(mapped_zip) @zip_fs_dir.file = @zip_fs_file @zip_fs_file.dir = @zip_fs_dir end # Returns a ZipFsDir which is much like ruby's builtin Dir (class) # object, except it works on the Zip::File on which this method is # invoked def dir @zip_fs_dir end # Returns a ZipFsFile which is much like ruby's builtin File (class) # object, except it works on the Zip::File on which this method is # invoked def file @zip_fs_file end # Instances of this class are normally accessed via the accessor # Zip::File::file. An instance of ZipFsFile behaves like ruby's # builtin File (class) object, except it works on Zip::File entries. # # The individual methods are not documented due to their # similarity with the methods in File class ZipFsFile attr_writer :dir # protected :dir class ZipFsStat class << self def delegate_to_fs_file(*methods) methods.each do |method| class_eval <<-END_EVAL, __FILE__, __LINE__ + 1 def #{method} # def file? @zip_fs_file.#{method}(@entry_name) # @zip_fs_file.file?(@entry_name) end # end END_EVAL end end end def initialize(zip_fs_file, entry_name) @zip_fs_file = zip_fs_file @entry_name = entry_name end def kind_of?(type) super || type == ::File::Stat end delegate_to_fs_file :file?, :directory?, :pipe?, :chardev?, :symlink?, :socket?, :blockdev?, :readable?, :readable_real?, :writable?, :ctime, :writable_real?, :executable?, :executable_real?, :sticky?, :owned?, :grpowned?, :setuid?, :setgid?, :zero?, :size, :size?, :mtime, :atime def blocks nil end def get_entry @zip_fs_file.__send__(:get_entry, @entry_name) end private :get_entry def gid e = get_entry if e.extra.member? 'IUnix' e.extra['IUnix'].gid || 0 else 0 end end def uid e = get_entry if e.extra.member? 'IUnix' e.extra['IUnix'].uid || 0 else 0 end end def ino 0 end def dev 0 end def rdev 0 end def rdev_major 0 end def rdev_minor 0 end def ftype if file? 'file' elsif directory? 'directory' else raise StandardError, 'Unknown file type' end end def nlink 1 end def blksize nil end def mode e = get_entry if e.fstype == 3 e.external_file_attributes >> 16 else 33_206 # 33206 is equivalent to -rw-rw-rw- end end end def initialize(mapped_zip) @mapped_zip = mapped_zip end def get_entry(filename) unless exists?(filename) raise Errno::ENOENT, "No such file or directory - #{filename}" end @mapped_zip.find_entry(filename) end private :get_entry def unix_mode_cmp(filename, mode) e = get_entry(filename) e.fstype == 3 && ((e.external_file_attributes >> 16) & mode) != 0 rescue Errno::ENOENT false end private :unix_mode_cmp def exists?(filename) expand_path(filename) == '/' || !@mapped_zip.find_entry(filename).nil? end alias exist? exists? # Permissions not implemented, so if the file exists it is accessible alias owned? exists? alias grpowned? exists? def readable?(filename) unix_mode_cmp(filename, 0o444) end alias readable_real? readable? def writable?(filename) unix_mode_cmp(filename, 0o222) end alias writable_real? writable? def executable?(filename) unix_mode_cmp(filename, 0o111) end alias executable_real? executable? def setuid?(filename) unix_mode_cmp(filename, 0o4000) end def setgid?(filename) unix_mode_cmp(filename, 0o2000) end def sticky?(filename) unix_mode_cmp(filename, 0o1000) end def umask(*args) ::File.umask(*args) end def truncate(_filename, _len) raise StandardError, 'truncate not supported' end def directory?(filename) entry = @mapped_zip.find_entry(filename) expand_path(filename) == '/' || (!entry.nil? && entry.directory?) end def open(filename, mode = 'r', permissions = 0o644, &block) mode = mode.delete('b') # ignore b option case mode when 'r' @mapped_zip.get_input_stream(filename, &block) when 'w' @mapped_zip.get_output_stream(filename, permissions, &block) else raise StandardError, "openmode '#{mode} not supported" unless mode == 'r' end end def new(filename, mode = 'r') self.open(filename, mode) end def size(filename) @mapped_zip.get_entry(filename).size end # Returns nil for not found and nil for directories def size?(filename) entry = @mapped_zip.find_entry(filename) entry.nil? || entry.directory? ? nil : entry.size end def chown(owner, group, *filenames) filenames.each do |filename| e = get_entry(filename) e.extra.create('IUnix') unless e.extra.member?('IUnix') e.extra['IUnix'].uid = owner e.extra['IUnix'].gid = group end filenames.size end def chmod(mode, *filenames) filenames.each do |filename| e = get_entry(filename) e.fstype = 3 # force convertion filesystem type to unix e.unix_perms = mode e.external_file_attributes = mode << 16 e.dirty = true end filenames.size end def zero?(filename) sz = size(filename) sz.nil? || sz == 0 rescue Errno::ENOENT false end def file?(filename) entry = @mapped_zip.find_entry(filename) !entry.nil? && entry.file? end def dirname(filename) ::File.dirname(filename) end def basename(filename) ::File.basename(filename) end def split(filename) ::File.split(filename) end def join(*fragments) ::File.join(*fragments) end def utime(modified_time, *filenames) filenames.each do |filename| get_entry(filename).time = modified_time end end def mtime(filename) @mapped_zip.get_entry(filename).mtime end def atime(filename) e = get_entry(filename) if e.extra.member? 'UniversalTime' e.extra['UniversalTime'].atime elsif e.extra.member? 'NTFS' e.extra['NTFS'].atime end end def ctime(filename) e = get_entry(filename) if e.extra.member? 'UniversalTime' e.extra['UniversalTime'].ctime elsif e.extra.member? 'NTFS' e.extra['NTFS'].ctime end end def pipe?(_filename) false end def blockdev?(_filename) false end def chardev?(_filename) false end def symlink?(_filename) false end def socket?(_filename) false end def ftype(filename) @mapped_zip.get_entry(filename).directory? ? 'directory' : 'file' end def readlink(_filename) raise NotImplementedError, 'The readlink() function is not implemented' end def symlink(_filename, _symlink_name) raise NotImplementedError, 'The symlink() function is not implemented' end def link(_filename, _symlink_name) raise NotImplementedError, 'The link() function is not implemented' end def pipe raise NotImplementedError, 'The pipe() function is not implemented' end def stat(filename) raise Errno::ENOENT, filename unless exists?(filename) ZipFsStat.new(self, filename) end alias lstat stat def readlines(filename) self.open(filename, &:readlines) end def read(filename) @mapped_zip.read(filename) end def popen(*args, &a_proc) ::File.popen(*args, &a_proc) end def foreach(filename, sep = $INPUT_RECORD_SEPARATOR, &a_proc) self.open(filename) { |is| is.each_line(sep, &a_proc) } end def delete(*args) args.each do |filename| if directory?(filename) raise Errno::EISDIR, "Is a directory - \"#{filename}\"" end @mapped_zip.remove(filename) end end def rename(file_to_rename, new_name) @mapped_zip.rename(file_to_rename, new_name) { true } end alias unlink delete def expand_path(path) @mapped_zip.expand_path(path) end end # Instances of this class are normally accessed via the accessor # ZipFile::dir. An instance of ZipFsDir behaves like ruby's # builtin Dir (class) object, except it works on ZipFile entries. # # The individual methods are not documented due to their # similarity with the methods in Dir class ZipFsDir def initialize(mapped_zip) @mapped_zip = mapped_zip end attr_writer :file def new(directory_name) ZipFsDirIterator.new(entries(directory_name)) end def open(directory_name) dir_iter = new(directory_name) if block_given? begin yield(dir_iter) return nil ensure dir_iter.close end end dir_iter end def pwd @mapped_zip.pwd end alias getwd pwd def chdir(directory_name) unless @file.stat(directory_name).directory? raise Errno::EINVAL, "Invalid argument - #{directory_name}" end @mapped_zip.pwd = @file.expand_path(directory_name) end def entries(directory_name) entries = [] foreach(directory_name) { |e| entries << e } entries end def glob(*args, &block) @mapped_zip.glob(*args, &block) end def foreach(directory_name) unless @file.stat(directory_name).directory? raise Errno::ENOTDIR, directory_name end path = @file.expand_path(directory_name) path << '/' unless path.end_with?('/') path = Regexp.escape(path) subdir_entry_regex = Regexp.new("^#{path}([^/]+)$") @mapped_zip.each do |filename| match = subdir_entry_regex.match(filename) yield(match[1]) unless match.nil? end end def delete(entry_name) unless @file.stat(entry_name).directory? raise Errno::EINVAL, "Invalid argument - #{entry_name}" end @mapped_zip.remove(entry_name) end alias rmdir delete alias unlink delete def mkdir(entry_name, permissions = 0o755) @mapped_zip.mkdir(entry_name, permissions) end def chroot(*_args) raise NotImplementedError, 'The chroot() function is not implemented' end end class ZipFsDirIterator # :nodoc:all include Enumerable def initialize(filenames) @filenames = filenames @index = 0 end def close @filenames = nil end def each(&a_proc) raise IOError, 'closed directory' if @filenames.nil? @filenames.each(&a_proc) end def read raise IOError, 'closed directory' if @filenames.nil? @filenames[(@index += 1) - 1] end def rewind raise IOError, 'closed directory' if @filenames.nil? @index = 0 end def seek(position) raise IOError, 'closed directory' if @filenames.nil? @index = position end def tell raise IOError, 'closed directory' if @filenames.nil? @index end end # All access to Zip::File from ZipFsFile and ZipFsDir goes through a # ZipFileNameMapper, which has one responsibility: ensure class ZipFileNameMapper # :nodoc:all include Enumerable def initialize(zip_file) @zip_file = zip_file @pwd = '/' end attr_accessor :pwd def find_entry(filename) @zip_file.find_entry(expand_to_entry(filename)) end def get_entry(filename) @zip_file.get_entry(expand_to_entry(filename)) end def get_input_stream(filename, &a_proc) @zip_file.get_input_stream(expand_to_entry(filename), &a_proc) end def get_output_stream(filename, permissions = nil, &a_proc) @zip_file.get_output_stream( expand_to_entry(filename), permissions, &a_proc ) end def glob(pattern, *flags, &block) @zip_file.glob(expand_to_entry(pattern), *flags, &block) end def read(filename) @zip_file.read(expand_to_entry(filename)) end def remove(filename) @zip_file.remove(expand_to_entry(filename)) end def rename(filename, new_name, &continue_on_exists_proc) @zip_file.rename( expand_to_entry(filename), expand_to_entry(new_name), &continue_on_exists_proc ) end def mkdir(filename, permissions = 0o755) @zip_file.mkdir(expand_to_entry(filename), permissions) end # Turns entries into strings and adds leading / # and removes trailing slash on directories def each @zip_file.each do |e| yield('/' + e.to_s.chomp('/')) end end def expand_path(path) expanded = path.start_with?('/') ? path.dup : ::File.join(@pwd, path) expanded.gsub!(/\/\.(\/|$)/, '') expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, '') expanded.empty? ? '/' : expanded end private def expand_to_entry(path) expand_path(path)[1..-1] end end end class File include FileSystem end end # Copyright (C) 2002, 2003 Thomas Sondergaard # rubyzip is free software; you can redistribute it and/or # modify it under the terms of the ruby license.