omnizip 0.3.52 → 0.3.54
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 +4 -4
- data/CHANGELOG.md +59 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +1 -1
- data/lib/omnizip/buffer/memory_archive.rb +55 -60
- data/lib/omnizip/buffer/memory_extractor.rb +26 -41
- data/lib/omnizip/buffer/seven_zip_bridge.rb +13 -0
- data/lib/omnizip/buffer.rb +38 -29
- data/lib/omnizip/checksums/verifier.rb +2 -2
- data/lib/omnizip/chunked.rb +14 -21
- data/lib/omnizip/formats/cpio/reader.rb +1 -1
- data/lib/omnizip/formats/iso/reader.rb +3 -3
- data/lib/omnizip/formats/iso/volume_descriptor.rb +2 -2
- data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +0 -20
- data/lib/omnizip/formats/rar/decompressor.rb +7 -7
- data/lib/omnizip/formats/rar/reader.rb +2 -2
- data/lib/omnizip/formats/rpm.rb +4 -4
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +2 -2
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +5 -5
- data/lib/omnizip/formats/seven_zip/encrypted_header.rb +2 -2
- data/lib/omnizip/formats/seven_zip/file_collector.rb +1 -1
- data/lib/omnizip/formats/seven_zip/header.rb +4 -4
- data/lib/omnizip/formats/seven_zip/header_encryptor.rb +2 -2
- data/lib/omnizip/formats/seven_zip/parser.rb +8 -8
- data/lib/omnizip/formats/seven_zip/reader.rb +15 -20
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +6 -6
- data/lib/omnizip/formats/seven_zip/stream_compressor.rb +1 -1
- data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +5 -5
- data/lib/omnizip/formats/seven_zip/writer.rb +1 -1
- data/lib/omnizip/formats/xar/reader.rb +2 -2
- data/lib/omnizip/formats/zip/reader.rb +96 -8
- data/lib/omnizip/formats/zip/writer.rb +13 -6
- data/lib/omnizip/models/conversion_result.rb +75 -63
- data/lib/omnizip/models/filter_config.rb +34 -44
- data/lib/omnizip/models/match_result.rb +35 -18
- data/lib/omnizip/models/optimization_suggestion.rb +45 -41
- data/lib/omnizip/models/performance_result.rb +53 -47
- data/lib/omnizip/models/profile_report.rb +70 -36
- data/lib/omnizip/parallel/engine.rb +9 -5
- data/lib/omnizip/parallel/job_scheduler.rb +5 -1
- data/lib/omnizip/parallel/parallel_extractor.rb +63 -152
- data/lib/omnizip/parallel/thread_pool.rb +60 -0
- data/lib/omnizip/parallel/worker_pool.rb +8 -4
- data/lib/omnizip/parallel.rb +1 -0
- data/lib/omnizip/profiler/report_generator.rb +1 -1
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip/zip/file.rb +1 -1
- data/lib/omnizip.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87089cc22eb4f5edd1cb37e7a0de297527fb2f7c0a2815f29d1ff271c73f7461
|
|
4
|
+
data.tar.gz: e6b7848399a98ae4f38f80cbc71833ada099e071d4f4126437ebbfcca820dc48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfd421337992988889d3e5e8d4e85177248c5586238c2c79a5d87e5fc2229de3620f53e93d582ac72992c90b213421b1832cd44ecb9e1ff94e94622a48fa6b88
|
|
7
|
+
data.tar.gz: 33ca53cfe50c72a1d31afbbf0848ed9559824867edf34f1a4fdc37484300c029fcbf36bdb3135d92f607e31c517d6a519e945065a6c17f817cfbdc0af9ee6de9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.53] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The last six hand-rolled `to_h` serializers migrate onto
|
|
14
|
+
lutaml-model like their siblings: `ConversionResult`,
|
|
15
|
+
`PerformanceResult`, `ProfileReport` (with a nested Summary model
|
|
16
|
+
built from aggregates), `OptimizationSuggestion`, `MatchResult`,
|
|
17
|
+
and `FilterConfig`. Computed report fields (size_reduction,
|
|
18
|
+
throughput, priority_score, summary aggregates) serialize through
|
|
19
|
+
lutaml derived attributes keyed under their documented names.
|
|
20
|
+
Serialized hashes now use string keys (`to_hash`, matching the
|
|
21
|
+
other migrated models).
|
|
22
|
+
- `Formats::Zip::Reader` gains `read_entry_stream`, yielding bounded
|
|
23
|
+
chunks (raw copy for STORE, incremental inflater for DEFLATE) with
|
|
24
|
+
a CRC check after the last chunk. `Chunked.decompress_file`
|
|
25
|
+
streams through it — the memory-efficient path no longer holds the
|
|
26
|
+
whole decompressed entry in memory to slice it. Extracting a
|
|
27
|
+
single 7z entry in memory (`MemoryExtractor`) extracts just that
|
|
28
|
+
entry instead of decompressing the entire archive.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Boundary errors stop masquerading as bare RuntimeErrors:
|
|
32
|
+
`Checksums::Verifier` raises `UnknownChecksumError` (the class
|
|
33
|
+
existed unused), and thirty more `raise "string"` sites gained
|
|
34
|
+
their matching classes (`Errno::ENOENT` for missing entries
|
|
35
|
+
— matching the handler-registry convention — `Errno::EEXIST`,
|
|
36
|
+
`IOError`, `InvalidArchiveError`, `DecompressionError`,
|
|
37
|
+
`RarNotAvailableError`). The never-called placeholder
|
|
38
|
+
`decode_escape_code` in the RAR PPMd decoder is deleted.
|
|
39
|
+
|
|
40
|
+
## [0.3.52] - 2026-09-01
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- The extract command's pattern path opens archives through the
|
|
44
|
+
Archive facade instead of a hand-rolled extension `case` that
|
|
45
|
+
misrouted `.cpio`/`.iso`/`.rpm` files into the 7z reader — pattern
|
|
46
|
+
extraction now works for every routed format. The metadata
|
|
47
|
+
command's 7z view lists through the handler registry (the detail
|
|
48
|
+
contract gains `:crc`), and SafeExtract extracts through
|
|
49
|
+
`extract_archive`, so every routed format gets rollback and
|
|
50
|
+
verification instead of ZIP-only with a `NotImplementedError`
|
|
51
|
+
for 7z. SelectiveExtractor understands both entry contracts
|
|
52
|
+
(legacy `entry_name` and reader-session `name`) and skips
|
|
53
|
+
directory entries when extracting.
|
|
54
|
+
- `CliOutputFormatter.format_size` is now the single byte formatter;
|
|
55
|
+
the nine private `format_bytes`/`format_size` copies (four
|
|
56
|
+
different shapes, two unit ceilings) become delegators.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- The long-deferred parity spec flake: `Par2Verifier` crashed five
|
|
60
|
+
frames deep on a nil PAR2 path (`File.exist?(nil)` → TypeError)
|
|
61
|
+
where the repairer already raised a clean ArgumentError; the
|
|
62
|
+
verifier now mirrors it, and the spec's `.verify` group gains the
|
|
63
|
+
nil-index fallback `.repair` already had. The dead shadowed
|
|
64
|
+
`analyze_file` definition in `Par2Creator` (silently overridden
|
|
65
|
+
by a second definition with a divergent file_id format) is
|
|
66
|
+
deleted. The tautological "LZMA2 is implicit" xz assertion became
|
|
67
|
+
a real block-header parse asserting the explicit 0x21 filter ID.
|
|
68
|
+
|
|
10
69
|
## [0.3.51] - 2026-09-01
|
|
11
70
|
|
|
12
71
|
### Changed
|
|
@@ -38,7 +38,7 @@ module Omnizip
|
|
|
38
38
|
extracted = []
|
|
39
39
|
reader.entries.each do |entry|
|
|
40
40
|
dest_path = ::File.join(output_dir, entry.filename)
|
|
41
|
-
raise "File exists: #{dest_path}" if ::File.exist?(dest_path)
|
|
41
|
+
raise Errno::EEXIST, "File exists: #{dest_path}" if ::File.exist?(dest_path)
|
|
42
42
|
|
|
43
43
|
reader.extract_entry(entry, output_dir)
|
|
44
44
|
extracted << dest_path
|
|
@@ -9,27 +9,24 @@ module Omnizip
|
|
|
9
9
|
# (for creating) and InputStream (for reading).
|
|
10
10
|
#
|
|
11
11
|
# @example Creating an archive
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# end
|
|
12
|
+
# writer = Formats::Zip::Writer.new(nil)
|
|
13
|
+
# archive = MemoryArchive.new(writer, :zip)
|
|
14
|
+
# archive.add('file.txt', 'content')
|
|
15
|
+
# writer.write_to_io(buffer)
|
|
17
16
|
#
|
|
18
17
|
# @example Reading an archive
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# puts entry.name
|
|
23
|
-
# end
|
|
18
|
+
# archive = MemoryArchive.new(Formats::Zip::Reader.new(path), :zip)
|
|
19
|
+
# archive.each_entry do |entry|
|
|
20
|
+
# puts entry.name
|
|
24
21
|
# end
|
|
25
22
|
class MemoryArchive
|
|
26
23
|
attr_reader :format, :stream
|
|
27
24
|
|
|
28
25
|
# Initialize memory archive wrapper
|
|
29
26
|
#
|
|
30
|
-
# @param stream [
|
|
31
|
-
#
|
|
32
|
-
# @param format [Symbol] Archive format (:zip
|
|
27
|
+
# @param stream [Formats::Zip::Writer, Formats::Zip::Reader]
|
|
28
|
+
# Native writer (write mode) or reader (read mode)
|
|
29
|
+
# @param format [Symbol] Archive format (:zip)
|
|
33
30
|
def initialize(stream, format)
|
|
34
31
|
@stream = stream
|
|
35
32
|
@format = format
|
|
@@ -57,13 +54,13 @@ module Omnizip
|
|
|
57
54
|
def add(name, data, **options)
|
|
58
55
|
ensure_write_mode!
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
stream.put_next_entry(name, **options)
|
|
63
|
-
stream.write(data) unless name.end_with?("/")
|
|
57
|
+
if name.end_with?("/") && data.to_s.empty?
|
|
58
|
+
stream.add_directory(name)
|
|
64
59
|
else
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
method = if options[:compression] == :store
|
|
61
|
+
Formats::Zip::Constants::COMPRESSION_STORE
|
|
62
|
+
end
|
|
63
|
+
stream.add_data(name, data, nil, compression_method: method)
|
|
67
64
|
end
|
|
68
65
|
|
|
69
66
|
self
|
|
@@ -101,15 +98,8 @@ module Omnizip
|
|
|
101
98
|
def each_entry
|
|
102
99
|
ensure_read_mode!
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
while (zip_entry = stream.get_next_entry)
|
|
107
|
-
entry = Entry.new(zip_entry, stream)
|
|
108
|
-
yield(entry)
|
|
109
|
-
end
|
|
110
|
-
else
|
|
111
|
-
raise NotImplementedError,
|
|
112
|
-
"Unsupported stream type: #{stream.class}"
|
|
101
|
+
stream.entries.each do |zip_entry|
|
|
102
|
+
yield(Entry.new(zip_entry, stream))
|
|
113
103
|
end
|
|
114
104
|
end
|
|
115
105
|
|
|
@@ -142,22 +132,10 @@ module Omnizip
|
|
|
142
132
|
def to_s
|
|
143
133
|
ensure_write_mode!
|
|
144
134
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
unless stream.closed?
|
|
150
|
-
raise "Archive must be closed before accessing data"
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# The buffer was passed in during creation, but we don't have
|
|
154
|
-
# direct access. This method should be called on the StringIO
|
|
155
|
-
# returned by Buffer.create instead.
|
|
156
|
-
raise NotImplementedError,
|
|
157
|
-
"Use Buffer.create return value instead"
|
|
158
|
-
else
|
|
159
|
-
raise "Cannot get string from read mode archive"
|
|
160
|
-
end
|
|
135
|
+
# The buffer is owned by Buffer.create; this method should be
|
|
136
|
+
# called on the StringIO it returns instead.
|
|
137
|
+
raise NotImplementedError,
|
|
138
|
+
"Use Buffer.create return value instead"
|
|
161
139
|
end
|
|
162
140
|
|
|
163
141
|
# Entry wrapper with read capability
|
|
@@ -169,17 +147,19 @@ module Omnizip
|
|
|
169
147
|
|
|
170
148
|
# Initialize entry wrapper
|
|
171
149
|
#
|
|
172
|
-
# @param entry [
|
|
173
|
-
# @param
|
|
174
|
-
def initialize(entry,
|
|
150
|
+
# @param entry [Formats::Zip::CentralDirectoryHeader] entry
|
|
151
|
+
# @param reader [Formats::Zip::Reader] Native archive reader
|
|
152
|
+
def initialize(entry, reader)
|
|
175
153
|
@entry = entry
|
|
176
|
-
@
|
|
177
|
-
@name = entry.
|
|
178
|
-
@size = entry.
|
|
154
|
+
@reader = reader
|
|
155
|
+
@name = entry.filename
|
|
156
|
+
@size = entry.uncompressed_size
|
|
179
157
|
@compressed_size = entry.compressed_size
|
|
180
158
|
@time = entry.time
|
|
181
|
-
@comment = entry.comment
|
|
159
|
+
@comment = entry.comment.to_s
|
|
182
160
|
@directory = entry.directory?
|
|
161
|
+
@content = nil
|
|
162
|
+
@pos = 0
|
|
183
163
|
end
|
|
184
164
|
|
|
185
165
|
# Read entry content
|
|
@@ -195,7 +175,20 @@ module Omnizip
|
|
|
195
175
|
# process_chunk(chunk)
|
|
196
176
|
# end
|
|
197
177
|
def read(size = nil)
|
|
198
|
-
|
|
178
|
+
# IO-like streaming semantics: successive reads return
|
|
179
|
+
# successive chunks, nil at end-of-content. Callers like
|
|
180
|
+
# Pipe::StreamDecompressor loop on this — a read that
|
|
181
|
+
# always returns data would loop forever.
|
|
182
|
+
@content ||= @reader.read_entry(@name)
|
|
183
|
+
return nil if @pos >= @content.bytesize
|
|
184
|
+
|
|
185
|
+
chunk = if size
|
|
186
|
+
@content.byteslice(@pos, size)
|
|
187
|
+
else
|
|
188
|
+
@content.byteslice(@pos, @content.bytesize - @pos)
|
|
189
|
+
end
|
|
190
|
+
@pos += chunk.bytesize
|
|
191
|
+
chunk
|
|
199
192
|
end
|
|
200
193
|
|
|
201
194
|
# Check if entry is a directory
|
|
@@ -229,22 +222,24 @@ module Omnizip
|
|
|
229
222
|
|
|
230
223
|
private
|
|
231
224
|
|
|
232
|
-
# Ensure stream is in write mode (
|
|
225
|
+
# Ensure stream is in write mode (native Writer)
|
|
233
226
|
#
|
|
234
|
-
# @raise [
|
|
227
|
+
# @raise [Omnizip::IOError] If not in write mode
|
|
235
228
|
def ensure_write_mode!
|
|
236
|
-
return if stream.is_a?(Omnizip::Zip::
|
|
229
|
+
return if stream.is_a?(Omnizip::Formats::Zip::Writer)
|
|
237
230
|
|
|
238
|
-
raise
|
|
231
|
+
raise Omnizip::IOError,
|
|
232
|
+
"Operation requires write mode (Formats::Zip::Writer)"
|
|
239
233
|
end
|
|
240
234
|
|
|
241
|
-
# Ensure stream is in read mode (
|
|
235
|
+
# Ensure stream is in read mode (native Reader)
|
|
242
236
|
#
|
|
243
|
-
# @raise [
|
|
237
|
+
# @raise [Omnizip::IOError] If not in read mode
|
|
244
238
|
def ensure_read_mode!
|
|
245
|
-
return if stream.is_a?(Omnizip::Zip::
|
|
239
|
+
return if stream.is_a?(Omnizip::Formats::Zip::Reader)
|
|
246
240
|
|
|
247
|
-
raise
|
|
241
|
+
raise Omnizip::IOError,
|
|
242
|
+
"Operation requires read mode (Formats::Zip::Reader)"
|
|
248
243
|
end
|
|
249
244
|
end
|
|
250
245
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
|
|
5
6
|
module Omnizip
|
|
6
7
|
module Buffer
|
|
@@ -151,38 +152,36 @@ module Omnizip
|
|
|
151
152
|
|
|
152
153
|
private
|
|
153
154
|
|
|
154
|
-
#
|
|
155
|
+
# Delegate to the shared buffer-side sniffer
|
|
155
156
|
#
|
|
156
157
|
# @return [Symbol] Detected format
|
|
157
158
|
# @raise [Omnizip::FormatError] If format cannot be detected
|
|
158
159
|
def detect_format
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
Omnizip::Buffer.detect_format(@buffer)
|
|
161
|
+
end
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"Unknown archive format (magic: #{magic.inspect})"
|
|
163
|
+
# The native ZIP reader parses from a path; spill the buffer to
|
|
164
|
+
# a temporary file and yield a reader over it.
|
|
165
|
+
def with_zip_reader
|
|
166
|
+
@buffer.rewind
|
|
167
|
+
Dir.mktmpdir("omnizip_extractor_zip") do |tmp|
|
|
168
|
+
path = File.join(tmp, "buffer.zip")
|
|
169
|
+
File.binwrite(path, @buffer.read)
|
|
170
|
+
yield Omnizip::Formats::Zip::Reader.new(path)
|
|
171
171
|
end
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
# Extract all entries from ZIP
|
|
174
|
+
# Extract all entries from ZIP through the native reader
|
|
175
175
|
#
|
|
176
176
|
# @param result [Hash] Hash to populate with entries
|
|
177
177
|
def extract_all_zip(result)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
while (entry = zis.get_next_entry)
|
|
178
|
+
with_zip_reader do |reader|
|
|
179
|
+
reader.entries.each do |entry|
|
|
181
180
|
next if entry.directory?
|
|
182
181
|
|
|
183
|
-
content =
|
|
184
|
-
result[entry.
|
|
185
|
-
@extracted_cache[entry.
|
|
182
|
+
content = reader.read_entry(entry.filename)
|
|
183
|
+
result[entry.filename] = content
|
|
184
|
+
@extracted_cache[entry.filename] = content
|
|
186
185
|
end
|
|
187
186
|
end
|
|
188
187
|
end
|
|
@@ -207,41 +206,27 @@ module Omnizip
|
|
|
207
206
|
def extract_entry_seven_zip(name)
|
|
208
207
|
@buffer.rewind
|
|
209
208
|
SevenZipBridge.open(@buffer) do |archive|
|
|
210
|
-
|
|
211
|
-
archive.extract_all_to_memory[name] if entry
|
|
209
|
+
archive.read_entry(name)
|
|
212
210
|
end
|
|
213
211
|
end
|
|
214
212
|
|
|
215
|
-
# Extract single entry from ZIP
|
|
213
|
+
# Extract single entry from ZIP through the native reader
|
|
216
214
|
#
|
|
217
215
|
# @param name [String] Entry name
|
|
218
216
|
# @return [String, nil] Entry content or nil if not found
|
|
219
217
|
def extract_entry_zip(name)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Omnizip::Zip::InputStream.open(@buffer) do |zis|
|
|
224
|
-
while (entry = zis.get_next_entry)
|
|
225
|
-
if entry.name == name
|
|
226
|
-
content = zis.read unless entry.directory?
|
|
227
|
-
break
|
|
228
|
-
end
|
|
229
|
-
end
|
|
218
|
+
with_zip_reader do |reader|
|
|
219
|
+
entry = reader.entries.find { |e| e.filename == name }
|
|
220
|
+
reader.read_entry(name) if entry && !entry.directory?
|
|
230
221
|
end
|
|
231
|
-
|
|
232
|
-
content
|
|
233
222
|
end
|
|
234
223
|
|
|
235
|
-
# List all entry names from ZIP
|
|
224
|
+
# List all entry names from ZIP through the native reader
|
|
236
225
|
#
|
|
237
226
|
# @param names [Array] Array to populate with names
|
|
238
227
|
def list_entries_zip(names)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Omnizip::Zip::InputStream.open(@buffer) do |zis|
|
|
242
|
-
while (entry = zis.get_next_entry)
|
|
243
|
-
names << entry.name
|
|
244
|
-
end
|
|
228
|
+
with_zip_reader do |reader|
|
|
229
|
+
reader.entries.each { |entry| names << entry.filename }
|
|
245
230
|
end
|
|
246
231
|
end
|
|
247
232
|
end
|
|
@@ -112,6 +112,19 @@ module Omnizip
|
|
|
112
112
|
@reader.list_files
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# Content of a single named file entry, or nil when absent
|
|
116
|
+
# (directories carry no content). Extracts just that entry —
|
|
117
|
+
# not the whole archive.
|
|
118
|
+
#
|
|
119
|
+
# @param name [String] Entry name
|
|
120
|
+
# @return [String, nil]
|
|
121
|
+
def read_entry(name)
|
|
122
|
+
entry = raw_entries.find { |e| e.name == name && !e.is_dir }
|
|
123
|
+
return nil unless entry
|
|
124
|
+
|
|
125
|
+
Entry.new(entry, @reader, @tmp).read
|
|
126
|
+
end
|
|
127
|
+
|
|
115
128
|
# Entry wrapper matching MemoryArchive::Entry's surface
|
|
116
129
|
class Entry
|
|
117
130
|
attr_reader :name, :size
|
data/lib/omnizip/buffer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
|
|
5
6
|
module Omnizip
|
|
6
7
|
# In-memory archive operations without filesystem I/O
|
|
@@ -107,27 +108,9 @@ module Omnizip
|
|
|
107
108
|
extractor.extract_all
|
|
108
109
|
end
|
|
109
110
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# @param format [Symbol] Archive format
|
|
114
|
-
# @param options [Hash] Format-specific options
|
|
115
|
-
# @return [StringIO] Complete archive in memory
|
|
116
|
-
#
|
|
117
|
-
# @example Create from Hash
|
|
118
|
-
# data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
|
|
119
|
-
# zip = Omnizip::Buffer.create_from_hash(data, :zip)
|
|
120
|
-
def create_from_hash(hash, format = :zip, **options)
|
|
121
|
-
create(format, **options) do |archive|
|
|
122
|
-
hash.each do |name, content|
|
|
123
|
-
archive.add(name, content)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
# Detect archive format from magic bytes
|
|
111
|
+
# Detect archive format from magic bytes. THE buffer-side
|
|
112
|
+
# sniffer — in-memory data has no extension to route by, so
|
|
113
|
+
# every consumer delegates here.
|
|
131
114
|
#
|
|
132
115
|
# @param buffer [StringIO] Buffer containing archive data
|
|
133
116
|
# @return [Symbol] Detected format
|
|
@@ -148,27 +131,53 @@ module Omnizip
|
|
|
148
131
|
end
|
|
149
132
|
end
|
|
150
133
|
|
|
151
|
-
# Create
|
|
134
|
+
# Create archive from Hash of filename => content
|
|
135
|
+
#
|
|
136
|
+
# @param hash [Hash<String, String>] Filename => content mapping
|
|
137
|
+
# @param format [Symbol] Archive format
|
|
138
|
+
# @param options [Hash] Format-specific options
|
|
139
|
+
# @return [StringIO] Complete archive in memory
|
|
140
|
+
#
|
|
141
|
+
# @example Create from Hash
|
|
142
|
+
# data = {'file1.txt' => 'content1', 'file2.txt' => 'content2'}
|
|
143
|
+
# zip = Omnizip::Buffer.create_from_hash(data, :zip)
|
|
144
|
+
def create_from_hash(hash, format = :zip, **options)
|
|
145
|
+
create(format, **options) do |archive|
|
|
146
|
+
hash.each do |name, content|
|
|
147
|
+
archive.add(name, content)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Create ZIP archive in buffer through the native writer
|
|
152
155
|
#
|
|
153
156
|
# @param buffer [StringIO] Buffer to write to
|
|
154
157
|
# @param options [Hash] ZIP-specific options
|
|
155
158
|
# @yield [archive] Block to populate archive
|
|
156
159
|
def create_zip(buffer, _options, &block)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
writer = Formats::Zip::Writer.new(nil)
|
|
161
|
+
archive = Buffer::MemoryArchive.new(writer, :zip)
|
|
162
|
+
block&.call(archive)
|
|
163
|
+
writer.write_to_io(buffer)
|
|
161
164
|
end
|
|
162
165
|
|
|
163
|
-
# Open ZIP archive from buffer
|
|
166
|
+
# Open ZIP archive from buffer through the native reader.
|
|
167
|
+
# The reader parses from a path, so the buffer spills to a
|
|
168
|
+
# temporary file (same pattern as the 7z bridge).
|
|
164
169
|
#
|
|
165
170
|
# @param buffer [StringIO] Buffer containing ZIP data
|
|
166
171
|
# @yield [archive] Block to read from archive
|
|
167
172
|
# @return [MemoryArchive, Object] Archive or block return value
|
|
168
173
|
def open_zip(buffer, &block)
|
|
169
174
|
result = nil
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
Dir.mktmpdir("omnizip_buffer_zip") do |tmp|
|
|
176
|
+
path = File.join(tmp, "buffer.zip")
|
|
177
|
+
File.binwrite(path, buffer.string)
|
|
178
|
+
|
|
179
|
+
archive = Buffer::MemoryArchive.new(Formats::Zip::Reader.new(path),
|
|
180
|
+
:zip)
|
|
172
181
|
result = block ? yield(archive) : archive
|
|
173
182
|
end
|
|
174
183
|
result
|
|
@@ -94,7 +94,7 @@ module Omnizip
|
|
|
94
94
|
when CHECK_SHA256
|
|
95
95
|
verify_sha256(data, expected)
|
|
96
96
|
else
|
|
97
|
-
raise "Unknown check type: #{check_type}"
|
|
97
|
+
raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -122,7 +122,7 @@ module Omnizip
|
|
|
122
122
|
when CHECK_SHA256
|
|
123
123
|
Digest::SHA256.digest(data)
|
|
124
124
|
else
|
|
125
|
-
raise "Unknown check type: #{check_type}"
|
|
125
|
+
raise Omnizip::UnknownChecksumError, "Unknown check type: #{check_type}"
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
end
|
data/lib/omnizip/chunked.rb
CHANGED
|
@@ -95,30 +95,23 @@ module Omnizip
|
|
|
95
95
|
"Input archive not found: #{input}"
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Omnizip::Zip::File.open(input) do |zip|
|
|
102
|
-
entry = zip.entries.first
|
|
103
|
-
total_size = entry.size
|
|
104
|
-
|
|
105
|
-
# Read the full entry content
|
|
106
|
-
content = zip.get_input_stream(entry)
|
|
98
|
+
reader = Omnizip::Formats::Zip::Reader.new(input)
|
|
99
|
+
entry = reader.entries.first
|
|
100
|
+
raise Omnizip::Error, "No entries to decompress in #{input}" unless entry
|
|
107
101
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
chunk = content.byteslice(offset, chunk_size) || ""
|
|
112
|
-
break if chunk.empty?
|
|
102
|
+
total_size = entry.uncompressed_size.to_i
|
|
103
|
+
processed = 0
|
|
104
|
+
writer = Writer.new(output, chunk_size: chunk_size)
|
|
113
105
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
# Streams entry-by-entry in bounded memory — the whole entry
|
|
107
|
+
# is never held at once
|
|
108
|
+
reader.read_entry_stream(entry.filename, chunk_size: chunk_size) do |chunk|
|
|
109
|
+
writer.write_chunk(chunk)
|
|
110
|
+
processed += chunk.bytesize
|
|
117
111
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
end
|
|
112
|
+
if progress
|
|
113
|
+
percentage = (processed.to_f / total_size * 100).round(2)
|
|
114
|
+
progress.call(processed, total_size, percentage)
|
|
122
115
|
end
|
|
123
116
|
end
|
|
124
117
|
|
|
@@ -64,7 +64,7 @@ module Omnizip
|
|
|
64
64
|
# @raise [RuntimeError] if entry not found
|
|
65
65
|
def extract_entry(entry_name, output_path)
|
|
66
66
|
entry = @entries.find { |e| e.name == entry_name }
|
|
67
|
-
raise "Entry not found: #{entry_name}" unless entry
|
|
67
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}" unless entry
|
|
68
68
|
|
|
69
69
|
extract_single_entry(entry, output_path)
|
|
70
70
|
end
|
|
@@ -67,7 +67,7 @@ module Omnizip
|
|
|
67
67
|
# @param output_path [String] Destination path
|
|
68
68
|
def extract_entry(entry_path, output_path)
|
|
69
69
|
entry = find_entry(entry_path)
|
|
70
|
-
raise "Entry not found: #{entry_path}" unless entry
|
|
70
|
+
raise Errno::ENOENT, "Entry not found: #{entry_path}" unless entry
|
|
71
71
|
|
|
72
72
|
if entry.directory?
|
|
73
73
|
FileUtils.mkdir_p(output_path)
|
|
@@ -111,7 +111,7 @@ module Omnizip
|
|
|
111
111
|
loop do
|
|
112
112
|
@io.seek(sector * Iso::SECTOR_SIZE)
|
|
113
113
|
data = @io.read(Iso::SECTOR_SIZE)
|
|
114
|
-
raise "Failed to read volume descriptor" unless data
|
|
114
|
+
raise Omnizip::InvalidArchiveError, "Failed to read volume descriptor" unless data
|
|
115
115
|
|
|
116
116
|
vd = VolumeDescriptor.parse(data)
|
|
117
117
|
|
|
@@ -127,7 +127,7 @@ module Omnizip
|
|
|
127
127
|
|
|
128
128
|
return if @primary_volume_descriptor
|
|
129
129
|
|
|
130
|
-
raise "No primary volume descriptor found"
|
|
130
|
+
raise Omnizip::InvalidArchiveError, "No primary volume descriptor found"
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
# Parse directory structure
|
|
@@ -29,7 +29,7 @@ module Omnizip
|
|
|
29
29
|
#
|
|
30
30
|
# @param data [String] Binary data
|
|
31
31
|
def parse(data)
|
|
32
|
-
raise "Invalid volume descriptor size" unless data.bytesize >= 2048
|
|
32
|
+
raise Omnizip::InvalidArchiveError, "Invalid volume descriptor size" unless data.bytesize >= 2048
|
|
33
33
|
|
|
34
34
|
# Byte 0: Volume descriptor type
|
|
35
35
|
@type = data.getbyte(0)
|
|
@@ -37,7 +37,7 @@ module Omnizip
|
|
|
37
37
|
# Bytes 1-5: Standard identifier "CD001"
|
|
38
38
|
@identifier = data[1, 5]
|
|
39
39
|
unless @identifier == ISO_IDENTIFIER
|
|
40
|
-
raise "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
|
|
40
|
+
raise Omnizip::InvalidArchiveError, "Invalid ISO identifier: expected #{ISO_IDENTIFIER}, got #{@identifier}"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Byte 6: Version (should be 1)
|
|
@@ -230,26 +230,6 @@ module Omnizip
|
|
|
230
230
|
# Decode as 8 direct bits
|
|
231
231
|
@range_decoder.decode_direct_bits(8)
|
|
232
232
|
end
|
|
233
|
-
|
|
234
|
-
# Decode RAR-specific escape code
|
|
235
|
-
#
|
|
236
|
-
# RAR variant H uses different escape code values
|
|
237
|
-
# and handling compared to standard PPMd7.
|
|
238
|
-
#
|
|
239
|
-
# Escape codes in RAR:
|
|
240
|
-
# - 0: New symbol follows
|
|
241
|
-
# - 1: Same as last symbol (run-length)
|
|
242
|
-
# - 2-255: Reserved for future use
|
|
243
|
-
#
|
|
244
|
-
# @return [Integer, nil] Escape code or nil
|
|
245
|
-
def decode_escape_code
|
|
246
|
-
# RAR escape codes differ from PPMd7
|
|
247
|
-
# This is a placeholder for the proper implementation
|
|
248
|
-
|
|
249
|
-
# For now, return 0 (new symbol follows)
|
|
250
|
-
# Real implementation would decode from range coder
|
|
251
|
-
0
|
|
252
|
-
end
|
|
253
233
|
end
|
|
254
234
|
end
|
|
255
235
|
end
|