omnizip 0.3.39 → 0.3.40

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f86f08dba1d67ec9a22053ee355dda5d461c42d29b45134ad75a1192584d0a5
4
- data.tar.gz: 31d78c577804b43509bfea9a8bc9fd5edfad4ac149752cabb0eef87eafebbf56
3
+ metadata.gz: 5013ae71553867968422f57b01425820f17c5cd6e471ea0cb5b5d46d1cc6a1bd
4
+ data.tar.gz: 94698e90386f195cdd7f5fd7a8ebae3010f2f206e70eb96463ce8e4d2ece09b0
5
5
  SHA512:
6
- metadata.gz: f6ee4bb68ede247277420ec455130140a8be8e1f417581f1c3f996cab48d34202ae26631862ef88ef301fb712c649f72100805c6d553afce51f2705747e68e28
7
- data.tar.gz: b7f3567b7f8d3c7679bbd7563fd9fabf666aa3c1fbfdff38afd0e9e2dc9034e1f78428b710f2ff72518e45eef956eeb61a679c27df6b00e2a8e81cbbc0b5eed1
6
+ metadata.gz: 415d4cdcb48375d0ba0d1d57dc2592c18d47fcafb9817d956dbe5a3ec7e1fa1048063372759de1ff0c0ef194d06eaa7f5855cfead144681e1fb454ac4fc510b2
7
+ data.tar.gz: a901b525b49f60b94098d43a5c4517f5163e07b64ef08099c02f693fc50775461c584ee35f2a6227167bbd461cfcbd9e3985b2383c586610817363170f45a2ef
data/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.39] - 2026-08-28
11
+
12
+ ### Fixed
13
+ - RAR4 writing was structurally fictional: HEAD_CRC used CRC-16/CCITT
14
+ instead of the low 16 bits of CRC32, HEAD_SIZE values excluded the
15
+ 2-byte CRC field, a duplicate marker block followed the signature
16
+ (the 7-byte signature IS the marker), and file headers lacked the
17
+ LONG_BLOCK flag. unrar rejected every archive with "Main archive
18
+ header is corrupt". STORE archives now round-trip through unrar
19
+ byte-identically, including explicit directory entries (marked with
20
+ the full 0xE0 dictionary mask — proven empirically: attribute-based
21
+ and trailing-backslash markers do not work), empty files, and
22
+ nested trees. `password:` now raises `NotImplementedError` instead
23
+ of writing the encrypted flag with no salt; `solid:`/`recovery:`/
24
+ `volume_size:` warn and are never advertised in header flags.
25
+ - RAR4 reading never parsed real WinRAR archives (the reader demanded
26
+ the same fictional layout the writer emitted). It now matches
27
+ `unrar l` exactly on libarchive fixtures — names (backslashes
28
+ normalized), sizes, directories, DOS timestamps — and native
29
+ decompression is CRC-verified before use: previously a real
30
+ WinRAR-compressed entry silently extracted garbage (e.g. 44308
31
+ bytes written for a 20111-byte file); now a CRC mismatch falls
32
+ back to unrar for byte-exact extraction while Omnizip-written
33
+ archives still decode natively.
34
+ - RAR3 writer/reader: 6 reserved bytes in the main header (was 4),
35
+ LONG_BLOCK flag, high size words before the filename (was after),
36
+ terminator flags 0. STORE output is `unrar t`-clean. The reader
37
+ dropped a fictional extended-time parser that produced year-2039
38
+ timestamps; base DOS time (minute precision) is used.
39
+ - RAR5 reader: mtime is a 32-bit Unix timestamp (the parser read a
40
+ vint plus FILETIME that do not exist in the format) and is nil
41
+ when the header carries none, instead of a fabricated `Time.now`.
42
+ Timestamps now match unrar exactly on fixtures.
43
+ - `Formats::Rar3`/`Formats::Rar5` never loaded: the autoload
44
+ registry in `formats/rar.rb` pointed at files defining a different
45
+ namespace. Both get proper parent autoload files
46
+ (`formats/rar3.rb`, `formats/rar5.rb`).
47
+ - Windows: the unrar program path was pre-wrapped in literal quotes
48
+ for a shell-string call form; shell-free array-form invocations
49
+ failed to start the program. Quotes now live only at the two
50
+ shell call sites, and unrar runs quietly (`-idq`).
51
+
52
+ ### Added
53
+ - unrar-gated RAR4 interop spec (STORE test + byte-identical
54
+ extraction with directories) and real WinRAR fixture parsing specs
55
+ (listing + unrar-fallback extraction with CRC assertions).
56
+
57
+ ### Documentation
58
+ - README RAR4/RAR5 sections rewritten to verified reality: STORE is
59
+ unrar-verified while `:fastest`-`:best` are Omnizip-round-trip-only
60
+ (previously all methods claimed "fully working"); the real writer
61
+ API (no `close`, no block form, `compression:` key); RAR5 "LZMA SDK
62
+ byte-for-byte" claims removed (the option falls back to STORE with
63
+ a warning). Fixed fictional `SevenZip::Writer` `close`/`algorithm=`
64
+ examples in the API-usage and architecture guides.
65
+
10
66
  ## [0.3.38] - 2026-08-28
11
67
 
12
68
  ### Added
data/README.adoc CHANGED
@@ -633,6 +633,37 @@ Omnizip.extract_archive('archive.zip', 'output/')
633
633
  Omnizip.list_archive('archive.zip')
634
634
  ----
635
635
 
636
+ ==== In-Memory Archives
637
+
638
+ `Omnizip::Buffer` creates and reads ZIP and 7z archives entirely in
639
+ memory (7z bridges through a temporary file internally):
640
+
641
+ [source,ruby]
642
+ ----
643
+ require 'omnizip'
644
+
645
+ # Create in memory
646
+ buffer = Omnizip::Buffer.create(:zip) do |archive|
647
+ archive.add('readme.txt', 'Hello World')
648
+ archive.add('docs/', '') # directory entry
649
+ end
650
+
651
+ # Same API for 7z
652
+ buffer7z = Omnizip::Buffer.create(:seven_zip) do |archive|
653
+ archive.add('data.json', '{"key": "value"}')
654
+ end
655
+
656
+ # Read back (format auto-detected)
657
+ files = Omnizip::Buffer.extract_to_memory(buffer.string)
658
+ # => {"readme.txt" => "Hello World"}
659
+
660
+ Omnizip::Buffer.open(buffer.string) do |archive|
661
+ archive.each_entry do |entry|
662
+ puts "#{entry.name}: #{entry.size} bytes" unless entry.directory?
663
+ end
664
+ end
665
+ ----
666
+
636
667
  ==== Rubyzip Compatibility Mode
637
668
 
638
669
  [source,ruby]
@@ -38,6 +38,13 @@ module Omnizip
38
38
  "Algorithm"
39
39
  end
40
40
 
41
+ # All builtin algorithms register lazily, so the plain storage
42
+ # keys are empty until each algorithm is first used. Listing
43
+ # (e.g. the CLI's `omnizip list`) must show the builtins too.
44
+ def available
45
+ (super + BUILTIN_ALGORITHMS.keys).uniq
46
+ end
47
+
41
48
  def register(name, klass)
42
49
  raise ArgumentError, "Algorithm name cannot be nil" if name.nil?
43
50
  raise ArgumentError, "Algorithm class cannot be nil" if klass.nil?
@@ -28,14 +28,11 @@ module Omnizip
28
28
 
29
29
  # Algorithm metadata
30
30
  def self.metadata
31
- {
32
- name: "Deflate64",
33
- type: :compression,
34
- streaming_supported: true,
35
- dictionary_size: DICTIONARY_SIZE,
36
- compression_method: 9,
37
- description: "Enhanced Deflate with 64KB window",
38
- }
31
+ Models::AlgorithmMetadata.new.tap do |meta|
32
+ meta.name = "deflate64"
33
+ meta.description = "Enhanced Deflate with 64KB window"
34
+ meta.version = "1.0.0"
35
+ end
39
36
  end
40
37
 
41
38
  # Compress input stream to output stream
@@ -49,7 +49,7 @@ module Omnizip
49
49
  when :zip
50
50
  extract_all_zip(result)
51
51
  when :seven_zip, :"7z"
52
- raise NotImplementedError, "7z format support coming in Phase 2"
52
+ extract_all_seven_zip(result)
53
53
  else
54
54
  raise ArgumentError, "Unsupported format: #{@format}"
55
55
  end
@@ -76,7 +76,7 @@ module Omnizip
76
76
  when :zip
77
77
  content = extract_entry_zip(name)
78
78
  when :seven_zip, :"7z"
79
- raise NotImplementedError, "7z format support coming in Phase 2"
79
+ content = extract_entry_seven_zip(name)
80
80
  else
81
81
  raise ArgumentError, "Unsupported format: #{@format}"
82
82
  end
@@ -100,7 +100,7 @@ module Omnizip
100
100
  when :zip
101
101
  list_entries_zip(names)
102
102
  when :seven_zip, :"7z"
103
- raise NotImplementedError, "7z format support coming in Phase 2"
103
+ SevenZipBridge.open(@buffer) { |archive| names.concat(archive.entry_names) }
104
104
  else
105
105
  raise ArgumentError, "Unsupported format: #{@format}"
106
106
  end
@@ -156,14 +156,14 @@ module Omnizip
156
156
  # @return [Symbol] Detected format
157
157
  # @raise [Omnizip::FormatError] If format cannot be detected
158
158
  def detect_format
159
- magic = @buffer.read(4)
159
+ magic = @buffer.read(4).to_s.b
160
160
  @buffer.rewind
161
161
 
162
162
  case magic
163
- when "PK\x03\x04", "PK\x05\x06", "PK\x07\x08"
163
+ when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
164
164
  # ZIP signatures: local file header, EOCD, data descriptor
165
165
  :zip
166
- when "7z\xBC\xAF"
166
+ when "7z\xBC\xAF".b
167
167
  :seven_zip
168
168
  else
169
169
  raise Omnizip::FormatError,
@@ -187,6 +187,31 @@ module Omnizip
187
187
  end
188
188
  end
189
189
 
190
+ # Extract every file entry from 7z
191
+ #
192
+ # @param result [Hash] Hash to populate with entries
193
+ def extract_all_seven_zip(result)
194
+ @buffer.rewind
195
+ SevenZipBridge.open(@buffer) do |archive|
196
+ archive.extract_all_to_memory.each do |name, content|
197
+ result[name] = content
198
+ @extracted_cache[name] = content
199
+ end
200
+ end
201
+ end
202
+
203
+ # Extract single entry from 7z
204
+ #
205
+ # @param name [String] Entry name
206
+ # @return [String, nil] Entry content or nil if not found
207
+ def extract_entry_seven_zip(name)
208
+ @buffer.rewind
209
+ SevenZipBridge.open(@buffer) do |archive|
210
+ entry = archive.raw_entries.find { |e| e.name == name }
211
+ archive.extract_all_to_memory[name] if entry
212
+ end
213
+ end
214
+
190
215
  # Extract single entry from ZIP
191
216
  #
192
217
  # @param name [String] Entry name
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "tmpdir"
5
+
6
+ module Omnizip
7
+ module Buffer
8
+ # Bridges the in-memory Buffer API onto the file-based 7z
9
+ # reader/writer through a temporary file, since the 7z format has
10
+ # no in-memory stream implementation.
11
+ module SevenZipBridge
12
+ class << self
13
+ # Create a 7z archive in memory
14
+ #
15
+ # @param buffer [StringIO] Destination buffer
16
+ # @param options [Hash] Writer options
17
+ # @yield [WriteProxy] Block adding entries
18
+ def create(buffer, options = {}, &block)
19
+ Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
20
+ archive_path = File.join(tmp, "buffer.7z")
21
+ writer = Formats::SevenZip::Writer.new(archive_path, options)
22
+ block&.call(WriteProxy.new(writer))
23
+ writer.write
24
+
25
+ buffer.write(File.binread(archive_path))
26
+ end
27
+ end
28
+
29
+ # Read a 7z archive from memory
30
+ #
31
+ # @param buffer [StringIO] Archive bytes
32
+ # @yield [ReadProxy] Block reading entries
33
+ # @return [ReadProxy, Object] Proxy or block result
34
+ def open(buffer, &block)
35
+ Dir.mktmpdir("omnizip_buffer_7z") do |tmp|
36
+ archive_path = File.join(tmp, "buffer.7z")
37
+ File.binwrite(archive_path, buffer.string)
38
+
39
+ reader = Formats::SevenZip::Reader.new(archive_path)
40
+ reader.open
41
+ proxy = ReadProxy.new(reader, tmp)
42
+
43
+ block ? yield(proxy) : proxy
44
+ end
45
+ end
46
+ end
47
+
48
+ # Mirrors the MemoryArchive write interface onto a 7z writer
49
+ class WriteProxy
50
+ # @param writer [Formats::SevenZip::Writer]
51
+ def initialize(writer)
52
+ @writer = writer
53
+ end
54
+
55
+ # Add an entry; a trailing slash with empty content adds a
56
+ # directory entry
57
+ #
58
+ # @param name [String] Entry name
59
+ # @param data [String] Entry content
60
+ # @return [self]
61
+ def add(name, data, **_options)
62
+ if name.end_with?("/") && data.to_s.empty?
63
+ @writer.add_directory_entry(name)
64
+ else
65
+ @writer.add_data(name, data)
66
+ end
67
+ self
68
+ end
69
+ end
70
+
71
+ # Mirrors the MemoryArchive read interface onto a 7z reader
72
+ class ReadProxy
73
+ # @param reader [Formats::SevenZip::Reader] opened reader
74
+ # @param tmp [String] Scratch directory for extraction
75
+ def initialize(reader, tmp)
76
+ @reader = reader
77
+ @tmp = tmp
78
+ end
79
+
80
+ # Yield an Entry wrapper per archive entry
81
+ def each_entry
82
+ @reader.list_files.each do |entry|
83
+ yield Entry.new(entry, @reader, @tmp)
84
+ end
85
+ end
86
+
87
+ # Filename => content for every file entry
88
+ #
89
+ # @return [Hash<String, String>]
90
+ def extract_all_to_memory
91
+ result = {}
92
+ each_entry do |entry|
93
+ result[entry.name] = entry.read unless entry.directory?
94
+ end
95
+ result
96
+ end
97
+
98
+ # Entry names; directories keep a trailing slash like the ZIP
99
+ # listing convention
100
+ #
101
+ # @return [Array<String>]
102
+ def entry_names
103
+ @reader.list_files.map do |entry|
104
+ entry.is_dir ? "#{entry.name}/" : entry.name
105
+ end
106
+ end
107
+
108
+ # Read-only access to the underlying entries
109
+ #
110
+ # @return [Array<Formats::SevenZip::Models::FileEntry>]
111
+ def raw_entries
112
+ @reader.list_files
113
+ end
114
+
115
+ # Entry wrapper matching MemoryArchive::Entry's surface
116
+ class Entry
117
+ attr_reader :name, :size
118
+
119
+ # @param entry [Formats::SevenZip::Models::FileEntry]
120
+ # @param reader [Formats::SevenZip::Reader]
121
+ # @param tmp [String] Scratch directory
122
+ def initialize(entry, reader, tmp)
123
+ @entry = entry
124
+ @reader = reader
125
+ @tmp = tmp
126
+ @name = entry.is_dir ? "#{entry.name}/" : entry.name
127
+ @size = entry.size
128
+ end
129
+
130
+ # @return [Boolean]
131
+ def directory?
132
+ @entry.is_dir
133
+ end
134
+
135
+ # @return [Time, nil]
136
+ def time
137
+ @entry.mtime
138
+ end
139
+
140
+ # Extract and return this entry's content
141
+ #
142
+ # @return [String]
143
+ def read(_size = nil)
144
+ return "" if directory? || @entry.has_stream == false
145
+
146
+ dest = File.join(@tmp, "entry", @entry.name)
147
+ @reader.extract_entry(@entry.name, dest)
148
+ File.binread(dest)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -30,6 +30,7 @@ module Omnizip
30
30
  # Nested classes - autoloaded
31
31
  autoload :MemoryArchive, "omnizip/buffer/memory_archive"
32
32
  autoload :MemoryExtractor, "omnizip/buffer/memory_extractor"
33
+ autoload :SevenZipBridge, "omnizip/buffer/seven_zip_bridge"
33
34
 
34
35
  class << self
35
36
  # Create archive in memory
@@ -54,7 +55,7 @@ module Omnizip
54
55
  when :zip
55
56
  create_zip(buffer, options, &block)
56
57
  when :seven_zip, :"7z"
57
- raise NotImplementedError, "7z format support coming in Phase 2"
58
+ SevenZipBridge.create(buffer, options, &block)
58
59
  else
59
60
  raise ArgumentError, "Unsupported format: #{format}"
60
61
  end
@@ -84,7 +85,7 @@ module Omnizip
84
85
  when :zip
85
86
  open_zip(buffer, &block)
86
87
  when :seven_zip, :"7z"
87
- raise NotImplementedError, "7z format support coming in Phase 2"
88
+ SevenZipBridge.open(buffer, &block)
88
89
  else
89
90
  raise ArgumentError, "Unsupported format: #{format}"
90
91
  end
@@ -132,14 +133,14 @@ module Omnizip
132
133
  # @return [Symbol] Detected format
133
134
  # @raise [Omnizip::FormatError] If format cannot be detected
134
135
  def detect_format(buffer)
135
- magic = buffer.read(4)
136
+ magic = buffer.read(4).to_s.b
136
137
  buffer.rewind
137
138
 
138
139
  case magic
139
- when "PK\x03\x04", "PK\x05\x06", "PK\x07\x08"
140
+ when "PK\x03\x04".b, "PK\x05\x06".b, "PK\x07\x08".b
140
141
  # ZIP signatures: local file header, EOCD, data descriptor
141
142
  :zip
142
- when "7z\xBC\xAF"
143
+ when "7z\xBC\xAF".b
143
144
  :seven_zip
144
145
  else
145
146
  raise Omnizip::FormatError,
@@ -154,8 +154,13 @@ module Omnizip
154
154
  writer_opts) do |rar|
155
155
  inputs.each do |input|
156
156
  if File.directory?(input)
157
- raise ArgumentError,
158
- "RAR5 writer does not support directories yet. Add individual files."
157
+ CliOutputFormatter.verbose_puts(
158
+ "Adding directory: #{input}",
159
+ verbose,
160
+ )
161
+ # Strip the parent so the tree lands under its own
162
+ # name, matching the .7z creation behavior
163
+ rar.add_directory(input, File.dirname(input))
159
164
  else
160
165
  CliOutputFormatter.verbose_puts(
161
166
  "Adding file: #{input}",
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  #
4
6
  # Copyright (C) 2024 Ribose Inc.
5
7
  #
@@ -31,12 +33,20 @@ module Omnizip
31
33
 
32
34
  # Execute the decompress command.
33
35
  #
36
+ # Archives (.zip/.7z/.tar/...) extract into the output
37
+ # directory; single-file streams decompress to the output file.
38
+ #
34
39
  # @param input_file [String] Path to input file
35
- # @param output_file [String] Path to output file
40
+ # @param output_file [String] Path to output file or directory
36
41
  # @return [void]
37
42
  def run(input_file, output_file)
38
43
  validate_inputs(input_file, output_file)
39
44
 
45
+ if archive_input?(input_file) || File.directory?(output_file)
46
+ run_archive_extract(input_file, output_file)
47
+ return
48
+ end
49
+
40
50
  algorithm_name = options[:algorithm] || detect_algorithm
41
51
  verbose = options[:verbose] || false
42
52
 
@@ -70,8 +80,26 @@ module Omnizip
70
80
  end
71
81
  end
72
82
 
83
+ ARCHIVE_EXTENSIONS = %w[.zip .7z .tar .rar .cpio .iso].freeze
84
+
73
85
  private
74
86
 
87
+ def archive_input?(path)
88
+ ext = File.extname(path).downcase
89
+ ARCHIVE_EXTENSIONS.include?(ext)
90
+ end
91
+
92
+ def run_archive_extract(input_file, output_dir)
93
+ FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
94
+
95
+ entries = Omnizip.list_archive(input_file)
96
+ Omnizip.extract_archive(input_file, output_dir)
97
+
98
+ puts "Extracted #{entries.size} file(s) to: #{output_dir}"
99
+ rescue StandardError => e
100
+ raise Omnizip::IOError, "Failed to extract archive: #{e.message}"
101
+ end
102
+
75
103
  def validate_inputs(input_file, output_file)
76
104
  unless File.exist?(input_file)
77
105
  raise Omnizip::IOError, "Input file not found: #{input_file}"
@@ -18,19 +18,83 @@ module Omnizip
18
18
  raise Errno::ENOENT, "Archive not found: #{archive_path}"
19
19
  end
20
20
 
21
+ if seven_zip?(archive_path)
22
+ show_seven_zip_metadata(archive_path, pattern)
23
+ return
24
+ end
25
+
26
+ unless zip?(archive_path)
27
+ raise ArgumentError,
28
+ "Metadata is supported for .zip and .7z archives: " \
29
+ "#{archive_path}"
30
+ end
31
+
21
32
  Omnizip::Zip::File.open(archive_path) do |archive|
22
- if options[:show]
33
+ if options[:show] || editing_nothing?
23
34
  show_metadata(archive, pattern)
24
35
  else
25
36
  edit_metadata(archive, pattern)
37
+ puts "Metadata updated successfully"
26
38
  end
27
39
  end
28
-
29
- puts "Metadata updated successfully" unless options[:show]
30
40
  end
31
41
 
32
42
  private
33
43
 
44
+ def zip?(path)
45
+ path.end_with?(".zip")
46
+ end
47
+
48
+ def seven_zip?(path)
49
+ path.end_with?(".7z")
50
+ end
51
+
52
+ # True when no edit option was given — editing would be a no-op
53
+ def editing_nothing?
54
+ %i[comment set_mtime chmod set_attribute].none? { |key| options[key] }
55
+ end
56
+
57
+ # Read-only view for 7z archives; the edit path is zip-only
58
+ def show_seven_zip_metadata(archive_path, pattern)
59
+ if options[:comment] || options[:set_mtime] ||
60
+ options[:chmod] || options[:set_attribute]
61
+ raise ArgumentError,
62
+ "Metadata editing is only supported for ZIP archives"
63
+ end
64
+
65
+ reader = Omnizip::Formats::SevenZip::Reader.new(archive_path)
66
+ reader.open
67
+
68
+ if pattern
69
+ entries = reader.list_files.select do |e|
70
+ File.fnmatch(pattern, e.name)
71
+ end
72
+ if entries.empty?
73
+ warn "No entries match pattern: #{pattern}"
74
+ return
75
+ end
76
+
77
+ entries.each do |entry|
78
+ puts "Entry: #{entry.name}"
79
+ puts " Type: #{entry.is_dir ? 'Directory' : 'File'}"
80
+ puts " Size: #{format_size(entry.size)}"
81
+ # Per-entry packed sizes are not tracked by the parser
82
+ if entry.compressed_size&.positive?
83
+ puts " Compressed: #{format_size(entry.compressed_size)}"
84
+ end
85
+ puts " Modified: #{entry.mtime}" if entry.mtime
86
+ puts " CRC32: 0x#{entry.crc.to_s(16).upcase}" if entry.crc
87
+ end
88
+ else
89
+ entries = reader.list_files
90
+ files = entries.reject(&:is_dir)
91
+ puts "Archive: #{archive_path}"
92
+ puts "Entries: #{entries.size} " \
93
+ "(#{files.size} files, #{entries.size - files.size} dirs)"
94
+ puts "Total size: #{format_size(files.sum(&:size))}"
95
+ end
96
+ end
97
+
34
98
  def show_metadata(archive, pattern)
35
99
  if pattern
36
100
  # Show entry metadata
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tmpdir"
4
+
3
5
  module Omnizip
4
6
  module Converter
5
7
  # Convert 7-Zip archives to ZIP format
@@ -8,18 +10,22 @@ module Omnizip
8
10
  # @return [ConversionResult] Conversion result
9
11
  def convert
10
12
  start_time = Time.now
11
- 0
12
13
 
13
- # Open source 7z archive
14
14
  reader = Omnizip::Formats::SevenZip::Reader.new(source_path)
15
- reader.read
15
+ reader.open
16
+ entry_count = reader.list_files.size
16
17
 
17
- # Collect entries
18
- entries_data = collect_seven_zip_entries(reader)
19
- entry_count = entries_data.size
18
+ Dir.mktmpdir("omnizip_convert") do |tmp|
19
+ reader.extract_all(tmp)
20
20
 
21
- # Create target ZIP archive
22
- create_zip(entries_data)
21
+ writer = Omnizip::Formats::Zip::Writer.new(target_path)
22
+ Dir.glob(File.join(tmp, "**", "*")).each do |path|
23
+ next if File.directory?(path)
24
+
25
+ writer.add_file(path, path.delete_prefix("#{tmp}/"))
26
+ end
27
+ writer.write
28
+ end
23
29
 
24
30
  create_result(start_time, entry_count)
25
31
  end
@@ -43,50 +49,6 @@ module Omnizip
43
49
  def self.can_convert?(source, target)
44
50
  source.end_with?(".7z") && target.end_with?(".zip")
45
51
  end
46
-
47
- private
48
-
49
- def collect_seven_zip_entries(reader)
50
- entries = []
51
-
52
- reader.entries.each do |entry|
53
- data = {
54
- name: entry.name,
55
- content: nil,
56
- mtime: entry.mtime || Time.now,
57
- }
58
-
59
- # Extract entry data
60
- unless entry.name.end_with?("/")
61
- File.open(source_path, "rb") do |io|
62
- data[:content] = reader.extract_entry_data(io, entry)
63
- end
64
- end
65
-
66
- entries << data
67
- end
68
-
69
- entries
70
- end
71
-
72
- def create_zip(entries)
73
- Omnizip::Zip::File.create(target_path) do |zip|
74
- entries.each do |entry_data|
75
- if entry_data[:name].end_with?("/")
76
- # Directory entry
77
- zip.add(entry_data[:name])
78
- else
79
- # File entry
80
- zip.add(entry_data[:name]) { entry_data[:content] }
81
- end
82
- end
83
-
84
- # Set archive comment if preserving metadata
85
- if options.preserve_metadata
86
- zip.comment = "Converted from 7z by Omnizip"
87
- end
88
- end
89
- end
90
52
  end
91
53
  end
92
54
  end
@@ -125,6 +125,24 @@ module Omnizip
125
125
  end
126
126
 
127
127
  class << self
128
+ # Create a RAR archive, yielding the writer for entries.
129
+ # RAR5 is the default; pass version: 4 for the RAR4 writer.
130
+ #
131
+ # @param path [String] Output archive path
132
+ # @param options [Hash] Writer options
133
+ # @return [String, Array<String>] Path(s) of the created
134
+ # archive (a volume set returns every volume)
135
+ def create(path, options = {})
136
+ version = options.fetch(:version, 5)
137
+ writer = if version == 4
138
+ Writer.new(path, options)
139
+ else
140
+ Rar5::Writer.new(path, options)
141
+ end
142
+ yield writer if block_given?
143
+ writer.write
144
+ end
145
+
128
146
  # Check if RAR extraction is available
129
147
  #
130
148
  # @return [Boolean] true if unrar available
@@ -68,6 +68,23 @@ module Omnizip
68
68
  @entries << entry
69
69
  end
70
70
 
71
+ # Add an explicit directory entry with no data or stream
72
+ #
73
+ # @param archive_path [String] Directory path inside the
74
+ # archive (trailing slash optional)
75
+ # @return [self]
76
+ def add_directory_entry(archive_path)
77
+ entry = Models::FileEntry.new
78
+ entry.name = archive_path.chomp("/")
79
+ entry.source_path = nil
80
+ entry.size = 0
81
+ entry.is_dir = true
82
+ entry.has_stream = false
83
+ entry.mtime = Time.now
84
+ @entries << entry
85
+ self
86
+ end
87
+
71
88
  def write
72
89
  # Collect any files from the collector (if add_file/add_directory was used)
73
90
  collected_entries = @collector.collect_files
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.39"
4
+ VERSION = "0.3.40"
5
5
  end
@@ -340,7 +340,7 @@ Zstandard offers fast compression with good ratios, using the native library.
340
340
 
341
341
  [source,ruby]
342
342
  ----
343
- algorithm = Omnizip::AlgorithmRegistry.get(:zstd).new(level: 3)
343
+ algorithm = Omnizip::AlgorithmRegistry.get(:zstandard).new(level: 3)
344
344
  algorithm.compress(input, output)
345
345
  ----
346
346
 
@@ -246,7 +246,7 @@ end
246
246
 
247
247
  # List all available
248
248
  algorithms = Omnizip::AlgorithmRegistry.available
249
- # => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstd]
249
+ # => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstandard]
250
250
  ----
251
251
 
252
252
  === Adding New Algorithms
@@ -329,7 +329,7 @@ Zstandard supports levels 1-22:
329
329
 
330
330
  [source,ruby]
331
331
  ----
332
- zstd = Omnizip::AlgorithmRegistry.get(:zstd).new(level: 3)
332
+ zstd = Omnizip::AlgorithmRegistry.get(:zstandard).new(level: 3)
333
333
  zstd.compress(input, output)
334
334
  ----
335
335
 
@@ -63,7 +63,7 @@ require 'omnizip'
63
63
 
64
64
  # Check available algorithms
65
65
  puts Omnizip::AlgorithmRegistry.available.inspect
66
- # => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstd]
66
+ # => [:lzma, :lzma2, :bzip2, :ppmd7, :ppmd8, :deflate, :deflate64, :zstandard]
67
67
 
68
68
  # Get version
69
69
  puts Omnizip::VERSION
@@ -302,7 +302,7 @@ def compare_algorithms(data, algorithms)
302
302
  end
303
303
 
304
304
  data = File.read('test_file.dat')
305
- compare_algorithms(data, [:deflate, :lzma, :bzip2, :zstd])
305
+ compare_algorithms(data, [:deflate, :lzma, :bzip2, :zstandard])
306
306
  ----
307
307
 
308
308
  === Example 3: Memory Leak Detection
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.39
4
+ version: 0.3.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -333,6 +333,7 @@ files:
333
333
  - lib/omnizip/buffer.rb
334
334
  - lib/omnizip/buffer/memory_archive.rb
335
335
  - lib/omnizip/buffer/memory_extractor.rb
336
+ - lib/omnizip/buffer/seven_zip_bridge.rb
336
337
  - lib/omnizip/checksum_registry.rb
337
338
  - lib/omnizip/checksums.rb
338
339
  - lib/omnizip/checksums/crc32.rb