Class: ZipTricks::Microzip

Inherits:
Object
  • Object
show all
Defined in:
lib/zip_tricks/microzip.rb

Overview

A replacement for RubyZip for streaming, with a couple of small differences. The first difference is that it is verbosely-written-to-the-spec and you can actually follow what is happening. It does not support quite a few fancy features of Rubyzip, but instead it can be digested in one reading, and has solid Zip64 support. It also does not attempt any tricks with Zip64 placeholder extra fields because the ZipTricks streaming engine assumes you know how large your file is (both compressed and uncompressed) and you have the file's CRC32 checksum upfront.

Just like Rubyzip it will switch to Zip64 automatically if required, but there is no global setting to enable that behavior - it is always on.

Constant Summary collapse

STORED =
0
DEFLATED =
8
TooMuch =
Class.new(StandardError)
PathError =
Class.new(StandardError)
DuplicateFilenames =
Class.new(StandardError)
UnknownMode =
Class.new(StandardError)

Instance Method Summary collapse

Constructor Details

#initializeMicrozip

Creates a new streaming writer. The writer is stateful and knows it's list of ZIP file entries as they are being added.



218
219
220
221
# File 'lib/zip_tricks/microzip.rb', line 218

def initialize
  @files = []
  @local_header_offsets = []
end

Instance Method Details

#add_local_file_header(io:, filename:, crc32:, compressed_size:, uncompressed_size:, storage_mode:, mtime: Time.now.utc) ⇒ void

This method returns an undefined value.

Adds a file to the entry list and immediately writes out it's local file header into the output stream.

Parameters:

  • io (#<<, #tell)

    the buffer to write the local file header to

  • filename (String)

    The name of the file

  • crc32 (Fixnum)

    The CRC32 checksum of the file

  • compressed_size (Fixnum)

    The size of the compressed (or stored) data - how much space it uses in the ZIP

  • uncompressed_size (Fixnum)

    The size of the file once extracted

  • storage_mode (Fixnum)

    Either 0 for "stored" or 8 for "deflated"

  • mtime (Time) (defaults to: Time.now.utc)

    What modification time to record for the file

Raises:



234
235
236
237
238
239
240
241
242
243
# File 'lib/zip_tricks/microzip.rb', line 234

def add_local_file_header(io:, filename:, crc32:, compressed_size:, uncompressed_size:, storage_mode:, mtime: Time.now.utc)
  if @files.any?{|e| e.filename == filename }
    raise DuplicateFilenames, "Filename #{filename.inspect} already used in the archive"
  end
  raise UnknownMode, "Unknown compression mode #{storage_mode}" unless [STORED, DEFLATED].include?(storage_mode)
  e = Entry.new(filename, crc32, compressed_size, uncompressed_size, storage_mode, mtime)
  @files << e
  @local_header_offsets << io.tell
  e.write_local_file_header(io)
end

#write_central_directory(io) ⇒ void

This method returns an undefined value.

Writes the central directory (including the Zip6 salient bits if necessary)

Parameters:

  • io (#<<, #tell)

    the buffer to write the central directory to. The method will use tell on the buffer since it has to know where the central directory is located



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/zip_tricks/microzip.rb', line 250

def write_central_directory(io)
  start_of_central_directory = io.tell

  # Central directory file headers, per file in order
  @files.each_with_index do |file, i|
    local_file_header_offset_from_start_of_file = @local_header_offsets.fetch(i)
    file.write_central_directory_file_header(io, local_file_header_offset_from_start_of_file)
  end
  central_dir_size = io.tell - start_of_central_directory

  zip64_required = central_dir_size > FOUR_BYTE_MAX_UINT ||
    start_of_central_directory > FOUR_BYTE_MAX_UINT ||
    @files.length > TWO_BYTE_MAX_UINT ||
    @files.any?(&:requires_zip64?)

  # Then, if zip64 is used
  if zip64_required
    # [zip64 end of central directory record]
    zip64_eocdr_offset = io.tell
                                              # zip64 end of central dir
    io << [0x06064b50].pack(C_V)             # signature                       4 bytes  (0x06064b50)
    io << [44].pack(C_Qe)                    # size of zip64 end of central
                                              # directory record                8 bytes
                                              # (this is ex. the 12 bytes of the signature and the size value itself).
                                              # Without the extensible data sector it is always 44.
    io << MADE_BY_SIGNATURE                                # version made by                 2 bytes
    io << [VERSION_NEEDED_TO_EXTRACT_ZIP64].pack(C_v)      # version needed to extract       2 bytes
    io << [0].pack(C_V)                                    # number of this disk             4 bytes
    io << [0].pack(C_V)                                    # number of the disk with the
                                                           # start of the central directory  4 bytes
    io << [@files.length].pack(C_Qe)                       # total number of entries in the
                                                           # central directory on this disk  8 bytes
    io << [@files.length].pack(C_Qe)                       # total number of entries in the
                                                           # central directory               8 bytes
    io << [central_dir_size].pack(C_Qe)                    # size of the central directory   8 bytes
                                                           # offset of start of central
                                                           # directory with respect to
    io << [start_of_central_directory].pack(C_Qe)          # the starting disk number        8 bytes
                                                           # zip64 extensible data sector    (variable size), blank for us

    # [zip64 end of central directory locator]
    io << [0x07064b50].pack(C_V)                           # zip64 end of central dir locator
                                                           # signature                       4 bytes  (0x07064b50)
    io << [0].pack(C_V)                                    # number of the disk with the
                                                           # start of the zip64 end of
                                                           # central directory               4 bytes
    io << [zip64_eocdr_offset].pack(C_Qe)                  # relative offset of the zip64
                                                           # end of central directory record 8 bytes
                                                           # (note: "relative" is actually "from the start of the file")
    io << [1].pack(C_V)                                    # total number of disks           4 bytes
  end

  # Then the end of central directory record:
  io << [0x06054b50].pack(C_V)                            # end of central dir signature     4 bytes  (0x06054b50)
  io << [0].pack(C_v)                                     # number of this disk              2 bytes
  io << [0].pack(C_v)                                     # number of the disk with the
                                                          # start of the central directory 2 bytes
  
  if zip64_required # the number of entries will be read from the zip64 part of the central directory
    io << [TWO_BYTE_MAX_UINT].pack(C_v)                   # total number of entries in the
                                                          # central directory on this disk   2 bytes
    io << [TWO_BYTE_MAX_UINT].pack(C_v)                   # total number of entries in
                                                          # the central directory            2 bytes
  else
    io << [@files.length].pack(C_v)                       # total number of entries in the
                                                          # central directory on this disk   2 bytes
    io << [@files.length].pack(C_v)                       # total number of entries in
                                                          # the central directory            2 bytes
  end
  
  if zip64_required
    io << [FOUR_BYTE_MAX_UINT].pack(C_V)                  # size of the central directory    4 bytes
    io << [FOUR_BYTE_MAX_UINT].pack(C_V)                  # offset of start of central
                                                          # directory with respect to
                                                          # the starting disk number        4 bytes
  else
    io << [central_dir_size].pack(C_V)                    # size of the central directory    4 bytes
    io << [start_of_central_directory].pack(C_V)          # offset of start of central
                                                          # directory with respect to
                                                          # the starting disk number        4 bytes
  end
  io << [0].pack(C_v)                                     # .ZIP file comment length        2 bytes
                                                          # .ZIP file comment       (variable size)
end