omnizip 0.3.36 → 0.3.38

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -9
  3. data/docs/getting-started/basic-usage.adoc +14 -22
  4. data/docs/guides/advanced-features/encryption.adoc +16 -39
  5. data/docs/guides/advanced-features/index.adoc +16 -6
  6. data/docs/guides/advanced-features/parallel-processing.adoc +63 -165
  7. data/docs/guides/advanced-features/progress-tracking.adoc +65 -261
  8. data/docs/guides/advanced-features/streaming.adoc +43 -113
  9. data/docs/guides/archive-formats/gzip-format.adoc +2 -4
  10. data/docs/guides/archive-formats/ole-format.adoc +54 -219
  11. data/docs/guides/archive-formats/rar5.adoc +15 -15
  12. data/docs/guides/archive-formats/rpm-format.adoc +48 -189
  13. data/docs/guides/compression-algorithms/bzip2.adoc +2 -1
  14. data/docs/guides/compression-algorithms/deflate.adoc +1 -7
  15. data/docs/guides/compression-algorithms/lzma.adoc +10 -14
  16. data/docs/guides/compression-algorithms/lzma2.adoc +34 -117
  17. data/docs/guides/compression-algorithms/ppmd.adoc +24 -27
  18. data/docs/guides/compression-algorithms/zstandard.adoc +85 -150
  19. data/docs/reference/api/overview.adoc +16 -17
  20. data/docs/troubleshooting/index.adoc +8 -9
  21. data/lib/omnizip/algorithms/zstandard.rb +2 -3
  22. data/lib/omnizip/archive/builder.rb +48 -0
  23. data/lib/omnizip/archive/reader_session.rb +67 -0
  24. data/lib/omnizip/archive.rb +86 -0
  25. data/lib/omnizip/archive_handler.rb +3 -0
  26. data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
  27. data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
  28. data/lib/omnizip/archive_handlers/rar_handler.rb +50 -0
  29. data/lib/omnizip/archive_handlers/seven_zip_handler.rb +6 -2
  30. data/lib/omnizip/archive_handlers/tar_handler.rb +4 -0
  31. data/lib/omnizip/archive_handlers/zip_handler.rb +25 -2
  32. data/lib/omnizip/archive_handlers.rb +3 -0
  33. data/lib/omnizip/cli.rb +25 -0
  34. data/lib/omnizip/convenience.rb +91 -11
  35. data/lib/omnizip/formats/iso/directory_builder.rb +24 -0
  36. data/lib/omnizip/formats/iso/reader.rb +2 -2
  37. data/lib/omnizip/formats/iso/writer.rb +20 -4
  38. data/lib/omnizip/formats/iso.rb +3 -1
  39. data/lib/omnizip/formats/rar/block_parser.rb +8 -12
  40. data/lib/omnizip/formats/rar/constants.rb +3 -1
  41. data/lib/omnizip/formats/rar/header.rb +16 -6
  42. data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +5 -3
  43. data/lib/omnizip/formats/rar/rar5/header.rb +50 -33
  44. data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +17 -34
  45. data/lib/omnizip/formats/rar/rar5/vint.rb +25 -37
  46. data/lib/omnizip/formats/rar/rar5/writer.rb +50 -14
  47. data/lib/omnizip/formats/rar/reader.rb +6 -2
  48. data/lib/omnizip/formats/rar.rb +5 -3
  49. data/lib/omnizip/formats/seven_zip/parser.rb +22 -7
  50. data/lib/omnizip/formats/seven_zip/writer.rb +78 -19
  51. data/lib/omnizip/version.rb +1 -1
  52. data/lib/omnizip.rb +1 -0
  53. metadata +7 -1
@@ -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)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Omnizip
6
+ module ArchiveHandlers
7
+ # Read-only adapter for the RAR format. RAR compression is
8
+ # patented software (see link:README[RAR write support]); this
9
+ # handler exposes extraction/listing/reading only, so the
10
+ # convenience API and the Archive facade can operate on existing
11
+ # archives truthfully instead of raising on every operation.
12
+ class RarHandler
13
+ def create(_path, **_options)
14
+ raise Omnizip::UnsupportedFormatError,
15
+ "RAR archives cannot be created (patented format); " \
16
+ "Omnizip reads RAR archives only"
17
+ end
18
+
19
+ def extract_to(path, output_dir, password: nil, **_)
20
+ Omnizip::Formats::Rar::Decompressor.extract(path, output_dir,
21
+ password: password)
22
+ end
23
+
24
+ def list(path, details: false, **_)
25
+ reader = Omnizip::Formats::Rar::Reader.new(path).open
26
+ entries = reader.list_files
27
+ if details
28
+ entries.map do |e|
29
+ { name: e.name, size: e.size, directory: e.is_dir,
30
+ mtime: e.mtime }
31
+ end
32
+ else
33
+ entries.map(&:name)
34
+ end
35
+ end
36
+
37
+ def read_entry(path, entry_name, password: nil, **_)
38
+ Dir.mktmpdir("omnizip-rar-entry") do |dir|
39
+ dest = File.join(dir, "entry")
40
+ Omnizip::Formats::Rar::Decompressor.extract_entry(
41
+ path, entry_name, dest, password: password
42
+ )
43
+ File.binread(dest)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ Omnizip::ArchiveHandler.register(:rar, Omnizip::ArchiveHandlers::RarHandler.new)
@@ -8,8 +8,8 @@ module Omnizip
8
8
  # Creates a .7z archive. The yielded proxy only supports
9
9
  # +add(name, source_path)+ for files — directory entries are not
10
10
  # part of the single-file convenience flow.
11
- def create(path, &block)
12
- Omnizip::Formats::SevenZip.create(path) do |writer|
11
+ def create(path, **options, &block)
12
+ Omnizip::Formats::SevenZip.create(path, options) do |writer|
13
13
  block&.call(FileProxy.new(writer))
14
14
  end
15
15
  end
@@ -73,6 +73,10 @@ module Omnizip
73
73
 
74
74
  @writer.add_file(source_path, name)
75
75
  end
76
+
77
+ def add_data(name, data)
78
+ @writer.add_data(name, data)
79
+ end
76
80
  end
77
81
  end
78
82
  end
@@ -50,6 +50,10 @@ module Omnizip
50
50
 
51
51
  @writer.add(name, source_path)
52
52
  end
53
+
54
+ def add_data(name, data)
55
+ @writer.add_data(name, data)
56
+ end
53
57
  end
54
58
 
55
59
  def read_entry(path, entry_name)
@@ -5,8 +5,13 @@ module Omnizip
5
5
  # Adapter that exposes the canonical +create+/+open+/+extract_to+
6
6
  # /+list+ interface for the ZIP format. Wraps +Omnizip::Zip::File+.
7
7
  class ZipHandler
8
- def create(path, &block)
9
- Omnizip::Zip::File.create(path, &block)
8
+ # Zip::File has no archive-level compression options (method and
9
+ # level are per-entry in Zip::OutputStream), so extra keywords —
10
+ # e.g. profile-injected ones — are absorbed here.
11
+ def create(path, **_options, &block)
12
+ Omnizip::Zip::File.create(path) do |file|
13
+ block&.call(FileProxy.new(file))
14
+ end
10
15
  end
11
16
 
12
17
  def open(path, &block)
@@ -66,6 +71,24 @@ module Omnizip
66
71
  def remove_entry(path, entry_name)
67
72
  Omnizip::Zip::File.open(path) { |zip| zip.remove(entry_name) }
68
73
  end
74
+
75
+ # Translates the generic +add(name, source_path)+ and
76
+ # +add_data(name, data)+ interface. +Zip::File#add_data+ is
77
+ # private, so in-memory content goes through the public block
78
+ # form +add(name) { data }+.
79
+ class FileProxy
80
+ def initialize(file)
81
+ @file = file
82
+ end
83
+
84
+ def add(name, source_path = nil)
85
+ @file.add(name, source_path)
86
+ end
87
+
88
+ def add_data(name, data)
89
+ @file.add(name) { data }
90
+ end
91
+ end
69
92
  end
70
93
  end
71
94
  end
@@ -8,5 +8,8 @@ module Omnizip
8
8
  autoload :ZipHandler, "omnizip/archive_handlers/zip_handler"
9
9
  autoload :TarHandler, "omnizip/archive_handlers/tar_handler"
10
10
  autoload :SevenZipHandler, "omnizip/archive_handlers/seven_zip_handler"
11
+ autoload :RarHandler, "omnizip/archive_handlers/rar_handler"
12
+ autoload :CpioHandler, "omnizip/archive_handlers/cpio_handler"
13
+ autoload :IsoHandler, "omnizip/archive_handlers/iso_handler"
11
14
  end
12
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?
@@ -40,13 +40,50 @@ module Omnizip
40
40
  return output_path
41
41
  end
42
42
 
43
- Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path) do |archive|
43
+ Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
44
44
  archive.add(::File.basename(input_path), input_path)
45
45
  end
46
46
 
47
47
  output_path
48
48
  end
49
49
 
50
+ # Decompress a single-file compressed stream (the counterpart of
51
+ # #compress_file). The format is resolved from the input
52
+ # extension: .gz, .bz2, .xz, .lzma, .lz.
53
+ #
54
+ # @param compressed_path [String] Path to the compressed file
55
+ # @param output_path [String] Path to write the decompressed data
56
+ # @return [String] +output_path+
57
+ def decompress_file(compressed_path, output_path)
58
+ require_archive!(compressed_path)
59
+
60
+ format_class = DECOMPRESSORS[::File.extname(compressed_path).downcase]
61
+ unless format_class
62
+ raise Omnizip::UnsupportedFormatError,
63
+ "decompress_file supports #{DECOMPRESSORS.keys.join(', ')}; " \
64
+ "use extract_archive for archive formats"
65
+ end
66
+
67
+ case format_class
68
+ when :xz
69
+ ::File.binwrite(output_path, Formats::Xz.decompress(compressed_path))
70
+ when :zstandard
71
+ zstd = Algorithms::Zstandard.new
72
+ ::File.open(compressed_path, "rb") do |input_io|
73
+ ::File.open(output_path, "wb") do |output_io|
74
+ zstd.decompress(input_io, output_io)
75
+ end
76
+ end
77
+ else
78
+ ::File.open(compressed_path, "rb") do |input_io|
79
+ ::File.open(output_path, "wb") do |output_io|
80
+ format_class.decompress_stream(input_io, output_io)
81
+ end
82
+ end
83
+ end
84
+ output_path
85
+ end
86
+
50
87
  # Compress a directory into an archive.
51
88
  #
52
89
  # @param input_dir [String] Path to input directory
@@ -64,12 +101,23 @@ module Omnizip
64
101
  raise ArgumentError, "Input is not a directory: #{input_dir}"
65
102
  end
66
103
 
104
+ # A directory cannot be written through a single-file stream
105
+ # format; routing it to the default would silently produce a
106
+ # ZIP under a foreign extension.
107
+ if format == DEFAULT_FORMAT && single_file_handler(output_path)
108
+ raise Omnizip::UnsupportedFormatError,
109
+ "#{::File.extname(output_path).downcase} is a single-file " \
110
+ "stream format and cannot contain a directory; use an " \
111
+ "archive format (.zip/.tar/.7z) or compress entries " \
112
+ "individually"
113
+ end
114
+
67
115
  if options[:profile]
68
116
  first_file = find_first_file(input_dir)
69
117
  apply_profile(first_file, options)
70
118
  end
71
119
 
72
- Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path) do |archive|
120
+ Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
73
121
  add_directory_contents(archive, input_dir, "", recursive: recursive)
74
122
  end
75
123
 
@@ -87,7 +135,7 @@ module Omnizip
87
135
  def extract_archive(archive_path, output_dir, format: DEFAULT_FORMAT,
88
136
  overwrite: false, **options)
89
137
  require_archive!(archive_path)
90
- Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
138
+ Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
91
139
  .extract_to(archive_path, output_dir,
92
140
  overwrite: overwrite, **options)
93
141
  end
@@ -102,7 +150,7 @@ module Omnizip
102
150
  def list_archive(archive_path, format: DEFAULT_FORMAT, details: false,
103
151
  **options)
104
152
  require_archive!(archive_path)
105
- Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
153
+ Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
106
154
  .list(archive_path, details: details, **options)
107
155
  end
108
156
 
@@ -114,7 +162,7 @@ module Omnizip
114
162
  # @return [String] Entry contents
115
163
  def read_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
116
164
  require_archive!(archive_path)
117
- Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format)).read_entry(archive_path, entry_name)
165
+ Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false)).read_entry(archive_path, entry_name)
118
166
  end
119
167
 
120
168
  # Add a file to an existing archive.
@@ -182,12 +230,34 @@ module Omnizip
182
230
  ".7z" => :seven_zip,
183
231
  }.freeze
184
232
 
185
- # Extensions naming real formats this gem can READ but not write
186
- # through the convenience API. Writing them a ZIP under a foreign
187
- # name was silent corruption; failing truthfully is the only
188
- # 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).
189
237
  READ_ONLY_FORMAT_EXTENSIONS = [".rar", ".iso", ".cpio"].freeze
190
238
 
239
+ # Extensions whose format has a READ-ONLY handler: extraction and
240
+ # listing route to it, while creation keeps raising.
241
+ READ_ARCHIVE_FORMAT_EXTENSIONS = {
242
+ ".rar" => :rar,
243
+ ".cpio" => :cpio,
244
+ ".iso" => :iso,
245
+ }.freeze
246
+
247
+ # Extension -> single-file decompressor (stream interface).
248
+ # The Gzip/Bzip2File/Xz classes take path or stream; use the
249
+ # ones exposing decompress_stream for symmetry.
250
+ # Xz exposes a path-based decompress rather than a stream one,
251
+ # so it is marked specially here.
252
+ DECOMPRESSORS = {
253
+ ".gz" => Formats::Gzip,
254
+ ".bz2" => Formats::Bzip2File,
255
+ ".xz" => :xz,
256
+ ".lzma" => Formats::LzmaAlone,
257
+ ".lz" => Formats::Lzip,
258
+ ".zst" => :zstandard,
259
+ }.freeze
260
+
191
261
  SINGLE_FILE_COMPRESSORS = {
192
262
  ".gz" => lambda do |input, output, options|
193
263
  Omnizip::Formats::Gzip.compress(input, output, options)
@@ -214,6 +284,14 @@ module Omnizip
214
284
  end
215
285
  end
216
286
  end,
287
+ ".zst" => lambda do |input, output, options|
288
+ ::File.open(input, "rb") do |input_io|
289
+ ::File.open(output, "wb") do |output_io|
290
+ Omnizip::Algorithms::Zstandard.new.compress(input_io, output_io,
291
+ options)
292
+ end
293
+ end
294
+ end,
217
295
  }.freeze
218
296
 
219
297
  # The single-file compressor matching the output extension, or nil.
@@ -225,12 +303,14 @@ module Omnizip
225
303
  # Archive format for a path: an explicit +format+ wins unless it
226
304
  # is the default; otherwise the extension routes. Known-but-
227
305
  # unwritable extensions raise instead of receiving a mislabeled
228
- # ZIP.
229
- def resolve_archive_format(path, format)
306
+ # ZIP. +writing:+ false (read operations) additionally routes
307
+ # extensions with read-only handlers.
308
+ def resolve_archive_format(path, format, writing: true)
230
309
  return format unless format == DEFAULT_FORMAT
231
310
 
232
311
  ext = ::File.extname(path).downcase
233
312
  routed = ARCHIVE_FORMAT_EXTENSIONS[ext]
313
+ routed ||= READ_ARCHIVE_FORMAT_EXTENSIONS[ext] unless writing
234
314
  return routed if routed
235
315
 
236
316
  if READ_ONLY_FORMAT_EXTENSIONS.include?(ext)
@@ -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
@@ -121,6 +121,10 @@ module Omnizip
121
121
 
122
122
  header_flags = read_vint(io)
123
123
 
124
+ # Optional ExtraAreaSize / DataSize precede the file fields
125
+ extra_size = header_flags.anybits?(RAR5_FLAG_EXTRA_AREA) ? read_vint(io) : 0
126
+ data_size = header_flags.anybits?(RAR5_FLAG_DATA_AREA) ? read_vint(io) : 0
127
+
124
128
  # Read file header
125
129
  file_flags = read_vint(io)
126
130
  unpack_size = read_vint(io)
@@ -145,7 +149,7 @@ module Omnizip
145
149
 
146
150
  # Set entry properties
147
151
  entry.size = unpack_size
148
- entry.compressed_size = 0 # Not directly available in header
152
+ entry.compressed_size = data_size
149
153
  entry.crc = crc
150
154
  entry.host_os = host_os
151
155
  entry.flags = file_flags
@@ -154,17 +158,9 @@ module Omnizip
154
158
  entry.is_dir = file_flags.anybits?(RAR5_FLAG_IS_DIR)
155
159
  entry.version = 5
156
160
 
157
- # Read extra area if present
158
- if header_flags.anybits?(RAR5_FLAG_EXTRA_AREA)
159
- extra_size = read_vint(io)
160
- io.read(extra_size) if extra_size.positive?
161
- end
162
-
163
- # Read data area if present
164
- if header_flags.anybits?(RAR5_FLAG_DATA_AREA)
165
- data_size = read_vint(io)
166
- io.read(data_size) if data_size.positive?
167
- end
161
+ # Skip extra area, then compressed data
162
+ io.read(extra_size) if extra_size.positive?
163
+ io.read(data_size) if data_size.positive?
168
164
 
169
165
  entry
170
166
  end
@@ -58,7 +58,9 @@ module Omnizip
58
58
  RAR5_FLAG_UNKNOWN_BLOCKS = 0x0004
59
59
  RAR5_FLAG_DATA_INHERITED = 0x0008
60
60
  RAR5_FLAG_CHILD_BLOCKS = 0x0010
61
- RAR5_FLAG_IS_DIR = 0x0020
61
+ # File-header-specific flags (FileFlags field): the directory
62
+ # bit is 0x0001 per the RAR5 spec.
63
+ RAR5_FLAG_IS_DIR = 0x0001
62
64
  RAR5_FLAG_MULTI_VOLUME = 0x0001
63
65
 
64
66
  # Compression methods
@@ -122,9 +122,11 @@ module Omnizip
122
122
  #
123
123
  # @param io [IO] Input stream
124
124
  def parse_rar5_header(io)
125
- # RAR5 uses variable-length integer encoding
125
+ # RAR5 uses variable-length integer encoding. Layout per the
126
+ # RAR 5.0 format: HeaderCRC32(4), Header size(vint), Header
127
+ # type(vint), Header flags(vint), then type-specific fields.
126
128
  read_uint32(io)
127
- read_vint(io)
129
+ read_vint(io) # header size (redundant: areas carry their own)
128
130
  header_type = read_vint(io)
129
131
  header_flags = read_vint(io)
130
132
 
@@ -133,12 +135,20 @@ module Omnizip
133
135
  end
134
136
 
135
137
  @flags = header_flags
136
- @is_multi_volume = header_flags.anybits?(RAR5_FLAG_MULTI_VOLUME)
137
138
 
138
- # Read extra area if present
139
- return unless header_flags.anybits?(RAR5_FLAG_EXTRA_AREA)
139
+ extra_size = 0
140
+ if header_flags.anybits?(RAR5_FLAG_EXTRA_AREA)
141
+ extra_size = read_vint(io)
142
+ end
143
+
144
+ # Archive flags: 0x0001 volume, 0x0002 volume number
145
+ # present, 0x0004 solid, 0x0008 recovery, 0x0010 locked
146
+ archive_flags = read_vint(io)
147
+ @is_multi_volume = archive_flags.anybits?(0x0001)
148
+ @is_solid = archive_flags.anybits?(0x0004)
149
+ @is_locked = archive_flags.anybits?(0x0010)
150
+ read_vint(io) if archive_flags.anybits?(0x0002) # volume number
140
151
 
141
- extra_size = read_vint(io)
142
152
  io.read(extra_size) if extra_size.positive?
143
153
  end
144
154