omnizip 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +51 -50
- data/Gemfile +2 -0
- data/Rakefile +3 -0
- data/TODO.refactor/00-overview.md +67 -0
- data/TODO.refactor/01-registry-base-class.md +81 -0
- data/TODO.refactor/02-error-hierarchy.md +57 -0
- data/TODO.refactor/03-autoload-entry-point.md +69 -0
- data/TODO.refactor/04-filter-base-consolidation.md +49 -0
- data/TODO.refactor/05-send-private-methods.md +64 -0
- data/TODO.refactor/06-instance-variable-access.md +69 -0
- data/TODO.refactor/07-respond-to-replacement.md +91 -0
- data/TODO.refactor/08-spec-doubles.md +27 -0
- data/TODO.refactor/09-cli-shared-module.md +35 -0
- data/TODO.refactor/10-format-detector-ocp.md +34 -0
- data/TODO.refactor/11-convenience-decoupling.md +38 -0
- data/TODO.refactor/12-thread-safety.md +8 -0
- data/TODO.refactor/13-lutaml-model-migration.md +82 -0
- data/TODO.refactor/14-add-missing-specs.md +25 -0
- data/lib/omnizip/algorithm.rb +0 -2
- data/lib/omnizip/algorithm_registry.rb +29 -57
- data/lib/omnizip/algorithms/lzma/bit_model.rb +1 -1
- data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
- data/lib/omnizip/algorithms/lzma/distance_coder.rb +5 -102
- data/lib/omnizip/algorithms/lzma/length_coder.rb +4 -92
- data/lib/omnizip/algorithms/lzma/literal_decoder.rb +4 -119
- data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
- data/lib/omnizip/algorithms/lzma/match_finder.rb +59 -29
- data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +20 -12
- data/lib/omnizip/algorithms/lzma/range_decoder.rb +77 -269
- data/lib/omnizip/algorithms/lzma/range_encoder.rb +0 -9
- data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +0 -15
- data/lib/omnizip/algorithms/lzma/xz_state.rb +0 -8
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +189 -968
- data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
- data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
- data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
- data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
- data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
- data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
- data/lib/omnizip/algorithms/ppmd8.rb +0 -2
- data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
- data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
- data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
- data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
- data/lib/omnizip/algorithms.rb +1 -0
- data/lib/omnizip/archive_handler.rb +75 -0
- data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
- data/lib/omnizip/archive_handlers.rb +11 -0
- data/lib/omnizip/checksum_registry.rb +23 -96
- data/lib/omnizip/cli/shared.rb +31 -0
- data/lib/omnizip/cli.rb +6 -43
- data/lib/omnizip/commands/archive_create_command.rb +0 -6
- data/lib/omnizip/commands/archive_extract_command.rb +0 -4
- data/lib/omnizip/commands/archive_list_command.rb +0 -4
- data/lib/omnizip/commands/archive_repair_command.rb +0 -2
- data/lib/omnizip/commands/archive_verify_command.rb +0 -2
- data/lib/omnizip/commands/compress_command.rb +0 -2
- data/lib/omnizip/commands/decompress_command.rb +0 -2
- data/lib/omnizip/commands/list_command.rb +0 -2
- data/lib/omnizip/commands/metadata_command.rb +0 -5
- data/lib/omnizip/commands/parity_create_command.rb +0 -2
- data/lib/omnizip/commands/parity_repair_command.rb +0 -2
- data/lib/omnizip/commands/parity_verify_command.rb +0 -2
- data/lib/omnizip/commands/profile_list_command.rb +0 -2
- data/lib/omnizip/commands/profile_show_command.rb +0 -2
- data/lib/omnizip/convenience.rb +111 -234
- data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
- data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
- data/lib/omnizip/error.rb +22 -18
- data/lib/omnizip/filter.rb +5 -3
- data/lib/omnizip/filter_registry.rb +74 -98
- data/lib/omnizip/filters/bcj2/decoder.rb +196 -130
- data/lib/omnizip/filters/bcj2.rb +0 -2
- data/lib/omnizip/filters/bcj_x86.rb +0 -2
- data/lib/omnizip/filters/filter_base.rb +4 -70
- data/lib/omnizip/filters/registry.rb +0 -2
- data/lib/omnizip/format_detector.rb +21 -12
- data/lib/omnizip/format_registry.rb +79 -38
- data/lib/omnizip/formats/bzip2_file.rb +0 -2
- data/lib/omnizip/formats/cpio/entry.rb +0 -2
- data/lib/omnizip/formats/cpio/reader.rb +0 -1
- data/lib/omnizip/formats/cpio/writer.rb +0 -1
- data/lib/omnizip/formats/cpio.rb +0 -1
- data/lib/omnizip/formats/format_spec_loader.rb +1 -1
- data/lib/omnizip/formats/gzip.rb +0 -2
- data/lib/omnizip/formats/iso/directory_record.rb +6 -3
- data/lib/omnizip/formats/iso/reader.rb +2 -2
- data/lib/omnizip/formats/iso.rb +0 -1
- data/lib/omnizip/formats/lzip.rb +5 -25
- data/lib/omnizip/formats/lzma_alone.rb +7 -25
- data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
- data/lib/omnizip/formats/msi/constants.rb +157 -0
- data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
- data/lib/omnizip/formats/msi/entry.rb +128 -0
- data/lib/omnizip/formats/msi/reader.rb +301 -0
- data/lib/omnizip/formats/msi/string_pool.rb +246 -0
- data/lib/omnizip/formats/msi/table_parser.rb +244 -0
- data/lib/omnizip/formats/msi.rb +84 -0
- data/lib/omnizip/formats/ole/allocation_table.rb +10 -1
- data/lib/omnizip/formats/ole/dirent.rb +20 -3
- data/lib/omnizip/formats/ole/header.rb +0 -1
- data/lib/omnizip/formats/ole/ranges_io.rb +21 -3
- data/lib/omnizip/formats/ole/storage.rb +19 -4
- data/lib/omnizip/formats/ole.rb +0 -2
- data/lib/omnizip/formats/rar/block_parser.rb +0 -1
- data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
- data/lib/omnizip/formats/rar/decompressor.rb +38 -1
- data/lib/omnizip/formats/rar/header.rb +0 -1
- data/lib/omnizip/formats/rar/rar5/writer.rb +0 -2
- data/lib/omnizip/formats/rar/reader.rb +2 -3
- data/lib/omnizip/formats/rar/writer.rb +0 -1
- data/lib/omnizip/formats/rar.rb +2 -2
- data/lib/omnizip/formats/rpm/entry.rb +0 -1
- data/lib/omnizip/formats/rpm/header.rb +3 -8
- data/lib/omnizip/formats/rpm/lead.rb +37 -26
- data/lib/omnizip/formats/rpm/tag.rb +0 -1
- data/lib/omnizip/formats/rpm/writer.rb +0 -1
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +98 -61
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
- data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
- data/lib/omnizip/formats/seven_zip/header_writer.rb +22 -61
- data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
- data/lib/omnizip/formats/seven_zip/parser.rb +14 -13
- data/lib/omnizip/formats/seven_zip/reader.rb +82 -19
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -15
- data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
- data/lib/omnizip/formats/seven_zip/writer.rb +22 -68
- data/lib/omnizip/formats/seven_zip.rb +63 -5
- data/lib/omnizip/formats/tar/entry.rb +0 -2
- data/lib/omnizip/formats/tar/header.rb +0 -2
- data/lib/omnizip/formats/tar/reader.rb +0 -2
- data/lib/omnizip/formats/tar/writer.rb +0 -2
- data/lib/omnizip/formats/tar.rb +0 -1
- data/lib/omnizip/formats/xar/entry.rb +0 -1
- data/lib/omnizip/formats/xar/header.rb +0 -1
- data/lib/omnizip/formats/xar/reader.rb +0 -1
- data/lib/omnizip/formats/xar/toc.rb +3 -3
- data/lib/omnizip/formats/xar/writer.rb +11 -3
- data/lib/omnizip/formats/xar.rb +0 -1
- data/lib/omnizip/formats/xz/reader.rb +7 -3
- data/lib/omnizip/formats/xz.rb +3 -40
- data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
- data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
- data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
- data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
- data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
- data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
- data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
- data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
- data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
- data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
- data/lib/omnizip/formats/zip/reader.rb +12 -2
- data/lib/omnizip/formats/zip/writer.rb +73 -1
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
- data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
- data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
- data/lib/omnizip/formats/zip.rb +0 -1
- data/lib/omnizip/formats.rb +9 -22
- data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
- data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
- data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +18 -185
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +3 -3
- data/lib/omnizip/io/source.rb +136 -0
- data/lib/omnizip/io.rb +2 -0
- data/lib/omnizip/link_handler.rb +7 -3
- data/lib/omnizip/metadata/metadata_editor.rb +1 -1
- data/lib/omnizip/models/algorithm_metadata.rb +33 -60
- data/lib/omnizip/models/compression_options.rb +40 -53
- data/lib/omnizip/models/conversion_options.rb +9 -18
- data/lib/omnizip/models/parallel_options.rb +25 -0
- data/lib/omnizip/optimization_registry.rb +8 -44
- data/lib/omnizip/parallel/job_scheduler.rb +26 -8
- data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
- data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
- data/lib/omnizip/parity/par2_repairer.rb +5 -5
- data/lib/omnizip/parity/par2_verifier.rb +27 -22
- data/lib/omnizip/parity.rb +4 -4
- data/lib/omnizip/password/encryption_registry.rb +11 -50
- data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
- data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
- data/lib/omnizip/profile/archive_profile.rb +0 -2
- data/lib/omnizip/profile/binary_profile.rb +0 -2
- data/lib/omnizip/profile/compression_profile.rb +0 -1
- data/lib/omnizip/profile/profile_detector.rb +0 -2
- data/lib/omnizip/profile/text_profile.rb +0 -2
- data/lib/omnizip/profile.rb +0 -2
- data/lib/omnizip/registry.rb +112 -0
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +20 -9
- data/lib/omnizip.rb +26 -55
- data/lib/tasks/msi_test_files.rake +66 -0
- metadata +57 -6
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Formats
|
|
5
|
+
module Msi
|
|
6
|
+
# MSI Table Parser
|
|
7
|
+
#
|
|
8
|
+
# Parses MSI database tables from OLE streams. MSI uses a column-major
|
|
9
|
+
# storage format where each column's data is stored contiguously.
|
|
10
|
+
#
|
|
11
|
+
# Key tables for file extraction:
|
|
12
|
+
# - File: File entries with name, size, component, sequence
|
|
13
|
+
# - Component: Links files to directories
|
|
14
|
+
# - Directory: Directory hierarchy
|
|
15
|
+
# - Media: Cabinet file references
|
|
16
|
+
class TableParser
|
|
17
|
+
include Omnizip::Formats::Msi::Constants
|
|
18
|
+
|
|
19
|
+
# @return [StringPool] String pool for string lookups
|
|
20
|
+
attr_reader :string_pool
|
|
21
|
+
|
|
22
|
+
# @return [Hash] Parsed tables (table_name => rows)
|
|
23
|
+
attr_reader :tables
|
|
24
|
+
|
|
25
|
+
# @return [Hash] Column definitions (table_name => columns)
|
|
26
|
+
attr_reader :columns
|
|
27
|
+
|
|
28
|
+
# @return [Array<String>] Table names
|
|
29
|
+
attr_reader :table_names
|
|
30
|
+
|
|
31
|
+
# @return [Proc] Stream reader method
|
|
32
|
+
attr_reader :read_stream
|
|
33
|
+
|
|
34
|
+
# Initialize table parser
|
|
35
|
+
#
|
|
36
|
+
# @param string_pool [StringPool] String pool for lookups
|
|
37
|
+
# @param stream_reader [Proc] Method to read streams
|
|
38
|
+
def initialize(string_pool, stream_reader)
|
|
39
|
+
@string_pool = string_pool
|
|
40
|
+
@read_stream = stream_reader
|
|
41
|
+
@tables = {}
|
|
42
|
+
@columns = {}
|
|
43
|
+
@table_names = []
|
|
44
|
+
load_table_list
|
|
45
|
+
load_column_defs
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get table rows
|
|
49
|
+
#
|
|
50
|
+
# @param table_name [String] Table name
|
|
51
|
+
# @return [Array<Hash>] Table rows (each row is a hash of column => value)
|
|
52
|
+
def table(table_name)
|
|
53
|
+
return @tables[table_name] if @tables.key?(table_name)
|
|
54
|
+
|
|
55
|
+
@tables[table_name] = parse_table(table_name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get column definitions for table
|
|
59
|
+
#
|
|
60
|
+
# @param table_name [String] Table name
|
|
61
|
+
# @return [Array<Hash>] Column definitions
|
|
62
|
+
def column_defs(table_name)
|
|
63
|
+
@columns[table_name] || []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if table exists
|
|
67
|
+
#
|
|
68
|
+
# @param table_name [String] Table name
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def table_exists?(table_name)
|
|
71
|
+
@table_names.include?(table_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Load list of table names from _Tables stream
|
|
77
|
+
def load_table_list
|
|
78
|
+
data = @read_stream.call(TABLES_STREAM)
|
|
79
|
+
return if data.nil? || data.empty?
|
|
80
|
+
|
|
81
|
+
# _Tables format: column-major with one column (Table string index)
|
|
82
|
+
# First 2 bytes appear to be header (skip)
|
|
83
|
+
# Then 2-byte string indices for each table name
|
|
84
|
+
return if data.bytesize < 4
|
|
85
|
+
|
|
86
|
+
# Skip first 2 bytes (header), read string indices
|
|
87
|
+
offset = 2
|
|
88
|
+
while offset + 2 <= data.bytesize
|
|
89
|
+
str_idx = data[offset, 2].unpack1("v")
|
|
90
|
+
name = @string_pool[str_idx]
|
|
91
|
+
@table_names << name if name
|
|
92
|
+
offset += 2
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Load column definitions from _Columns stream
|
|
97
|
+
def load_column_defs
|
|
98
|
+
data = @read_stream.call(COLUMNS_STREAM)
|
|
99
|
+
return if data.nil? || data.empty?
|
|
100
|
+
|
|
101
|
+
# _Columns format: column-major with 4 columns
|
|
102
|
+
# Column 1: Table (string index, 2 bytes)
|
|
103
|
+
# Column 2: Number (signed short, 2 bytes)
|
|
104
|
+
# Column 3: Name (string index, 2 bytes)
|
|
105
|
+
# Column 4: Type (unsigned short, 2 bytes)
|
|
106
|
+
|
|
107
|
+
# Determine number of rows
|
|
108
|
+
num_rows = data.bytesize / 8 # 4 columns * 2 bytes each
|
|
109
|
+
|
|
110
|
+
# Extract each column (column-major order)
|
|
111
|
+
col_size = num_rows * 2
|
|
112
|
+
|
|
113
|
+
table_col = data[0, col_size].unpack("v*")
|
|
114
|
+
number_col = data[col_size, col_size].unpack("v*")
|
|
115
|
+
name_col = data[col_size * 2, col_size].unpack("v*")
|
|
116
|
+
type_col = data[col_size * 3, col_size].unpack("v*")
|
|
117
|
+
|
|
118
|
+
# Build column definitions per table
|
|
119
|
+
num_rows.times do |i|
|
|
120
|
+
table_name = @string_pool[table_col[i]]
|
|
121
|
+
next unless table_name
|
|
122
|
+
|
|
123
|
+
col_name = @string_pool[name_col[i]]
|
|
124
|
+
next unless col_name
|
|
125
|
+
|
|
126
|
+
# Number column: signed short with nullable bit
|
|
127
|
+
raw_num = number_col[i]
|
|
128
|
+
col_num = raw_num & 0x7FFF
|
|
129
|
+
is_primary_key = raw_num.anybits?(0x8000)
|
|
130
|
+
|
|
131
|
+
# Type column: determine storage width
|
|
132
|
+
type_val = type_col[i]
|
|
133
|
+
col_type = parse_column_type(type_val)
|
|
134
|
+
|
|
135
|
+
@columns[table_name] ||= []
|
|
136
|
+
@columns[table_name] << {
|
|
137
|
+
name: col_name,
|
|
138
|
+
number: col_num,
|
|
139
|
+
type: col_type[:type],
|
|
140
|
+
width: col_type[:width],
|
|
141
|
+
nullable: col_type[:nullable],
|
|
142
|
+
primary_key: is_primary_key,
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Sort columns by number for each table
|
|
147
|
+
@columns.each_value do |cols|
|
|
148
|
+
cols.sort_by! { |c| c[:number] }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Parse column type from raw type value
|
|
153
|
+
#
|
|
154
|
+
# Type encoding:
|
|
155
|
+
# - Low byte: length or type code
|
|
156
|
+
# - High byte: type category with nullable bit
|
|
157
|
+
#
|
|
158
|
+
# @param raw [Integer] Raw type value
|
|
159
|
+
# @return [Hash] Parsed type info
|
|
160
|
+
def parse_column_type(raw)
|
|
161
|
+
low_byte = raw & 0xFF
|
|
162
|
+
high_byte = (raw >> 8) & 0xFF
|
|
163
|
+
nullable = high_byte.anybits?(0x80)
|
|
164
|
+
type_id = high_byte & 0x7F
|
|
165
|
+
|
|
166
|
+
# Determine type and width based on type_id
|
|
167
|
+
# Type 1 = int, Type >= 0x10 = string
|
|
168
|
+
if type_id == 1
|
|
169
|
+
# Integer type
|
|
170
|
+
if low_byte == 2
|
|
171
|
+
{ type: :i2, width: 2, nullable: nullable }
|
|
172
|
+
else
|
|
173
|
+
{ type: :i4, width: 4, nullable: nullable }
|
|
174
|
+
end
|
|
175
|
+
elsif type_id == 2
|
|
176
|
+
# Long integer
|
|
177
|
+
{ type: :i4, width: 4, nullable: nullable }
|
|
178
|
+
elsif type_id >= 0x10
|
|
179
|
+
# String type - width is always 2 (string pool index)
|
|
180
|
+
{ type: :string, width: 2, nullable: nullable }
|
|
181
|
+
else
|
|
182
|
+
# Default to string
|
|
183
|
+
{ type: :string, width: 2, nullable: nullable }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Parse a table stream into rows
|
|
188
|
+
#
|
|
189
|
+
# @param table_name [String] Table name
|
|
190
|
+
# @return [Array<Hash>] Parsed rows
|
|
191
|
+
def parse_table(table_name)
|
|
192
|
+
rows = []
|
|
193
|
+
data = @read_stream.call(table_name)
|
|
194
|
+
return rows if data.nil? || data.empty?
|
|
195
|
+
|
|
196
|
+
column_defs = @columns[table_name]
|
|
197
|
+
return rows if column_defs.nil? || column_defs.empty?
|
|
198
|
+
|
|
199
|
+
# Calculate row count from data size and column widths
|
|
200
|
+
total_width = column_defs.sum { |c| c[:width] }
|
|
201
|
+
return rows if total_width.zero?
|
|
202
|
+
|
|
203
|
+
num_rows = data.bytesize / total_width
|
|
204
|
+
|
|
205
|
+
# Parse column by column (column-major order)
|
|
206
|
+
offset = 0
|
|
207
|
+
rows = Array.new(num_rows) { {} }
|
|
208
|
+
|
|
209
|
+
column_defs.each do |col|
|
|
210
|
+
num_rows.times do |row_idx|
|
|
211
|
+
value = read_column_value(data, offset, col)
|
|
212
|
+
rows[row_idx][col[:name]] = value
|
|
213
|
+
offset += col[:width]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
rows
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Read a single column value from data
|
|
221
|
+
#
|
|
222
|
+
# @param data [String] Raw table data
|
|
223
|
+
# @param offset [Integer] Current offset
|
|
224
|
+
# @param col [Hash] Column definition
|
|
225
|
+
# @return [Object] Parsed value
|
|
226
|
+
def read_column_value(data, offset, col)
|
|
227
|
+
case col[:type]
|
|
228
|
+
when :string
|
|
229
|
+
str_idx = data[offset, 2]&.unpack1("v") || 0
|
|
230
|
+
@string_pool[str_idx]
|
|
231
|
+
when :i2
|
|
232
|
+
val = data[offset, 2]&.unpack1("v") || 0
|
|
233
|
+
# Handle nullable marker
|
|
234
|
+
col[:nullable] ? (val & 0x7FFF) : val
|
|
235
|
+
when :i4
|
|
236
|
+
val = data[offset, 4]&.unpack1("V") || 0
|
|
237
|
+
# Handle nullable marker (high bit)
|
|
238
|
+
col[:nullable] ? (val & 0x7FFFFFFF) : val
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Formats
|
|
5
|
+
# MSI (Microsoft Installer) format support
|
|
6
|
+
#
|
|
7
|
+
# Provides read access to MSI packages, extracting files from
|
|
8
|
+
# embedded or external cabinet archives.
|
|
9
|
+
#
|
|
10
|
+
# MSI files are OLE compound documents containing:
|
|
11
|
+
# - Database tables (File, Component, Directory, Media, etc.)
|
|
12
|
+
# - String pool for interned strings
|
|
13
|
+
# - Embedded cabinets (in _Streams or direct OLE streams)
|
|
14
|
+
module Msi
|
|
15
|
+
autoload :Constants, "omnizip/formats/msi/constants"
|
|
16
|
+
autoload :Entry, "omnizip/formats/msi/entry"
|
|
17
|
+
autoload :StringPool, "omnizip/formats/msi/string_pool"
|
|
18
|
+
autoload :TableParser, "omnizip/formats/msi/table_parser"
|
|
19
|
+
autoload :DirectoryResolver, "omnizip/formats/msi/directory_resolver"
|
|
20
|
+
autoload :CabExtractor, "omnizip/formats/msi/cab_extractor"
|
|
21
|
+
autoload :Reader, "omnizip/formats/msi/reader"
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Open MSI file and return reader
|
|
25
|
+
#
|
|
26
|
+
# @param path [String] Path to MSI file
|
|
27
|
+
# @yield [Reader] Reader instance
|
|
28
|
+
# @return [Reader]
|
|
29
|
+
def open(path)
|
|
30
|
+
reader = Reader.new(path)
|
|
31
|
+
reader.open
|
|
32
|
+
if block_given?
|
|
33
|
+
begin
|
|
34
|
+
yield reader
|
|
35
|
+
ensure
|
|
36
|
+
reader.close
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
reader
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# List files in MSI package
|
|
44
|
+
#
|
|
45
|
+
# @param path [String] Path to MSI file
|
|
46
|
+
# @return [Array<String>] File paths
|
|
47
|
+
def list(path)
|
|
48
|
+
open(path) { |r| r.files.map(&:path) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract all files from MSI package
|
|
52
|
+
#
|
|
53
|
+
# @param path [String] Path to MSI file
|
|
54
|
+
# @param output_dir [String] Output directory
|
|
55
|
+
# @return [Array<String>] Extracted file paths
|
|
56
|
+
def extract(path, output_dir)
|
|
57
|
+
open(path) do |reader|
|
|
58
|
+
reader.extract(output_dir)
|
|
59
|
+
reader.files.map { |f| File.join(output_dir, f.path) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get information about MSI package
|
|
64
|
+
#
|
|
65
|
+
# @param path [String] Path to MSI file
|
|
66
|
+
# @return [Hash] Package information
|
|
67
|
+
def info(path)
|
|
68
|
+
open(path, &:info)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Register MSI format with format registry
|
|
72
|
+
#
|
|
73
|
+
# This overrides OLE's .msi registration.
|
|
74
|
+
def register!
|
|
75
|
+
FormatRegistry.register(".msi", self)
|
|
76
|
+
FormatRegistry.register(".msp", self)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Auto-register MSI format (overrides OLE's .msi registration)
|
|
84
|
+
Omnizip::Formats::Msi.register!
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
4
|
|
|
5
|
-
require "omnizip/formats/ole"
|
|
6
5
|
module Omnizip
|
|
7
6
|
module Formats
|
|
8
7
|
module Ole
|
|
@@ -63,6 +62,16 @@ module Omnizip
|
|
|
63
62
|
@entries.length
|
|
64
63
|
end
|
|
65
64
|
|
|
65
|
+
# Free resources to prevent memory leaks
|
|
66
|
+
#
|
|
67
|
+
# Call this when done with the allocation table to release memory.
|
|
68
|
+
# @return [void]
|
|
69
|
+
def free
|
|
70
|
+
@entries = nil
|
|
71
|
+
@ole = nil
|
|
72
|
+
@io = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
# Follow chain from starting index
|
|
67
76
|
#
|
|
68
77
|
# @param idx [Integer] Starting block index
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "omnizip/formats/ole"
|
|
4
3
|
module Omnizip
|
|
5
4
|
module Formats
|
|
6
5
|
module Ole
|
|
@@ -74,6 +73,12 @@ module Omnizip
|
|
|
74
73
|
# @return [Array<Dirent>] Child entries (for directories)
|
|
75
74
|
attr_reader :parent
|
|
76
75
|
|
|
76
|
+
# Public read access to the per-dirent name lookup hash. Used by
|
|
77
|
+
# children to update the parent's index when their name changes.
|
|
78
|
+
def name_lookup
|
|
79
|
+
@name_lookup ||= {}
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
# @return [Integer, nil] Index in dirent array (used during loading)
|
|
78
83
|
attr_accessor :idx
|
|
79
84
|
|
|
@@ -154,7 +159,7 @@ module Omnizip
|
|
|
154
159
|
# @param value [String] Entry name
|
|
155
160
|
def name=(value)
|
|
156
161
|
if @parent
|
|
157
|
-
map = @parent.
|
|
162
|
+
map = @parent.name_lookup
|
|
158
163
|
map&.delete(@name)
|
|
159
164
|
map&.store(value, self)
|
|
160
165
|
end
|
|
@@ -314,7 +319,7 @@ module Omnizip
|
|
|
314
319
|
raise Errno::EISDIR unless file?
|
|
315
320
|
|
|
316
321
|
io = RangesIOMigrateable.new(self, mode)
|
|
317
|
-
@modify_time = Time.now if io.
|
|
322
|
+
@modify_time = Time.now if io.is_a?(RangesIOMigrateable) && io.writeable?
|
|
318
323
|
|
|
319
324
|
if block_given?
|
|
320
325
|
begin
|
|
@@ -366,6 +371,18 @@ module Omnizip
|
|
|
366
371
|
@children&.each(&block)
|
|
367
372
|
end
|
|
368
373
|
|
|
374
|
+
# Free resources to prevent memory leaks
|
|
375
|
+
#
|
|
376
|
+
# Call this when done with the dirent to release memory.
|
|
377
|
+
# @return [void]
|
|
378
|
+
def free
|
|
379
|
+
@children&.each { |child| child.free if child.is_a?(Dirent) }
|
|
380
|
+
@children = nil
|
|
381
|
+
@parent = nil
|
|
382
|
+
@ole = nil
|
|
383
|
+
@data = nil
|
|
384
|
+
end
|
|
385
|
+
|
|
369
386
|
# Flatten tree to array for serialization
|
|
370
387
|
#
|
|
371
388
|
# @param dirents [Array<Dirent>] Output array
|
|
@@ -73,6 +73,15 @@ module Omnizip
|
|
|
73
73
|
@pos = 0
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Free resources to prevent memory leaks
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
def free
|
|
80
|
+
@io = nil
|
|
81
|
+
@ranges = nil
|
|
82
|
+
@offsets = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Set position
|
|
77
86
|
#
|
|
78
87
|
# @param new_pos [Integer]
|
|
@@ -170,7 +179,7 @@ module Omnizip
|
|
|
170
179
|
# @param data [String] Data to write
|
|
171
180
|
# @return [Integer] Bytes written
|
|
172
181
|
def write(data)
|
|
173
|
-
data = data.dup.force_encoding(Encoding::ASCII_8BIT) if data.
|
|
182
|
+
data = data.dup.force_encoding(Encoding::ASCII_8BIT) if data.is_a?(String)
|
|
174
183
|
return 0 if data.empty?
|
|
175
184
|
|
|
176
185
|
# Grow if needed
|
|
@@ -217,9 +226,9 @@ module Omnizip
|
|
|
217
226
|
raise NotImplementedError, "truncate not supported"
|
|
218
227
|
end
|
|
219
228
|
|
|
220
|
-
# Close
|
|
229
|
+
# Close and free resources
|
|
221
230
|
def close
|
|
222
|
-
|
|
231
|
+
free
|
|
223
232
|
end
|
|
224
233
|
|
|
225
234
|
# Inspect
|
|
@@ -262,6 +271,15 @@ module Omnizip
|
|
|
262
271
|
max_pos = @ranges.map { |pos, len| pos + len }.max || 0
|
|
263
272
|
@io.truncate(max_pos) if max_pos > @io.size
|
|
264
273
|
end
|
|
274
|
+
|
|
275
|
+
# Free resources to prevent memory leaks
|
|
276
|
+
#
|
|
277
|
+
# @return [void]
|
|
278
|
+
def free
|
|
279
|
+
@bat = nil
|
|
280
|
+
@blocks = nil
|
|
281
|
+
super
|
|
282
|
+
end
|
|
265
283
|
end
|
|
266
284
|
|
|
267
285
|
# RangesIO that can migrate between BAT and SBAT based on size
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
4
|
|
|
5
|
-
require "omnizip/formats/ole"
|
|
6
5
|
module Omnizip
|
|
7
6
|
module Formats
|
|
8
7
|
module Ole
|
|
@@ -80,7 +79,7 @@ module Omnizip
|
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
# Force binary encoding
|
|
83
|
-
@io.set_encoding(Encoding::ASCII_8BIT) if @io.
|
|
82
|
+
@io.set_encoding(Encoding::ASCII_8BIT) if @io.is_a?(::IO)
|
|
84
83
|
|
|
85
84
|
# Determine if writable
|
|
86
85
|
@writeable = determine_writeable(mode)
|
|
@@ -175,11 +174,27 @@ module Omnizip
|
|
|
175
174
|
|
|
176
175
|
# Close storage
|
|
177
176
|
def close
|
|
178
|
-
@sb_file&.close
|
|
179
177
|
flush if @writeable
|
|
178
|
+
free
|
|
180
179
|
@io.close if @close_parent
|
|
181
180
|
end
|
|
182
181
|
|
|
182
|
+
# Free internal resources to prevent memory leaks
|
|
183
|
+
#
|
|
184
|
+
# Call this to release memory while keeping the IO open.
|
|
185
|
+
# @return [void]
|
|
186
|
+
def free
|
|
187
|
+
@sb_file&.free
|
|
188
|
+
@sb_file = nil
|
|
189
|
+
@sbat&.free
|
|
190
|
+
@sbat = nil
|
|
191
|
+
@bbat&.free
|
|
192
|
+
@bbat = nil
|
|
193
|
+
@dirents = nil
|
|
194
|
+
@root = nil
|
|
195
|
+
@header = nil
|
|
196
|
+
end
|
|
197
|
+
|
|
183
198
|
# Flush all changes to disk
|
|
184
199
|
#
|
|
185
200
|
# Writes all metadata (dirents, allocation tables, header) to the file.
|
|
@@ -442,7 +457,7 @@ module Omnizip
|
|
|
442
457
|
else
|
|
443
458
|
begin
|
|
444
459
|
@io.flush
|
|
445
|
-
@io.write_nonblock("") if @io.
|
|
460
|
+
@io.write_nonblock("") if @io.is_a?(::IO)
|
|
446
461
|
true
|
|
447
462
|
rescue IOError, Errno::EBADF
|
|
448
463
|
false
|
data/lib/omnizip/formats/ole.rb
CHANGED
|
@@ -119,12 +119,10 @@ module Omnizip
|
|
|
119
119
|
|
|
120
120
|
# Register OLE format in registry
|
|
121
121
|
def register!
|
|
122
|
-
require "omnizip/format_registry"
|
|
123
122
|
FormatRegistry.register(".ole", Storage)
|
|
124
123
|
FormatRegistry.register(".doc", Storage)
|
|
125
124
|
FormatRegistry.register(".xls", Storage)
|
|
126
125
|
FormatRegistry.register(".ppt", Storage)
|
|
127
|
-
FormatRegistry.register(".msi", Storage)
|
|
128
126
|
end
|
|
129
127
|
|
|
130
128
|
private
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "English"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
module Omnizip
|
|
5
6
|
module Formats
|
|
6
7
|
module Rar
|
|
@@ -114,12 +115,48 @@ module Omnizip
|
|
|
114
115
|
#
|
|
115
116
|
# @return [String, nil] Command path or nil
|
|
116
117
|
def find_command
|
|
118
|
+
if Gem.win_platform?
|
|
119
|
+
find_command_windows
|
|
120
|
+
else
|
|
121
|
+
find_command_unix
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Find unrar command on Unix
|
|
126
|
+
#
|
|
127
|
+
# @return [String, nil] Command path or nil
|
|
128
|
+
def find_command_unix
|
|
117
129
|
["unrar", "/usr/bin/unrar", "/usr/local/bin/unrar"].each do |cmd|
|
|
118
|
-
return cmd if system("which #{cmd} >
|
|
130
|
+
return cmd if system("which #{cmd} > #{File::NULL} 2>&1")
|
|
119
131
|
end
|
|
120
132
|
nil
|
|
121
133
|
end
|
|
122
134
|
|
|
135
|
+
# Find unrar command on Windows
|
|
136
|
+
#
|
|
137
|
+
# @return [String, nil] Command path or nil
|
|
138
|
+
def find_command_windows
|
|
139
|
+
# Check for unrar in PATH
|
|
140
|
+
["UnRAR.exe", "unrar.exe"].each do |cmd|
|
|
141
|
+
return cmd if system("where #{cmd} > #{File::NULL} 2>&1")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check common WinRAR install locations
|
|
145
|
+
[
|
|
146
|
+
File.join(ENV.fetch("ProgramFiles", 'C:\Program Files'),
|
|
147
|
+
"WinRAR", "UnRAR.exe"),
|
|
148
|
+
File.join(
|
|
149
|
+
ENV.fetch("ProgramFiles(x86)",
|
|
150
|
+
'C:\Program Files (x86)'),
|
|
151
|
+
"WinRAR", "UnRAR.exe"
|
|
152
|
+
),
|
|
153
|
+
].each do |path|
|
|
154
|
+
return "\"#{path}\"" if File.exist?(path)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
123
160
|
# Get gem version
|
|
124
161
|
#
|
|
125
162
|
# @return [String] Version string
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
|
-
require "omnizip/formats/rar"
|
|
6
5
|
module Omnizip
|
|
7
6
|
module Formats
|
|
8
7
|
module Rar
|
|
@@ -243,7 +242,7 @@ module Omnizip
|
|
|
243
242
|
return false unless @header.version == 4
|
|
244
243
|
|
|
245
244
|
# Check if we have the compression method
|
|
246
|
-
return false
|
|
245
|
+
return false if entry.nil? || entry.method.nil?
|
|
247
246
|
|
|
248
247
|
# All RAR4 methods are supported by our Dispatcher
|
|
249
248
|
true
|
|
@@ -261,7 +260,7 @@ module Omnizip
|
|
|
261
260
|
File.open(output_path, "wb") do |output|
|
|
262
261
|
# For now, assume METHOD_STORE (0x30)
|
|
263
262
|
# Real implementation would get method from entry
|
|
264
|
-
method = entry.
|
|
263
|
+
method = entry.method || 0x30
|
|
265
264
|
|
|
266
265
|
Compression::Dispatcher.decompress(method, compressed_data, output)
|
|
267
266
|
end
|
data/lib/omnizip/formats/rar.rb
CHANGED
|
@@ -164,8 +164,7 @@ module Omnizip
|
|
|
164
164
|
|
|
165
165
|
# Auto-register .rar format
|
|
166
166
|
def register!
|
|
167
|
-
|
|
168
|
-
FormatRegistry.register(".rar", Reader)
|
|
167
|
+
FormatRegistry.register(".rar", "Omnizip::Formats::Rar::Reader")
|
|
169
168
|
end
|
|
170
169
|
end
|
|
171
170
|
end
|
|
@@ -173,4 +172,5 @@ module Omnizip
|
|
|
173
172
|
end
|
|
174
173
|
|
|
175
174
|
# Auto-register when file is loaded
|
|
175
|
+
# Note: Registration happens after all classes are loaded to avoid circular dependency
|
|
176
176
|
Omnizip::Formats::Rar.register!
|