• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hainesr / rubyzip / 17511682986

06 Sep 2025 07:24AM UTC coverage: 96.857% (-0.02%) from 96.872%
17511682986

push

github

hainesr
Add AES reference documentation.

These notes were originally published on the WinZip website, and were
converted to Markdown by an unknown source.

1107 of 1404 branches covered (78.85%)

2219 of 2291 relevant lines covered (96.86%)

89669.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.57
/lib/zip/entry.rb
1
# frozen_string_literal: true
2

3
require 'pathname'
14✔
4

5
require_relative 'constants'
14✔
6
require_relative 'dirtyable'
14✔
7

8
module Zip
14✔
9
  # Zip::Entry represents an entry in a Zip archive.
10
  class Entry
14✔
11
    include Dirtyable
14✔
12

13
    # Constant used to specify that the entry is stored (i.e., not compressed).
14
    STORED   = ::Zip::COMPRESSION_METHOD_STORE
14✔
15

16
    # Constant used to specify that the entry is deflated (i.e., compressed).
17
    DEFLATED = ::Zip::COMPRESSION_METHOD_DEFLATE
14✔
18

19
    # Language encoding flag (EFS) bit
20
    EFS = 0b100000000000 # :nodoc:
14✔
21

22
    # Compression level flags (used as part of the gp flags).
23
    COMPRESSION_LEVEL_SUPERFAST_GPFLAG = 0b110 # :nodoc:
14✔
24
    COMPRESSION_LEVEL_FAST_GPFLAG = 0b100      # :nodoc:
14✔
25
    COMPRESSION_LEVEL_MAX_GPFLAG = 0b010       # :nodoc:
14✔
26

27
    attr_accessor :comment, :compressed_size, :follow_symlinks, :name,
14✔
28
                  :restore_ownership, :restore_permissions, :restore_times,
29
                  :unix_gid, :unix_perms, :unix_uid
30

31
    attr_accessor :crc, :external_file_attributes, :fstype, :gp_flags,
14✔
32
                  :internal_file_attributes, :local_header_offset # :nodoc:
33

34
    attr_reader :extra, :compression_level, :filepath # :nodoc:
14✔
35

36
    attr_writer :size # :nodoc:
14✔
37

38
    mark_dirty :comment=, :compressed_size=, :external_file_attributes=,
14✔
39
               :fstype=, :gp_flags=, :name=, :size=,
40
               :unix_gid=, :unix_perms=, :unix_uid=
41

42
    def set_default_vars_values # :nodoc:
14✔
43
      @local_header_offset      = 0
1,866,802✔
44
      @local_header_size        = nil # not known until local entry is created or read
1,866,802✔
45
      @internal_file_attributes = 1
1,866,802✔
46
      @external_file_attributes = 0
1,866,802✔
47
      @header_signature         = ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
1,866,802✔
48

49
      @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT
1,866,802✔
50
      @version                   = VERSION_MADE_BY
1,866,802✔
51

52
      @ftype           = nil          # unspecified or unknown
1,866,802✔
53
      @filepath        = nil
1,866,802✔
54
      @gp_flags        = 0
1,866,802✔
55
      if ::Zip.unicode_names
1,866,802!
56
        @gp_flags |= EFS
57
        @version = 63
×
58
      end
59
      @follow_symlinks = false
1,866,802✔
60

61
      @restore_times       = DEFAULT_RESTORE_OPTIONS[:restore_times]
1,866,802✔
62
      @restore_permissions = DEFAULT_RESTORE_OPTIONS[:restore_permissions]
1,866,802✔
63
      @restore_ownership   = DEFAULT_RESTORE_OPTIONS[:restore_ownership]
1,866,802✔
64
      # BUG: need an extra field to support uid/gid's
65
      @unix_uid            = nil
1,866,802✔
66
      @unix_gid            = nil
1,866,802✔
67
      @unix_perms          = nil
1,866,802✔
68
    end
69

70
    def check_name(name) # :nodoc:
14✔
71
      raise EntryNameError, name if name.start_with?('/')
1,866,830✔
72
      raise EntryNameError if name.length > 65_535
1,866,816✔
73
    end
74

75
    # Create a new Zip::Entry.
76
    def initialize(
14✔
77
      zipfile = '', name = '',
78
      comment: '', size: nil, compressed_size: 0, crc: 0,
79
      compression_method: DEFLATED,
80
      compression_level: ::Zip.default_compression,
81
      time: ::Zip::DOSTime.now, extra: ::Zip::ExtraField.new
82
    )
83
      super()
1,866,830✔
84
      @name = name
1,866,830✔
85
      check_name(@name)
1,866,830✔
86

87
      set_default_vars_values
1,866,802✔
88
      @fstype = ::Zip::RUNNING_ON_WINDOWS ? ::Zip::FSTYPE_FAT : ::Zip::FSTYPE_UNIX
1,866,802!
89

90
      @zipfile            = zipfile
1,866,802✔
91
      @comment            = comment || ''
1,866,802✔
92
      @compression_method = compression_method || DEFLATED
1,866,802✔
93
      @compression_level  = compression_level || ::Zip.default_compression
1,866,802✔
94
      @compressed_size    = compressed_size || 0
1,866,802✔
95
      @crc                = crc || 0
1,866,802✔
96
      @size               = size
1,866,802✔
97
      @time               = case time
1,866,802✔
98
                            when ::Zip::DOSTime
666,600✔
99
                              time
1,866,480✔
100
                            when Time
15✔
101
                              ::Zip::DOSTime.from_time(time)
42✔
102
                            else
100✔
103
                              ::Zip::DOSTime.now
280✔
104
                            end
105
      @extra              =
266,686✔
106
        extra.kind_of?(ExtraField) ? extra : ExtraField.new(extra.to_s)
1,866,800✔
107

108
      set_compression_level_flags
1,866,802✔
109
    end
110

111
    # Is this entry encrypted?
112
    def encrypted?
14✔
113
      gp_flags & 1 == 1
2,898✔
114
    end
115

116
    def incomplete? # :nodoc:
14✔
117
      (gp_flags & 8 == 8) && (crc == 0 || size == 0 || compressed_size == 0)
9,744✔
118
    end
119

120
    # The uncompressed size of the entry.
121
    def size
14✔
122
      @size || 0
9,422✔
123
    end
124

125
    # Get a timestamp component of this entry.
126
    #
127
    # Returns modification time by default.
128
    def time(component: :mtime)
14✔
129
      time =
640✔
130
        if @extra['UniversalTime']
4,478✔
131
          @extra['UniversalTime'].send(component)
1,162✔
132
        elsif @extra['NTFS']
3,316✔
133
          @extra['NTFS'].send(component)
42✔
134
        end
135

136
      # Standard time field in central directory has local time
137
      # under archive creator. Then, we can't get timezone.
138
      time || (@time if component == :mtime)
4,480✔
139
    end
140

141
    alias mtime time
14✔
142

143
    # Get the last access time of this entry, if available.
144
    def atime
14✔
145
      time(component: :atime)
126✔
146
    end
147

148
    # Get the creation time of this entry, if available.
149
    def ctime
14✔
150
      time(component: :ctime)
126✔
151
    end
152

153
    # Set a timestamp component of this entry.
154
    #
155
    # Sets modification time by default.
156
    def time=(value, component: :mtime)
14✔
157
      @dirty = true
98✔
158
      unless @extra.member?('UniversalTime') || @extra.member?('NTFS')
98✔
159
        @extra.create('UniversalTime')
84✔
160
      end
161

162
      value = DOSTime.from_time(value)
98✔
163
      comp = "#{component}=" unless component.to_s.end_with?('=')
98!
164
      (@extra['UniversalTime'] || @extra['NTFS']).send(comp, value)
98✔
165
      @time = value if component == :mtime
98✔
166
    end
167

168
    alias mtime= time=
14✔
169

170
    # Set the last access time of this entry.
171
    def atime=(value)
14✔
172
      send(:time=, value, component: :atime)
14✔
173
    end
174

175
    # Set the creation time of this entry.
176
    def ctime=(value)
14✔
177
      send(:time=, value, component: :ctime)
14✔
178
    end
179

180
    # Does this entry return time fields with accurate timezone information?
181
    def absolute_time?
14✔
182
      @extra.member?('UniversalTime') || @extra.member?('NTFS')
28✔
183
    end
184

185
    # Return the compression method for this entry.
186
    #
187
    # Returns STORED if the entry is a directory or if the compression
188
    # level is 0.
189
    def compression_method
14✔
190
      return STORED if ftype == :directory || @compression_level == 0
2,803,878✔
191

192
      @compression_method
2,800,658✔
193
    end
194

195
    # Set the compression method for this entry.
196
    def compression_method=(method)
14✔
197
      @dirty = true
×
198
      @compression_method = (ftype == :directory ? STORED : method)
×
199
    end
200

201
    # Does this entry use the ZIP64 extensions?
202
    def zip64?
14✔
203
      !@extra['Zip64'].nil?
1,896,468✔
204
    end
205

206
    # Is this entry encrypted with AES encryption?
207
    def aes?
14✔
208
      !@extra['AES'].nil?
945,364✔
209
    end
210

211
    def file_type_is?(type) # :nodoc:
14✔
212
      ftype == type
6,104✔
213
    end
214

215
    def ftype # :nodoc:
14✔
216
      @ftype ||= name_is_directory? ? :directory : :file
3,738,350✔
217
    end
218

219
    # Dynamic checkers
220
    %w[directory file symlink].each do |k|
14✔
221
      define_method :"#{k}?" do
42✔
222
        file_type_is?(k.to_sym)
6,104✔
223
      end
224
    end
225

226
    def name_is_directory? # :nodoc:
14✔
227
      @name.end_with?('/')
1,873,242✔
228
    end
229

230
    # Is the name a relative path, free of `..` patterns that could lead to
231
    # path traversal attacks? This does NOT handle symlinks; if the path
232
    # contains symlinks, this check is NOT enough to guarantee safety.
233
    def name_safe? # :nodoc:
14✔
234
      cleanpath = Pathname.new(@name).cleanpath
×
235
      return false unless cleanpath.relative?
×
236

237
      root = ::File::SEPARATOR
×
238
      naive = Regexp.escape(::File.join(root, cleanpath.to_s))
×
239
      # Allow for Windows drive mappings at the root.
240
      ::File.absolute_path(cleanpath.to_s, root).match?(/([A-Z]:)?#{naive}/i)
×
241
    end
242

243
    def local_entry_offset # :nodoc:
14✔
244
      local_header_offset + @local_header_size
1,190✔
245
    end
246

247
    def name_size # :nodoc:
14✔
248
      @name ? @name.bytesize : 0
940,926!
249
    end
250

251
    def extra_size # :nodoc:
14✔
252
      @extra ? @extra.local_size : 0
11,032!
253
    end
254

255
    def comment_size # :nodoc:
14✔
256
      @comment ? @comment.bytesize : 0
921,676!
257
    end
258

259
    def calculate_local_header_size # :nodoc:
14✔
260
      LOCAL_ENTRY_STATIC_HEADER_LENGTH + name_size + extra_size
11,032✔
261
    end
262

263
    # check before rewriting an entry (after file sizes are known)
264
    # that we didn't change the header size (and thus clobber file data or something)
265
    def verify_local_header_size! # :nodoc:
14✔
266
      return if @local_header_size.nil?
4,088!
267

268
      new_size = calculate_local_header_size
4,088✔
269
      return unless @local_header_size != new_size
4,088!
270

271
      raise Error,
×
272
            "Local header size changed (#{@local_header_size} -> #{new_size})"
273
    end
274

275
    def cdir_header_size # :nodoc:
14✔
276
      CDIR_ENTRY_STATIC_HEADER_LENGTH + name_size +
×
277
        (@extra ? @extra.c_dir_size : 0) + comment_size
×
278
    end
279

280
    def next_header_offset # :nodoc:
14✔
281
      local_entry_offset + compressed_size
1,190✔
282
    end
283

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

293
      unless extract_path.start_with?(dest_dir)
882✔
294
        warn "WARNING: skipped extracting '#{@name}' to '#{extract_path}' as unsafe."
42✔
295
        return self
36✔
296
      end
297

298
      block ||= proc { ::Zip.on_exists_proc }
840✔
299

300
      raise "unknown file type #{inspect}" unless directory? || file? || symlink?
840!
301

302
      __send__(:"create_#{ftype}", extract_path, &block)
840✔
303
      self
714✔
304
    end
305

306
    def to_s # :nodoc:
14✔
307
      @name
1,873,146✔
308
    end
309

310
    class << self
14✔
311
      def read_c_dir_entry(io) # :nodoc:
14✔
312
        path = if io.respond_to?(:path)
941,052✔
313
                 io.path
940,590✔
314
               else
165✔
315
                 io
462✔
316
               end
317
        entry = new(path)
941,052✔
318
        entry.read_c_dir_entry(io)
941,052✔
319
        entry
940,870✔
320
      rescue Error
321
        nil
182✔
322
      end
323

324
      def read_local_entry(io) # :nodoc:
14✔
325
        entry = new(io)
4,788✔
326
        entry.read_local_entry(io)
4,788✔
327
        entry
4,480✔
328
      rescue SplitArchiveError
329
        raise
28✔
330
      rescue Error
331
        nil
280✔
332
      end
333
    end
334

335
    def unpack_local_entry(buf) # :nodoc:
14✔
336
      @header_signature,
682✔
337
        @version,
338
        @fstype,
339
        @gp_flags,
340
        @compression_method,
341
        @last_mod_time,
342
        @last_mod_date,
343
        @crc,
344
        @compressed_size,
345
        @size,
346
        @name_length,
347
        @extra_length = buf.unpack('VCCvvvvVVVvv')
4,090✔
348
    end
349

350
    def read_local_entry(io) # :nodoc:
14✔
351
      @dirty = false # No changes at this point.
4,802✔
352
      current_offset = io.tell
4,802✔
353

354
      read_local_header_fields(io)
4,802✔
355

356
      if @header_signature == SPLIT_FILE_SIGNATURE
4,774✔
357
        raise SplitArchiveError if current_offset.zero?
28!
358

359
        # Rewind, skipping the data descriptor, then try to read the local header again.
360
        current_offset += 16
361
        io.seek(current_offset)
×
362
        read_local_header_fields(io)
×
363
      end
364

365
      unless @header_signature == LOCAL_ENTRY_SIGNATURE
4,746✔
366
        raise Error, "Zip local header magic not found at location '#{current_offset}'"
266✔
367
      end
368

369
      @local_header_offset = current_offset
4,480✔
370

371
      set_time(@last_mod_date, @last_mod_time)
4,480✔
372

373
      @name = io.read(@name_length)
4,480✔
374
      if ::Zip.force_entry_names_encoding
4,480!
375
        @name.force_encoding(::Zip.force_entry_names_encoding)
×
376
      end
377
      @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
4,480✔
378

379
      # We need to do this here because `initialize` has so many side-effects.
380
      # :-(
381
      @ftype = name_is_directory? ? :directory : :file
4,480✔
382

383
      extra = io.read(@extra_length)
4,480✔
384
      if extra && extra.bytesize != @extra_length
4,480!
385
        raise ::Zip::Error, 'Truncated local zip entry header'
×
386
      end
387

388
      read_extra_field(extra, local: true)
4,480✔
389
      parse_zip64_extra(true)
4,480✔
390
      parse_aes_extra
4,480✔
391
      @local_header_size = calculate_local_header_size
4,480✔
392
    end
393

394
    def pack_local_entry # :nodoc:
14✔
395
      zip64 = @extra['Zip64']
8,218✔
396
      [::Zip::LOCAL_ENTRY_SIGNATURE,
7,044✔
397
       @version_needed_to_extract, # version needed to extract
398
       @gp_flags, # @gp_flags
399
       compression_method,
400
       @time.to_binary_dos_time, # @last_mod_time
401
       @time.to_binary_dos_date, # @last_mod_date
402
       @crc,
403
       zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
8,216✔
404
       zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
8,218✔
405
       name_size,
406
       @extra ? @extra.local_size : 0].pack('VvvvvvVVVvv')
9,390!
407
    end
408

409
    def write_local_entry(io, rewrite: false) # :nodoc:
14✔
410
      prep_local_zip64_extra
8,218✔
411
      verify_local_header_size! if rewrite
8,218✔
412
      @local_header_offset = io.tell
8,218✔
413

414
      io << pack_local_entry
8,218✔
415

416
      io << @name
8,218✔
417
      io << @extra.to_local_bin if @extra
8,218!
418
      @local_header_size = io.tell - @local_header_offset
8,218✔
419
    end
420

421
    def unpack_c_dir_entry(buf) # :nodoc:
14✔
422
      @header_signature,
134,432✔
423
        @version, # version of encoding software
424
        @fstype, # filesystem type
425
        @version_needed_to_extract,
426
        @gp_flags,
427
        @compression_method,
428
        @last_mod_time,
429
        @last_mod_date,
430
        @crc,
431
        @compressed_size,
432
        @size,
433
        @name_length,
434
        @extra_length,
435
        @comment_length,
436
        _, # diskNumberStart
437
        @internal_file_attributes,
438
        @external_file_attributes,
439
        @local_header_offset,
440
        @name,
441
        @extra,
442
        @comment = buf.unpack('VCCvvvvvVVVvvvvvVV')
806,590✔
443
    end
444

445
    def set_ftype_from_c_dir_entry # :nodoc:
14✔
446
      @ftype = case @fstype
940,884✔
447
               when ::Zip::FSTYPE_UNIX
335,610✔
448
                 @unix_perms = (@external_file_attributes >> 16) & 0o7777
939,708✔
449
                 case (@external_file_attributes >> 28)
805,464✔
450
                 when ::Zip::FILE_TYPE_DIR
2,365✔
451
                   :directory
6,622✔
452
                 when ::Zip::FILE_TYPE_FILE
333,195✔
453
                   :file
932,946✔
454
                 when ::Zip::FILE_TYPE_SYMLINK
40✔
455
                   :symlink
112✔
456
                 else
457
                   # Best case guess for whether it is a file or not.
458
                   # Otherwise this would be set to unknown and that
459
                   # entry would never be able to be extracted.
10✔
460
                   if name_is_directory?
28!
461
                     :directory
×
462
                   else
10✔
463
                     :file
28✔
464
                   end
465
                 end
466
               else
420✔
467
                 if name_is_directory?
1,176!
468
                   :directory
×
469
                 else
420✔
470
                   :file
1,176✔
471
                 end
472
               end
473
    end
474

475
    def check_c_dir_entry_static_header_length(buf) # :nodoc:
14✔
476
      return unless buf.nil? || buf.bytesize != ::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH
941,094✔
477

478
      raise Error, 'Premature end of file. Not enough data for zip cdir entry header'
70✔
479
    end
480

481
    def check_c_dir_entry_signature # :nodoc:
14✔
482
      return if @header_signature == ::Zip::CENTRAL_DIRECTORY_ENTRY_SIGNATURE
941,024✔
483

484
      raise Error, "Zip local header magic not found at location '#{local_header_offset}'"
140✔
485
    end
486

487
    def check_c_dir_entry_comment_size # :nodoc:
14✔
488
      return if @comment && @comment.bytesize == @comment_length
940,884!
489

490
      raise ::Zip::Error, 'Truncated cdir zip entry header'
×
491
    end
492

493
    def read_extra_field(buf, local: false) # :nodoc:
14✔
494
      if @extra.kind_of?(::Zip::ExtraField)
1,885,170✔
495
        @extra.merge(buf, local: local) if buf
944,286!
496
      else
336,030✔
497
        @extra = ::Zip::ExtraField.new(buf, local: local)
940,884✔
498
      end
499
    end
500

501
    def read_c_dir_entry(io) # :nodoc:
14✔
502
      @dirty = false # No changes at this point.
941,094✔
503
      static_sized_fields_buf = io.read(::Zip::CDIR_ENTRY_STATIC_HEADER_LENGTH)
941,094✔
504
      check_c_dir_entry_static_header_length(static_sized_fields_buf)
941,094✔
505
      unpack_c_dir_entry(static_sized_fields_buf)
941,024✔
506
      check_c_dir_entry_signature
941,024✔
507
      set_time(@last_mod_date, @last_mod_time)
940,884✔
508

509
      @name = io.read(@name_length)
940,884✔
510
      if ::Zip.force_entry_names_encoding
940,884✔
511
        @name.force_encoding(::Zip.force_entry_names_encoding)
84✔
512
      end
513
      @name.tr!('\\', '/') # Normalise filepath separators after encoding set.
940,884✔
514

515
      read_extra_field(io.read(@extra_length))
940,884✔
516
      @comment = io.read(@comment_length)
940,884✔
517
      check_c_dir_entry_comment_size
940,884✔
518
      set_ftype_from_c_dir_entry
940,884✔
519
      parse_zip64_extra(false)
940,884✔
520
      parse_aes_extra
940,884✔
521
    end
522

523
    def file_stat(path) # :nodoc:
14✔
524
      if @follow_symlinks
1,512!
525
        ::File.stat(path)
×
526
      else
540✔
527
        ::File.lstat(path)
1,512✔
528
      end
529
    end
530

531
    def get_extra_attributes_from_path(path) # :nodoc:
14✔
532
      stat = file_stat(path)
756✔
533
      @time = DOSTime.from_time(stat.mtime)
756✔
534
      return if ::Zip::RUNNING_ON_WINDOWS
756!
535

536
      @unix_uid   = stat.uid
756✔
537
      @unix_gid   = stat.gid
756✔
538
      @unix_perms = stat.mode & 0o7777
756✔
539
    end
540

541
    # rubocop:disable Style/GuardClause
542
    def set_unix_attributes_on_path(dest_path) # :nodoc:
14✔
543
      # Ignore setuid/setgid bits by default. Honour if @restore_ownership.
544
      unix_perms_mask = (@restore_ownership ? 0o7777 : 0o1777)
616!
545
      if @restore_permissions && @unix_perms
616✔
546
        ::FileUtils.chmod(@unix_perms & unix_perms_mask, dest_path)
574✔
547
      end
548
      if @restore_ownership && @unix_uid && @unix_gid && ::Process.egid == 0
616!
549
        ::FileUtils.chown(@unix_uid, @unix_gid, dest_path)
×
550
      end
551
    end
552
    # rubocop:enable Style/GuardClause
553

554
    def set_extra_attributes_on_path(dest_path) # :nodoc:
14✔
555
      return unless file? || directory?
616!
556

557
      case @fstype
528!
558
      when ::Zip::FSTYPE_UNIX
220✔
559
        set_unix_attributes_on_path(dest_path)
616✔
560
      end
561

562
      # Restore the timestamp on a file. This will either have come from the
563
      # original source file that was copied into the archive, or from the
564
      # creation date of the archive if there was no original source file.
565
      ::FileUtils.touch(dest_path, mtime: time) if @restore_times
616✔
566
    end
567

568
    def pack_c_dir_entry # :nodoc:
14✔
569
      zip64 = @extra['Zip64']
921,676✔
570
      [
131,668✔
571
        @header_signature,
658,340✔
572
        @version, # version of encoding software
573
        @fstype, # filesystem type
574
        @version_needed_to_extract, # @versionNeededToExtract
575
        @gp_flags, # @gp_flags
576
        compression_method,
577
        @time.to_binary_dos_time, # @last_mod_time
578
        @time.to_binary_dos_date, # @last_mod_date
579
        @crc,
580
        zip64 && zip64.compressed_size ? 0xFFFFFFFF : @compressed_size,
921,674✔
581
        zip64 && zip64.original_size ? 0xFFFFFFFF : (@size || 0),
921,676✔
582
        name_size,
583
        @extra ? @extra.c_dir_size : 0,
921,674!
584
        comment_size,
585
        zip64 && zip64.disk_start_number ? 0xFFFF : 0, # disk number start
921,674!
586
        @internal_file_attributes, # file type (binary=0, text=1)
587
        @external_file_attributes, # native filesystem attributes
588
        zip64 && zip64.relative_header_offset ? 0xFFFFFFFF : @local_header_offset,
921,674✔
589
        @name,
590
        @extra,
591
        @comment
592
      ].pack('VCCvvvvvVVVvvvvvVV')
263,334✔
593
    end
594

595
    def write_c_dir_entry(io) # :nodoc:
14✔
596
      prep_cdir_zip64_extra
921,676✔
597

598
      case @fstype
790,008!
599
      when ::Zip::FSTYPE_UNIX
329,170✔
600
        ft = case ftype
921,676!
601
             when :file
328,925✔
602
               @unix_perms ||= 0o644
920,990✔
603
               ::Zip::FILE_TYPE_FILE
920,990✔
604
             when :directory
245✔
605
               @unix_perms ||= 0o755
686✔
606
               ::Zip::FILE_TYPE_DIR
686✔
607
             when :symlink
×
608
               @unix_perms ||= 0o755
×
609
               ::Zip::FILE_TYPE_SYMLINK
×
610
             end
611

612
        unless ft.nil?
921,676!
613
          @external_file_attributes = ((ft << 12) | (@unix_perms & 0o7777)) << 16
921,676✔
614
        end
615
      end
616

617
      io << pack_c_dir_entry
921,676✔
618

619
      io << @name
921,676✔
620
      io << (@extra ? @extra.to_c_dir_bin : '')
921,676!
621
      io << @comment
921,676✔
622
    end
623

624
    def ==(other) # :nodoc:
14✔
625
      return false unless other.class == self.class
966✔
626

627
      # Compares contents of local entry and exposed fields
628
      %w[compression_method crc compressed_size size name extra filepath time].all? do |k|
728✔
629
        other.__send__(k.to_sym) == __send__(k.to_sym)
4,564✔
630
      end
631
    end
632

633
    def <=>(other) # :nodoc:
14✔
634
      to_s <=> other.to_s
1,310✔
635
    end
636

637
    # Returns an IO like object for the given ZipEntry.
638
    # Warning: may behave weird with symlinks.
639
    def get_input_stream(&block)
14✔
640
      if ftype == :directory
2,044!
641
        yield ::Zip::NullInputStream if block
×
642
        ::Zip::NullInputStream
✔
643
      elsif @filepath
2,042✔
644
        case ftype
648!
645
        when :file
270✔
646
          ::File.open(@filepath, 'rb', &block)
756✔
647
        when :symlink
×
648
          linkpath = ::File.readlink(@filepath)
×
649
          stringio = ::StringIO.new(linkpath)
×
650
          yield(stringio) if block
×
651
          stringio
×
652
        else
×
653
          raise "unknown @file_type #{ftype}"
×
654
        end
655
      else
460✔
656
        zis = ::Zip::InputStream.new(@zipfile, offset: local_header_offset)
1,288✔
657
        zis.instance_variable_set(:@complete_entry, self)
1,288✔
658
        zis.get_next_entry
1,288✔
659
        if block
1,288✔
660
          begin
144✔
661
            yield(zis)
1,022✔
662
          ensure
663
            zis.close
1,022✔
664
          end
665
        else
95✔
666
          zis
266✔
667
        end
668
      end
669
    end
670

671
    def gather_fileinfo_from_srcpath(src_path) # :nodoc:
14✔
672
      stat   = file_stat(src_path)
756✔
673
      @ftype = case stat.ftype
756!
674
               when 'file'
250✔
675
                 if name_is_directory?
700!
676
                   raise ArgumentError,
×
677
                         "entry name '#{newEntry}' indicates directory entry, but " \
678
                         "'#{src_path}' is not a directory"
679
                 end
680
                 :file
700✔
681
               when 'directory'
20✔
682
                 @name += '/' unless name_is_directory?
56!
683
                 :directory
56✔
684
               when 'link'
×
685
                 if name_is_directory?
×
686
                   raise ArgumentError,
×
687
                         "entry name '#{newEntry}' indicates directory entry, but " \
688
                         "'#{src_path}' is not a directory"
689
                 end
690
                 :symlink
×
691
               else
×
692
                 raise "unknown file type: #{src_path.inspect} #{stat.inspect}"
×
693
               end
694

695
      @filepath = src_path
756✔
696
      @size = stat.size
756✔
697
      get_extra_attributes_from_path(@filepath)
756✔
698
    end
699

700
    def write_to_zip_output_stream(zip_output_stream) # :nodoc:
14✔
701
      if ftype == :directory
2,954✔
702
        zip_output_stream.put_next_entry(self)
686✔
703
      elsif @filepath
2,266✔
704
        zip_output_stream.put_next_entry(self)
672✔
705
        get_input_stream do |is|
672✔
706
          ::Zip::IOExtras.copy_stream(zip_output_stream, is)
672✔
707
        end
708
      else
570✔
709
        zip_output_stream.copy_raw_entry(self)
1,596✔
710
      end
711
    end
712

713
    def parent_as_string # :nodoc:
14✔
714
      entry_name  = name.chomp('/')
126✔
715
      slash_index = entry_name.rindex('/')
126✔
716
      slash_index ? entry_name.slice(0, slash_index + 1) : nil
126✔
717
    end
718

719
    def get_raw_input_stream(&block) # :nodoc:
14✔
720
      if @zipfile.respond_to?(:seek) && @zipfile.respond_to?(:read)
1,596!
721
        yield @zipfile
×
722
      else
570✔
723
        ::File.open(@zipfile, 'rb', &block)
1,596✔
724
      end
725
    end
726

727
    def clean_up # :nodoc:
14✔
728
      @dirty = false # Any changes are written at this point.
3,346✔
729
    end
730

731
    private
14✔
732

733
    def read_local_header_fields(io) # :nodoc:
14✔
734
      static_sized_fields_buf = io.read(::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH) || ''
4,802✔
735

736
      unless static_sized_fields_buf.bytesize == ::Zip::LOCAL_ENTRY_STATIC_HEADER_LENGTH
4,802✔
737
        raise Error, 'Premature end of file. Not enough data for zip entry local header'
28✔
738
      end
739

740
      unpack_local_entry(static_sized_fields_buf)
4,774✔
741
    end
742

743
    def set_time(binary_dos_date, binary_dos_time)
14✔
744
      @time = ::Zip::DOSTime.parse_binary_dos_format(binary_dos_date, binary_dos_time)
945,364✔
745
    rescue ArgumentError
746
      warn 'WARNING: invalid date/time in zip entry.' if ::Zip.warn_invalid_date
1,176✔
747
    end
748

749
    def create_file(dest_path, _continue_on_exists_proc = proc { Zip.continue_on_exists_proc })
14✔
750
      if ::File.exist?(dest_path) && !yield(self, dest_path)
574✔
751
        raise ::Zip::DestinationExistsError, dest_path
14✔
752
      end
753

754
      ::File.open(dest_path, 'wb') do |os|
560✔
755
        get_input_stream do |is|
504✔
756
          bytes_written = 0
504✔
757
          warned = false
504✔
758
          buf = +''
504✔
759
          while (buf = is.sysread(::Zip::Decompressor::CHUNK_SIZE, buf))
2,148✔
760
            os << buf
1,526✔
761
            bytes_written += buf.bytesize
1,308✔
762
            next unless bytes_written > size && !warned
1,526✔
763

764
            error = ::Zip::EntrySizeError.new(self)
56✔
765
            raise error if ::Zip.validate_entry_sizes
56✔
766

767
            warn "WARNING: #{error.message}"
28✔
768
            warned = true
28✔
769
          end
770
        end
771
      end
772

773
      set_extra_attributes_on_path(dest_path)
476✔
774
    end
775

776
    def create_directory(dest_path)
14✔
777
      return if ::File.directory?(dest_path)
182✔
778

779
      if ::File.exist?(dest_path)
168✔
780
        raise ::Zip::DestinationExistsError, dest_path unless block_given? && yield(self, dest_path)
56✔
781

782
        ::FileUtils.rm_f dest_path
28✔
783
      end
784

785
      ::FileUtils.mkdir_p(dest_path)
140✔
786
      set_extra_attributes_on_path(dest_path)
140✔
787
    end
788

789
    # BUG: create_symlink() does not use &block
790
    def create_symlink(dest_path)
14✔
791
      # TODO: Symlinks pose security challenges. Symlink support temporarily
792
      # removed in view of https://github.com/rubyzip/rubyzip/issues/369 .
793
      warn "WARNING: skipped symlink '#{dest_path}'."
84✔
794
    end
795

796
    # apply missing data from the zip64 extra information field, if present
797
    # (required when file sizes exceed 2**32, but can be used for all files)
798
    def parse_zip64_extra(for_local_header) # :nodoc:
14✔
799
      return unless zip64?
945,364✔
800

801
      if for_local_header
1,694✔
802
        @size, @compressed_size = @extra['Zip64'].parse(@size, @compressed_size)
714✔
803
      else
350✔
804
        @size, @compressed_size, @local_header_offset = @extra['Zip64'].parse(
980✔
805
          @size, @compressed_size, @local_header_offset
806
        )
807
      end
808
    end
809

810
    def parse_aes_extra # :nodoc:
14✔
811
      return unless aes?
945,364✔
812

813
      if @extra['AES'].vendor_id != 'AE'
70!
814
        raise Error, "Unsupported encryption method #{@extra['AES'].vendor_id}"
×
815
      end
816

817
      unless ::Zip::AESEncryption::VERSIONS.include? @extra['AES'].vendor_version
70!
818
        raise Error, "Unsupported encryption style #{@extra['AES'].vendor_version}"
×
819
      end
820

821
      @compression_method = @extra['AES'].compression_method if ftype != :directory
70!
822
    end
823

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

835
      case @compression_level
1,599,564✔
836
      when 1
35✔
837
        @gp_flags |= COMPRESSION_LEVEL_SUPERFAST_GPFLAG
84✔
838
      when 2
5✔
839
        @gp_flags |= COMPRESSION_LEVEL_FAST_GPFLAG
12✔
840
      when 8, 9
40✔
841
        @gp_flags |= COMPRESSION_LEVEL_MAX_GPFLAG
82✔
842
      end
843
    end
844

845
    # rubocop:disable Style/GuardClause
846
    def prep_local_zip64_extra
14✔
847
      return unless ::Zip.write_zip64_support
8,218✔
848
      return if (!zip64? && @size && @size < 0xFFFFFFFF) || !file?
7,980✔
849

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

856
        # Local header always includes size and compressed size.
857
        zip64.original_size = @size || 0
2,170✔
858
        zip64.compressed_size = @compressed_size
2,170✔
859
      end
860
    end
861

862
    def prep_cdir_zip64_extra
14✔
863
      return unless ::Zip.write_zip64_support
921,676✔
864

865
      if (@size && @size >= 0xFFFFFFFF) || @compressed_size >= 0xFFFFFFFF ||
921,550✔
866
         @local_header_offset >= 0xFFFFFFFF
20✔
867
        @version_needed_to_extract = VERSION_NEEDED_TO_EXTRACT_ZIP64
56✔
868
        zip64 = @extra['Zip64'] || @extra.create('Zip64')
56✔
869

870
        # Central directory entry entries include whichever fields are necessary.
871
        zip64.original_size = @size if @size && @size >= 0xFFFFFFFF
56✔
872
        zip64.compressed_size = @compressed_size if @compressed_size >= 0xFFFFFFFF
56✔
873
        zip64.relative_header_offset = @local_header_offset if @local_header_offset >= 0xFFFFFFFF
56✔
874
      end
875
    end
876
    # rubocop:enable Style/GuardClause
877
  end
878
end
879

880
# Copyright (C) 2002, 2003 Thomas Sondergaard
881
# rubyzip is free software; you can redistribute it and/or
882
# modify it under the terms of the ruby license.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc