Class: Omnizip::Formats::SevenZip::Parser
- Inherits:
-
Object
- Object
- Omnizip::Formats::SevenZip::Parser
- Includes:
- Constants
- Defined in:
- lib/omnizip/formats/seven_zip/parser.rb
Overview
Binary data parser for .7z format Implements variable-length encoding and bit vector handling
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
-
#data ⇒ Object
readonly
Returns the value of attribute data.
-
#position ⇒ Object
readonly
Returns the value of attribute position.
Instance Method Summary collapse
-
#eof? ⇒ Boolean
Check if at end of data.
-
#expect_property(expected) ⇒ Object
Expect specific property ID.
-
#initialize(data) ⇒ Parser
constructor
Initialize parser with binary data.
-
#peek_byte ⇒ Integer
Peek at next byte without advancing position.
-
#read_anti(entries) ⇒ Object
Read anti flags.
-
#read_attributes(entries) ⇒ Object
Read file attributes.
-
#read_bit_vector(num_items) ⇒ Array<Boolean>
Read bit vector Format: 1 byte flag, then either all 1s or bit array.
-
#read_byte ⇒ Integer
Read a single byte.
-
#read_bytes(count) ⇒ String
Read raw bytes.
-
#read_coder(coder) ⇒ Object
Read coder definition.
-
#read_empty_file(entries) ⇒ Object
Read empty file flags.
-
#read_empty_stream(entries) ⇒ Object
Read empty stream flags.
-
#read_files_info ⇒ Array<Models::FileEntry>
Read files info section Contains file metadata (names, timestamps, attributes).
-
#read_folder(folder) ⇒ Object
Read single folder definition.
-
#read_folders(stream_info) ⇒ Object
Read folders section Contains compression method and coder information.
-
#read_id ⇒ Integer
Read property ID.
-
#read_names(entries) ⇒ Object
Read file names.
-
#read_number ⇒ Integer
Read variable-length integer (7-Zip VLI format).
-
#read_number32 ⇒ Integer
Read 32-bit variable-length number.
-
#read_pack_info(stream_info) ⇒ Object
Read pack info section Contains information about packed streams.
-
#read_substreams_info(stream_info) ⇒ Object
Read substreams info section Maps files to compressed streams.
-
#read_timestamps(entries, attr) ⇒ Object
Read timestamps.
-
#read_uint32 ⇒ Integer
Read fixed-size unsigned 32-bit integer (little-endian).
-
#read_uint64 ⇒ Integer
Read fixed-size unsigned 64-bit integer (little-endian).
-
#read_unpack_info(stream_info) ⇒ Object
Read unpack info section Contains information about unpacked streams.
-
#remaining ⇒ Integer
Get remaining byte count.
-
#skip(count) ⇒ Object
Skip bytes.
-
#skip_data ⇒ Object
Skip property data.
-
#skip_size ⇒ Object
Skip size field.
-
#windows_time_to_unix(windows_time) ⇒ Time
Convert Windows FILETIME to Unix timestamp.
Constructor Details
#initialize(data) ⇒ Parser
Initialize parser with binary data
16 17 18 19 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 16 def initialize(data) @data = data.b @position = 0 end |
Instance Attribute Details
#data ⇒ Object (readonly)
Returns the value of attribute data.
11 12 13 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 11 def data @data end |
#position ⇒ Object (readonly)
Returns the value of attribute position.
11 12 13 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 11 def position @position end |
Instance Method Details
#eof? ⇒ Boolean
Check if at end of data
172 173 174 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 172 def eof? @position >= @data.bytesize end |
#expect_property(expected) ⇒ Object
Expect specific property ID
595 596 597 598 599 600 601 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 595 def expect_property(expected) actual = read_byte return if actual == expected raise "Expected property 0x#{expected.to_s(16)}, " \ "got 0x#{actual.to_s(16)}" end |
#peek_byte ⇒ Integer
Peek at next byte without advancing position
163 164 165 166 167 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 163 def peek_byte raise EOFError if @position >= @data.bytesize @data.getbyte(@position) end |
#read_anti(entries) ⇒ Object
Read anti flags
546 547 548 549 550 551 552 553 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 546 def read_anti(entries) skip_size anti_files = entries.select { |e| !e.has_stream && !e.is_empty } anti_bits = read_bit_vector(anti_files.size) anti_files.each_with_index do |entry, i| entry.is_anti = anti_bits[i] end end |
#read_attributes(entries) ⇒ Object
Read file attributes
579 580 581 582 583 584 585 586 587 588 589 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 579 def read_attributes(entries) skip_size defined_bits = read_bit_vector(entries.size) external = read_byte return unless external.zero? entries.each_with_index do |entry, i| entry.attributes = read_uint32 if defined_bits[i] end end |
#read_bit_vector(num_items) ⇒ Array<Boolean>
Read bit vector Format: 1 byte flag, then either all 1s or bit array
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 124 def read_bit_vector(num_items) all_defined = read_byte num_bytes = (num_items + 7) / 8 if all_defined.zero? # Explicit bit vector raise EOFError if @position + num_bytes > @data.bytesize bits_data = @data[@position, num_bytes] @position += num_bytes decode_bit_vector(bits_data, num_items) else # All bits are set (return 1 for each item, not true) Array.new(num_items, 1) end end |
#read_byte ⇒ Integer
Read a single byte
25 26 27 28 29 30 31 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 25 def read_byte raise EOFError, "End of data" if @position >= @data.bytesize byte = @data.getbyte(@position) @position += 1 byte end |
#read_bytes(count) ⇒ String
Read raw bytes
145 146 147 148 149 150 151 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 145 def read_bytes(count) raise EOFError if @position + count > @data.bytesize bytes = @data[@position, count] @position += count bytes end |
#read_coder(coder) ⇒ Object
Read coder definition
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 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 302 def read_coder(coder) main_byte = read_byte # Extract coder flags id_size = main_byte & 0x0F has_attributes = main_byte.anybits?(0x20) complex_streams = main_byte.anybits?(0x10) # Read method ID method_id = 0 id_size.times do method_id = (method_id << 8) | read_byte end coder.method_id = method_id # Read stream counts if complex if complex_streams coder.num_in_streams = read_number coder.num_out_streams = read_number else coder.num_in_streams = 1 coder.num_out_streams = 1 end # Read properties if present return unless has_attributes props_size = read_number coder.properties = read_bytes(props_size) end |
#read_empty_file(entries) ⇒ Object
Read empty file flags
534 535 536 537 538 539 540 541 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 534 def read_empty_file(entries) skip_size empty_files = entries.reject(&:has_stream) empty_bits = read_bit_vector(empty_files.size) empty_files.each_with_index do |entry, i| entry.is_empty = !empty_bits[i] end end |
#read_empty_stream(entries) ⇒ Object
Read empty stream flags
520 521 522 523 524 525 526 527 528 529 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 520 def read_empty_stream(entries) skip_size empty_stream = read_bit_vector(entries.size) entries.each_with_index do |entry, i| # Bit vector: 0 = has stream (file), 1 = empty (directory) # Convert to boolean: has_stream = (bit == 0) entry.has_stream = (empty_stream[i].zero?) entry.is_dir = (empty_stream[i] == 1) end end |
#read_files_info ⇒ Array<Models::FileEntry>
Read files info section Contains file metadata (names, timestamps, attributes)
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 451 def read_files_info num_files = read_number entries = Array.new(num_files) { Models::FileEntry.new } # Read file properties until eof? || peek_byte == PropertyId::K_END prop_type = read_byte case prop_type when PropertyId::NAME read_names(entries) when PropertyId::EMPTY_STREAM read_empty_stream(entries) when PropertyId::EMPTY_FILE read_empty_file(entries) when PropertyId::ANTI read_anti(entries) when PropertyId::CTIME (entries, :ctime) when PropertyId::ATIME (entries, :atime) when PropertyId::MTIME (entries, :mtime) when PropertyId::WIN_ATTRIB read_attributes(entries) when PropertyId::DUMMY skip_data else skip_data end end read_byte if !eof? && peek_byte == PropertyId::K_END entries end |
#read_folder(folder) ⇒ Object
Read single folder definition
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 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 256 def read_folder(folder) num_coders = read_number raise "Too many coders" if num_coders > Constants::MAX_NUM_CODERS num_in_streams = 0 num_out_streams = 0 # Read coders num_coders.times do coder = Models::CoderInfo.new read_coder(coder) folder.coders << coder num_in_streams += coder.num_in_streams num_out_streams += coder.num_out_streams end # Read bind pairs num_bind_pairs = num_out_streams - 1 num_bind_pairs.times do in_index = read_number out_index = read_number folder.bind_pairs << [in_index, out_index] end # Read pack stream indices num_pack_streams = num_in_streams - num_bind_pairs if num_pack_streams == 1 # Single pack stream - find unused input (0...num_in_streams).each do |i| used = folder.bind_pairs.any? { |pair| pair[0] == i } unless used folder.pack_stream_indices << i break end end else # Multiple pack streams - read indices num_pack_streams.times do folder.pack_stream_indices << read_number end end end |
#read_folders(stream_info) ⇒ Object
Read folders section Contains compression method and coder information
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 233 def read_folders(stream_info) num_folders = read_number # Read External flag (similar to names) external = read_byte # Only read folders if they're stored inline (external == 0) if external.zero? # Read each folder num_folders.times do folder = Models::Folder.new read_folder(folder) stream_info.folders << folder end else # External folders not supported raise "External folders not supported" end end |
#read_id ⇒ Integer
Read property ID
93 94 95 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 93 def read_id read_number end |
#read_names(entries) ⇒ Object
Read file names
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 491 def read_names(entries) # Size of all names in bytes size = read_number start_pos = @position external = read_byte if external.zero? # Names stored inline as UTF-16LE null-terminated strings entries.each do |entry| name_bytes = +"" loop do ch1 = read_byte ch2 = read_byte break if ch1.zero? && ch2.zero? name_bytes << [ch1, ch2].pack("CC") end entry.name = name_bytes.encode("UTF-8", "UTF-16LE") end end # Ensure we consumed expected bytes consumed = @position - start_pos skip(size - consumed) if consumed < size end |
#read_number ⇒ Integer
Read variable-length integer (7-Zip VLI format)
7-Zip VLI encoding uses the first byte's high bits to determine the number of additional bytes:
0xxxxxxx : value = xxxxxxx (0-127)
10xxxxxx BYTE y[1] : value = (xxxxxx << 8) + y
110xxxxx BYTE y[2] : value = (xxxxx << 16) + y
1110xxxx BYTE y[3] : value = (xxxx << 24) + y
...up to 8 bytes total
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 44 def read_number first_byte = read_byte # Single byte encoding (0-127) return first_byte if first_byte.nobits?(0x80) # 7-Zip VLI: extra bytes are in little-endian order. # Per 7-Zip SDK CInByte2::ReadNumber(): # - Each set bit in first_byte (from MSB) means one more extra byte # - Extra bytes are placed at increasing byte positions (LE) # - The remaining data bits from first_byte go at the highest position mask = 0x80 value = 0 shift = 0 while first_byte.anybits?(mask) value |= (read_byte << shift) shift += 8 mask >>= 1 end # The data bits from first_byte go at the highest position # When mask reaches 0 (first_byte == 0xFF, 8 extra bytes), there are no data bits data_bits = mask.zero? ? 0 : (first_byte & (mask - 1)) value |= (data_bits << shift) value end |
#read_number32 ⇒ Integer
Read 32-bit variable-length number
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 77 def read_number32 first_byte = peek_byte if first_byte.nobits?(0x80) @position += 1 return first_byte end value = read_number raise "Unsupported 32-bit value" if value >= 0x80000000 value end |
#read_pack_info(stream_info) ⇒ Object
Read pack info section Contains information about packed streams
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 187 def read_pack_info(stream_info) # Read pack position stream_info.pack_pos = read_number # Read number of pack streams num_pack_streams = read_number # Read properties in any order (7z format allows variable ordering) sizes_read = false until eof? || peek_byte == PropertyId::K_END prop_type = peek_byte case prop_type when PropertyId::SIZE read_byte num_pack_streams.times do stream_info.pack_sizes << read_number end sizes_read = true when PropertyId::CRC read_byte defined_vec = read_bit_vector(num_pack_streams) num_pack_streams.times do |i| stream_info.pack_crcs << (defined_vec[i] ? read_uint32 : nil) end when PropertyId::K_END break else # Unknown property - skip it read_byte skip_data unless eof? end end # Verify required SIZE property was present raise "Missing SIZE property in pack info" unless sizes_read # K_END is optional in some 7-Zip versions read_byte if !eof? && peek_byte == PropertyId::K_END end |
#read_substreams_info(stream_info) ⇒ Object
Read substreams info section Maps files to compressed streams
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 387 def read_substreams_info(stream_info) # Read number of unpack streams per folder if !eof? && peek_byte == PropertyId::NUM_UNPACK_STREAM read_byte stream_info.folders.each do stream_info.num_unpack_streams_in_folders << read_number end else # Default: one stream per folder stream_info.folders.size.times do stream_info.num_unpack_streams_in_folders << 1 end end # Read unpack sizes if !eof? && peek_byte == PropertyId::SIZE read_byte stream_info.folders.each_with_index do |folder, i| num_streams = stream_info.num_unpack_streams_in_folders[i] start_idx = stream_info.unpack_sizes.size if num_streams > 1 (num_streams - 1).times do size = read_number stream_info.unpack_sizes << size end end # Last stream size = folder's final output size - sum of sizes we just read for this folder folder_total_size = folder.uncompressed_size sum = stream_info.unpack_sizes[start_idx..]&.sum || 0 last_size = folder_total_size - sum stream_info.unpack_sizes << last_size end else # No SIZE property - use folder unpack sizes directly # This happens when each folder has exactly one stream stream_info.folders.each_with_index do |folder, i| num_streams = stream_info.num_unpack_streams_in_folders[i] if num_streams == 1 # Single stream - use folder's total unpack size folder_size = folder.unpack_sizes.empty? ? 0 : folder.unpack_sizes.sum stream_info.unpack_sizes << folder_size end end end # Read digests (CRCs) num_digests = stream_info.num_unpack_streams_in_folders.sum if !(eof? || peek_byte == PropertyId::K_END) && (peek_byte == PropertyId::CRC) read_byte defined_vec = read_bit_vector(num_digests) num_digests.times do |i| stream_info.digests << (defined_vec[i] ? read_uint32 : nil) end end # K_END is optional in some 7-Zip versions read_byte if !eof? && peek_byte == PropertyId::K_END end |
#read_timestamps(entries, attr) ⇒ Object
Read timestamps
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 559 def (entries, attr) skip_size defined_bits = read_bit_vector(entries.size) external = read_byte return unless external.zero? entries.each_with_index do |entry, i| next unless defined_bits[i] time_val = read_uint64 # Convert Windows FILETIME to Ruby Time # (100-nanosecond intervals since 1601-01-01) entry.public_send(:"#{attr}=", windows_time_to_unix(time_val)) end end |
#read_uint32 ⇒ Integer
Read fixed-size unsigned 32-bit integer (little-endian)
100 101 102 103 104 105 106 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 100 def read_uint32 raise EOFError if @position + 4 > @data.bytesize value = @data[@position, 4].unpack1("V") @position += 4 value end |
#read_uint64 ⇒ Integer
Read fixed-size unsigned 64-bit integer (little-endian)
111 112 113 114 115 116 117 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 111 def read_uint64 raise EOFError if @position + 8 > @data.bytesize value = @data[@position, 8].unpack1("Q<") @position += 8 value end |
#read_unpack_info(stream_info) ⇒ Object
Read unpack info section Contains information about unpacked streams
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 337 def read_unpack_info(stream_info) # Read properties in any order (7z format allows variable ordering) folders_read = false unpack_sizes_read = false until eof? || peek_byte == PropertyId::K_END prop_type = read_byte case prop_type when PropertyId::FOLDER read_folders(stream_info) folders_read = true when PropertyId::CODERS_UNPACK_SIZE # Read unpack sizes for each folder based on numOutStreams stream_info.folders.each do |folder| # For folders with 0 coders (Copy method), treat as having 1 output stream num_out_streams = folder.num_out_streams num_out_streams = 1 if num_out_streams.zero? num_out_streams.times do folder.unpack_sizes << read_number end end unpack_sizes_read = true when PropertyId::CRC # Optional: read CRCs defined_vec = read_bit_vector(stream_info.num_folders) stream_info.num_folders.times do |i| stream_info.folders[i].unpack_crc = read_uint32 if defined_vec[i] end when PropertyId::K_END break else # Skip unknown properties skip_data unless eof? end end # Verify required properties were present raise "Missing FOLDER property in unpack info" unless folders_read raise "Missing CODERS_UNPACK_SIZE property in unpack info" unless unpack_sizes_read # K_END is optional in some 7-Zip versions read_byte if !eof? && peek_byte == PropertyId::K_END end |
#remaining ⇒ Integer
Get remaining byte count
179 180 181 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 179 def remaining @data.bytesize - @position end |
#skip(count) ⇒ Object
Skip bytes
156 157 158 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 156 def skip(count) @position += count end |
#skip_data ⇒ Object
Skip property data
609 610 611 612 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 609 def skip_data size = read_number skip(size) end |
#skip_size ⇒ Object
Skip size field
604 605 606 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 604 def skip_size read_number end |
#windows_time_to_unix(windows_time) ⇒ Time
Convert Windows FILETIME to Unix timestamp
618 619 620 621 622 623 624 625 626 |
# File 'lib/omnizip/formats/seven_zip/parser.rb', line 618 def windows_time_to_unix(windows_time) # Windows FILETIME epoch: 1601-01-01 # Unix epoch: 1970-01-01 # Difference: 11644473600 seconds unix_time = (windows_time / 10_000_000) - 11_644_473_600 Time.at(unix_time) rescue StandardError nil end |