omnizip 0.3.35 → 0.3.37

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  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/compression-algorithms/bzip2.adoc +2 -1
  10. data/docs/guides/compression-algorithms/deflate.adoc +1 -7
  11. data/docs/guides/compression-algorithms/lzma.adoc +10 -14
  12. data/docs/guides/compression-algorithms/lzma2.adoc +34 -117
  13. data/docs/guides/compression-algorithms/ppmd.adoc +24 -27
  14. data/docs/guides/compression-algorithms/zstandard.adoc +85 -150
  15. data/docs/troubleshooting/index.adoc +7 -5
  16. data/lib/omnizip/algorithms/zstandard.rb +2 -3
  17. data/lib/omnizip/archive/builder.rb +48 -0
  18. data/lib/omnizip/archive/reader_session.rb +67 -0
  19. data/lib/omnizip/archive.rb +86 -0
  20. data/lib/omnizip/archive_handler.rb +1 -0
  21. data/lib/omnizip/archive_handlers/rar_handler.rb +50 -0
  22. data/lib/omnizip/archive_handlers/seven_zip_handler.rb +6 -2
  23. data/lib/omnizip/archive_handlers/tar_handler.rb +4 -0
  24. data/lib/omnizip/archive_handlers/zip_handler.rb +25 -2
  25. data/lib/omnizip/archive_handlers.rb +1 -0
  26. data/lib/omnizip/convenience.rb +91 -11
  27. data/lib/omnizip/formats/rar/block_parser.rb +8 -12
  28. data/lib/omnizip/formats/rar/constants.rb +3 -1
  29. data/lib/omnizip/formats/rar/header.rb +16 -6
  30. data/lib/omnizip/formats/rar/reader.rb +6 -2
  31. data/lib/omnizip/version.rb +1 -1
  32. data/lib/omnizip.rb +1 -0
  33. metadata +6 -2
@@ -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,6 @@ 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"
11
12
  end
12
13
  end
@@ -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.
@@ -131,13 +179,14 @@ module Omnizip
131
179
  raise Errno::ENOENT, "Source file not found: #{source_path}"
132
180
  end
133
181
 
134
- handler = Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
182
+ resolved = resolve_archive_format(archive_path, format)
183
+ handler = Omnizip::ArchiveHandler.for(resolved)
135
184
  # allowed: handler is a registered duck; missing add_entry raises below
136
185
  if handler.respond_to?(:add_entry)
137
186
  handler.add_entry(archive_path, entry_name, source_path)
138
187
  else
139
188
  raise Omnizip::UnsupportedFormatError,
140
- "Format #{format.inspect} does not support adding entries"
189
+ "Format #{resolved.inspect} does not support adding entries"
141
190
  end
142
191
 
143
192
  archive_path
@@ -151,11 +200,12 @@ module Omnizip
151
200
  # @return [String] +archive_path+
152
201
  def remove_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
153
202
  require_archive!(archive_path)
154
- handler = Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
203
+ resolved = resolve_archive_format(archive_path, format)
204
+ handler = Omnizip::ArchiveHandler.for(resolved)
155
205
  # allowed: handler is a registered duck; missing remove_entry raises below
156
206
  unless handler.respond_to?(:remove_entry)
157
207
  raise Omnizip::UnsupportedFormatError,
158
- "Format #{format.inspect} does not support removing entries"
208
+ "Format #{resolved.inspect} does not support removing entries"
159
209
  end
160
210
 
161
211
  handler.remove_entry(archive_path, entry_name)
@@ -186,6 +236,26 @@ module Omnizip
186
236
  # honest behavior.
187
237
  READ_ONLY_FORMAT_EXTENSIONS = [".rar", ".iso", ".cpio"].freeze
188
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
+ }.freeze
244
+
245
+ # Extension -> single-file decompressor (stream interface).
246
+ # The Gzip/Bzip2File/Xz classes take path or stream; use the
247
+ # ones exposing decompress_stream for symmetry.
248
+ # Xz exposes a path-based decompress rather than a stream one,
249
+ # so it is marked specially here.
250
+ DECOMPRESSORS = {
251
+ ".gz" => Formats::Gzip,
252
+ ".bz2" => Formats::Bzip2File,
253
+ ".xz" => :xz,
254
+ ".lzma" => Formats::LzmaAlone,
255
+ ".lz" => Formats::Lzip,
256
+ ".zst" => :zstandard,
257
+ }.freeze
258
+
189
259
  SINGLE_FILE_COMPRESSORS = {
190
260
  ".gz" => lambda do |input, output, options|
191
261
  Omnizip::Formats::Gzip.compress(input, output, options)
@@ -212,6 +282,14 @@ module Omnizip
212
282
  end
213
283
  end
214
284
  end,
285
+ ".zst" => lambda do |input, output, options|
286
+ ::File.open(input, "rb") do |input_io|
287
+ ::File.open(output, "wb") do |output_io|
288
+ Omnizip::Algorithms::Zstandard.new.compress(input_io, output_io,
289
+ options)
290
+ end
291
+ end
292
+ end,
215
293
  }.freeze
216
294
 
217
295
  # The single-file compressor matching the output extension, or nil.
@@ -223,12 +301,14 @@ module Omnizip
223
301
  # Archive format for a path: an explicit +format+ wins unless it
224
302
  # is the default; otherwise the extension routes. Known-but-
225
303
  # unwritable extensions raise instead of receiving a mislabeled
226
- # ZIP.
227
- def resolve_archive_format(path, format)
304
+ # ZIP. +writing:+ false (read operations) additionally routes
305
+ # extensions with read-only handlers.
306
+ def resolve_archive_format(path, format, writing: true)
228
307
  return format unless format == DEFAULT_FORMAT
229
308
 
230
309
  ext = ::File.extname(path).downcase
231
310
  routed = ARCHIVE_FORMAT_EXTENSIONS[ext]
311
+ routed ||= READ_ARCHIVE_FORMAT_EXTENSIONS[ext] unless writing
232
312
  return routed if routed
233
313
 
234
314
  if READ_ONLY_FORMAT_EXTENSIONS.include?(ext)
@@ -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
 
@@ -142,8 +142,12 @@ module Omnizip
142
142
  # Detect recovery records
143
143
  detect_recovery_records
144
144
 
145
- # Parse entries using decompressor
146
- parse_entries_with_decompressor
145
+ # Parse entries natively first (full metadata: sizes,
146
+ # directory flags); fall back to the external decompressor
147
+ # when native parsing yields nothing (e.g. encrypted
148
+ # headers).
149
+ parse_entries_from_header
150
+ parse_entries_with_decompressor if @entries.empty?
147
151
  end
148
152
 
149
153
  # Detect recovery records in archive
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.35"
4
+ VERSION = "0.3.37"
5
5
  end
data/lib/omnizip.rb CHANGED
@@ -83,6 +83,7 @@ module Omnizip
83
83
  autoload :Implementations, "omnizip/implementations"
84
84
  autoload :ArchiveHandler, "omnizip/archive_handler"
85
85
  autoload :ArchiveHandlers, "omnizip/archive_handlers"
86
+ autoload :Archive, "omnizip/archive"
86
87
  autoload :Password, "omnizip/password"
87
88
  autoload :Convenience, "omnizip/convenience"
88
89
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.35
4
+ version: 0.3.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-27 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -319,8 +319,12 @@ files:
319
319
  - lib/omnizip/algorithms/zstandard/sequences.rb
320
320
  - lib/omnizip/algorithms/zstandard/sequences_encoder.rb
321
321
  - lib/omnizip/algorithms/zstandard/xxhash.rb
322
+ - lib/omnizip/archive.rb
323
+ - lib/omnizip/archive/builder.rb
324
+ - lib/omnizip/archive/reader_session.rb
322
325
  - lib/omnizip/archive_handler.rb
323
326
  - lib/omnizip/archive_handlers.rb
327
+ - lib/omnizip/archive_handlers/rar_handler.rb
324
328
  - lib/omnizip/archive_handlers/seven_zip_handler.rb
325
329
  - lib/omnizip/archive_handlers/tar_handler.rb
326
330
  - lib/omnizip/archive_handlers/zip_handler.rb