Class: Omnizip::Formats::SevenZip::HeaderWriter

Inherits:
Object
  • Object
show all
Includes:
Constants
Defined in:
lib/omnizip/formats/seven_zip/header_writer.rb

Overview

Writes .7z archive header with metadata Handles variable-length encoding and property sequences

Constant Summary

Constants included from Constants

Constants::MAJOR_VERSION, Constants::MAX_NUM_BONDS, Constants::MAX_NUM_CODERS, Constants::MAX_NUM_PACK_STREAMS, Constants::MINOR_VERSION, Constants::SIGNATURE, Constants::SIGNATURE_SIZE, Constants::START_HEADER_SIZE

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHeaderWriter

Initialize writer



14
15
16
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 14

def initialize
  @buffer = String.new(encoding: "BINARY")
end

Instance Attribute Details

#bufferObject (readonly)

Returns the value of attribute buffer.



11
12
13
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 11

def buffer
  @buffer
end

Instance Method Details

#write_bit_vector(bits) ⇒ String

Write bit vector

Parameters:

  • bits (Array<Boolean>)

    Bit values

Returns:

  • (String)

    Encoded bit vector



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 92

def write_bit_vector(bits)
  return [1].pack("C") if bits.all?

  data = [0].pack("C") # Not all defined
  num_bytes = (bits.size + 7) / 8

  bytes = Array.new(num_bytes, 0)
  bits.each_with_index do |bit, i|
    byte_idx = i / 8
    bit_idx = 7 - (i % 8)
    bytes[byte_idx] |= (1 << bit_idx) if bit
  end

  data << bytes.pack("C*")
  data
end

#write_coder(method_id, properties = nil) ⇒ String

Write coder info

Parameters:

  • method_id (Integer)

    Compression method ID

  • properties (String, nil) (defaults to: nil)

    Coder properties

Returns:

  • (String)

    Encoded coder



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 147

def write_coder(method_id, properties = nil)
  data = String.new(encoding: "BINARY")

  # Determine ID size
  id_bytes = []
  temp_id = method_id
  while temp_id.positive?
    id_bytes.unshift(temp_id & 0xFF)
    temp_id >>= 8
  end
  id_bytes = [0] if id_bytes.empty?

  # Main byte
  main_byte = id_bytes.size
  main_byte |= 0x20 if properties # Has properties

  data << [main_byte].pack("C")
  data << id_bytes.pack("C*")

  # Properties
  if properties
    data << write_number(properties.bytesize)
    data << properties
  end

  data
end

#write_files_info(entries) ⇒ String

Write files info section

Parameters:

Returns:

  • (String)

    Encoded files info



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
334
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 291

def write_files_info(entries)
  data = String.new(encoding: "BINARY")

  data << [PropertyId::FILES_INFO].pack("C")
  data << write_number(entries.size)

  # Names
  data << [PropertyId::NAME].pack("C")
  data << write_names(entries.map(&:name))

  # Empty stream flags
  empty_bits = entries.map { |e| !e.has_stream }
  if empty_bits.any?
    data << [PropertyId::EMPTY_STREAM].pack("C")
    empty_data = write_bit_vector(empty_bits)
    data << write_number(empty_data.bytesize)
    data << empty_data
  end

  # Modification times
  mtimes = entries.map(&:mtime)
  if mtimes.any?
    data << [PropertyId::MTIME].pack("C")
    data << write_timestamps(PropertyId::MTIME, mtimes)
  end

  # Attributes
  attrs = entries.map(&:attributes)
  if attrs.any?
    data << [PropertyId::WIN_ATTRIB].pack("C")
    defined_bits = attrs.map { |a| !a.nil? }
    attrs_data = String.new(encoding: "BINARY")
    attrs_data << write_bit_vector(defined_bits)
    attrs_data << [0].pack("C") # Not external
    attrs.each do |attr|
      attrs_data << [attr].pack("V") if attr
    end
    data << write_number(attrs_data.bytesize)
    data << attrs_data
  end

  data << [PropertyId::K_END].pack("C")
  data
end

#write_folder(method_id, properties) ⇒ String

Write folder definition

Parameters:

  • method_id (Integer)

    Compression method

  • properties (String, nil)

    Properties

Returns:

  • (String)

    Encoded folder data



180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 180

def write_folder(method_id, properties)
  data = String.new(encoding: "BINARY")

  # Number of coders
  data << write_number(1)

  # Coder info
  data << write_coder(method_id, properties)

  # For simple case: no bind pairs needed
  # (single coder with single in/out stream)

  data
end

#write_folders(folders) ⇒ String

Write folders section

Parameters:

  • folders (Array<Hash>)

    Folder specs

Returns:

  • (String)

    Encoded folders



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 199

def write_folders(folders)
  data = String.new(encoding: "BINARY")
  data << [PropertyId::FOLDER].pack("C")
  data << write_number(folders.size)

  # Write external flag (0 = inline folders, 1 = external)
  data << [0].pack("C")

  folders.each do |folder|
    data << write_folder(
      folder[:method_id],
      folder[:properties],
    )
  end

  data
end

#write_names(names) ⇒ String

Write file names

Parameters:

  • names (Array<String>)

    File names

Returns:

  • (String)

    Encoded names



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 241

def write_names(names)
  data = String.new(encoding: "BINARY")

  # Encode names as UTF-16LE
  names_data = String.new(encoding: "BINARY")
  names.each do |name|
    name.encode("UTF-16LE").each_byte do |byte|
      names_data << [byte].pack("C")
    end
    names_data << [0, 0].pack("CC") # Null terminator
  end

  data << write_number(names_data.bytesize + 1)
  data << [0].pack("C") # Not external
  data << names_data

  data
end

#write_next_header(options) ⇒ String

Write complete next header

Parameters:

  • options (Hash)

    All header information

Returns:

  • (String)

    Encoded next header



411
412
413
414
415
416
417
418
419
420
421
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 411

def write_next_header(options)
  data = String.new(encoding: "BINARY")

  data << [PropertyId::HEADER].pack("C")
  data << write_streams_info(options[:streams]) if
    options[:streams]
  data << write_files_info(options[:entries]) if options[:entries]
  data << [PropertyId::K_END].pack("C")

  data
end

#write_number(value) ⇒ String

Encode variable-length number (7-Zip format)

Parameters:

  • value (Integer)

    Number to encode

Returns:

  • (String)

    Encoded bytes



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 60

def write_number(value)
  return [value].pack("C") if value < 0x80

  # 7-Zip VLI encoding: extra bytes are little-endian per SDK ReadNumberSpec().
  # Reference: https://py7zr.readthedocs.io/en/latest/archive_format.html
  result = String.new(encoding: "BINARY")

  # Determine how many extra bytes are needed
  thresholds = [0x80, 0x4000, 0x20_0000, 0x1000_0000,
                0x8_0000_0000, 0x400_0000_0000,
                0x2_0000_0000_0000, 0x100_0000_0000_0000]
  num_extra = thresholds.index { |t| value < t } || 8

  # Build first byte: num_extra leading 1-bits, then 0-bit, then data bits
  first_byte = 0
  num_extra.times { |i| first_byte |= (0x80 >> i) }
  data_bits_shift = 8 * num_extra
  first_byte |= (value >> data_bits_shift) & ((0x80 >> num_extra) - 1)
  result << [first_byte].pack("C")

  # Extra bytes in little-endian order (low byte first)
  num_extra.times do |i|
    result << [(value >> (8 * i)) & 0xFF].pack("C")
  end

  result
end

#write_pack_info(pack_pos, pack_sizes, pack_crcs = []) ⇒ String

Write pack info section

Parameters:

  • pack_pos (Integer)

    Pack position

  • pack_sizes (Array<Integer>)

    Pack sizes

  • pack_crcs (Array<Integer, nil>) (defaults to: [])

    Pack CRCs (optional)

Returns:

  • (String)

    Encoded pack info



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 115

def write_pack_info(pack_pos, pack_sizes, pack_crcs = [])
  data = String.new(encoding: "BINARY")

  data << [PropertyId::PACK_INFO].pack("C")
  data << write_number(pack_pos)
  data << write_number(pack_sizes.size)

  # Sizes
  data << [PropertyId::SIZE].pack("C")
  pack_sizes.each do |size|
    data << write_number(size)
  end

  # CRCs (optional)
  unless pack_crcs.empty?
    data << [PropertyId::CRC].pack("C")
    defined_bits = pack_crcs.map { |crc| !crc.nil? }
    data << write_bit_vector(defined_bits)
    pack_crcs.each do |crc|
      data << [crc].pack("V") if crc
    end
  end

  data << [PropertyId::K_END].pack("C")
  data
end

#write_start_header(next_header_data, next_header_offset) ⇒ String

Write archive signature and header

Parameters:

  • next_header_data (String)

    Encoded next header

  • next_header_offset (Integer)

    Offset to next header

Returns:

  • (String)

    Complete archive header



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 23

def write_start_header(next_header_data, next_header_offset)
  header = String.new(encoding: "BINARY")

  # Signature (6 bytes)
  header << SIGNATURE

  # Version (2 bytes)
  header << [MAJOR_VERSION, MINOR_VERSION].pack("CC")

  # Calculate CRC for next header info
  next_header_info = String.new(encoding: "BINARY")
  next_header_info << [next_header_offset].pack("Q<")
  next_header_info << [next_header_data.bytesize].pack("Q<")

  crc = Omnizip::Checksums::Crc32.new
  crc.update(next_header_data)
  next_header_crc = crc.value

  next_header_info << [next_header_crc].pack("V")

  # Calculate CRC for next header info
  info_crc = Omnizip::Checksums::Crc32.new
  info_crc.update(next_header_info)

  # Start header CRC (4 bytes)
  header << [info_crc.value].pack("V")

  # Next header info (20 bytes)
  header << next_header_info

  header
end

#write_streams_info(options) ⇒ String

Write main streams info

Parameters:

  • options (Hash)

    Stream information

Returns:

  • (String)

    Encoded streams info



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 340

def write_streams_info(options)
  data = String.new(encoding: "BINARY")

  data << [PropertyId::MAIN_STREAMS_INFO].pack("C")
  data << write_pack_info(
    options[:pack_pos],
    options[:pack_sizes],
    options[:pack_crcs],
  )
  data << write_unpack_info(options[:folders])

  # Substreams info - needed for solid archives with multiple files
  if options[:digests] && !options[:digests].empty?
    data << write_substreams_info(
      options[:unpack_sizes] || [],
      options[:digests],
      options[:folders],
    )
  end

  data << [PropertyId::K_END].pack("C")
  data
end

#write_substreams_info(unpack_sizes, digests, folders) ⇒ String

Write substreams info section

Parameters:

  • unpack_sizes (Array<Integer>)

    Unpack sizes per file

  • digests (Array<Integer>)

    CRC per file

  • folders (Array<Hash>)

    Folder information

Returns:

  • (String)

    Encoded substreams info



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 370

def write_substreams_info(unpack_sizes, digests, folders)
  data = String.new(encoding: "BINARY")

  data << [PropertyId::SUBSTREAMS_INFO].pack("C")

  # Write NUM_UNPACK_STREAM if multiple files in folder
  if digests.size > folders.size
    data << [PropertyId::NUM_UNPACK_STREAM].pack("C")
    folders.each do |_folder|
      # For solid archives, all files are in one folder
      data << write_number(digests.size)
    end
  end

  # Write unpack sizes if we have them and multiple files
  if unpack_sizes.size > 1
    data << [PropertyId::SIZE].pack("C")
    # Write all but last size (last is calculated from folder unpack size)
    unpack_sizes[0...-1].each do |size|
      data << write_number(size)
    end
  end

  # Write CRCs
  if digests && !digests.empty?
    data << [PropertyId::CRC].pack("C")
    defined_bits = digests.map { |d| !d.nil? }
    data << write_bit_vector(defined_bits)
    digests.each do |crc|
      data << [crc].pack("V") if crc
    end
  end

  data << [PropertyId::K_END].pack("C")
  data
end

#write_timestamps(_prop_id, times) ⇒ String

Write timestamps

Parameters:

  • prop_id (Integer)

    Property ID

  • times (Array<Time, nil>)

    Timestamps

Returns:

  • (String)

    Encoded timestamps



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 265

def write_timestamps(_prop_id, times)
  data = String.new(encoding: "BINARY")

  defined_bits = times.map { |t| !t.nil? }
  times_data = String.new(encoding: "BINARY")
  times_data << write_bit_vector(defined_bits)
  times_data << [0].pack("C") # Not external

  times.each do |time|
    next unless time

    # Convert to Windows FILETIME
    unix_time = time.to_i
    windows_time = (unix_time + 11_644_473_600) * 10_000_000
    times_data << [windows_time].pack("Q<")
  end

  data << write_number(times_data.bytesize)
  data << times_data
  data
end

#write_unpack_info(folders) ⇒ String

Write unpack info section

Parameters:

  • folders (Array<Hash>)

    Folder information

Returns:

  • (String)

    Encoded unpack info



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/omnizip/formats/seven_zip/header_writer.rb', line 221

def write_unpack_info(folders)
  data = String.new(encoding: "BINARY")

  data << [PropertyId::UNPACK_INFO].pack("C")
  data << write_folders(folders)

  # Coders unpack size
  data << [PropertyId::CODERS_UNPACK_SIZE].pack("C")
  folders.each do |folder|
    data << write_number(folder[:unpack_size])
  end

  data << [PropertyId::K_END].pack("C")
  data
end