omnizip 0.3.37 → 0.3.39

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -9
  3. data/README.adoc +108 -140
  4. data/config/formats/rar3_spec.yml +1 -0
  5. data/docs/guides/archive-formats/gzip-format.adoc +2 -4
  6. data/docs/guides/archive-formats/ole-format.adoc +54 -219
  7. data/docs/guides/archive-formats/rar5.adoc +15 -15
  8. data/docs/guides/archive-formats/rpm-format.adoc +48 -189
  9. data/docs/reference/api/overview.adoc +16 -17
  10. data/docs/troubleshooting/index.adoc +1 -4
  11. data/lib/omnizip/archive_handler.rb +2 -0
  12. data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
  13. data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
  14. data/lib/omnizip/archive_handlers.rb +2 -0
  15. data/lib/omnizip/cli.rb +25 -0
  16. data/lib/omnizip/convenience.rb +6 -4
  17. data/lib/omnizip/formats/iso/directory_builder.rb +24 -0
  18. data/lib/omnizip/formats/iso/reader.rb +2 -2
  19. data/lib/omnizip/formats/iso/writer.rb +20 -4
  20. data/lib/omnizip/formats/iso.rb +3 -1
  21. data/lib/omnizip/formats/rar/block_parser.rb +18 -8
  22. data/lib/omnizip/formats/rar/constants.rb +4 -0
  23. data/lib/omnizip/formats/rar/decompressor.rb +12 -8
  24. data/lib/omnizip/formats/rar/header.rb +7 -15
  25. data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +5 -3
  26. data/lib/omnizip/formats/rar/rar5/header.rb +50 -33
  27. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +17 -34
  28. data/lib/omnizip/formats/rar/rar5/vint.rb +25 -37
  29. data/lib/omnizip/formats/rar/rar5/writer.rb +50 -14
  30. data/lib/omnizip/formats/rar/reader.rb +18 -14
  31. data/lib/omnizip/formats/rar/writer.rb +121 -136
  32. data/lib/omnizip/formats/rar.rb +5 -11
  33. data/lib/omnizip/formats/rar3/reader.rb +11 -50
  34. data/lib/omnizip/formats/rar3/writer.rb +13 -10
  35. data/lib/omnizip/formats/rar3.rb +15 -0
  36. data/lib/omnizip/formats/rar5/reader.rb +8 -10
  37. data/lib/omnizip/formats/rar5.rb +15 -0
  38. data/lib/omnizip/formats/seven_zip/parser.rb +22 -7
  39. data/lib/omnizip/formats/seven_zip/writer.rb +78 -19
  40. data/lib/omnizip/formats.rb +2 -0
  41. data/lib/omnizip/version.rb +1 -1
  42. data/readme-docs/api-usage.adoc +5 -4
  43. data/readme-docs/architecture.adoc +5 -4
  44. metadata +5 -1
@@ -99,15 +99,16 @@ writer.close
99
99
  [source,ruby]
100
100
  ----
101
101
  # 7z archive
102
- writer = Omnizip::Formats::SevenZip::Writer.new('archive.7z')
103
- writer.add_file('document.txt')
104
- writer.add_directory('photos/')
105
- writer.close
102
+ Omnizip::Formats::SevenZip.create('archive.7z') do |writer|
103
+ writer.add_file('document.txt')
104
+ writer.add_directory('photos/')
105
+ end
106
106
 
107
107
  # ZIP archive
108
108
  writer = Omnizip::Formats::Zip::Writer.new('archive.zip')
109
109
  writer.add_file('document.txt')
110
- writer.close
110
+ writer.add_directory('photos/')
111
+ writer.write
111
112
  ----
112
113
 
113
114
  ==== Extracting Archives
@@ -115,26 +116,24 @@ writer.close
115
116
  [source,ruby]
116
117
  ----
117
118
  # 7z archive
118
- reader = Omnizip::Formats::SevenZip::Reader.new('archive.7z')
119
- reader.extract_all('output/')
120
- reader.close
119
+ Omnizip::Formats::SevenZip.open('archive.7z') do |reader|
120
+ reader.extract_all('output/')
121
+ end
121
122
 
122
- # ZIP archive
123
- reader = Omnizip::Formats::Zip::Reader.new('archive.zip')
123
+ # ZIP archive (call .read to parse before extracting)
124
+ reader = Omnizip::Formats::Zip::Reader.new('archive.zip').read
124
125
  reader.extract_all('output/')
125
- reader.close
126
126
  ----
127
127
 
128
128
  ==== Listing Contents
129
129
 
130
130
  [source,ruby]
131
131
  ----
132
- reader = Omnizip::Formats::SevenZip::Reader.new('archive.7z')
133
- reader.open
134
- reader.each_file do |entry|
135
- puts "#{entry.name}: #{entry.size} bytes"
132
+ Omnizip::Formats::SevenZip.open('archive.7z') do |reader|
133
+ reader.list_files.each do |entry|
134
+ puts "#{entry.name}: #{entry.size} bytes"
135
+ end
136
136
  end
137
- reader.close
138
137
  ----
139
138
 
140
139
  === Error Handling
@@ -145,7 +144,7 @@ begin
145
144
  Omnizip.compress_file('input.txt', 'output.lzma')
146
145
  rescue Omnizip::CompressionError => e
147
146
  puts "Compression failed: #{e.message}"
148
- rescue Omnizip::AlgorithmNotFound => e
147
+ rescue Omnizip::AlgorithmNotFoundError => e
149
148
  puts "Algorithm not found: #{e.message}"
150
149
  rescue IOError => e
151
150
  puts "I/O error: #{e.message}"
@@ -188,11 +188,8 @@ omnizip archive create archive.zip files/ \
188
188
 
189
189
  [source,ruby]
190
190
  ----
191
- # Try RAR4 reader
191
+ # One reader covers both RAR4 and RAR5 (version auto-detected)
192
192
  reader = Omnizip::Formats::Rar::Reader.new('archive.rar')
193
-
194
- # Or RAR5 reader
195
- reader = Omnizip::Formats::Rar::Rar5::Reader.new('archive.rar')
196
193
  ----
197
194
 
198
195
  ### Getting Help
@@ -71,6 +71,8 @@ module Omnizip
71
71
  tar: -> { Omnizip::ArchiveHandlers::TarHandler },
72
72
  seven_zip: -> { Omnizip::ArchiveHandlers::SevenZipHandler },
73
73
  rar: -> { Omnizip::ArchiveHandlers::RarHandler },
74
+ cpio: -> { Omnizip::ArchiveHandlers::CpioHandler },
75
+ iso: -> { Omnizip::ArchiveHandlers::IsoHandler },
74
76
  }.freeze
75
77
  private_constant :LAZY_LOAD_TRIGGERS
76
78
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Omnizip
6
+ module ArchiveHandlers
7
+ # Read-only adapter for the CPIO format (initramfs images, RPM
8
+ # payloads). Extraction and listing are supported; CPIO writing
9
+ # exists at the format level but is not exposed through the
10
+ # convenience archive API.
11
+ class CpioHandler
12
+ def create(_path, **_options)
13
+ raise Omnizip::UnsupportedFormatError,
14
+ "cpio archives cannot be created through the convenience " \
15
+ "API; use Omnizip::Formats::Cpio.create directly"
16
+ end
17
+
18
+ def extract_to(path, output_dir, **_)
19
+ Omnizip::Formats::Cpio.extract(path, output_dir)
20
+ end
21
+
22
+ def list(path, details: false, **_)
23
+ entries = Omnizip::Formats::Cpio.list(path)
24
+ if details
25
+ entries.map do |e|
26
+ { name: e.name, size: e.data.bytesize,
27
+ directory: e.name.end_with?("/"), mtime: nil }
28
+ end
29
+ else
30
+ entries.map(&:name)
31
+ end
32
+ end
33
+
34
+ def read_entry(path, entry_name, **_)
35
+ entry = Omnizip::Formats::Cpio.list(path)
36
+ .find { |e| e.name == entry_name }
37
+ unless entry
38
+ raise Errno::ENOENT, "Entry not found: #{entry_name}"
39
+ end
40
+
41
+ entry.data
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ Omnizip::ArchiveHandler.register(:cpio, Omnizip::ArchiveHandlers::CpioHandler.new)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Omnizip
6
+ module ArchiveHandlers
7
+ # Read-only adapter for the ISO 9660 format (disk images).
8
+ # Extraction and listing are supported; ISO writing exists at the
9
+ # format level but is not exposed through the convenience archive
10
+ # API.
11
+ class IsoHandler
12
+ def create(_path, **_options)
13
+ raise Omnizip::UnsupportedFormatError,
14
+ "iso images cannot be created through the convenience " \
15
+ "API; use Omnizip::Formats::Iso.create directly"
16
+ end
17
+
18
+ def extract_to(path, output_dir, **_)
19
+ Omnizip::Formats::Iso.extract(path, output_dir)
20
+ end
21
+
22
+ def list(path, details: false, **_)
23
+ entries = Omnizip::Formats::Iso.list(path)
24
+ if details
25
+ entries.map do |e|
26
+ { name: e.full_path, size: e.size, directory: e.directory?,
27
+ mtime: e.recording_date }
28
+ end
29
+ else
30
+ entries.map(&:full_path)
31
+ end
32
+ end
33
+
34
+ def read_entry(path, entry_name, **_)
35
+ entry = Omnizip::Formats::Iso.list(path)
36
+ .find { |e| e.full_path == entry_name }
37
+ unless entry
38
+ raise Errno::ENOENT, "Entry not found: #{entry_name}"
39
+ end
40
+ raise Errno::EISDIR, "Entry is a directory: #{entry_name}" if entry.directory?
41
+
42
+ Dir.mktmpdir("omnizip-iso-entry") do |dir|
43
+ dest = File.join(dir, "entry")
44
+ Omnizip::Formats::Iso.open(path) do |iso|
45
+ iso.extract_entry(entry_name, dest)
46
+ end
47
+ File.binread(dest)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ Omnizip::ArchiveHandler.register(:iso, Omnizip::ArchiveHandlers::IsoHandler.new)
@@ -9,5 +9,7 @@ module Omnizip
9
9
  autoload :TarHandler, "omnizip/archive_handlers/tar_handler"
10
10
  autoload :SevenZipHandler, "omnizip/archive_handlers/seven_zip_handler"
11
11
  autoload :RarHandler, "omnizip/archive_handlers/rar_handler"
12
+ autoload :CpioHandler, "omnizip/archive_handlers/cpio_handler"
13
+ autoload :IsoHandler, "omnizip/archive_handlers/iso_handler"
12
14
  end
13
15
  end
data/lib/omnizip/cli.rb CHANGED
@@ -25,6 +25,28 @@ module Omnizip
25
25
  # under Omnizip::Cli would re-trigger this file's autoload
26
26
  # mid-load.
27
27
  module Shared
28
+ # Thor 1.x does not treat --help/-h on subcommands as a request
29
+ # for help: the flag reaches the command as a positional argument
30
+ # and arity checking fails. Classes extending this module route
31
+ # those flags to the standard help output instead.
32
+ module HelpDispatch
33
+ def dispatch(meth, given_args, given_opts, config)
34
+ wants_help = [Array(given_args), Array(given_opts)].any? do |list|
35
+ list.include?("--help") || list.include?("-h")
36
+ end
37
+ if wants_help
38
+ name = meth || Array(given_args).grep_v(/\A-/).first
39
+ # Subcommand groups intercept their own --help (e.g.
40
+ # "archive create --help" must show the create usage).
41
+ return super if subcommands.include?(name)
42
+
43
+ return start(["help", name].compact, config)
44
+ end
45
+
46
+ super
47
+ end
48
+ end
49
+
28
50
  # Print a formatted error message and exit nonzero.
29
51
  #
30
52
  # @param error [StandardError] the error to display
@@ -51,6 +73,7 @@ module Omnizip
51
73
  # Profile commands subcommand group
52
74
  class ProfileCommands < Thor
53
75
  include Shared
76
+ extend Shared::HelpDispatch
54
77
 
55
78
  class << self
56
79
  def exit_on_failure?
@@ -99,6 +122,7 @@ module Omnizip
99
122
  # Archive commands subcommand group
100
123
  class ArchiveCommands < Thor
101
124
  include Shared
125
+ extend Shared::HelpDispatch
102
126
 
103
127
  class << self
104
128
  def exit_on_failure?
@@ -328,6 +352,7 @@ module Omnizip
328
352
  # files using various compression algorithms.
329
353
  class Cli < Thor
330
354
  include Shared
355
+ extend Shared::HelpDispatch
331
356
 
332
357
  class << self
333
358
  def exit_on_failure?
@@ -230,16 +230,18 @@ module Omnizip
230
230
  ".7z" => :seven_zip,
231
231
  }.freeze
232
232
 
233
- # Extensions naming real formats this gem can READ but not write
234
- # through the convenience API. Writing them a ZIP under a foreign
235
- # name was silent corruption; failing truthfully is the only
236
- # honest behavior.
233
+ # Extensions naming formats with read-only convenience routing.
234
+ # Writing them a ZIP under a foreign name was silent corruption;
235
+ # creation raises truthfully instead (the format-level writers
236
+ # exist for RAR/CPIO/ISO but are not part of the archive API).
237
237
  READ_ONLY_FORMAT_EXTENSIONS = [".rar", ".iso", ".cpio"].freeze
238
238
 
239
239
  # Extensions whose format has a READ-ONLY handler: extraction and
240
240
  # listing route to it, while creation keeps raising.
241
241
  READ_ARCHIVE_FORMAT_EXTENSIONS = {
242
242
  ".rar" => :rar,
243
+ ".cpio" => :cpio,
244
+ ".iso" => :iso,
243
245
  }.freeze
244
246
 
245
247
  # Extension -> single-file decompressor (stream interface).
@@ -43,6 +43,12 @@ module Omnizip
43
43
  # Allocate sectors for directories and files
44
44
  allocate_sectors(tree)
45
45
 
46
+ # Tree nodes are separate hashes from @files entries; copy
47
+ # the allocated file locations onto the tree so directory
48
+ # records reference the real extents (otherwise every
49
+ # record points at sector 0).
50
+ assign_file_locations(tree)
51
+
46
52
  # Build path table
47
53
  path_table = build_path_table(tree)
48
54
 
@@ -143,6 +149,24 @@ module Omnizip
143
149
  allocate_file_sectors
144
150
  end
145
151
 
152
+ # Copy allocated file locations onto the matching tree nodes
153
+ #
154
+ # @param tree [Hash] Directory tree root
155
+ def assign_file_locations(tree)
156
+ walk = lambda do |node|
157
+ (node[:children] || []).each do |child|
158
+ if child[:directory]
159
+ walk.call(child)
160
+ else
161
+ match = @files.find { |f| f[:iso_path] == child[:iso_path] }
162
+ child[:location] = match[:location] if match
163
+ child[:size] = match[:size] if match
164
+ end
165
+ end
166
+ end
167
+ walk.call(tree)
168
+ end
169
+
146
170
  # Allocate sectors for a directory
147
171
  #
148
172
  # @param dir_node [Hash] Directory node
@@ -90,8 +90,8 @@ module Omnizip
90
90
  @entries.each do |entry|
91
91
  next if entry.current_directory? || entry.parent_directory?
92
92
 
93
- output_path = File.join(output_dir, entry.name)
94
- extract_entry(entry.name, output_path)
93
+ output_path = File.join(output_dir, entry.full_path)
94
+ extract_entry(entry.full_path, output_path)
95
95
  end
96
96
  end
97
97
 
@@ -178,8 +178,13 @@ module Omnizip
178
178
  preparer: "OMNIZIP #{Omnizip::VERSION}",
179
179
  application: "OMNIZIP",
180
180
  level: 2,
181
- rock_ridge: true,
182
- joliet: true,
181
+ # Rock Ridge System Use fields and Joliet UCS-2 directory
182
+ # trees are not implemented; advertising either extension
183
+ # makes readers misparse the image (e.g. 7-Zip reads the
184
+ # ASCII names as UCS-2 via the cloned SVD), so both stay
185
+ # off until real implementations land.
186
+ rock_ridge: false,
187
+ joliet: false,
183
188
  }
184
189
  end
185
190
 
@@ -193,6 +198,7 @@ module Omnizip
193
198
  source: File.expand_path(dir_path),
194
199
  iso_path: iso_path,
195
200
  stat: File.stat(dir_path),
201
+ directory: true,
196
202
  }
197
203
 
198
204
  # Add all contents
@@ -219,6 +225,7 @@ module Omnizip
219
225
  source: File.expand_path(dir_path),
220
226
  iso_path: iso_path,
221
227
  stat: File.stat(dir_path),
228
+ directory: true,
222
229
  }
223
230
 
224
231
  Dir.foreach(dir_path) do |entry|
@@ -283,13 +290,22 @@ module Omnizip
283
290
  # @param builder [VolumeBuilder] Volume builder
284
291
  # @param dir_structure [Hash] Directory structure
285
292
  def write_volume_descriptors(io, builder, dir_structure)
293
+ # Fixed layout: 16 PVD, 17 terminator, 18 path table (L),
294
+ # 19 path table (BE), directory data from sector 22.
295
+ root = dir_structure[:root].merge(
296
+ total_sectors: dir_structure[:total_sectors],
297
+ path_table_size: dir_structure[:path_table_size],
298
+ path_table_location: 18,
299
+ path_table_location_be: 19,
300
+ )
301
+
286
302
  # Write primary volume descriptor
287
- pvd = builder.build_primary(dir_structure[:root])
303
+ pvd = builder.build_primary(root)
288
304
  io.write(pvd)
289
305
 
290
306
  # Write Joliet supplementary descriptor if enabled
291
307
  if @joliet
292
- svd = builder.build_joliet(dir_structure[:root])
308
+ svd = builder.build_joliet(root)
293
309
  io.write(svd)
294
310
  end
295
311
 
@@ -69,7 +69,9 @@ module Omnizip
69
69
  # @param path [String] Path to ISO file
70
70
  # @return [Array<DirectoryRecord>] Directory entries
71
71
  def self.list(path)
72
- open(path, &:entries)
72
+ entries = nil
73
+ open(path) { |iso| entries = iso.entries }
74
+ entries
73
75
  end
74
76
 
75
77
  # Extract ISO contents
@@ -76,6 +76,9 @@ module Omnizip
76
76
  # Read file name
77
77
  name_bytes = io.read(name_size)
78
78
  entry.name = decode_filename(name_bytes, head_flags)
79
+ # RAR4 archives store DOS-style backslash separators;
80
+ # normalize to forward slashes like unrar on Unix.
81
+ entry.name = entry.name.tr("\\", "/")
79
82
 
80
83
  # Set entry properties
81
84
  entry.size = unpack_size
@@ -88,18 +91,25 @@ module Omnizip
88
91
  entry.attributes = attr
89
92
  entry.mtime = dos_time_to_time(file_time)
90
93
 
91
- # Set flags
92
- entry.is_dir = head_flags.anybits?(FILE_DIRECTORY)
94
+ # Set flags. RAR4 directory entries carry the full
95
+ # dictionary mask 0xE0 in HEAD_FLAGS (a directory has no
96
+ # dictionary); the exact match avoids misreading a large
97
+ # dictionary size on files as the marker.
98
+ entry.is_dir = head_flags.allbits?(FILE_DIRECTORY)
99
+ # WinRAR writes directory names with a trailing backslash;
100
+ # normalize it away for host paths.
101
+ entry.name = entry.name.chomp("\\") if entry.is_dir
93
102
  entry.encrypted = head_flags.anybits?(FILE_ENCRYPTED)
94
103
  entry.split_before = head_flags.anybits?(FILE_SPLIT_BEFORE)
95
104
  entry.split_after = head_flags.anybits?(FILE_SPLIT_AFTER)
96
105
 
97
- # Skip remaining header data and file data
98
- # Fixed fields: TYPE(1) + FLAGS(2) + SIZE(2) + PACK_SIZE(4) + UNPACK_SIZE(4) +
99
- # HOST_OS(1) + FILE_CRC(4) + FILE_TIME(4) + VERSION(1) + METHOD(1) +
100
- # NAME_SIZE(2) + ATTR(4) = 30 bytes
101
- remaining = head_size - (name_size + 30)
102
- remaining += 8 if head_flags.anybits?(FILE_LARGE)
106
+ # Skip remaining header data and file data. HEAD_SIZE
107
+ # counts the whole block including the 2-byte HEAD_CRC;
108
+ # consumed so far: 2 (CRC) + 30 (fixed fields) + name, plus
109
+ # 8 more when high-size words were present. Leftover bytes
110
+ # cover salt/extra time fields without interpreting them.
111
+ remaining = head_size - (name_size + 32)
112
+ remaining -= 8 if head_flags.anybits?(FILE_LARGE)
103
113
  io.read(remaining) if remaining.positive?
104
114
  io.read(pack_size) # Skip compressed data
105
115
 
@@ -21,6 +21,10 @@ module Omnizip
21
21
  BLOCK_SUBBLOCK = 0x7A
22
22
  BLOCK_ENDARC = 0x7B
23
23
 
24
+ # HEAD_FLAGS bit 0x8000 (LONG_BLOCK): the block carries a
25
+ # data area whose size is the PACK_SIZE field at offset 7
26
+ BLOCK_LONG = 0x8000
27
+
24
28
  # Archive flags
25
29
  ARCHIVE_VOLUME = 0x0001
26
30
  ARCHIVE_COMMENT = 0x0002
@@ -152,7 +152,7 @@ module Omnizip
152
152
  "WinRAR", "UnRAR.exe"
153
153
  ),
154
154
  ].each do |path|
155
- return "\"#{path}\"" if File.exist?(path)
155
+ return path if File.exist?(path)
156
156
  end
157
157
 
158
158
  nil
@@ -176,7 +176,7 @@ module Omnizip
176
176
  def command_version
177
177
  return nil unless command_available?
178
178
 
179
- output = `#{command_path} 2>&1`
179
+ output = `"#{command_path}" 2>&1`
180
180
  output.match(/UNRAR\s+([\d.]+)/i)&.captures&.first || "unknown"
181
181
  end
182
182
 
@@ -191,7 +191,7 @@ module Omnizip
191
191
  # Extract with system command
192
192
  def extract_with_command(archive_path, output_dir, password)
193
193
  cmd = build_extract_command(archive_path, output_dir, password)
194
- return if system(cmd)
194
+ return if system(*cmd)
195
195
 
196
196
  raise "Command extraction failed: #{archive_path}"
197
197
  end
@@ -215,7 +215,7 @@ module Omnizip
215
215
 
216
216
  # List with system command
217
217
  def list_with_command(archive_path)
218
- output = `#{command_path} vb "#{archive_path}" 2>&1`
218
+ output = `"#{command_path}" vb "#{archive_path}" 2>&1`
219
219
  raise "Command listing failed" unless $CHILD_STATUS.success?
220
220
 
221
221
  output.split("\n").map do |line|
@@ -240,7 +240,7 @@ module Omnizip
240
240
  output_path, password)
241
241
  temp_dir = Dir.mktmpdir
242
242
  cmd = build_extract_command(archive_path, temp_dir, password)
243
- unless system(cmd)
243
+ unless system(*cmd)
244
244
  raise "Command entry extraction failed: #{entry_name}"
245
245
  end
246
246
 
@@ -252,9 +252,13 @@ module Omnizip
252
252
 
253
253
  # Build extract command
254
254
  def build_extract_command(archive_path, output_dir, password)
255
- cmd = "#{command_path} x -y"
256
- cmd += " -p#{password}" if password
257
- cmd += " \"#{archive_path}\" \"#{output_dir}/\""
255
+ # Array form: no shell involved, so paths and passwords
256
+ # with special characters stay literal. -idq silences the
257
+ # per-file progress banner.
258
+ cmd = [command_path, "x", "-idq", "-y"]
259
+ cmd << "-p#{password}" if password
260
+ cmd << archive_path
261
+ cmd << "#{output_dir}/"
258
262
  cmd
259
263
  end
260
264
 
@@ -84,20 +84,12 @@ module Omnizip
84
84
 
85
85
  # Parse RAR4 header
86
86
  #
87
+ # The 7-byte signature consumed by #parse IS the marker
88
+ # block, so the main header follows immediately.
89
+ #
87
90
  # @param io [IO] Input stream
88
91
  def parse_rar4_header(io)
89
- # Read marker block
90
- read_uint16(io)
91
- head_type = io.read(1)&.ord
92
- read_uint16(io)
93
- read_uint16(io)
94
-
95
- unless head_type == BLOCK_MARKER
96
- raise "Expected marker block, got 0x#{head_type.to_s(16)}"
97
- end
98
-
99
- # Read archive header
100
- read_uint16(io)
92
+ read_uint16(io) # HEAD_CRC
101
93
  head_type = io.read(1)&.ord
102
94
  head_flags = read_uint16(io)
103
95
  head_size = read_uint16(io)
@@ -112,9 +104,9 @@ module Omnizip
112
104
  @is_locked = head_flags.anybits?(ARCHIVE_LOCKED)
113
105
  @comment_present = head_flags.anybits?(ARCHIVE_COMMENT)
114
106
 
115
- # Skip rest of archive header
116
- # head_size includes TYPE(1) + FLAGS(2) + SIZE(2) = 5 bytes already read
117
- remaining = head_size - 5
107
+ # Skip rest of archive header. HEAD_SIZE counts the whole
108
+ # block including the 2 HEAD_CRC bytes; 7 bytes consumed.
109
+ remaining = head_size - 7
118
110
  io.read(remaining) if remaining.positive?
119
111
  end
120
112
 
@@ -42,9 +42,11 @@ module Omnizip
42
42
  #
43
43
  # @return [Boolean] true if implemented
44
44
  def available?
45
- # Full LZSS decoder is now implemented
46
- # Encoder is not yet compatible with official RAR tools
47
- true
45
+ # Decoder is implemented; the encoder is NOT compatible
46
+ # with official RAR tools (unrar "tests" its output as
47
+ # OK while extracting empty files). Callers fall back
48
+ # to STORE until a RAR-compatible encoder lands.
49
+ false
48
50
  end
49
51
 
50
52
  # Compress data using RAR5 LZSS