philiprehberger-gzip_kit 0.3.0 → 0.4.0
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 +7 -0
- data/README.md +40 -5
- data/lib/philiprehberger/gzip_kit/version.rb +1 -1
- data/lib/philiprehberger/gzip_kit.rb +99 -15
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc38727681cc1557d80be3bdf12969c479c53bdf2b67d48b88c35286ed2bfed2
|
|
4
|
+
data.tar.gz: 723760091ce30d192a0bc0fe8a921dc2c5da331b3c068c7d3e5bc70edcd71c76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4db3b719e3188b6c0011eecf3e26ea2e4f2386fb76d0ed0a2aac0f82d43a383107e2acdec7cc8adeee288aa5716ce9114678b0d468afadf991741e29a9a13f11
|
|
7
|
+
data.tar.gz: 363c8fdd0ca90ace3235e6c27e3f8b014391709d8208f2d5bb64a881832dfd8159159412ae2abe848a1175b46f324df183f52383f1838990262a7b40fbf05800
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Optional `chunk_size:` keyword on `compress_stream`, `decompress_stream`, `compress_file`, `decompress_file` (defaults to 64 KB).
|
|
14
|
+
- Optional `stats:` keyword on `decompress` — returns `{ data:, ratio: }` when true.
|
|
15
|
+
- Module-level YARD overview and `@example` blocks on primary methods.
|
|
16
|
+
|
|
10
17
|
## [0.3.0] - 2026-04-16
|
|
11
18
|
|
|
12
19
|
### Added
|
data/README.md
CHANGED
|
@@ -108,6 +108,41 @@ File.open("output.gz", "rb") do |input|
|
|
|
108
108
|
end
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### Custom chunk size
|
|
112
|
+
|
|
113
|
+
Streaming and file methods read in 64 KB chunks by default. Override with `chunk_size:` to tune for memory constraints or throughput:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
require "philiprehberger/gzip_kit"
|
|
117
|
+
|
|
118
|
+
# Smaller chunks for memory-constrained environments
|
|
119
|
+
File.open("input.txt", "rb") do |input|
|
|
120
|
+
File.open("output.gz", "wb") do |output|
|
|
121
|
+
Philiprehberger::GzipKit.compress_stream(input, output, chunk_size: 4 * 1024)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Larger chunks for bulk throughput
|
|
126
|
+
Philiprehberger::GzipKit.compress_file("input.bin", "output.gz", chunk_size: 1 * 1024 * 1024)
|
|
127
|
+
Philiprehberger::GzipKit.decompress_file("output.gz", "restored.bin", chunk_size: 1 * 1024 * 1024)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`chunk_size:` must be a positive `Integer`; any other value raises `ArgumentError`.
|
|
131
|
+
|
|
132
|
+
### Decompress with stats
|
|
133
|
+
|
|
134
|
+
Mirror of `compress(..., stats: true)` — returns a hash with the decompressed payload and the compressed-to-decompressed size ratio:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
require "philiprehberger/gzip_kit"
|
|
138
|
+
|
|
139
|
+
compressed = Philiprehberger::GzipKit.compress("a" * 10_000)
|
|
140
|
+
result = Philiprehberger::GzipKit.decompress(compressed, stats: true)
|
|
141
|
+
# => { data: "aaaa...", ratio: 0.0041 }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The ratio is `compressed_size / decompressed_size` (inverse of compression ratio). For zero-length output, ratio is `0.0`.
|
|
145
|
+
|
|
111
146
|
### Stream Concatenation
|
|
112
147
|
|
|
113
148
|
```ruby
|
|
@@ -148,12 +183,12 @@ header = Philiprehberger::GzipKit.inspect_header(gzip_data)
|
|
|
148
183
|
| Method | Description |
|
|
149
184
|
|--------|-------------|
|
|
150
185
|
| `GzipKit.compress(string, level:, stats:)` | Compress a string to gzip bytes; returns stats hash when `stats: true` |
|
|
151
|
-
| `GzipKit.decompress(data)` | Decompress gzip bytes to a string |
|
|
186
|
+
| `GzipKit.decompress(data, stats:)` | Decompress gzip bytes to a string; returns `{ data:, ratio: }` when `stats: true` |
|
|
152
187
|
| `GzipKit.compressed?(data)` | Check if data is gzip-compressed via magic bytes |
|
|
153
|
-
| `GzipKit.compress_file(src, dest, level:, &block)` | Compress a file to a gzip file with optional progress callback |
|
|
154
|
-
| `GzipKit.decompress_file(src, dest, &block)` | Decompress a gzip file with optional progress callback |
|
|
155
|
-
| `GzipKit.compress_stream(io_in, io_out, level:)` | Streaming compression
|
|
156
|
-
| `GzipKit.decompress_stream(io_in, io_out)` | Streaming decompression
|
|
188
|
+
| `GzipKit.compress_file(src, dest, level:, chunk_size:, &block)` | Compress a file to a gzip file with optional progress callback and chunk size |
|
|
189
|
+
| `GzipKit.decompress_file(src, dest, chunk_size:, &block)` | Decompress a gzip file with optional progress callback and chunk size |
|
|
190
|
+
| `GzipKit.compress_stream(io_in, io_out, level:, chunk_size:)` | Streaming compression (64 KB chunks by default) |
|
|
191
|
+
| `GzipKit.decompress_stream(io_in, io_out, chunk_size:)` | Streaming decompression (64 KB chunks by default) |
|
|
157
192
|
| `GzipKit.concat(data_a, data_b)` | Concatenate two gzip-compressed strings |
|
|
158
193
|
| `GzipKit.equivalent?(blob_a, blob_b)` | Check whether two gzip blobs decompress to equal byte strings |
|
|
159
194
|
| `GzipKit.inspect_header(data)` | Read gzip header metadata without decompressing |
|
|
@@ -5,6 +5,22 @@ require 'stringio'
|
|
|
5
5
|
require_relative 'gzip_kit/version'
|
|
6
6
|
|
|
7
7
|
module Philiprehberger
|
|
8
|
+
# GzipKit provides gzip compression and decompression with streaming support.
|
|
9
|
+
#
|
|
10
|
+
# The module exposes both string-oriented and IO-oriented entry points:
|
|
11
|
+
#
|
|
12
|
+
# - {GzipKit.compress} / {GzipKit.decompress} for in-memory string data
|
|
13
|
+
# - {GzipKit.compress_stream} / {GzipKit.decompress_stream} for IO-to-IO streaming
|
|
14
|
+
# - {GzipKit.compress_file} / {GzipKit.decompress_file} for file-to-file transforms
|
|
15
|
+
# - {GzipKit.compressed?} / {GzipKit.inspect_header} for gzip detection and header inspection
|
|
16
|
+
# - {GzipKit.concat} / {GzipKit.equivalent?} for combining and comparing gzip blobs
|
|
17
|
+
#
|
|
18
|
+
# Streaming and file methods read in 64 KB chunks by default. The chunk size can be
|
|
19
|
+
# tuned via the +chunk_size:+ keyword when dealing with very small or very large payloads.
|
|
20
|
+
#
|
|
21
|
+
# @example Compress and decompress a string
|
|
22
|
+
# compressed = Philiprehberger::GzipKit.compress('hello')
|
|
23
|
+
# Philiprehberger::GzipKit.decompress(compressed) # => "hello"
|
|
8
24
|
module GzipKit
|
|
9
25
|
class Error < StandardError; end
|
|
10
26
|
|
|
@@ -17,6 +33,14 @@ module Philiprehberger
|
|
|
17
33
|
# @param level [Integer] compression level (Zlib::DEFAULT_COMPRESSION by default)
|
|
18
34
|
# @param stats [Boolean] when true, return a hash with compression statistics
|
|
19
35
|
# @return [String, Hash] gzip-compressed bytes, or a stats hash when stats: true
|
|
36
|
+
#
|
|
37
|
+
# @example Compress a string
|
|
38
|
+
# Philiprehberger::GzipKit.compress('hello, world!')
|
|
39
|
+
# # => "\x1F\x8B\b\x00..." (binary gzip bytes)
|
|
40
|
+
#
|
|
41
|
+
# @example Compress with stats
|
|
42
|
+
# Philiprehberger::GzipKit.compress('a' * 10_000, stats: true)
|
|
43
|
+
# # => { data: "...", ratio: 0.99, original_size: 10000, compressed_size: 41 }
|
|
20
44
|
def self.compress(string, level: Zlib::DEFAULT_COMPRESSION, stats: false)
|
|
21
45
|
io_out = StringIO.new
|
|
22
46
|
io_out.binmode
|
|
@@ -43,9 +67,19 @@ module Philiprehberger
|
|
|
43
67
|
# Decompress gzip bytes to a string.
|
|
44
68
|
#
|
|
45
69
|
# @param data [String] gzip-compressed bytes
|
|
46
|
-
# @
|
|
70
|
+
# @param stats [Boolean] when true, return a hash with decompression statistics
|
|
71
|
+
# @return [String, Hash] decompressed string, or a stats hash when stats: true
|
|
47
72
|
# @raise [Zlib::GzipFile::Error] if the data is not valid gzip
|
|
48
|
-
|
|
73
|
+
#
|
|
74
|
+
# @example Decompress gzip bytes
|
|
75
|
+
# compressed = Philiprehberger::GzipKit.compress('hello')
|
|
76
|
+
# Philiprehberger::GzipKit.decompress(compressed) # => "hello"
|
|
77
|
+
#
|
|
78
|
+
# @example Decompress with stats
|
|
79
|
+
# compressed = Philiprehberger::GzipKit.compress('a' * 10_000)
|
|
80
|
+
# Philiprehberger::GzipKit.decompress(compressed, stats: true)
|
|
81
|
+
# # => { data: "aaaa...", ratio: 0.0041 }
|
|
82
|
+
def self.decompress(data, stats: false)
|
|
49
83
|
io_in = StringIO.new(data)
|
|
50
84
|
io_in.binmode
|
|
51
85
|
result = String.new(encoding: Encoding::BINARY)
|
|
@@ -62,7 +96,16 @@ module Philiprehberger
|
|
|
62
96
|
end
|
|
63
97
|
end
|
|
64
98
|
|
|
65
|
-
result.force_encoding(Encoding::UTF_8)
|
|
99
|
+
decompressed = result.force_encoding(Encoding::UTF_8)
|
|
100
|
+
|
|
101
|
+
if stats
|
|
102
|
+
decompressed_size = decompressed.bytesize
|
|
103
|
+
compressed_size = data.bytesize
|
|
104
|
+
ratio = decompressed_size.zero? ? 0.0 : compressed_size.to_f / decompressed_size
|
|
105
|
+
{ data: decompressed, ratio: ratio }
|
|
106
|
+
else
|
|
107
|
+
decompressed
|
|
108
|
+
end
|
|
66
109
|
end
|
|
67
110
|
|
|
68
111
|
# Check if data is gzip-compressed by inspecting magic bytes.
|
|
@@ -81,25 +124,29 @@ module Philiprehberger
|
|
|
81
124
|
# @param src [String] path to the source file
|
|
82
125
|
# @param dest [String] path to the destination gzip file
|
|
83
126
|
# @param level [Integer] compression level (Zlib::DEFAULT_COMPRESSION by default)
|
|
127
|
+
# @param chunk_size [Integer] bytes per read chunk (defaults to 64 KB)
|
|
84
128
|
# @yield [bytes_processed, total_bytes] progress callback
|
|
85
129
|
# @yieldparam bytes_processed [Integer] bytes processed so far
|
|
86
130
|
# @yieldparam total_bytes [Integer] total file size
|
|
87
131
|
# @return [void]
|
|
88
|
-
|
|
132
|
+
# @raise [ArgumentError] if chunk_size is not a positive Integer
|
|
133
|
+
def self.compress_file(src, dest, level: Zlib::DEFAULT_COMPRESSION, chunk_size: CHUNK_SIZE, &block)
|
|
134
|
+
validate_chunk_size!(chunk_size)
|
|
135
|
+
|
|
89
136
|
File.open(src, 'rb') do |io_in|
|
|
90
137
|
File.open(dest, 'wb') do |io_out|
|
|
91
138
|
if block
|
|
92
139
|
total_bytes = File.size(src)
|
|
93
140
|
bytes_processed = 0
|
|
94
141
|
gz = Zlib::GzipWriter.new(io_out, level)
|
|
95
|
-
while (chunk = io_in.read(
|
|
142
|
+
while (chunk = io_in.read(chunk_size))
|
|
96
143
|
gz.write(chunk)
|
|
97
144
|
bytes_processed += chunk.bytesize
|
|
98
145
|
block.call(bytes_processed, total_bytes)
|
|
99
146
|
end
|
|
100
147
|
gz.finish
|
|
101
148
|
else
|
|
102
|
-
compress_stream(io_in, io_out, level: level)
|
|
149
|
+
compress_stream(io_in, io_out, level: level, chunk_size: chunk_size)
|
|
103
150
|
end
|
|
104
151
|
end
|
|
105
152
|
end
|
|
@@ -109,24 +156,28 @@ module Philiprehberger
|
|
|
109
156
|
#
|
|
110
157
|
# @param src [String] path to the gzip source file
|
|
111
158
|
# @param dest [String] path to the destination file
|
|
159
|
+
# @param chunk_size [Integer] bytes per read chunk (defaults to 64 KB)
|
|
112
160
|
# @yield [bytes_processed, total_bytes] progress callback
|
|
113
161
|
# @yieldparam bytes_processed [Integer] bytes decompressed so far
|
|
114
162
|
# @yieldparam total_bytes [nil] always nil (total unknown until decompression completes)
|
|
115
163
|
# @return [void]
|
|
116
|
-
|
|
164
|
+
# @raise [ArgumentError] if chunk_size is not a positive Integer
|
|
165
|
+
def self.decompress_file(src, dest, chunk_size: CHUNK_SIZE, &block)
|
|
166
|
+
validate_chunk_size!(chunk_size)
|
|
167
|
+
|
|
117
168
|
File.open(src, 'rb') do |io_in|
|
|
118
169
|
File.open(dest, 'wb') do |io_out|
|
|
119
170
|
if block
|
|
120
171
|
gz = Zlib::GzipReader.new(io_in)
|
|
121
172
|
bytes_processed = 0
|
|
122
|
-
while (chunk = gz.read(
|
|
173
|
+
while (chunk = gz.read(chunk_size))
|
|
123
174
|
io_out.write(chunk)
|
|
124
175
|
bytes_processed += chunk.bytesize
|
|
125
176
|
block.call(bytes_processed, nil)
|
|
126
177
|
end
|
|
127
178
|
gz.close
|
|
128
179
|
else
|
|
129
|
-
decompress_stream(io_in, io_out)
|
|
180
|
+
decompress_stream(io_in, io_out, chunk_size: chunk_size)
|
|
130
181
|
end
|
|
131
182
|
end
|
|
132
183
|
end
|
|
@@ -191,32 +242,65 @@ module Philiprehberger
|
|
|
191
242
|
gz&.close
|
|
192
243
|
end
|
|
193
244
|
|
|
194
|
-
# Streaming compression from one IO to another
|
|
245
|
+
# Streaming compression from one IO to another.
|
|
195
246
|
#
|
|
196
247
|
# @param io_in [IO] readable input stream
|
|
197
248
|
# @param io_out [IO] writable output stream
|
|
198
249
|
# @param level [Integer] compression level (Zlib::DEFAULT_COMPRESSION by default)
|
|
250
|
+
# @param chunk_size [Integer] bytes per read chunk (defaults to 64 KB)
|
|
199
251
|
# @return [void]
|
|
200
|
-
|
|
252
|
+
# @raise [ArgumentError] if chunk_size is not a positive Integer
|
|
253
|
+
#
|
|
254
|
+
# @example Compress from one IO to another
|
|
255
|
+
# File.open('input.txt', 'rb') do |io_in|
|
|
256
|
+
# File.open('output.gz', 'wb') do |io_out|
|
|
257
|
+
# Philiprehberger::GzipKit.compress_stream(io_in, io_out)
|
|
258
|
+
# end
|
|
259
|
+
# end
|
|
260
|
+
#
|
|
261
|
+
# @example Tune the chunk size for small payloads
|
|
262
|
+
# Philiprehberger::GzipKit.compress_stream(io_in, io_out, chunk_size: 4 * 1024)
|
|
263
|
+
def self.compress_stream(io_in, io_out, level: Zlib::DEFAULT_COMPRESSION, chunk_size: CHUNK_SIZE)
|
|
264
|
+
validate_chunk_size!(chunk_size)
|
|
265
|
+
|
|
201
266
|
gz = Zlib::GzipWriter.new(io_out, level)
|
|
202
|
-
while (chunk = io_in.read(
|
|
267
|
+
while (chunk = io_in.read(chunk_size))
|
|
203
268
|
gz.write(chunk)
|
|
204
269
|
end
|
|
205
270
|
gz.finish
|
|
206
271
|
end
|
|
207
272
|
|
|
208
|
-
# Streaming decompression from one IO to another
|
|
273
|
+
# Streaming decompression from one IO to another.
|
|
209
274
|
#
|
|
210
275
|
# @param io_in [IO] readable input stream containing gzip data
|
|
211
276
|
# @param io_out [IO] writable output stream
|
|
277
|
+
# @param chunk_size [Integer] bytes per read chunk (defaults to 64 KB)
|
|
212
278
|
# @return [void]
|
|
213
|
-
|
|
279
|
+
# @raise [ArgumentError] if chunk_size is not a positive Integer
|
|
280
|
+
#
|
|
281
|
+
# @example Decompress from one IO to another
|
|
282
|
+
# File.open('output.gz', 'rb') do |io_in|
|
|
283
|
+
# File.open('restored.txt', 'wb') do |io_out|
|
|
284
|
+
# Philiprehberger::GzipKit.decompress_stream(io_in, io_out)
|
|
285
|
+
# end
|
|
286
|
+
# end
|
|
287
|
+
def self.decompress_stream(io_in, io_out, chunk_size: CHUNK_SIZE)
|
|
288
|
+
validate_chunk_size!(chunk_size)
|
|
289
|
+
|
|
214
290
|
gz = Zlib::GzipReader.new(io_in)
|
|
215
|
-
while (chunk = gz.read(
|
|
291
|
+
while (chunk = gz.read(chunk_size))
|
|
216
292
|
io_out.write(chunk)
|
|
217
293
|
end
|
|
218
294
|
ensure
|
|
219
295
|
gz&.close
|
|
220
296
|
end
|
|
297
|
+
|
|
298
|
+
# @api private
|
|
299
|
+
def self.validate_chunk_size!(chunk_size)
|
|
300
|
+
return if chunk_size.is_a?(Integer) && chunk_size.positive?
|
|
301
|
+
|
|
302
|
+
raise ArgumentError, "chunk_size must be a positive Integer, got: #{chunk_size.inspect}"
|
|
303
|
+
end
|
|
304
|
+
private_class_method :validate_chunk_size!
|
|
221
305
|
end
|
|
222
306
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-gzip_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Simple API for gzip compression and decompression with support for strings,
|
|
14
14
|
files, and IO streams. Configurable compression level. Built on Ruby stdlib zlib.
|