omnizip 0.3.36 → 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 +59 -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 +85 -7
  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 +5 -1
@@ -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.
@@ -188,6 +236,26 @@ module Omnizip
188
236
  # honest behavior.
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
+ }.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
+
191
259
  SINGLE_FILE_COMPRESSORS = {
192
260
  ".gz" => lambda do |input, output, options|
193
261
  Omnizip::Formats::Gzip.compress(input, output, options)
@@ -214,6 +282,14 @@ module Omnizip
214
282
  end
215
283
  end
216
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,
217
293
  }.freeze
218
294
 
219
295
  # The single-file compressor matching the output extension, or nil.
@@ -225,12 +301,14 @@ module Omnizip
225
301
  # Archive format for a path: an explicit +format+ wins unless it
226
302
  # is the default; otherwise the extension routes. Known-but-
227
303
  # unwritable extensions raise instead of receiving a mislabeled
228
- # ZIP.
229
- 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)
230
307
  return format unless format == DEFAULT_FORMAT
231
308
 
232
309
  ext = ::File.extname(path).downcase
233
310
  routed = ARCHIVE_FORMAT_EXTENSIONS[ext]
311
+ routed ||= READ_ARCHIVE_FORMAT_EXTENSIONS[ext] unless writing
234
312
  return routed if routed
235
313
 
236
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.36"
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.36
4
+ version: 0.3.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -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