module Zip class ZipEntry STORED = 0 DEFLATED = 8 FSTYPE_FAT = 0 FSTYPE_AMIGA = 1 FSTYPE_VMS = 2 FSTYPE_UNIX = 3 FSTYPE_VM_CMS = 4 FSTYPE_ATARI = 5 FSTYPE_HPFS = 6 FSTYPE_MAC = 7 FSTYPE_Z_SYSTEM = 8 FSTYPE_CPM = 9 FSTYPE_TOPS20 = 10 FSTYPE_NTFS = 11 FSTYPE_QDOS = 12 FSTYPE_ACORN = 13 FSTYPE_VFAT = 14 FSTYPE_MVS = 15 FSTYPE_BEOS = 16 FSTYPE_TANDEM = 17 FSTYPE_THEOS = 18 FSTYPE_MAC_OSX = 19 FSTYPE_ATHEOS = 30 FSTYPES = { FSTYPE_FAT => 'FAT'.freeze, FSTYPE_AMIGA => 'Amiga'.freeze, FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze, FSTYPE_UNIX => 'Unix'.freeze, FSTYPE_VM_CMS => 'VM/CMS'.freeze, FSTYPE_ATARI => 'Atari ST'.freeze, FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze, FSTYPE_MAC => 'Macintosh'.freeze, FSTYPE_Z_SYSTEM => 'Z-System'.freeze, FSTYPE_CPM => 'CP/M'.freeze, FSTYPE_TOPS20 => 'TOPS-20'.freeze, FSTYPE_NTFS => 'NTFS'.freeze, FSTYPE_QDOS => 'SMS/QDOS'.freeze, FSTYPE_ACORN => 'Acorn RISC OS'.freeze, FSTYPE_VFAT => 'Win32 VFAT'.freeze, FSTYPE_MVS => 'MVS'.freeze, FSTYPE_BEOS => 'BeOS'.freeze, FSTYPE_TANDEM => 'Tandem NSK'.freeze, FSTYPE_THEOS => 'Theos'.freeze, FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze, FSTYPE_ATHEOS => 'AtheOS'.freeze, }.freeze attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature attr_accessor :follow_symlinks attr_accessor :restore_times, :restore_permissions, :restore_ownership attr_accessor :unix_uid, :unix_gid, :unix_perms attr_accessor :dirty attr_reader :ftype, :filepath # :nodoc: def initialize(zipfile = "", name = "", comment = "", extra = "", compressed_size = 0, crc = 0, compression_method = ZipEntry::DEFLATED, size = 0, time = DOSTime.now) super() if name.start_with?("/") raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" end @localHeaderOffset = 0 @local_header_size = 0 @internalFileAttributes = 1 @externalFileAttributes = 0 @header_signature = CENTRAL_DIRECTORY_ENTRY_SIGNATURE @versionNeededToExtract = VERSION_NEEDED_TO_EXTRACT @version = 52 # this library's version @ftype = nil # unspecified or unknown @filepath = nil if Zip::RUNNING_ON_WINDOWS @fstype = FSTYPE_FAT else @fstype = FSTYPE_UNIX end @zipfile = zipfile @comment = comment @compressed_size = compressed_size @crc = crc @extra = extra @compression_method = compression_method @name = name @size = size @time = time @gp_flags = 0 @follow_symlinks = false @restore_times = true @restore_permissions = false @restore_ownership = false # BUG: need an extra field to support uid/gid's @unix_uid = nil @unix_gid = nil @unix_perms = nil # @posix_acl = nil # @ntfs_acl = nil if name_is_directory? @ftype = :directory else @ftype = :file end unless ZipExtraField === @extra @extra = ZipExtraField.new(@extra.to_s) end @dirty = false end def time if @extra["UniversalTime"] @extra["UniversalTime"].mtime else # Standard time field in central directory has local time # under archive creator. Then, we can't get timezone. @time end end alias :mtime :time def time=(aTime) unless @extra.member?("UniversalTime") @extra.create("UniversalTime") end @extra["UniversalTime"].mtime = aTime @time = aTime end # Returns +true+ if the entry is a directory. def directory? raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype @ftype == :directory end alias :is_directory :directory? # Returns +true+ if the entry is a file. def file? raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype @ftype == :file end # Returns +true+ if the entry is a symlink. def symlink? raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype @ftype == :symlink end def name_is_directory? #:nodoc:all (%r{\/$} =~ @name) != nil end def local_entry_offset #:nodoc:all localHeaderOffset + @local_header_size end def calculate_local_header_size #:nodoc:all LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.bytesize : 0) + (@extra ? @extra.local_size : 0) end def cdir_header_size #:nodoc:all CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.bytesize : 0) + (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.bytesize : 0) end def next_header_offset #:nodoc:all local_entry_offset + self.compressed_size end # Extracts entry to file destPath (defaults to @name). def extract(destPath = @name, &onExistsProc) onExistsProc ||= proc { Zip.options[:on_exists_proc] } if directory? create_directory(destPath, &onExistsProc) elsif file? write_file(destPath, &onExistsProc) elsif symlink? create_symlink(destPath, &onExistsProc) else raise RuntimeError, "unknown file type #{self.inspect}" end self end def to_s @name end protected class << self def read_zip_short(io) # :nodoc: io.read(2).unpack('v')[0] end def read_zip_long(io) # :nodoc: io.read(4).unpack('V')[0] end def read_c_dir_entry(io) #:nodoc:all entry = new(io.path) entry.read_c_dir_entry(io) entry rescue ZipError nil end def read_local_entry(io) entry = new(io.path) entry.read_local_entry(io) entry rescue ZipError nil end end public LOCAL_ENTRY_SIGNATURE = 0x04034b50 LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30 LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4 VERSION_NEEDED_TO_EXTRACT = 20 def read_local_entry(io) #:nodoc:all @localHeaderOffset = io.tell staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH) unless (staticSizedFieldsBuf.size == LOCAL_ENTRY_STATIC_HEADER_LENGTH) raise ZipError, "Premature end of file. Not enough data for zip entry local header" end @header_signature , @version , @fstype , @gp_flags , @compression_method, lastModTime , lastModDate , @crc , @compressed_size , @size , nameLength , extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv') unless (@header_signature == LOCAL_ENTRY_SIGNATURE) raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" end set_time(lastModDate, lastModTime) @name = io.read(nameLength) extra = io.read(extraLength) if (extra && extra.bytesize != extraLength) raise ZipError, "Truncated local zip entry header" else if ZipExtraField === @extra @extra.merge(extra) else @extra = ZipExtraField.new(extra) end end @local_header_size = calculate_local_header_size end def write_local_entry(io) #:nodoc:all @localHeaderOffset = io.tell io << [LOCAL_ENTRY_SIGNATURE , @versionNeededToExtract , # version needed to extract @gp_flags , # @gp_flags , @compression_method , @time.to_binary_dos_time , # @lastModTime , @time.to_binary_dos_date , # @lastModDate , @crc , @compressed_size , @size , @name ? @name.bytesize : 0, @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv') io << @name io << (@extra ? @extra.to_local_bin : "") end CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50 CDIR_ENTRY_STATIC_HEADER_LENGTH = 46 def read_c_dir_entry(io) #:nodoc:all staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH) unless (staticSizedFieldsBuf.bytesize == CDIR_ENTRY_STATIC_HEADER_LENGTH) raise ZipError, "Premature end of file. Not enough data for zip cdir entry header" end @header_signature , @version , # version of encoding software @fstype , # filesystem type @versionNeededToExtract , @gp_flags , @compression_method , lastModTime , lastModDate , @crc , @compressed_size , @size , nameLength , extraLength , commentLength , diskNumberStart , @internalFileAttributes , @externalFileAttributes , @localHeaderOffset , @name , @extra , @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV') unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE) raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" end set_time(lastModDate, lastModTime) @name = io.read(nameLength) if ZipExtraField === @extra @extra.merge(io.read(extraLength)) else @extra = ZipExtraField.new(io.read(extraLength)) end @comment = io.read(commentLength) unless (@comment && @comment.bytesize == commentLength) raise ZipError, "Truncated cdir zip entry header" end case @fstype when FSTYPE_UNIX @unix_perms = (@externalFileAttributes >> 16) & 07777 case (@externalFileAttributes >> 28) when 04 @ftype = :directory when 010 @ftype = :file when 012 @ftype = :symlink else #best case guess for whether it is a file or not #Otherwise this would be set to unknown and that entry would never be able to extracted if name_is_directory? @ftype = :directory else @ftype = :file end end else if name_is_directory? @ftype = :directory else @ftype = :file end end @local_header_size = calculate_local_header_size end def file_stat(path) # :nodoc: if @follow_symlinks return File::stat(path) else return File::lstat(path) end end def get_extra_attributes_from_path(path) # :nodoc: unless Zip::RUNNING_ON_WINDOWS stat = file_stat(path) @unix_uid = stat.uid @unix_gid = stat.gid @unix_perms = stat.mode & 07777 end end def set_extra_attributes_on_path(destPath) # :nodoc: return unless (file? or directory?) case @fstype when FSTYPE_UNIX # BUG: does not update timestamps into account # ignore setuid/setgid bits by default. honor if @restore_ownership unix_perms_mask = 01777 unix_perms_mask = 07777 if (@restore_ownership) FileUtils::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms) FileUtils::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0) # File::utimes() end end def write_c_dir_entry(io) #:nodoc:all case @fstype when FSTYPE_UNIX ft = nil case @ftype when :file ft = 010 @unix_perms ||= 0644 when :directory ft = 004 @unix_perms ||= 0755 when :symlink ft = 012 @unix_perms ||= 0755 end if (!ft.nil?) @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16 end end tmp = [ @header_signature, @version , # version of encoding software @fstype , # filesystem type @versionNeededToExtract , # @versionNeededToExtract , @gp_flags , # @gp_flags , @compression_method , @time.to_binary_dos_time , # @lastModTime , @time.to_binary_dos_date , # @lastModDate , @crc , @compressed_size , @size , @name ? @name.bytesize : 0 , @extra ? @extra.c_dir_length : 0 , @comment ? @comment.bytesize : 0 , 0 , # disk number start @internalFileAttributes , # file type (binary=0, text=1) @externalFileAttributes , # native filesystem attributes @localHeaderOffset , @name , @extra , @comment ] io << tmp.pack('VCCvvvvvVVVvvvvvVV') io << @name io << (@extra ? @extra.to_c_dir_bin : "") io << @comment end def == (other) return false unless other.class == self.class # Compares contents of local entry and exposed fields (@compression_method == other.compression_method && @crc == other.crc && @compressed_size == other.compressed_size && @size == other.size && @name == other.name && @extra == other.extra && @filepath == other.filepath && self.time.dos_equals(other.time)) end def <=> (other) return to_s <=> other.to_s end # Returns an IO like object for the given ZipEntry. # Warning: may behave weird with symlinks. def get_input_stream(&aProc) if @ftype == :directory return yield(NullInputStream.instance) if block_given? return NullInputStream.instance elsif @filepath case @ftype when :file return ::File.open(@filepath, "rb", &aProc) when :symlink linkpath = ::File::readlink(@filepath) stringio = StringIO.new(linkpath) return yield(stringio) if block_given? return stringio else raise "unknown @ftype #{@ftype}" end else zis = ZipInputStream.new(@zipfile, localHeaderOffset) zis.get_next_entry if block_given? begin return yield(zis) ensure zis.close end else return zis end end end def gather_fileinfo_from_srcpath(srcPath) # :nodoc: stat = file_stat(srcPath) case stat.ftype when 'file' if name_is_directory? raise ArgumentError, "entry name '#{newEntry}' indicates directory entry, but "+ "'#{srcPath}' is not a directory" end @ftype = :file when 'directory' if ! name_is_directory? @name += "/" end @ftype = :directory when 'link' if name_is_directory? raise ArgumentError, "entry name '#{newEntry}' indicates directory entry, but "+ "'#{srcPath}' is not a directory" end @ftype = :symlink else raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}" end @filepath = srcPath get_extra_attributes_from_path(@filepath) end def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all if @ftype == :directory aZipOutputStream.put_next_entry(self) elsif @filepath aZipOutputStream.put_next_entry(self) get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } else aZipOutputStream.copy_raw_entry(self) end end def parent_as_string entry_name = name.chomp("/") slash_index = entry_name.rindex("/") slash_index ? entry_name.slice(0, slash_index+1) : nil end def get_raw_input_stream(&aProc) ::File.open(@zipfile, "rb", &aProc) end private def set_time(binaryDosDate, binaryDosTime) @time = DOSTime.parse_binary_dos_format(binaryDosDate, binaryDosTime) rescue ArgumentError puts "Invalid date/time in zip entry" end def write_file(destPath, continueOnExistsProc = proc { Zip.options[:continue_on_exists_proc] }) if ::File.exists?(destPath) && ! yield(self, destPath) raise ZipDestinationFileExistsError, "Destination '#{destPath}' already exists" end ::File.open(destPath, "wb") do |os| get_input_stream do |is| set_extra_attributes_on_path(destPath) buf = '' while buf = is.sysread(Decompressor::CHUNK_SIZE, buf) os << buf end end end end def create_directory(destPath) if ::File.directory?(destPath) return elsif ::File.exists?(destPath) if block_given? && yield(self, destPath) FileUtils::rm_f destPath else raise ZipDestinationFileExistsError, "Cannot create directory '#{destPath}'. "+ "A file already exists with that name" end end Dir.mkdir destPath set_extra_attributes_on_path(destPath) end # BUG: create_symlink() does not use &onExistsProc def create_symlink(destPath) stat = nil begin stat = ::File::lstat(destPath) rescue Errno::ENOENT end io = get_input_stream linkto = io.read if stat if stat.symlink? if ::File::readlink(destPath) == linkto return else raise ZipDestinationFileExistsError, "Cannot create symlink '#{destPath}'. "+ "A symlink already exists with that name" end else raise ZipDestinationFileExistsError, "Cannot create symlink '#{destPath}'. "+ "A file already exists with that name" end end ::File::symlink(linkto, destPath) end 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.