Archive::Zip::Entry

The Archive::Zip::Entry mixin provides classes with methods implementing many of the common features of all entry types. Some of these methods, such as dump_local_file_record and dump_central_file_record, are required by Archive::Zip in order to store the entry into an archive. Those should be left alone. Others, such as ftype and mode=, are expected to be overridden to provide sensible information for the new entry type.

A class using this mixin must provide 2 methods: extract and dump_file_data. extract should be a public method with the following signature:

def extract(options = {})
  ...
end

This method should extract the contents of the entry to the filesystem. options should be an optional Hash containing a mapping of option names to option values. Please refer to Archive::Zip::Entry::File#extract, Archive::Zip::Entry::Symlink#extract, and Archive::Zip::Entry::Directory#extract for examples of the options currently supported.

dump_file_data should be a private method with the following signature:

def dump_file_data(io)
  ...
end

This method should use the write method of io to write all file data. io will be a writable, IO-like object.

The class methods from_file and parse are factories for creating the 3 kinds of concrete entries currently implemented: File, Directory, and Symlink. While it is possible to create new archives using custom entry implementations, it is not possible to load those same entries from the archive since the parse factory method does not know about them. Patches to support new entry types are welcome.

Constants

CFHRecord
FLAG_DATA_DESCRIPTOR_FOLLOWS

When this flag is set in the general purpose flags, it indicates that the read data descriptor record for a local file record is located after the entry's file data.

FLAG_ENCRYPTED

When this flag is set in the general purpose flags, it indicates that the entry's file data is encrypted using the original (weak) algorithm.

LFHRecord

Attributes

atime[RW]

The last accessed time.

comment[RW]

The comment associated with this entry.

compression_codec[RW]

The selected compression codec.

encryption_codec[RW]

The selected encryption codec.

expected_data_descriptor[RW]

An Archive::Zip::DataDescriptor instance which should contain the expected CRC32 checksum, compressed size, and uncompressed size for the file data. When not nil, this is used by extract to confirm that the data extraction was successful.

gid[RW]

The group ID of the owner of this entry.

mode[RW]

The file mode/permission bits for this entry.

mtime[RW]

The last modified time.

password[RW]

The password used with the encryption codec to encrypt or decrypt the file data for an entry.

raw_data[RW]

The raw, possibly compressed and/or encrypted file data for an entry.

uid[RW]

The user ID of the owner of this entry.

zip_path[R]

The path for this entry in the ZIP archive.

Public Class Methods

expand_path(zip_path) click to toggle source

Cleans up and returns zip_path by eliminating . and .. references, leading and trailing /'s, and runs of /'s.

# File lib/archive/zip/entry.rb, line 93
def self.expand_path(zip_path)
  result = []
  source = zip_path.split('/')

  source.each do |e|
    next if e.empty? || e == '.'

    if e == '..' && ! (result.last.nil? || result.last == '..') then
      result.pop
    else
      result.push(e)
    end
  end
  result.shift while result.first == '..'

  result.join('/')
end
from_file(file_path, options = {}) click to toggle source

Creates a new Entry based upon a file, symlink, or directory. file_path points to the source item. options is a Hash optionally containing the following:

:zip_path

The path for the entry in the archive where `/' is the file separator character. This defaults to the basename of file_path if unspecified.

:follow_symlinks

When set to true (the default), symlinks are treated as the files or directories to which they point.

:compression_codec

Specifies a proc, lambda, or class. If a proc or lambda is used, it must take a single argument containing a zip entry and return a compression codec class to be instantiated and used with the entry. Otherwise, a compression codec class must be specified directly. When unset, the default compression codec for each entry type is used.

:encryption_codec

Specifies a proc, lambda, or class. If a proc or lambda is used, it must take a single argument containing a zip entry and return an encryption codec class to be instantiated and used with the entry. Otherwise, an encryption codec class must be specified directly. When unset, the default encryption codec for each entry type is used.

Raises Archive::Zip::EntryError if processing the given file path results in a file not found error.

# File lib/archive/zip/entry.rb, line 135
def self.from_file(file_path, options = {})
  zip_path        = options.has_key?(:zip_path) ?
                    expand_path(options[:zip_path]) :
                    ::File.basename(file_path)
  follow_symlinks = options.has_key?(:follow_symlinks) ?
                    options[:follow_symlinks] :
                    true

  # Avoid repeatedly stat'ing the file by storing the stat structure once.
  begin
    stat = follow_symlinks ?
           ::File.stat(file_path) :
           ::File.lstat(file_path)
  rescue Errno::ENOENT
    if ::File.symlink?(file_path) then
      raise Zip::EntryError,
        "symlink at `#{file_path}' points to a non-existent file `#{::File.readlink(file_path)}'"
    else
      raise Zip::EntryError, "no such file or directory `#{file_path}'"
    end
  end

  # Ensure that zip paths for directories end with '/'.
  if stat.directory? then
    zip_path += '/'
  end

  # Instantiate the entry.
  if stat.symlink? then
    entry = Entry::Symlink.new(zip_path)
    entry.link_target = ::File.readlink(file_path)
  elsif stat.file? then
    entry = Entry::File.new(zip_path)
    entry.file_path = file_path
  elsif stat.directory? then
    entry = Entry::Directory.new(zip_path)
  else
    raise Zip::EntryError,
      "unsupported file type `#{stat.ftype}' for file `#{file_path}'"
  end

  # Set the compression and encryption codecs.
  unless options[:compression_codec].nil? then
    if options[:compression_codec].kind_of?(Proc) then
      entry.compression_codec = options[:compression_codec][entry].new
    else
      entry.compression_codec = options[:compression_codec].new
    end
  end
  unless options[:encryption_codec].nil? then
    if options[:encryption_codec].kind_of?(Proc) then
      entry.encryption_codec = options[:encryption_codec][entry].new
    else
      entry.encryption_codec = options[:encryption_codec].new
    end
  end

  # Set the entry's metadata.
  entry.uid = stat.uid
  entry.gid = stat.gid
  entry.mtime = stat.mtime
  entry.atime = stat.atime
  entry.mode = stat.mode

  entry
end
new(zip_path, raw_data = nil) click to toggle source

Creates a new, uninitialized Entry instance using the Store compression method. The zip path is initialized to zip_path. raw_data, if specified, must be a readable, IO-like object containing possibly compressed/encrypted file data for the entry. It is intended to be used primarily by the parse class method.

# File lib/archive/zip/entry.rb, line 472
def initialize(zip_path, raw_data = nil)
  self.zip_path = zip_path
  self.mtime = Time.now
  self.atime = @mtime
  self.uid = nil
  self.gid = nil
  self.mode = 0777
  self.comment = ''
  self.expected_data_descriptor = nil
  self.compression_codec = Zip::Codec::Store.new
  self.encryption_codec = Zip::Codec::NullEncryption.new
  self.password = nil
  @raw_data = raw_data
  @extra_fields = []
end
parse(io) click to toggle source

Creates and returns a new entry object by parsing from the current position of io. io must be a readable, IO-like object which is positioned at the start of a central file record following the signature for that record.

NOTE: For now io MUST be seekable.

Currently, the only entry objects returned are instances of Archive::Zip::Entry::File, Archive::Zip::Entry::Directory, and Archive::Zip::Entry::Symlink. Any other kind of entry will be mapped into an instance of Archive::Zip::Entry::File.

Raises Archive::Zip::EntryError for any other errors related to processing the entry.

# File lib/archive/zip/entry.rb, line 216
def self.parse(io)
  # Parse the central file record and then use the information found there
  # to locate and parse the corresponding local file record.
  cfr = parse_central_file_record(io)
  next_record_position = io.pos
  io.seek(cfr.local_header_position)
  unless IOExtensions.read_exactly(io, 4) == LFH_SIGNATURE then
    raise Zip::EntryError, 'bad local file header signature'
  end
  lfr = parse_local_file_record(io, cfr.compressed_size)

  # Check to ensure that the contents of the central file record and the
  # local file record which are supposed to be duplicated are in fact the
  # same.
  compare_file_records(lfr, cfr)

  begin
    # Load the correct compression codec.
    compression_codec = Codec.create_compression_codec(
      cfr.compression_method,
      cfr.general_purpose_flags
    )
  rescue Zip::Error => e
    raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
  end

  begin
    # Load the correct encryption codec.
    encryption_codec = Codec.create_encryption_codec(
      cfr.general_purpose_flags
    )
  rescue Zip::Error => e
    raise Zip::EntryError, "`#{cfr.zip_path}': #{e.message}"
  end

  # Set up a data descriptor with expected values for later comparison.
  expected_data_descriptor = DataDescriptor.new(
    cfr.crc32,
    cfr.compressed_size,
    cfr.uncompressed_size
  )

  # Create the entry.
  expanded_path = expand_path(cfr.zip_path)
  io_window = IOWindow.new(io, io.pos, cfr.compressed_size)
  if cfr.zip_path[-1..-1] == '/' then
    # This is a directory entry.
    entry = Entry::Directory.new(expanded_path, io_window)
  elsif (cfr.external_file_attributes >> 16) & 0770000 == 0120000 then
    # This is a symlink entry.
    entry = Entry::Symlink.new(expanded_path, io_window)
  else
    # Anything else is a file entry.
    entry = Entry::File.new(expanded_path, io_window)
  end

  # Set the expected data descriptor so that extraction can be verified.
  entry.expected_data_descriptor = expected_data_descriptor
  # Record the compression codec.
  entry.compression_codec = compression_codec
  # Record the encryption codec.
  entry.encryption_codec = encryption_codec
  # Set some entry metadata.
  entry.mtime = cfr.mtime
  # Only set mode bits for the entry if the external file attributes are
  # Unix-compatible.
  if cfr.made_by_version & 0xFF00 == 0x0300 then
    entry.mode = cfr.external_file_attributes >> 16
  end
  entry.comment = cfr.comment
  cfr.extra_fields.each { |ef| entry.add_extra_field(ef) }
  lfr.extra_fields.each { |ef| entry.add_extra_field(ef) }

  # Return to the beginning of the next central directory record.
  io.seek(next_record_position)

  entry
end

Public Instance Methods

add_extra_field(extra_field) click to toggle source

Adds extra_field as an extra field specification to both the central file record and the local file record of this entry.

If extra_field is an instance of Archive::Zip::Entry::ExtraField::ExtendedTimestamp, the values of that field are used to set mtime and atime for this entry. If extra_field is an instance of Archive::Zip::Entry::ExtraField::Unix, the values of that field are used to set mtime, atime, uid, and gid for this entry.

# File lib/archive/zip/entry.rb, line 557
def add_extra_field(extra_field)
  # Try to find an extra field with the same header ID already in the list
  # and merge the new one with that if one exists; otherwise, add the new
  # one to the list.
  existing_extra_field = @extra_fields.find do |ef|
    ef.header_id == extra_field.header_id
  end
  if existing_extra_field.nil? then
    @extra_fields << extra_field
  else
    extra_field = existing_extra_field.merge(extra_field)
  end

  # Set some attributes of this entry based on the settings in select types
  # of extra fields.
  if extra_field.kind_of?(ExtraField::ExtendedTimestamp) then
    self.mtime = extra_field.mtime unless extra_field.mtime.nil?
    self.atime = extra_field.atime unless extra_field.atime.nil?
  elsif extra_field.kind_of?(ExtraField::Unix) then
    self.mtime = extra_field.mtime unless extra_field.mtime.nil?
    self.atime = extra_field.atime unless extra_field.atime.nil?
    self.uid   = extra_field.uid unless extra_field.uid.nil?
    self.gid   = extra_field.gid unless extra_field.uid.nil?
  end
  self
end
directory?() click to toggle source

Returns false.

# File lib/archive/zip/entry.rb, line 545
def directory?
  false
end
dump_central_file_record(io) click to toggle source

Writes the central file record for this entry to io, a writable, IO-like object which provides a write method. Returns the number of bytes written.

NOTE: This method should only be called by Archive::Zip.

# File lib/archive/zip/entry.rb, line 693
def dump_central_file_record(io)
  bytes_written = 0

  # Assume that no trailing data descriptor will be necessary.
  need_trailing_data_descriptor = false
  begin
    io.pos
  rescue Errno::ESPIPE
    # A trailing data descriptor is required for non-seekable IO.
    need_trailing_data_descriptor = true
  end
  if encryption_codec.class == Codec::TraditionalEncryption then
    # HACK:
    # According to the ZIP specification, a trailing data descriptor should
    # only be required when writing to non-seekable IO , but InfoZIP
    # *always* does this when using traditional encryption even though it
    # will also write the data descriptor in the usual place if possible.
    # Failure to emulate InfoZIP in this behavior will prevent InfoZIP
    # compatibility with traditionally encrypted entries.
    need_trailing_data_descriptor = true
  end

  # Set the general purpose flags.
  general_purpose_flags  = compression_codec.general_purpose_flags
  general_purpose_flags |= encryption_codec.general_purpose_flags
  if need_trailing_data_descriptor then
    general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
  end

  # Select the minimum ZIP specification version needed to extract this
  # entry.
  version_needed_to_extract = compression_codec.version_needed_to_extract
  if encryption_codec.version_needed_to_extract > version_needed_to_extract then
    version_needed_to_extract = encryption_codec.version_needed_to_extract
  end

  # Write the data.
  bytes_written += io.write(CFH_SIGNATURE)
  bytes_written += io.write(
    [
      version_made_by,
      version_needed_to_extract,
      general_purpose_flags,
      compression_codec.compression_method,
      mtime.to_dos_time.to_i
    ].pack('vvvvV')
  )
  bytes_written += @data_descriptor.dump(io)
  extra_field_data = central_extra_field_data
  bytes_written += io.write(
    [
      zip_path.length,
      extra_field_data.length,
      comment.length,
      0,
      internal_file_attributes,
      external_file_attributes,
      @local_file_record_position
    ].pack('vvvvvVV')
  )
  bytes_written += io.write(zip_path)
  bytes_written += io.write(extra_field_data)
  bytes_written += io.write(comment)

  bytes_written
end
dump_local_file_record(io, local_file_record_position) click to toggle source

Writes the local file record for this entry to io, a writable, IO-like object which provides a write method. local_file_record_position is the offset within io at which writing will begin. This is used so that when writing to a non-seekable IO object it is possible to avoid calling the pos method of io. Returns the number of bytes written.

NOTE: This method should only be called by Archive::Zip.

# File lib/archive/zip/entry.rb, line 591
def dump_local_file_record(io, local_file_record_position)
  @local_file_record_position = local_file_record_position
  bytes_written = 0

  # Assume that no trailing data descriptor will be necessary.
  need_trailing_data_descriptor = false
  begin
    io.pos
  rescue Errno::ESPIPE
    # A trailing data descriptor is required for non-seekable IO.
    need_trailing_data_descriptor = true
  end
  if encryption_codec.class == Codec::TraditionalEncryption then
    # HACK:
    # According to the ZIP specification, a trailing data descriptor should
    # only be required when writing to non-seekable IO , but InfoZIP
    # *always* does this when using traditional encryption even though it
    # will also write the data descriptor in the usual place if possible.
    # Failure to emulate InfoZIP in this behavior will prevent InfoZIP
    # compatibility with traditionally encrypted entries.
    need_trailing_data_descriptor = true
    # HACK:
    # The InfoZIP implementation of traditional encryption requires that the
    # the last modified file time be used as part of the encryption header.
    # This is a deviation from the ZIP specification.
    encryption_codec.mtime = mtime
  end

  # Set the general purpose flags.
  general_purpose_flags  = compression_codec.general_purpose_flags
  general_purpose_flags |= encryption_codec.general_purpose_flags
  if need_trailing_data_descriptor then
    general_purpose_flags |= FLAG_DATA_DESCRIPTOR_FOLLOWS
  end

  # Select the minimum ZIP specification version needed to extract this
  # entry.
  version_needed_to_extract = compression_codec.version_needed_to_extract
  if encryption_codec.version_needed_to_extract > version_needed_to_extract then
    version_needed_to_extract = encryption_codec.version_needed_to_extract
  end

  # Write the data.
  bytes_written += io.write(LFH_SIGNATURE)
  extra_field_data = local_extra_field_data
  bytes_written += io.write(
    [
      version_needed_to_extract,
      general_purpose_flags,
      compression_codec.compression_method,
      mtime.to_dos_time.to_i,
      0,
      0,
      0,
      zip_path.length,
      extra_field_data.length
    ].pack('vvvVVVVvv')
  )
  bytes_written += io.write(zip_path)
  bytes_written += io.write(extra_field_data)

  # Pipeline a compressor into an encryptor, write all the file data to the
  # compressor, and get a data descriptor from it.
  encryption_codec.encryptor(io, password) do |e|
    compression_codec.compressor(e) do |c|
      dump_file_data(c)
      c.close(false)
      @data_descriptor = DataDescriptor.new(
        c.data_descriptor.crc32,
        c.data_descriptor.compressed_size + encryption_codec.header_size,
        c.data_descriptor.uncompressed_size
      )
    end
    e.close(false)
  end
  bytes_written += @data_descriptor.compressed_size

  # Write the trailing data descriptor if necessary.
  if need_trailing_data_descriptor then
    bytes_written += io.write(DD_SIGNATURE)
    bytes_written += @data_descriptor.dump(io)
  end

  begin
    # Update the data descriptor located before the compressed data for the
    # entry.
    saved_position = io.pos
    io.pos = @local_file_record_position + 14
    @data_descriptor.dump(io)
    io.pos = saved_position
  rescue Errno::ESPIPE
    # Ignore a failed attempt to update the data descriptor.
  end

  bytes_written
end
file?() click to toggle source

Returns false.

# File lib/archive/zip/entry.rb, line 535
def file?
  false
end
ftype() click to toggle source

Returns the file type of this entry as the symbol :unknown.

Override this in concrete subclasses to return an appropriate symbol.

# File lib/archive/zip/entry.rb, line 530
def ftype
  :unknown
end
zip_path=(zip_path) click to toggle source

Sets the path in the archive for this entry to zip_path after passing it through Archive::Zip::Entry.expand_path and ensuring that the result is not empty.

# File lib/archive/zip/entry.rb, line 520
def zip_path=(zip_path)
  @zip_path = Archive::Zip::Entry.expand_path(zip_path)
  if @zip_path.empty? then
    raise ArgumentError, "zip path expands to empty string"
  end
end

[Validate]

Generated with the Darkfish Rdoc Generator 2.