omnizip 0.3.53 → 0.3.55
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 +65 -0
- data/lib/omnizip/algorithms/lzma/dictionary.rb +2 -2
- data/lib/omnizip/algorithms/lzma/lzma_alone_decoder.rb +7 -7
- data/lib/omnizip/algorithms/lzma/xz_buffered_range_encoder.rb +4 -4
- data/lib/omnizip/algorithms/lzma/xz_range_encoder_exact.rb +7 -7
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +8 -8
- data/lib/omnizip/buffer/memory_archive.rb +55 -60
- data/lib/omnizip/buffer/memory_extractor.rb +25 -27
- data/lib/omnizip/buffer.rb +15 -8
- data/lib/omnizip/commands/metadata_command.rb +14 -6
- 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 +14 -19
- data/lib/omnizip/formats/seven_zip/split_archive_reader.rb +5 -5
- 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/zip/writer.rb +13 -6
- data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +5 -5
- data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma/range_decoder.rb +1 -1
- data/lib/omnizip/implementations/seven_zip/lzma/state_machine.rb +1 -1
- data/lib/omnizip/metadata/metadata_registry.rb +9 -2
- data/lib/omnizip/parallel/engine.rb +9 -5
- data/lib/omnizip/parallel/job_scheduler.rb +5 -1
- data/lib/omnizip/parallel/parallel_extractor.rb +62 -151
- data/lib/omnizip/parallel/thread_pool.rb +60 -0
- data/lib/omnizip/parallel/worker_pool.rb +5 -1
- data/lib/omnizip/parallel.rb +1 -0
- data/lib/omnizip/version.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: 7de280d85fcea7072c88798c925b5aa3e03bcff124c1294e72abeb6f976cc5e8
|
|
4
|
+
data.tar.gz: 579759d99f27af57c070e5e1b6a6d6b121651932b10681da88e96331adb36bee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a08a53e33284555a3fe7493834f522e0bf00a8609705e06d6c05578bb342bb3db4e4ba1c3c0793f18a001ef55305fd3396a7300b6c7f1c82196a9702931fc374
|
|
7
|
+
data.tar.gz: 7b0550e981ff0cb317beffdcb7eff7b233a7fff0bc4812eae01a566f1a0d255de8896e217d1d1ce62618b56bb5ea284dc414c98b53c171fa4af0b54a1a048b86
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.54] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The 7z tree's 38 bare `raise "string"` sites gain their matching
|
|
14
|
+
classes: `PasswordError` for encrypted-header problems (with a
|
|
15
|
+
new autoload entry — the class lives at the top of password.rb,
|
|
16
|
+
so referencing it never loaded the file),
|
|
17
|
+
`InvalidArchiveError`/`UnsupportedFormatError` for parse and
|
|
18
|
+
structure failures, `ChecksumError` for CRC mismatches,
|
|
19
|
+
`AlgorithmNotFoundError`/`UnknownFilterError` in CoderChain, and
|
|
20
|
+
`Errno::ENOENT` for missing entries and volumes. The reader's
|
|
21
|
+
encrypted-header probe re-raises password errors by class instead
|
|
22
|
+
of matching message text.
|
|
23
|
+
- In-memory ZIP leaves the rubyzip compat layer: `Buffer.create`
|
|
24
|
+
writes through the native Writer (which gains a per-entry
|
|
25
|
+
compression-method override so `:store` entries work beside
|
|
26
|
+
deflated ones), and `Buffer.open`/`MemoryExtractor` read through
|
|
27
|
+
the native Reader via a temporary-file spill.
|
|
28
|
+
`MemoryArchive::Entry#read` now has true IO streaming semantics —
|
|
29
|
+
successive chunks and nil at EOF (an always-returning read looped
|
|
30
|
+
forever in `Pipe::StreamDecompressor`).
|
|
31
|
+
- The parallel subsystem is revived: Engine drives our own
|
|
32
|
+
WorkerPool instead of a phantom `Fractor::WorkerPool`,
|
|
33
|
+
ParallelExtractor reads through the native ZIP reader and runs on
|
|
34
|
+
a bounded ThreadPool — Ractor workers cannot trigger Omnizip's
|
|
35
|
+
autoloads or cross into Proc-holding codec constants on
|
|
36
|
+
Ruby <= 3.3 — JobScheduler's implemented round_robin/bin_packing
|
|
37
|
+
strategies are reachable, directory entries extract idempotently,
|
|
38
|
+
and the subsystem gains specs.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- par2_compatibility_spec was missing `require "tmpdir"` and failed
|
|
42
|
+
9 of 15 examples whenever no earlier spec happened to load tmpdir
|
|
43
|
+
first.
|
|
44
|
+
|
|
45
|
+
## [0.3.53] - 2026-09-01
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- The last six hand-rolled `to_h` serializers migrate onto
|
|
49
|
+
lutaml-model like their siblings: `ConversionResult`,
|
|
50
|
+
`PerformanceResult`, `ProfileReport` (with a nested Summary model
|
|
51
|
+
built from aggregates), `OptimizationSuggestion`, `MatchResult`,
|
|
52
|
+
and `FilterConfig`. Computed report fields (size_reduction,
|
|
53
|
+
throughput, priority_score, summary aggregates) serialize through
|
|
54
|
+
lutaml derived attributes keyed under their documented names.
|
|
55
|
+
Serialized hashes now use string keys (`to_hash`, matching the
|
|
56
|
+
other migrated models).
|
|
57
|
+
- `Formats::Zip::Reader` gains `read_entry_stream`, yielding bounded
|
|
58
|
+
chunks (raw copy for STORE, incremental inflater for DEFLATE) with
|
|
59
|
+
a CRC check after the last chunk. `Chunked.decompress_file`
|
|
60
|
+
streams through it — the memory-efficient path no longer holds the
|
|
61
|
+
whole decompressed entry in memory to slice it. Extracting a
|
|
62
|
+
single 7z entry in memory (`MemoryExtractor`) extracts just that
|
|
63
|
+
entry instead of decompressing the entire archive.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- Boundary errors stop masquerading as bare RuntimeErrors:
|
|
67
|
+
`Checksums::Verifier` raises `UnknownChecksumError` (the class
|
|
68
|
+
existed unused), and thirty more `raise "string"` sites gained
|
|
69
|
+
their matching classes (`Errno::ENOENT` for missing entries
|
|
70
|
+
— matching the handler-registry convention — `Errno::EEXIST`,
|
|
71
|
+
`IOError`, `InvalidArchiveError`, `DecompressionError`,
|
|
72
|
+
`RarNotAvailableError`). The never-called placeholder
|
|
73
|
+
`decode_escape_code` in the RAR PPMd decoder is deleted.
|
|
74
|
+
|
|
10
75
|
## [0.3.52] - 2026-09-01
|
|
11
76
|
|
|
12
77
|
### Changed
|
|
@@ -31,7 +31,7 @@ module Omnizip
|
|
|
31
31
|
|
|
32
32
|
# Read bytes from dictionary at a distance back
|
|
33
33
|
def read_bytes(distance, length)
|
|
34
|
-
raise "Invalid distance: #{distance}" if distance > @buffer.bytesize
|
|
34
|
+
raise Omnizip::DecompressionError, "Invalid distance: #{distance}" if distance > @buffer.bytesize
|
|
35
35
|
|
|
36
36
|
result = String.new(encoding: Encoding::BINARY)
|
|
37
37
|
src_pos = @buffer.bytesize - distance
|
|
@@ -46,7 +46,7 @@ module Omnizip
|
|
|
46
46
|
|
|
47
47
|
# Get byte at distance back
|
|
48
48
|
def get_byte(distance)
|
|
49
|
-
raise "Invalid distance: #{distance}" if distance > @buffer.bytesize
|
|
49
|
+
raise Omnizip::DecompressionError, "Invalid distance: #{distance}" if distance > @buffer.bytesize
|
|
50
50
|
|
|
51
51
|
@buffer.getbyte(@buffer.bytesize - distance)
|
|
52
52
|
end
|
|
@@ -118,12 +118,12 @@ module Omnizip
|
|
|
118
118
|
# Step 1: Parse properties byte (SEQ_PROPERTIES)
|
|
119
119
|
# Reference: alone_decoder.c:64-68
|
|
120
120
|
props = @input.getbyte
|
|
121
|
-
raise "Invalid .lzma header: missing properties byte" if props.nil?
|
|
121
|
+
raise Omnizip::DecompressionError, "Invalid .lzma header: missing properties byte" if props.nil?
|
|
122
122
|
|
|
123
123
|
# Use XZ Utils property byte parsing
|
|
124
124
|
# Reference: /Users/mulgogi/src/external/xz/src/liblzma/lzma/lzma_decoder.c:1216-1228
|
|
125
125
|
if props > MAX_PROPERTY_BYTE
|
|
126
|
-
raise "Invalid .lzma header: properties byte #{props} exceeds maximum #{MAX_PROPERTY_BYTE}"
|
|
126
|
+
raise Omnizip::DecompressionError, "Invalid .lzma header: properties byte #{props} exceeds maximum #{MAX_PROPERTY_BYTE}"
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
# Parse lc, lp, pb from properties byte
|
|
@@ -136,7 +136,7 @@ module Omnizip
|
|
|
136
136
|
# Validate lc + lp <= 4 (LZMA_LCLP_MAX)
|
|
137
137
|
# Reference: lzma_decoder.c:1227
|
|
138
138
|
if @lc + @lp > 4
|
|
139
|
-
raise "Invalid .lzma header: lc (#{@lc}) + lp (#{@lp}) exceeds maximum 4"
|
|
139
|
+
raise Omnizip::DecompressionError, "Invalid .lzma header: lc (#{@lc}) + lp (#{@lp}) exceeds maximum 4"
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# Step 2: Parse dictionary size (SEQ_DICTIONARY_SIZE)
|
|
@@ -144,7 +144,7 @@ module Omnizip
|
|
|
144
144
|
@dict_size = 0
|
|
145
145
|
4.times do |i|
|
|
146
146
|
byte = @input.getbyte
|
|
147
|
-
raise "Incomplete .lzma header: missing dictionary size byte" if byte.nil?
|
|
147
|
+
raise Omnizip::DecompressionError, "Incomplete .lzma header: missing dictionary size byte" if byte.nil?
|
|
148
148
|
|
|
149
149
|
@dict_size |= (byte << (i * 8))
|
|
150
150
|
end
|
|
@@ -163,7 +163,7 @@ module Omnizip
|
|
|
163
163
|
d += 1
|
|
164
164
|
|
|
165
165
|
if d != @dict_size
|
|
166
|
-
raise "Invalid .lzma header: dictionary size #{@dict_size} is not 2^n or 2^n + 2^(n-1)"
|
|
166
|
+
raise Omnizip::DecompressionError, "Invalid .lzma header: dictionary size #{@dict_size} is not 2^n or 2^n + 2^(n-1)"
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
@@ -172,7 +172,7 @@ module Omnizip
|
|
|
172
172
|
@uncompressed_size = 0
|
|
173
173
|
8.times do |i|
|
|
174
174
|
byte = @input.getbyte
|
|
175
|
-
raise "Incomplete .lzma header: missing uncompressed size byte" if byte.nil?
|
|
175
|
+
raise Omnizip::DecompressionError, "Incomplete .lzma header: missing uncompressed size byte" if byte.nil?
|
|
176
176
|
|
|
177
177
|
@uncompressed_size |= (byte << (i * 8))
|
|
178
178
|
end
|
|
@@ -182,7 +182,7 @@ module Omnizip
|
|
|
182
182
|
# Reference: alone_decoder.c:116-120
|
|
183
183
|
if @picky && @uncompressed_size != 0xFFFFFFFFFFFFFFFF &&
|
|
184
184
|
@uncompressed_size >= MAX_UNCOMPRESSED_SIZE
|
|
185
|
-
raise "Invalid .lzma header: uncompressed size #{@uncompressed_size} exceeds maximum #{MAX_UNCOMPRESSED_SIZE}"
|
|
185
|
+
raise Omnizip::DecompressionError, "Invalid .lzma header: uncompressed size #{@uncompressed_size} exceeds maximum #{MAX_UNCOMPRESSED_SIZE}"
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
# Note: XZ Utils uses UINT64_MAX (0xFFFFFFFFFFFFFFFF) for unknown size
|
|
@@ -97,7 +97,7 @@ module Omnizip
|
|
|
97
97
|
# @param prob [BitModel] Probability model
|
|
98
98
|
# @param bit [Integer] Bit value (0 or 1)
|
|
99
99
|
def queue_bit(prob, bit)
|
|
100
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
100
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
101
101
|
|
|
102
102
|
@symbols[@count] = bit.zero? ? RC_BIT_0 : RC_BIT_1
|
|
103
103
|
@probs[@count] = prob
|
|
@@ -111,7 +111,7 @@ module Omnizip
|
|
|
111
111
|
def queue_direct_bits(value, num_bits)
|
|
112
112
|
num_bits.downto(1) do |i|
|
|
113
113
|
bit = (value >> (i - 1)) & 1
|
|
114
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
114
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
115
115
|
|
|
116
116
|
@symbols[@count] = bit.zero? ? RC_DIRECT_0 : RC_DIRECT_1
|
|
117
117
|
@probs[@count] = nil
|
|
@@ -122,7 +122,7 @@ module Omnizip
|
|
|
122
122
|
# Queue flush operation
|
|
123
123
|
def queue_flush
|
|
124
124
|
5.times do
|
|
125
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
125
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
126
126
|
|
|
127
127
|
@symbols[@count] = RC_FLUSH
|
|
128
128
|
@probs[@count] = nil
|
|
@@ -237,7 +237,7 @@ module Omnizip
|
|
|
237
237
|
|
|
238
238
|
# Forget queued symbols (e.g., when output limit reached)
|
|
239
239
|
def forget
|
|
240
|
-
raise "Cannot forget with partial encoding" unless @pos.zero?
|
|
240
|
+
raise Omnizip::CompressionError, "Cannot forget with partial encoding" unless @pos.zero?
|
|
241
241
|
|
|
242
242
|
@count = 0
|
|
243
243
|
end
|
|
@@ -63,7 +63,7 @@ module Omnizip
|
|
|
63
63
|
|
|
64
64
|
# Forget pending symbols (matches XZ Utils rc_forget)
|
|
65
65
|
def forget
|
|
66
|
-
raise "Cannot forget while encoding" if @pos != 0
|
|
66
|
+
raise Omnizip::CompressionError, "Cannot forget while encoding" if @pos != 0
|
|
67
67
|
|
|
68
68
|
@count = 0
|
|
69
69
|
end
|
|
@@ -73,7 +73,7 @@ module Omnizip
|
|
|
73
73
|
# @param prob [Probability] Probability model
|
|
74
74
|
# @param bit [Integer] Bit value (0 or 1)
|
|
75
75
|
def bit(prob, bit)
|
|
76
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
76
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
77
77
|
|
|
78
78
|
@symbols[@count] = bit
|
|
79
79
|
@probs[@count] = prob
|
|
@@ -118,7 +118,7 @@ module Omnizip
|
|
|
118
118
|
# @param bit_count [Integer] Number of bits
|
|
119
119
|
def direct(value, bit_count)
|
|
120
120
|
bit_count.times do
|
|
121
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
121
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
122
122
|
|
|
123
123
|
@symbols[@count] = RC_DIRECT_0 | ((value >> (bit_count -= 1)) & 1)
|
|
124
124
|
@probs[@count] = nil
|
|
@@ -129,7 +129,7 @@ module Omnizip
|
|
|
129
129
|
# Queue flush operation (matches XZ Utils rc_flush)
|
|
130
130
|
def flush
|
|
131
131
|
5.times do
|
|
132
|
-
raise "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
132
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count >= RC_SYMBOLS_MAX
|
|
133
133
|
|
|
134
134
|
@symbols[@count] = RC_FLUSH
|
|
135
135
|
@probs[@count] = nil
|
|
@@ -159,7 +159,7 @@ module Omnizip
|
|
|
159
159
|
# @param out_size [Integer] Output buffer size
|
|
160
160
|
# @return [Boolean] True if output buffer filled before encoding complete
|
|
161
161
|
def encode(out, out_pos, out_size)
|
|
162
|
-
raise "Symbol buffer overflow" if @count > RC_SYMBOLS_MAX
|
|
162
|
+
raise Omnizip::CompressionError, "Symbol buffer overflow" if @count > RC_SYMBOLS_MAX
|
|
163
163
|
|
|
164
164
|
skip_increment = false
|
|
165
165
|
|
|
@@ -214,7 +214,7 @@ module Omnizip
|
|
|
214
214
|
break
|
|
215
215
|
|
|
216
216
|
else
|
|
217
|
-
raise "Unknown symbol type: #{@symbols[@pos]}"
|
|
217
|
+
raise Omnizip::CompressionError, "Unknown symbol type: #{@symbols[@pos]}"
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
@pos += 1 unless skip_increment
|
|
@@ -288,7 +288,7 @@ module Omnizip
|
|
|
288
288
|
|
|
289
289
|
# Forget pending symbols (matches XZ Utils rc_forget)
|
|
290
290
|
def forget
|
|
291
|
-
raise "Cannot forget while encoding" if @pos != 0
|
|
291
|
+
raise Omnizip::CompressionError, "Cannot forget while encoding" if @pos != 0
|
|
292
292
|
|
|
293
293
|
@count = 0
|
|
294
294
|
end
|
|
@@ -318,7 +318,7 @@ check_rc_finished: true)
|
|
|
318
318
|
|
|
319
319
|
# Handle nil @pos or limit gracefully
|
|
320
320
|
if limit && (@pos.nil? || limit.nil?)
|
|
321
|
-
raise "Invalid state: @pos=#{@pos.inspect}, limit=#{limit.inspect}"
|
|
321
|
+
raise Omnizip::DecompressionError, "Invalid state: @pos=#{@pos.inspect}, limit=#{limit.inspect}"
|
|
322
322
|
end
|
|
323
323
|
|
|
324
324
|
# Circular buffer: flush output before it can be overwritten.
|
|
@@ -757,7 +757,7 @@ check_rc_finished: true)
|
|
|
757
757
|
def read_header
|
|
758
758
|
# Property byte
|
|
759
759
|
props = @input.getbyte
|
|
760
|
-
raise "Invalid LZMA header" if props.nil?
|
|
760
|
+
raise Omnizip::DecompressionError, "Invalid LZMA header" if props.nil?
|
|
761
761
|
|
|
762
762
|
@lc = props % 9
|
|
763
763
|
remainder = props / 9
|
|
@@ -768,7 +768,7 @@ check_rc_finished: true)
|
|
|
768
768
|
@dict_size = 0
|
|
769
769
|
4.times do |i|
|
|
770
770
|
byte = @input.getbyte
|
|
771
|
-
raise "Incomplete header" if byte.nil?
|
|
771
|
+
raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
|
|
772
772
|
|
|
773
773
|
@dict_size |= (byte << (i * 8))
|
|
774
774
|
end
|
|
@@ -780,7 +780,7 @@ check_rc_finished: true)
|
|
|
780
780
|
@uncompressed_size = 0
|
|
781
781
|
8.times do |i|
|
|
782
782
|
byte = @input.getbyte
|
|
783
|
-
raise "Incomplete header" if byte.nil?
|
|
783
|
+
raise Omnizip::DecompressionError, "Incomplete header" if byte.nil?
|
|
784
784
|
|
|
785
785
|
@uncompressed_size |= (byte << (i * 8))
|
|
786
786
|
end
|
|
@@ -791,9 +791,9 @@ check_rc_finished: true)
|
|
|
791
791
|
# @return [void]
|
|
792
792
|
# @raise [RuntimeError] If parameters are invalid
|
|
793
793
|
def validate_parameters
|
|
794
|
-
raise "Invalid lc (#{@lc})" unless @lc.between?(0, 8)
|
|
795
|
-
raise "Invalid lp (#{@lp})" unless @lp.between?(0, 4)
|
|
796
|
-
raise "Invalid pb (#{@pb})" unless @pb.between?(0, 4)
|
|
794
|
+
raise Omnizip::DecompressionError, "Invalid lc (#{@lc})" unless @lc.between?(0, 8)
|
|
795
|
+
raise Omnizip::DecompressionError, "Invalid lp (#{@lp})" unless @lp.between?(0, 4)
|
|
796
|
+
raise Omnizip::DecompressionError, "Invalid pb (#{@pb})" unless @pb.between?(0, 4)
|
|
797
797
|
end
|
|
798
798
|
|
|
799
799
|
# Initialize probability models
|
|
@@ -1201,7 +1201,7 @@ check_rc_finished: true)
|
|
|
1201
1201
|
# @dict_full is clamped at @dict_size. Use actual bytes written for validation.
|
|
1202
1202
|
actual_bytes_written = @pos - LZ_DICT_INIT_POS
|
|
1203
1203
|
unless actual_bytes_written > distance
|
|
1204
|
-
raise "Invalid rep distance: #{distance} (bytes_written: #{actual_bytes_written})"
|
|
1204
|
+
raise Omnizip::DecompressionError, "Invalid rep distance: #{distance} (bytes_written: #{actual_bytes_written})"
|
|
1205
1205
|
end
|
|
1206
1206
|
|
|
1207
1207
|
# IMPORTANT: Limit match length to not exceed uncompressed_size
|
|
@@ -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
|
|
@@ -159,18 +160,28 @@ module Omnizip
|
|
|
159
160
|
Omnizip::Buffer.detect_format(@buffer)
|
|
160
161
|
end
|
|
161
162
|
|
|
162
|
-
#
|
|
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
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Extract all entries from ZIP through the native reader
|
|
163
175
|
#
|
|
164
176
|
# @param result [Hash] Hash to populate with entries
|
|
165
177
|
def extract_all_zip(result)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
while (entry = zis.get_next_entry)
|
|
178
|
+
with_zip_reader do |reader|
|
|
179
|
+
reader.entries.each do |entry|
|
|
169
180
|
next if entry.directory?
|
|
170
181
|
|
|
171
|
-
content =
|
|
172
|
-
result[entry.
|
|
173
|
-
@extracted_cache[entry.
|
|
182
|
+
content = reader.read_entry(entry.filename)
|
|
183
|
+
result[entry.filename] = content
|
|
184
|
+
@extracted_cache[entry.filename] = content
|
|
174
185
|
end
|
|
175
186
|
end
|
|
176
187
|
end
|
|
@@ -199,36 +210,23 @@ module Omnizip
|
|
|
199
210
|
end
|
|
200
211
|
end
|
|
201
212
|
|
|
202
|
-
# Extract single entry from ZIP
|
|
213
|
+
# Extract single entry from ZIP through the native reader
|
|
203
214
|
#
|
|
204
215
|
# @param name [String] Entry name
|
|
205
216
|
# @return [String, nil] Entry content or nil if not found
|
|
206
217
|
def extract_entry_zip(name)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Omnizip::Zip::InputStream.open(@buffer) do |zis|
|
|
211
|
-
while (entry = zis.get_next_entry)
|
|
212
|
-
if entry.name == name
|
|
213
|
-
content = zis.read unless entry.directory?
|
|
214
|
-
break
|
|
215
|
-
end
|
|
216
|
-
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?
|
|
217
221
|
end
|
|
218
|
-
|
|
219
|
-
content
|
|
220
222
|
end
|
|
221
223
|
|
|
222
|
-
# List all entry names from ZIP
|
|
224
|
+
# List all entry names from ZIP through the native reader
|
|
223
225
|
#
|
|
224
226
|
# @param names [Array] Array to populate with names
|
|
225
227
|
def list_entries_zip(names)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Omnizip::Zip::InputStream.open(@buffer) do |zis|
|
|
229
|
-
while (entry = zis.get_next_entry)
|
|
230
|
-
names << entry.name
|
|
231
|
-
end
|
|
228
|
+
with_zip_reader do |reader|
|
|
229
|
+
reader.entries.each { |entry| names << entry.filename }
|
|
232
230
|
end
|
|
233
231
|
end
|
|
234
232
|
end
|
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
|
|
@@ -150,27 +151,33 @@ module Omnizip
|
|
|
150
151
|
|
|
151
152
|
private
|
|
152
153
|
|
|
153
|
-
# Create ZIP archive in buffer
|
|
154
|
+
# Create ZIP archive in buffer through the native writer
|
|
154
155
|
#
|
|
155
156
|
# @param buffer [StringIO] Buffer to write to
|
|
156
157
|
# @param options [Hash] ZIP-specific options
|
|
157
158
|
# @yield [archive] Block to populate archive
|
|
158
159
|
def create_zip(buffer, _options, &block)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
writer = Formats::Zip::Writer.new(nil)
|
|
161
|
+
archive = Buffer::MemoryArchive.new(writer, :zip)
|
|
162
|
+
block&.call(archive)
|
|
163
|
+
writer.write_to_io(buffer)
|
|
163
164
|
end
|
|
164
165
|
|
|
165
|
-
# 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).
|
|
166
169
|
#
|
|
167
170
|
# @param buffer [StringIO] Buffer containing ZIP data
|
|
168
171
|
# @yield [archive] Block to read from archive
|
|
169
172
|
# @return [MemoryArchive, Object] Archive or block return value
|
|
170
173
|
def open_zip(buffer, &block)
|
|
171
174
|
result = nil
|
|
172
|
-
|
|
173
|
-
|
|
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)
|
|
174
181
|
result = block ? yield(archive) : archive
|
|
175
182
|
end
|
|
176
183
|
result
|