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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +51 -50
  3. data/Gemfile +2 -0
  4. data/Rakefile +3 -0
  5. data/TODO.refactor/00-overview.md +67 -0
  6. data/TODO.refactor/01-registry-base-class.md +81 -0
  7. data/TODO.refactor/02-error-hierarchy.md +57 -0
  8. data/TODO.refactor/03-autoload-entry-point.md +69 -0
  9. data/TODO.refactor/04-filter-base-consolidation.md +49 -0
  10. data/TODO.refactor/05-send-private-methods.md +64 -0
  11. data/TODO.refactor/06-instance-variable-access.md +69 -0
  12. data/TODO.refactor/07-respond-to-replacement.md +91 -0
  13. data/TODO.refactor/08-spec-doubles.md +27 -0
  14. data/TODO.refactor/09-cli-shared-module.md +35 -0
  15. data/TODO.refactor/10-format-detector-ocp.md +34 -0
  16. data/TODO.refactor/11-convenience-decoupling.md +38 -0
  17. data/TODO.refactor/12-thread-safety.md +8 -0
  18. data/TODO.refactor/13-lutaml-model-migration.md +82 -0
  19. data/TODO.refactor/14-add-missing-specs.md +25 -0
  20. data/lib/omnizip/algorithm.rb +0 -2
  21. data/lib/omnizip/algorithm_registry.rb +29 -57
  22. data/lib/omnizip/algorithms/lzma/bit_model.rb +1 -1
  23. data/lib/omnizip/algorithms/lzma/dictionary.rb +4 -3
  24. data/lib/omnizip/algorithms/lzma/distance_coder.rb +5 -102
  25. data/lib/omnizip/algorithms/lzma/length_coder.rb +4 -92
  26. data/lib/omnizip/algorithms/lzma/literal_decoder.rb +4 -119
  27. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +2 -0
  28. data/lib/omnizip/algorithms/lzma/match_finder.rb +59 -29
  29. data/lib/omnizip/algorithms/lzma/optimal_encoder.rb +20 -12
  30. data/lib/omnizip/algorithms/lzma/range_decoder.rb +77 -269
  31. data/lib/omnizip/algorithms/lzma/range_encoder.rb +0 -9
  32. data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +0 -15
  33. data/lib/omnizip/algorithms/lzma/xz_state.rb +0 -8
  34. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +189 -968
  35. data/lib/omnizip/algorithms/lzma2/encoder.rb +0 -2
  36. data/lib/omnizip/algorithms/lzma2/lzma2_chunk.rb +0 -2
  37. data/lib/omnizip/algorithms/lzma2/properties.rb +0 -2
  38. data/lib/omnizip/algorithms/ppmd7/model.rb +1 -1
  39. data/lib/omnizip/algorithms/ppmd8/context.rb +0 -2
  40. data/lib/omnizip/algorithms/ppmd8/model.rb +1 -1
  41. data/lib/omnizip/algorithms/ppmd8.rb +0 -2
  42. data/lib/omnizip/algorithms/ppmd_base.rb +0 -2
  43. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +0 -5
  44. data/lib/omnizip/algorithms/xz_lzma2.rb +0 -5
  45. data/lib/omnizip/algorithms/zstandard/frame/header.rb +3 -5
  46. data/lib/omnizip/algorithms.rb +1 -0
  47. data/lib/omnizip/archive_handler.rb +75 -0
  48. data/lib/omnizip/archive_handlers/tar_handler.rb +51 -0
  49. data/lib/omnizip/archive_handlers/zip_handler.rb +73 -0
  50. data/lib/omnizip/archive_handlers.rb +11 -0
  51. data/lib/omnizip/checksum_registry.rb +23 -96
  52. data/lib/omnizip/cli/shared.rb +31 -0
  53. data/lib/omnizip/cli.rb +6 -43
  54. data/lib/omnizip/commands/archive_create_command.rb +0 -6
  55. data/lib/omnizip/commands/archive_extract_command.rb +0 -4
  56. data/lib/omnizip/commands/archive_list_command.rb +0 -4
  57. data/lib/omnizip/commands/archive_repair_command.rb +0 -2
  58. data/lib/omnizip/commands/archive_verify_command.rb +0 -2
  59. data/lib/omnizip/commands/compress_command.rb +0 -2
  60. data/lib/omnizip/commands/decompress_command.rb +0 -2
  61. data/lib/omnizip/commands/list_command.rb +0 -2
  62. data/lib/omnizip/commands/metadata_command.rb +0 -5
  63. data/lib/omnizip/commands/parity_create_command.rb +0 -2
  64. data/lib/omnizip/commands/parity_repair_command.rb +0 -2
  65. data/lib/omnizip/commands/parity_verify_command.rb +0 -2
  66. data/lib/omnizip/commands/profile_list_command.rb +0 -2
  67. data/lib/omnizip/commands/profile_show_command.rb +0 -2
  68. data/lib/omnizip/convenience.rb +111 -234
  69. data/lib/omnizip/converter/seven_zip_to_zip_strategy.rb +1 -1
  70. data/lib/omnizip/crypto/aes256/cipher.rb +6 -1
  71. data/lib/omnizip/error.rb +22 -18
  72. data/lib/omnizip/filter.rb +5 -3
  73. data/lib/omnizip/filter_registry.rb +74 -98
  74. data/lib/omnizip/filters/bcj2/decoder.rb +196 -130
  75. data/lib/omnizip/filters/bcj2.rb +0 -2
  76. data/lib/omnizip/filters/bcj_x86.rb +0 -2
  77. data/lib/omnizip/filters/filter_base.rb +4 -70
  78. data/lib/omnizip/filters/registry.rb +0 -2
  79. data/lib/omnizip/format_detector.rb +21 -12
  80. data/lib/omnizip/format_registry.rb +79 -38
  81. data/lib/omnizip/formats/bzip2_file.rb +0 -2
  82. data/lib/omnizip/formats/cpio/entry.rb +0 -2
  83. data/lib/omnizip/formats/cpio/reader.rb +0 -1
  84. data/lib/omnizip/formats/cpio/writer.rb +0 -1
  85. data/lib/omnizip/formats/cpio.rb +0 -1
  86. data/lib/omnizip/formats/format_spec_loader.rb +1 -1
  87. data/lib/omnizip/formats/gzip.rb +0 -2
  88. data/lib/omnizip/formats/iso/directory_record.rb +6 -3
  89. data/lib/omnizip/formats/iso/reader.rb +2 -2
  90. data/lib/omnizip/formats/iso.rb +0 -1
  91. data/lib/omnizip/formats/lzip.rb +5 -25
  92. data/lib/omnizip/formats/lzma_alone.rb +7 -25
  93. data/lib/omnizip/formats/msi/cab_extractor.rb +181 -0
  94. data/lib/omnizip/formats/msi/constants.rb +157 -0
  95. data/lib/omnizip/formats/msi/directory_resolver.rb +188 -0
  96. data/lib/omnizip/formats/msi/entry.rb +128 -0
  97. data/lib/omnizip/formats/msi/reader.rb +301 -0
  98. data/lib/omnizip/formats/msi/string_pool.rb +246 -0
  99. data/lib/omnizip/formats/msi/table_parser.rb +244 -0
  100. data/lib/omnizip/formats/msi.rb +84 -0
  101. data/lib/omnizip/formats/ole/allocation_table.rb +10 -1
  102. data/lib/omnizip/formats/ole/dirent.rb +20 -3
  103. data/lib/omnizip/formats/ole/header.rb +0 -1
  104. data/lib/omnizip/formats/ole/ranges_io.rb +21 -3
  105. data/lib/omnizip/formats/ole/storage.rb +19 -4
  106. data/lib/omnizip/formats/ole.rb +0 -2
  107. data/lib/omnizip/formats/rar/block_parser.rb +0 -1
  108. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +1 -1
  109. data/lib/omnizip/formats/rar/decompressor.rb +38 -1
  110. data/lib/omnizip/formats/rar/header.rb +0 -1
  111. data/lib/omnizip/formats/rar/rar5/writer.rb +0 -2
  112. data/lib/omnizip/formats/rar/reader.rb +2 -3
  113. data/lib/omnizip/formats/rar/writer.rb +0 -1
  114. data/lib/omnizip/formats/rar.rb +2 -2
  115. data/lib/omnizip/formats/rpm/entry.rb +0 -1
  116. data/lib/omnizip/formats/rpm/header.rb +3 -8
  117. data/lib/omnizip/formats/rpm/lead.rb +37 -26
  118. data/lib/omnizip/formats/rpm/tag.rb +0 -1
  119. data/lib/omnizip/formats/rpm/writer.rb +0 -1
  120. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +98 -61
  121. data/lib/omnizip/formats/seven_zip/coder_chain.rb +1 -2
  122. data/lib/omnizip/formats/seven_zip/encoded_header.rb +0 -1
  123. data/lib/omnizip/formats/seven_zip/encrypted_header.rb +0 -1
  124. data/lib/omnizip/formats/seven_zip/header.rb +0 -1
  125. data/lib/omnizip/formats/seven_zip/header_encryptor.rb +0 -1
  126. data/lib/omnizip/formats/seven_zip/header_writer.rb +22 -61
  127. data/lib/omnizip/formats/seven_zip/models/file_entry.rb +1 -1
  128. data/lib/omnizip/formats/seven_zip/parser.rb +14 -13
  129. data/lib/omnizip/formats/seven_zip/reader.rb +82 -19
  130. data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +0 -15
  131. data/lib/omnizip/formats/seven_zip/split_archive_writer.rb +0 -1
  132. data/lib/omnizip/formats/seven_zip/writer.rb +22 -68
  133. data/lib/omnizip/formats/seven_zip.rb +63 -5
  134. data/lib/omnizip/formats/tar/entry.rb +0 -2
  135. data/lib/omnizip/formats/tar/header.rb +0 -2
  136. data/lib/omnizip/formats/tar/reader.rb +0 -2
  137. data/lib/omnizip/formats/tar/writer.rb +0 -2
  138. data/lib/omnizip/formats/tar.rb +0 -1
  139. data/lib/omnizip/formats/xar/entry.rb +0 -1
  140. data/lib/omnizip/formats/xar/header.rb +0 -1
  141. data/lib/omnizip/formats/xar/reader.rb +0 -1
  142. data/lib/omnizip/formats/xar/toc.rb +3 -3
  143. data/lib/omnizip/formats/xar/writer.rb +11 -3
  144. data/lib/omnizip/formats/xar.rb +0 -1
  145. data/lib/omnizip/formats/xz/reader.rb +7 -3
  146. data/lib/omnizip/formats/xz.rb +3 -40
  147. data/lib/omnizip/formats/xz_impl/block_decoder.rb +4 -10
  148. data/lib/omnizip/formats/xz_impl/block_encoder.rb +0 -2
  149. data/lib/omnizip/formats/xz_impl/block_header_parser.rb +0 -2
  150. data/lib/omnizip/formats/xz_impl/index_decoder.rb +0 -1
  151. data/lib/omnizip/formats/xz_impl/index_encoder.rb +0 -1
  152. data/lib/omnizip/formats/xz_impl/stream_encoder.rb +1 -1
  153. data/lib/omnizip/formats/xz_impl/stream_footer.rb +0 -1
  154. data/lib/omnizip/formats/xz_impl/stream_footer_parser.rb +0 -1
  155. data/lib/omnizip/formats/xz_impl/stream_header.rb +0 -1
  156. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +0 -2
  157. data/lib/omnizip/formats/zip/central_directory_header.rb +2 -3
  158. data/lib/omnizip/formats/zip/end_of_central_directory.rb +0 -2
  159. data/lib/omnizip/formats/zip/local_file_header.rb +0 -2
  160. data/lib/omnizip/formats/zip/reader.rb +12 -2
  161. data/lib/omnizip/formats/zip/writer.rb +73 -1
  162. data/lib/omnizip/formats/zip/zip64_end_of_central_directory.rb +0 -2
  163. data/lib/omnizip/formats/zip/zip64_end_of_central_directory_locator.rb +0 -2
  164. data/lib/omnizip/formats/zip/zip64_extra_field.rb +0 -2
  165. data/lib/omnizip/formats/zip.rb +0 -1
  166. data/lib/omnizip/formats.rb +9 -22
  167. data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +2 -2
  168. data/lib/omnizip/implementations/seven_zip/lzma/range_encoder.rb +1 -1
  169. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +0 -6
  170. data/lib/omnizip/implementations/xz_utils/lzma2/decoder.rb +18 -185
  171. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +3 -3
  172. data/lib/omnizip/io/source.rb +136 -0
  173. data/lib/omnizip/io.rb +2 -0
  174. data/lib/omnizip/link_handler.rb +7 -3
  175. data/lib/omnizip/metadata/metadata_editor.rb +1 -1
  176. data/lib/omnizip/models/algorithm_metadata.rb +33 -60
  177. data/lib/omnizip/models/compression_options.rb +40 -53
  178. data/lib/omnizip/models/conversion_options.rb +9 -18
  179. data/lib/omnizip/models/parallel_options.rb +25 -0
  180. data/lib/omnizip/optimization_registry.rb +8 -44
  181. data/lib/omnizip/parallel/job_scheduler.rb +26 -8
  182. data/lib/omnizip/parallel/parallel_compressor.rb +10 -20
  183. data/lib/omnizip/parallel/parallel_extractor.rb +20 -11
  184. data/lib/omnizip/parity/par2_repairer.rb +5 -5
  185. data/lib/omnizip/parity/par2_verifier.rb +27 -22
  186. data/lib/omnizip/parity.rb +4 -4
  187. data/lib/omnizip/password/encryption_registry.rb +11 -50
  188. data/lib/omnizip/password/zip_crypto_strategy.rb +1 -4
  189. data/lib/omnizip/pipe/stream_decompressor.rb +2 -3
  190. data/lib/omnizip/profile/archive_profile.rb +0 -2
  191. data/lib/omnizip/profile/binary_profile.rb +0 -2
  192. data/lib/omnizip/profile/compression_profile.rb +0 -1
  193. data/lib/omnizip/profile/profile_detector.rb +0 -2
  194. data/lib/omnizip/profile/text_profile.rb +0 -2
  195. data/lib/omnizip/profile.rb +0 -2
  196. data/lib/omnizip/registry.rb +112 -0
  197. data/lib/omnizip/version.rb +1 -1
  198. data/lib/omnizip/zip/file.rb +20 -9
  199. data/lib/omnizip.rb +26 -55
  200. data/lib/tasks/msi_test_files.rake +66 -0
  201. 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.instance_variable_get(:@name_lookup)
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.respond_to?(:writeable?) && io.writeable?
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
@@ -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
@@ -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.respond_to?(:encoding)
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 (no-op by default)
229
+ # Close and free resources
221
230
  def close
222
- # No-op
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.respond_to?(:set_encoding)
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.respond_to?(:write_nonblock)
460
+ @io.write_nonblock("") if @io.is_a?(::IO)
446
461
  true
447
462
  rescue IOError, Errno::EBADF
448
463
  false
@@ -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,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/formats/rar"
4
3
  module Omnizip
5
4
  module Formats
6
5
  module Rar
@@ -52,7 +52,7 @@ module Omnizip
52
52
 
53
53
  # Accessor for memory size (for testing)
54
54
  def memory_size
55
- @model.instance_variable_get(:@mem_size)
55
+ @model.mem_size
56
56
  end
57
57
 
58
58
  # Initialize the RAR PPMd encoder
@@ -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} > /dev/null 2>&1")
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/formats/rar"
4
3
  module Omnizip
5
4
  module Formats
6
5
  module Rar
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/parity/par2_creator"
4
-
5
3
  module Omnizip
6
4
  module Formats
7
5
  module Rar
@@ -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 unless entry.respond_to?(:method) && entry.method
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.respond_to?(:method) ? entry.method : 0x30
263
+ method = entry.method || 0x30
265
264
 
266
265
  Compression::Dispatcher.decompress(method, compressed_data, output)
267
266
  end
@@ -4,7 +4,6 @@ require "fileutils"
4
4
  require "zlib"
5
5
  require "stringio"
6
6
 
7
- require "omnizip/formats/rar"
8
7
  module Omnizip
9
8
  module Formats
10
9
  module Rar
@@ -164,8 +164,7 @@ module Omnizip
164
164
 
165
165
  # Auto-register .rar format
166
166
  def register!
167
- require "omnizip/format_registry"
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!
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/formats/rpm"
4
3
  module Omnizip
5
4
  module Formats
6
5
  module Rpm