module Zip class Entry STORED = 0 DEFLATED = 8 # Language encoding flag (EFS) bit EFS = 0b100000000000 attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, :name, :size, :local_header_offset, :zipfile, :fstype, :external_file_attributes, :gp_flags, :header_signature, :follow_symlinks, :restore_times, :restore_permissions, :restore_ownership, :unix_uid, :unix_gid, :unix_perms, :dirty attr_reader :ftype, :filepath # :nodoc: def set_default_vars_values @local_header_offset = 0 @local_header_size = nil # not known until local entry is created or read @internal_file_attributes = 1 @external_file_attributes = 0 @header_signature = ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT @version = VERSION_MADE_BY @ftype = nil # unspecified or unknown @filepath = nil @gp_flags = 0 if ::Zip.unicode_names @gp_flags |= EFS @version = 63 end @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 @dirty = false end def check_name(name) if name.start_with?('/') raise ::Zip::EntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" end end def initialize(*args) name = args[1] || '' check_name(name) set_default_vars_values @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX @zipfile = args[0] || '' @name = name @comment = args[2] || '' @extra = args[3] || '' @compressed_size = args[4] || 0 @crc = args[5] || 0 @compression_method = args[6] || ::Zip::Entry::DEFLATED @size = args[7] || 0 @time = args[8] || ::Zip::DOSTime.now @ftype = name_is_directory? ? :directory : :file @extra = ::Zip::ExtraField.new(@extra.to_s) unless ::Zip::ExtraField === @extra 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=(value) unless @extra.member?('UniversalTime') @extra.create('UniversalTime') end @extra['UniversalTime'].mtime = value @time = value end def file_type_is?(type) raise InternalError, "current filetype is unknown: #{self.inspect}" unless @ftype @ftype == type end # Dynamic checkers %w(directory file symlink).each do |k| define_method "#{k}?" do file_type_is?(k.to_sym) end end def name_is_directory? #:nodoc:all @name.end_with?('/') end def local_entry_offset #:nodoc:all local_header_offset + @local_header_size end def name_size @name ? @name.bytesize : 0 end def extra_size @extra ? @extra.local_size : 0 end def comment_size @comment ? @comment.bytesize : 0 end def calculate_local_header_size #:nodoc:all LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size end # check before rewriting an entry (after file sizes are known) # that we didn't change the header size (and thus clobber file data or something) def verify_local_header_size! return if @local_header_size.nil? new_size = calculate_local_header_size raise Error, "local header size changed (#{@local_header_size} -> #{new_size})" if @local_header_size != new_size end def cdir_header_size #:nodoc:all CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size + (@extra ? @extra.c_dir_size : 0) + comment_size end def next_header_offset #:nodoc:all local_entry_offset + self.compressed_size end # Extracts entry to file dest_path (defaults to @name). def extract(dest_path = @name, &block) block ||= proc { ::Zip.on_exists_proc } if directory? || file? || symlink? self.__send__("create_#{@ftype}", dest_path, &block) 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_zip_64_long(io) # :nodoc: io.read(8).unpack('Q<')[0] end def read_c_dir_entry(io) #:nodoc:all path = if io.is_a?(::IO) io.path else io end entry = new(path) entry.read_c_dir_entry(io) entry rescue Error nil end def read_local_entry(io) entry = self.new(io) entry.read_local_entry(io) entry rescue Error nil end end public def unpack_local_entry(buf) @header_signature, @version, @fstype, @gp_flags, @compression_method, @last_mod_time, @last_mod_date, @crc, @compressed_size, @size, @name_length, @extra_length = buf.unpack('VCCvvvvVVVvv') end def read_local_entry(io) #:nodoc:all @local_header_offset = io.tell static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH raise Error, "Premature end of file. Not enough data for zip entry local header" end unpack_local_entry(static_sized_fields_buf) unless @header_signature == ::Zip::LOCAL_ENTRY_SIGNATURE raise ::Zip::Error, "Zip local header magic not found at location '#{local_header_offset}'" end set_time(@last_mod_date, @last_mod_time) @name = io.read(@name_length) extra = io.read(@extra_length) @name.gsub!('\\', '/') if extra && extra.bytesize != @extra_length raise ::Zip::Error, "Truncated local zip entry header" else if ::Zip::ExtraField === @extra @extra.merge(extra) else @extra = ::Zip::ExtraField.new(extra) end end parse_zip64_extra(true) @local_header_size = calculate_local_header_size end def pack_local_entry zip64 = @extra['Zip64'] [::Zip::LOCAL_ENTRY_SIGNATURE, @version_needed_to_extract, # version needed to extract @gp_flags, # @gp_flags , @compression_method, @time.to_binary_dos_time, # @last_mod_time , @time.to_binary_dos_date, # @last_mod_date , @crc, (zip64 && zip64.compressed_size) ? 0xFFFFFFFF : @compressed_size, (zip64 && zip64.original_size) ? 0xFFFFFFFF : @size, name_size, @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv') end def write_local_entry(io, rewrite = false) #:nodoc:all prep_zip64_extra(true) verify_local_header_size! if rewrite @local_header_offset = io.tell io << pack_local_entry io << @name io << (@extra ? @extra.to_local_bin : '') @local_header_size = io.tell - @local_header_offset end def unpack_c_dir_entry(buf) @header_signature, @version, # version of encoding software @fstype, # filesystem type @version_needed_to_extract, @gp_flags, @compression_method, @last_mod_time, @last_mod_date, @crc, @compressed_size, @size, @name_length, @extra_length, @comment_length, _, # diskNumberStart @internal_file_attributes, @external_file_attributes, @local_header_offset, @name, @extra, @comment = buf.unpack('VCCvvvvvVVVvvvvvVV') end def set_ftype_from_c_dir_entry @ftype = case @fstype when ::Zip::FSTYPE_UNIX @unix_perms = (@external_file_attributes >> 16) & 07777 case (@external_file_attributes >> 28) when ::Zip::FILE_TYPE_DIR :directory when ::Zip::FILE_TYPE_FILE :file when ::Zip::FILE_TYPE_SYMLINK :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? :directory else :file end end else if name_is_directory? :directory else :file end end end def check_c_dir_entry_static_header_length(buf) unless buf.bytesize == ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH raise Error, 'Premature end of file. Not enough data for zip cdir entry header' end end def check_c_dir_entry_signature unless header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE raise Error, "Zip local header magic not found at location '#{local_header_offset}'" end end def check_c_dir_entry_comment_size unless @comment && @comment.bytesize == @comment_length raise ::Zip::Error, "Truncated cdir zip entry header" end end def read_c_dir_extra_field(io) if @extra.is_a?(::Zip::ExtraField) @extra.merge(io.read(@extra_length)) else @extra = ::Zip::ExtraField.new(io.read(@extra_length)) end end def read_c_dir_entry(io) #:nodoc:all static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH) check_c_dir_entry_static_header_length(static_sized_fields_buf) unpack_c_dir_entry(static_sized_fields_buf) check_c_dir_entry_signature set_time(@last_mod_date, @last_mod_time) @name = io.read(@name_length).gsub('\\', '/') read_c_dir_extra_field(io) @comment = io.read(@comment_length) check_c_dir_entry_comment_size set_ftype_from_c_dir_entry parse_zip64_extra(false) end def file_stat(path) # :nodoc: if @follow_symlinks ::File::stat(path) else ::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_unix_permissions_on_path(dest_path) # 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, dest_path) if @restore_permissions && @unix_perms ::FileUtils.chown(@unix_uid, @unix_gid, dest_path) if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0 # File::utimes() end def set_extra_attributes_on_path(dest_path) # :nodoc: return unless (file? || directory?) case @fstype when ::Zip::FSTYPE_UNIX set_unix_permissions_on_path(dest_path) end end def pack_c_dir_entry zip64 = @extra['Zip64'] [ @header_signature, @version, # version of encoding software @fstype, # filesystem type @version_needed_to_extract, # @versionNeededToExtract , @gp_flags, # @gp_flags , @compression_method, @time.to_binary_dos_time, # @last_mod_time , @time.to_binary_dos_date, # @last_mod_date , @crc, (zip64 && zip64.compressed_size) ? 0xFFFFFFFF : @compressed_size, (zip64 && zip64.original_size) ? 0xFFFFFFFF : @size, name_size, @extra ? @extra.c_dir_size : 0, comment_size, (zip64 && zip64.disk_start_number) ? 0xFFFF : 0, # disk number start @internal_file_attributes, # file type (binary=0, text=1) @external_file_attributes, # native filesystem attributes (zip64 && zip64.relative_header_offset) ? 0xFFFFFFFF : @local_header_offset, @name, @extra, @comment ].pack('VCCvvvvvVVVvvvvvVV') end def write_c_dir_entry(io) #:nodoc:all prep_zip64_extra(false) case @fstype when ::Zip::FSTYPE_UNIX ft = case @ftype when :file @unix_perms ||= 0644 ::Zip::FILE_TYPE_FILE when :directory @unix_perms ||= 0755 ::Zip::FILE_TYPE_DIR when :symlink @unix_perms ||= 0755 ::Zip::FILE_TYPE_SYMLINK end unless ft.nil? @external_file_attributes = (ft << 12 | (@unix_perms & 07777)) << 16 end end io << pack_c_dir_entry 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 keys_equal = %w(compression_method crc compressed_size size name extra filepath).all? do |k| other.__send__(k.to_sym) == self.__send__(k.to_sym) end keys_equal && self.time.dos_equals(other.time) end def <=> (other) self.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(&block) if @ftype == :directory yield ::Zip::NullInputStream if block_given? ::Zip::NullInputStream elsif @filepath case @ftype when :file ::File.open(@filepath, 'rb', &block) when :symlink linkpath = ::File.readlink(@filepath) stringio = ::StringIO.new(linkpath) yield(stringio) if block_given? stringio else raise "unknown @file_type #{@ftype}" end else zis = ::Zip::InputStream.new(@zipfile, local_header_offset) zis.get_next_entry if block_given? begin yield(zis) ensure zis.close end else zis end end end def gather_fileinfo_from_srcpath(src_path) # :nodoc: stat = file_stat(src_path) @ftype = case stat.ftype when 'file' if name_is_directory? raise ArgumentError, "entry name '#{newEntry}' indicates directory entry, but "+ "'#{src_path}' is not a directory" end :file when 'directory' @name += '/' unless name_is_directory? :directory when 'link' if name_is_directory? raise ArgumentError, "entry name '#{newEntry}' indicates directory entry, but "+ "'#{src_path}' is not a directory" end :symlink else raise RuntimeError, "unknown file type: #{src_path.inspect} #{stat.inspect}" end @filepath = src_path get_extra_attributes_from_path(@filepath) end def write_to_zip_output_stream(zip_output_stream) #:nodoc:all if @ftype == :directory zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::STORED) elsif @filepath zip_output_stream.put_next_entry(self, nil, nil, ::Zip::Entry::DEFLATED) get_input_stream { |is| ::Zip::IOExtras.copy_stream(zip_output_stream, is) } else zip_output_stream.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(&block) if @zipfile.is_a?(::IO) || @zipfile.is_a?(::StringIO) yield @zipfile else ::File.open(@zipfile, "rb", &block) end end private def set_time(binary_dos_date, binary_dos_time) @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time) rescue ArgumentError puts "Invalid date/time in zip entry" end def create_file(dest_path, continue_on_exists_proc = proc { Zip.continue_on_exists_proc }) if ::File.exist?(dest_path) && !yield(self, dest_path) raise ::Zip::DestinationFileExistsError, "Destination '#{dest_path}' already exists" end ::File.open(dest_path, "wb") do |os| get_input_stream do |is| set_extra_attributes_on_path(dest_path) buf = '' while buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf) os << buf end end end end def create_directory(dest_path) return if ::File.directory?(dest_path) if ::File.exist?(dest_path) if block_given? && yield(self, dest_path) ::FileUtils::rm_f dest_path else raise ::Zip::DestinationFileExistsError, "Cannot create directory '#{dest_path}'. "+ "A file already exists with that name" end end ::FileUtils.mkdir_p(dest_path) set_extra_attributes_on_path(dest_path) end # BUG: create_symlink() does not use &block def create_symlink(dest_path) stat = nil begin stat = ::File.lstat(dest_path) rescue Errno::ENOENT end io = get_input_stream linkto = io.read if stat if stat.symlink? if ::File.readlink(dest_path) == linkto return else raise ::Zip::DestinationFileExistsError, "Cannot create symlink '#{dest_path}'. "+ "A symlink already exists with that name" end else raise ::Zip::DestinationFileExistsError, "Cannot create symlink '#{dest_path}'. "+ "A file already exists with that name" end end ::File.symlink(linkto, dest_path) end # apply missing data from the zip64 extra information field, if present # (required when file sizes exceed 2**32, but can be used for all files) def parse_zip64_extra(for_local_header) #:nodoc:all if zip64 = @extra['Zip64'] if for_local_header @size, @compressed_size = zip64.parse(@size, @compressed_size) else @size, @compressed_size, @local_header_offset = zip64.parse(@size, @compressed_size, @local_header_offset) end end end # create a zip64 extra information field if we need one def prep_zip64_extra(for_local_header) #:nodoc:all return unless ::Zip.write_zip64_support need_zip64 = @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF unless for_local_header need_zip64 ||= @local_header_offset >= 0xFFFFFFFF end if need_zip64 @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64 @extra.delete('Zip64Placeholder') zip64 = @extra.create('Zip64') if for_local_header # local header always includes size and compressed size zip64.original_size = @size zip64.compressed_size = @compressed_size else # central directory entry entries include whichever fields are necessary zip64.original_size = @size if @size >= 0xFFFFFFFF zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF end else @extra.delete('Zip64') # if this is a local header entry, create a placeholder # so we have room to write a zip64 extra field afterward # (we won't know if it's needed until the file data is written) if for_local_header @extra.create('Zip64Placeholder') else @extra.delete('Zip64Placeholder') end end 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.