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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fef29fd051e2bb798e75f90e078d1cfa96ebab01bd9fbbffa55d1a88a4b2538b
4
- data.tar.gz: 3e283a8e03b6bae036eb2aee525d16aa59d8ff86751fb2e098087e6ee91d4e82
3
+ metadata.gz: fc38727681cc1557d80be3bdf12969c479c53bdf2b67d48b88c35286ed2bfed2
4
+ data.tar.gz: 723760091ce30d192a0bc0fe8a921dc2c5da331b3c068c7d3e5bc70edcd71c76
5
5
  SHA512:
6
- metadata.gz: 84fc3cbc8cea9b3f23936d1649c2ee0028aec9d3a726e06c36d0ef7572da7ee07b853387d41e2024a0357b72ec1dd5a9a67b7f4bd7bea7a79edd7d98102afab9
7
- data.tar.gz: 1546559f476ca7d4e2980abc3a88f27fb9205c7a7499f87ba1b876b26bc4c36cf5eb716d485db6c4a11562dd9af30ef6777b1bd3ea76c4e7e7dc46f221810c81
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 in 64KB chunks |
156
- | `GzipKit.decompress_stream(io_in, io_out)` | Streaming decompression in 64KB chunks |
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 |
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module GzipKit
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -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
- # @return [String] decompressed string
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
- def self.decompress(data)
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
- def self.compress_file(src, dest, level: Zlib::DEFAULT_COMPRESSION, &block)
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(CHUNK_SIZE))
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
- def self.decompress_file(src, dest, &block)
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(CHUNK_SIZE))
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, reading in 64KB chunks.
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
- def self.compress_stream(io_in, io_out, level: Zlib::DEFAULT_COMPRESSION)
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(CHUNK_SIZE))
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, reading in 64KB chunks.
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
- def self.decompress_stream(io_in, io_out)
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(CHUNK_SIZE))
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.3.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-16 00:00:00.000000000 Z
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.