5 # The ZipFileSystem API provides an API for accessing entries in
6 # a zip archive that is similar to ruby's builtin File and Dir
9 # Requiring 'zip/zipfilesystem' includes this module in ZipFile
10 # making the methods in this module available on ZipFile objects.
12 # Using this API the following example creates a new zip file
13 # <code>my.zip</code> containing a normal entry with the name
14 # <code>first.txt</code>, a directory entry named <code>mydir</code>
15 # and finally another normal entry named <code>second.txt</code>
17 # require 'zip/zipfilesystem'
19 # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
21 # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" }
22 # zipfile.dir.mkdir("mydir")
23 # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" }
26 # Reading is as easy as writing, as the following example shows. The
27 # example writes the contents of <code>first.txt</code> from zip archive
28 # <code>my.zip</code> to standard out.
30 # require 'zip/zipfilesystem'
32 # Zip::ZipFile.open("my.zip") {
34 # puts zipfile.file.read("first.txt")
39 def initialize # :nodoc:
40 mappedZip = ZipFileNameMapper.new(self)
41 @zipFsDir = ZipFsDir.new(mappedZip)
42 @zipFsFile = ZipFsFile.new(mappedZip)
43 @zipFsDir.file = @zipFsFile
44 @zipFsFile.dir = @zipFsDir
47 # Returns a ZipFsDir which is much like ruby's builtin Dir (class)
48 # object, except it works on the ZipFile on which this method is
54 # Returns a ZipFsFile which is much like ruby's builtin File (class)
55 # object, except it works on the ZipFile on which this method is
61 # Instances of this class are normally accessed via the accessor
62 # ZipFile::file. An instance of ZipFsFile behaves like ruby's
63 # builtin File (class) object, except it works on ZipFile entries.
65 # The individual methods are not documented due to their
66 # similarity with the methods in File
73 def initialize(zipFsFile, entryName)
74 @zipFsFile = zipFsFile
75 @entryName = entryName
78 def forward_invoke(msg)
79 @zipFsFile.send(msg, @entryName)
83 super || t == ::File::Stat
86 forward_message :forward_invoke, :file?, :directory?, :pipe?, :chardev?
87 forward_message :forward_invoke, :symlink?, :socket?, :blockdev?
88 forward_message :forward_invoke, :readable?, :readable_real?
89 forward_message :forward_invoke, :writable?, :writable_real?
90 forward_message :forward_invoke, :executable?, :executable_real?
91 forward_message :forward_invoke, :sticky?, :owned?, :grpowned?
92 forward_message :forward_invoke, :setuid?, :setgid?
93 forward_message :forward_invoke, :zero?
94 forward_message :forward_invoke, :size, :size?
95 forward_message :forward_invoke, :mtime, :atime, :ctime
100 @zipFsFile.__send__(:get_entry, @entryName)
106 if e.extra.member? "IUnix"
107 e.extra["IUnix"].gid || 0
115 if e.extra.member? "IUnix"
116 e.extra["IUnix"].uid || 0
128 def rdev_major; 0; end
130 def rdev_minor; 0; end
138 raise StandardError, "Unknown file type"
144 def blksize; nil; end
149 e.externalFileAttributes >> 16
151 33206 # 33206 is equivalent to -rw-rw-rw-
156 def initialize(mappedZip)
157 @mappedZip = mappedZip
160 def get_entry(fileName)
161 if ! exists?(fileName)
162 raise Errno::ENOENT, "No such file or directory - #{fileName}"
164 @mappedZip.find_entry(fileName)
168 def unix_mode_cmp(fileName, mode)
170 e = get_entry(fileName)
171 e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0
176 private :unix_mode_cmp
178 def exists?(fileName)
179 expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil
181 alias :exist? :exists?
183 # Permissions not implemented, so if the file exists it is accessible
185 alias grpowned? exists?
187 def readable?(fileName)
188 unix_mode_cmp(fileName, 0444)
190 alias readable_real? readable?
192 def writable?(fileName)
193 unix_mode_cmp(fileName, 0222)
195 alias writable_real? writable?
197 def executable?(fileName)
198 unix_mode_cmp(fileName, 0111)
200 alias executable_real? executable?
202 def setuid?(fileName)
203 unix_mode_cmp(fileName, 04000)
206 def setgid?(fileName)
207 unix_mode_cmp(fileName, 02000)
210 def sticky?(fileName)
211 unix_mode_cmp(fileName, 01000)
218 def truncate(fileName, len)
219 raise StandardError, "truncate not supported"
222 def directory?(fileName)
223 entry = @mappedZip.find_entry(fileName)
224 expand_path(fileName) == "/" || (entry != nil && entry.directory?)
227 def open(fileName, openMode = "r", &block)
230 @mappedZip.get_input_stream(fileName, &block)
232 @mappedZip.get_output_stream(fileName, &block)
234 raise StandardError, "openmode '#{openMode} not supported" unless openMode == "r"
238 def new(fileName, openMode = "r")
239 open(fileName, openMode)
243 @mappedZip.get_entry(fileName).size
246 # Returns nil for not found and nil for directories
248 entry = @mappedZip.find_entry(fileName)
249 return (entry == nil || entry.directory?) ? nil : entry.size
252 def chown(ownerInt, groupInt, *filenames)
253 filenames.each { |fileName|
254 e = get_entry(fileName)
255 unless e.extra.member?("IUnix")
256 e.extra.create("IUnix")
258 e.extra["IUnix"].uid = ownerInt
259 e.extra["IUnix"].gid = groupInt
264 def chmod (modeInt, *filenames)
265 filenames.each { |fileName|
266 e = get_entry(fileName)
267 e.fstype = 3 # force convertion filesystem type to unix
268 e.externalFileAttributes = modeInt << 16
281 entry = @mappedZip.find_entry(fileName)
282 entry != nil && entry.file?
285 def dirname(fileName)
286 ::File.dirname(fileName)
289 def basename(fileName)
290 ::File.basename(fileName)
294 ::File.split(fileName)
298 ::File.join(*fragments)
301 def utime(modifiedTime, *fileNames)
302 fileNames.each { |fileName|
303 get_entry(fileName).time = modifiedTime
308 @mappedZip.get_entry(fileName).mtime
312 e = get_entry(fileName)
313 if e.extra.member? "UniversalTime"
314 e.extra["UniversalTime"].atime
321 e = get_entry(fileName)
322 if e.extra.member? "UniversalTime"
323 e.extra["UniversalTime"].ctime
333 def blockdev?(filename)
337 def chardev?(filename)
341 def symlink?(fileName)
345 def socket?(fileName)
350 @mappedZip.get_entry(fileName).directory? ? "directory" : "file"
353 def readlink(fileName)
354 raise NotImplementedError, "The readlink() function is not implemented"
357 def symlink(fileName, symlinkName)
358 raise NotImplementedError, "The symlink() function is not implemented"
361 def link(fileName, symlinkName)
362 raise NotImplementedError, "The link() function is not implemented"
366 raise NotImplementedError, "The pipe() function is not implemented"
370 if ! exists?(fileName)
371 raise Errno::ENOENT, fileName
373 ZipFsStat.new(self, fileName)
378 def readlines(fileName)
379 open(fileName) { |is| is.readlines }
383 @mappedZip.read(fileName)
386 def popen(*args, &aProc)
387 File.popen(*args, &aProc)
390 def foreach(fileName, aSep = $/, &aProc)
391 open(fileName) { |is| is.each_line(aSep, &aProc) }
397 if directory?(fileName)
398 raise Errno::EISDIR, "Is a directory - \"#{fileName}\""
400 @mappedZip.remove(fileName)
404 def rename(fileToRename, newName)
405 @mappedZip.rename(fileToRename, newName) { true }
408 alias :unlink :delete
410 def expand_path(aPath)
411 @mappedZip.expand_path(aPath)
415 # Instances of this class are normally accessed via the accessor
416 # ZipFile::dir. An instance of ZipFsDir behaves like ruby's
417 # builtin Dir (class) object, except it works on ZipFile entries.
419 # The individual methods are not documented due to their
420 # similarity with the methods in Dir
423 def initialize(mappedZip)
424 @mappedZip = mappedZip
429 def new(aDirectoryName)
430 ZipFsDirIterator.new(entries(aDirectoryName))
433 def open(aDirectoryName)
434 dirIt = new(aDirectoryName)
446 def pwd; @mappedZip.pwd; end
449 def chdir(aDirectoryName)
450 unless @file.stat(aDirectoryName).directory?
451 raise Errno::EINVAL, "Invalid argument - #{aDirectoryName}"
453 @mappedZip.pwd = @file.expand_path(aDirectoryName)
456 def entries(aDirectoryName)
458 foreach(aDirectoryName) { |e| entries << e }
462 def foreach(aDirectoryName)
463 unless @file.stat(aDirectoryName).directory?
464 raise Errno::ENOTDIR, aDirectoryName
466 path = @file.expand_path(aDirectoryName).ensure_end("/")
468 subDirEntriesRegex = Regexp.new("^#{path}([^/]+)$")
471 match = subDirEntriesRegex.match(fileName)
472 yield(match[1]) unless match == nil
476 def delete(entryName)
477 unless @file.stat(entryName).directory?
478 raise Errno::EINVAL, "Invalid argument - #{entryName}"
480 @mappedZip.remove(entryName)
485 def mkdir(entryName, permissionInt = 0755)
486 @mappedZip.mkdir(entryName, permissionInt)
490 raise NotImplementedError, "The chroot() function is not implemented"
495 class ZipFsDirIterator # :nodoc:all
498 def initialize(arrayOfFileNames)
499 @fileNames = arrayOfFileNames
508 raise IOError, "closed directory" if @fileNames == nil
509 @fileNames.each(&aProc)
513 raise IOError, "closed directory" if @fileNames == nil
514 @fileNames[(@index+=1)-1]
518 raise IOError, "closed directory" if @fileNames == nil
522 def seek(anIntegerPosition)
523 raise IOError, "closed directory" if @fileNames == nil
524 @index = anIntegerPosition
528 raise IOError, "closed directory" if @fileNames == nil
533 # All access to ZipFile from ZipFsFile and ZipFsDir goes through a
534 # ZipFileNameMapper, which has one responsibility: ensure
535 class ZipFileNameMapper # :nodoc:all
538 def initialize(zipFile)
545 def find_entry(fileName)
546 @zipFile.find_entry(expand_to_entry(fileName))
549 def get_entry(fileName)
550 @zipFile.get_entry(expand_to_entry(fileName))
553 def get_input_stream(fileName, &aProc)
554 @zipFile.get_input_stream(expand_to_entry(fileName), &aProc)
557 def get_output_stream(fileName, &aProc)
558 @zipFile.get_output_stream(expand_to_entry(fileName), &aProc)
562 @zipFile.read(expand_to_entry(fileName))
566 @zipFile.remove(expand_to_entry(fileName))
569 def rename(fileName, newName, &continueOnExistsProc)
570 @zipFile.rename(expand_to_entry(fileName), expand_to_entry(newName),
571 &continueOnExistsProc)
574 def mkdir(fileName, permissionInt = 0755)
575 @zipFile.mkdir(expand_to_entry(fileName), permissionInt)
578 # Turns entries into strings and adds leading /
579 # and removes trailing slash on directories
583 yield("/"+e.to_s.chomp("/"))
587 def expand_path(aPath)
588 expanded = aPath.starts_with("/") ? aPath : @pwd.ensure_end("/") + aPath
589 expanded.gsub!(/\/\.(\/|$)/, "")
590 expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, "")
591 expanded.empty? ? "/" : expanded
596 def expand_to_entry(aPath)
597 expand_path(aPath).lchop
603 include ZipFileSystem
607 # Copyright (C) 2002, 2003 Thomas Sondergaard
608 # rubyzip is free software; you can redistribute it and/or
609 # modify it under the terms of the ruby license.