rubyzip/rubyzip

View on GitHub
lib/zip/entry.rb

Summary

Maintainability
F
4 days
Test Coverage
# frozen_string_literal: true

require 'pathname'

require_relative 'dirtyable'

module Zip
  # Zip::Entry represents an entry in a Zip archive.
  class Entry
    include Dirtyable

    STORED   = ::Zip::COMPRESSION_METHOD_STORE
    DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE

    # Language encoding flag (EFS) bit
    EFS = 0b100000000000

    # Compression level flags (used as part of the gp flags).
    COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110
    COMPRESSION_LEVEL_FAST_GPFLAG = 0b100
    COMPRESSION_LEVEL_MAX_GPFLAG = 0b010

    attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
                  :restore_ownership, :restore_permissions, :restore_times,
                  :unix_gid, :unix_perms, :unix_uid

    attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
                  :internal_file_attributes, :local_header_offset # :nodoc:

    attr_reader :extra, :compression_level, :filepath # :nodoc:

    attr_writer :size # :nodoc:

    mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
               :fstype=, :gp_flags=, :name=, :size=,
               :unix_gid=, :unix_perms=, :unix_uid=

    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       = DEFAULT_RESTORE_OPTIONS[:restore_times]
      @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
      @restore_ownership   = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
      # BUG: need an extra field to support uid/gid's
      @unix_uid            = nil
      @unix_gid            = nil
      @unix_perms          = nil
    end

    def check_name(name)
      raise EntryNameError, name if name.start_with?('/')
      raise EntryNameError if name.length > 65_535
    end

    def initialize(
      zipfile = '', name = '',
      comment: '', size: nil, compressed_size: 0, crc: 0,
      compression_method: DEFLATED,
      compression_level: ::Zip.default_compression,
      time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
    )
      super()
      @name = name
      check_name(@name)

      set_default_vars_values
      @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX

      @zipfile            = zipfile
      @comment            = comment || ''
      @compression_method = compression_method || DEFLATED
      @compression_level  = compression_level || ::Zip.default_compression
      @compressed_size    = compressed_size || 0
      @crc                = crc || 0
      @size               = size
      @time               = case time
                            when ::Zip::DOSTime
                              time
                            when Time
                              ::Zip::DOSTime.from_time(time)
                            else
                              ::Zip::DOSTime.now
                            end
      @extra              =
        extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)

      set_compression_level_flags
    end

    def encrypted?
      gp_flags & 1 == 1
    end

    def incomplete?
      gp_flags & 8 == 8
    end

    def size
      @size || 0
    end

    def time(component: :mtime)
      time =
        if @extra['UniversalTime']
          @extra['UniversalTime'].send(component)
        elsif @extra['NTFS']
          @extra['NTFS'].send(component)
        end

      # Standard time field in central directory has local time
      # under archive creator. Then, we can't get timezone.
      time || (@time if component == :mtime)
    end

    alias mtime time

    def atime
      time(component: :atime)
    end

    def ctime
      time(component: :ctime)
    end

    def time=(value, component: :mtime)
      @dirty = true
      unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
        @extra.create('UniversalTime')
      end

      value = DOSTime.from_time(value)
      comp = "#{component}=" unless component.to_s.end_with?('=')
      (@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
      @time = value if component == :mtime
    end

    alias mtime= time=

    def atime=(value)
      send(:time=, value, component: :atime)
    end

    def ctime=(value)
      send(:time=, value, component: :ctime)
    end

    def compression_method
      return STORED if ftype == :directory || @compression_level == 0

      @compression_method
    end

    def compression_method=(method)
      @dirty = true
      @compression_method = (ftype == :directory ? STORED : method)
    end

    def zip64?
      !@extra['Zip64'].nil?
    end

    def file_type_is?(type)
      ftype == type
    end

    def ftype # :nodoc:
      @ftype ||= name_is_directory? ? :directory : :file
    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

    # Is the name a relative path, free of `..` patterns that could lead to
    # path traversal attacks? This does NOT handle symlinks; if the path
    # contains symlinks, this check is NOT enough to guarantee safety.
    def name_safe?
      cleanpath = Pathname.new(@name).cleanpath
      return false unless cleanpath.relative?

      root = ::File::SEPARATOR
      naive = ::File.join(root, cleanpath.to_s)
      # Allow for Windows drive mappings at the root.
      ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
    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
      return unless @local_header_size != new_size

      raise Error,
            "Local header size changed (#{@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 + compressed_size
    end

    # Extracts this entry to a file at `entry_path`, with
    # `destination_directory` as the base location in the filesystem.
    #
    # NB: The caller is responsible for making sure `destination_directory` is
    # safe, if it is passed.
    def extract(entry_path = @name, destination_directory: '.', &block)
      dest_dir = ::File.absolute_path(destination_directory || '.')
      extract_path = ::File.absolute_path(::File.join(dest_dir, entry_path))

      unless extract_path.start_with?(dest_dir)
        warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
        return self
      end

      block ||= proc { ::Zip.on_exists_proc }

      raise "unknown file type #{inspect}" unless directory? || file? || symlink?

      __send__(:"create_#{ftype}", extract_path, &block)
      self
    end

    def to_s
      @name
    end

    class << self
      def read_c_dir_entry(io) # :nodoc:all
        path = if io.respond_to?(:path)
                 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 = new(io)
        entry.read_local_entry(io)
        entry
      rescue SplitArchiveError
        raise
      rescue Error
        nil
      end
    end

    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
      @dirty = false # No changes at this point.
      @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 == LOCAL_ENTRY_SIGNATURE
        if @header_signature == SPLIT_FILE_SIGNATURE
          raise SplitArchiveError
        end

        raise 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)
      if ::Zip.force_entry_names_encoding
        @name.force_encoding(::Zip.force_entry_names_encoding)
      end
      @name.tr!('\\', '/') # Normalise filepath separators after encoding set.

      # We need to do this here because `initialize` has so many side-effects.
      # :-(
      @ftype = name_is_directory? ? :directory : :file

      extra = io.read(@extra_length)
      if extra && extra.bytesize != @extra_length
        raise ::Zip::Error, 'Truncated local zip entry header'
      end

      read_extra_field(extra, local: true)
      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 || 0),
       name_size,
       @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
    end

    def write_local_entry(io, rewrite: false) # :nodoc:all
      prep_local_zip64_extra
      verify_local_header_size! if rewrite
      @local_header_offset = io.tell

      io << pack_local_entry

      io << @name
      io << @extra.to_local_bin if @extra
      @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) & 0o7777
                 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 be 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)
      return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH

      raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
    end

    def check_c_dir_entry_signature
      return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE

      raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
    end

    def check_c_dir_entry_comment_size
      return if @comment && @comment.bytesize == @comment_length

      raise ::Zip::Error, 'Truncated cdir zip entry header'
    end

    def read_extra_field(buf, local: false)
      if @extra.kind_of?(::Zip::ExtraField)
        @extra.merge(buf, local: local) if buf
      else
        @extra = ::Zip::ExtraField.new(buf, local: local)
      end
    end

    def read_c_dir_entry(io) # :nodoc:all
      @dirty = false # No changes at this point.
      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)
      if ::Zip.force_entry_names_encoding
        @name.force_encoding(::Zip.force_entry_names_encoding)
      end
      @name.tr!('\\', '/') # Normalise filepath separators after encoding set.

      read_extra_field(io.read(@extra_length))
      @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:
      stat = file_stat(path)
      @time = DOSTime.from_time(stat.mtime)
      return if ::Zip::RUNNING_ON_WINDOWS

      @unix_uid   = stat.uid
      @unix_gid   = stat.gid
      @unix_perms = stat.mode & 0o7777
    end

    # rubocop:disable Style/GuardClause
    def set_unix_attributes_on_path(dest_path)
      # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
      unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
      if @restore_permissions && @unix_perms
        ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
      end
      if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
        ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
      end
    end
    # rubocop:enable Style/GuardClause

    def set_extra_attributes_on_path(dest_path) # :nodoc:
      return unless file? || directory?

      case @fstype
      when ::Zip::FSTYPE_UNIX
        set_unix_attributes_on_path(dest_path)
      end

      # Restore the timestamp on a file. This will either have come from the
      # original source file that was copied into the archive, or from the
      # creation date of the archive if there was no original source file.
      ::FileUtils.touch(dest_path, mtime: time) if @restore_times
    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 || 0),
        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_cdir_zip64_extra

      case @fstype
      when ::Zip::FSTYPE_UNIX
        ft = case ftype
             when :file
               @unix_perms ||= 0o644
               ::Zip::FILE_TYPE_FILE
             when :directory
               @unix_perms ||= 0o755
               ::Zip::FILE_TYPE_DIR
             when :symlink
               @unix_perms ||= 0o755
               ::Zip::FILE_TYPE_SYMLINK
             end

        unless ft.nil?
          @external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 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
      %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
        other.__send__(k.to_sym) == __send__(k.to_sym)
      end
    end

    def <=>(other)
      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
        ::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
          stringio
        else
          raise "unknown @file_type #{ftype}"
        end
      else
        zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
        zis.instance_variable_set(:@complete_entry, self)
        zis.get_next_entry
        if block
          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 "unknown file type: #{src_path.inspect} #{stat.inspect}"
               end

      @filepath = src_path
      @size = stat.size
      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)
      elsif @filepath
        zip_output_stream.put_next_entry(self)
        get_input_stream do |is|
          ::Zip::IOExtras.copy_stream(zip_output_stream, is)
        end
      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.respond_to?(:seek) && @zipfile.respond_to?(:read)
        yield @zipfile
      else
        ::File.open(@zipfile, 'rb', &block)
      end
    end

    def clean_up
      @dirty = false # Any changes are written at this point.
    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
      warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
    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::DestinationExistsError, dest_path
      end

      ::File.open(dest_path, 'wb') do |os|
        get_input_stream do |is|
          bytes_written = 0
          warned = false
          buf = +''
          while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
            os << buf
            bytes_written += buf.bytesize
            next unless bytes_written > size && !warned

            error = ::Zip::EntrySizeError.new(self)
            raise error if ::Zip.validate_entry_sizes

            warn "WARNING: #{error.message}"
            warned = true
          end
        end
      end

      set_extra_attributes_on_path(dest_path)
    end

    def create_directory(dest_path)
      return if ::File.directory?(dest_path)

      if ::File.exist?(dest_path)
        raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)

        ::FileUtils.rm_f dest_path
      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)
      # TODO: Symlinks pose security challenges. Symlink support temporarily
      # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
      warn "WARNING: skipped symlink '#{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
      return unless zip64?

      if for_local_header
        @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
      else
        @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
          @size, @compressed_size, @local_header_offset
        )
      end
    end

    # For DEFLATED compression *only*: set the general purpose flags 1 and 2 to
    # indicate compression level. This seems to be mainly cosmetic but they are
    # generally set by other tools - including in docx files. It is these flags
    # that are used by commandline tools (and elsewhere) to give an indication
    # of how compressed a file is. See the PKWARE APPNOTE for more information:
    # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
    #
    # It's safe to simply OR these flags here as compression_level is read only.
    def set_compression_level_flags
      return unless compression_method == DEFLATED

      case @compression_level
      when 1
        @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
      when 2
        @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
      when 8, 9
        @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
      end
    end

    # rubocop:disable Style/GuardClause
    def prep_local_zip64_extra
      return unless ::Zip.write_zip64_support
      return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?

      # Might not know size here, so need ZIP64 just in case.
      # If we already have a ZIP64 extra (placeholder) then we must fill it in.
      if zip64? || @size.nil? || @size >= 0xFFFFFFFF || @compressed_size >= 0xFFFFFFFF
        @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
        zip64 = @extra['Zip64'] || @extra.create('Zip64')

        # Local header always includes size and compressed size.
        zip64.original_size = @size || 0
        zip64.compressed_size = @compressed_size
      end
    end

    def prep_cdir_zip64_extra
      return unless ::Zip.write_zip64_support

      if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
         @local_header_offset >= 0xFFFFFFFF
        @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
        zip64 = @extra['Zip64'] || @extra.create('Zip64')

        # Central directory entry entries include whichever fields are necessary.
        zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
        zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
        zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
      end
    end
    # rubocop:enable Style/GuardClause
  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.