brotli_splice 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0810be973e8e1ad5dd1b28a4733754bd2ec87f6e5d5c5c3e6a29d44589e505ab'
4
+ data.tar.gz: cc459d36cbf640e4fb47d426dbce7dcb68f4458cf6daf49080e7fa9d67d5e0bd
5
+ SHA512:
6
+ metadata.gz: 8af6e37050b29d440cf25ae1f97ea502d2dffa513516e8592216fd16664a3a68c8796121704dde5f10618eea4353ba39a9a117820f35de34a281a24775913b72
7
+ data.tar.gz: 5742e2a3183a99e2965d8dd2e8dbe3e3f93c8c7ea1cbe6e2bcfb065bf3ab82c5eb88b7f32590a4f36b98a6b18bf17025fa14600260e2334b2b797e1c0ece604f
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright 2026-present, Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # BrotliSplice
2
+
3
+ A Ruby C extension that produces Brotli-compressed streams with a fixed-length
4
+ **spliceable slot** — a region whose raw bytes can be overwritten without
5
+ decompressing or re-encoding the rest of the stream.
6
+
7
+ ## Use case
8
+
9
+ You have a large HTML document with a small secret (token, nonce, personalized
10
+ data) embedded at a known position. You want to:
11
+
12
+ 1. Brotli-compress the document **once** at build/deploy time
13
+ 2. Swap in a different secret **per request** with a simple `memcpy`
14
+
15
+ ## How it works
16
+
17
+ The stream is split into three chunks:
18
+
19
+ ```
20
+ [chunk1: compressed part1] [chunk2: uncompressed slot] [chunk3: compressed part3]
21
+ (FLUSH) (raw bytes) (STREAM_OFFSET + FINISH)
22
+ ```
23
+
24
+ - **chunk1** — standard Brotli compression of everything before the secret
25
+ - **chunk2** — an uncompressed meta-block (3-byte header + raw data), whose
26
+ bytes sit at a known offset in the stream
27
+ - **chunk3** — standard Brotli compression of everything after the secret,
28
+ produced by a second encoder with `BROTLI_PARAM_STREAM_OFFSET` so that
29
+ distance calculations, the ring buffer, and the WBITS header are all correct
30
+
31
+ The last 2 bytes of the secret slot are reserved as a fixed **context suffix**
32
+ (`\r\n`). These are baked into chunk3 and cannot be changed — the Brotli encoder
33
+ uses the preceding 2 bytes as literal context when compressing part3. The
34
+ replaceable portion is therefore `secret_length - 2` bytes.
35
+
36
+ Compression overhead vs standard Brotli is small — typically 100–200 bytes
37
+ for realistic HTML documents. Two sources of overhead:
38
+
39
+ 1. **FLUSH boundary** — the encoder can't carry pending matches from part1
40
+ across the slot. A very small part1 may show proportionally higher
41
+ overhead since this cost isn't amortized.
42
+ 2. **Part3 can't back-reference part1** — `STREAM_OFFSET` starts a fresh
43
+ encoder with an empty hash table, so part3 compresses in isolation.
44
+ If part1 and part3 share significant repeated content (e.g. identical
45
+ nav markup in header and footer), this gap widens. For typical HTML
46
+ where each section has plenty of self-repetition, the loss is small.
47
+
48
+ **Recommendation:** place the secret early in the document (e.g. in a
49
+ `<script>` tag right after `<head>`) so that part1 is small. This keeps
50
+ the FLUSH cost negligible and gives part3 nearly the entire document to
51
+ compress against itself.
52
+
53
+ ## Installation
54
+
55
+ Requires the Brotli C library (`libbrotlienc`, `libbrotlidec`, `libbrotlicommon`).
56
+
57
+ Add the gem to your Gemfile:
58
+
59
+ ```ruby
60
+ gem "brotli_splice"
61
+ ```
62
+
63
+ ```sh
64
+ # macOS (Homebrew)
65
+ brew install brotli
66
+
67
+ # Build and install the gem from this checkout
68
+ gem build brotli_splice.gemspec
69
+ gem install ./brotli_splice-*.gem
70
+ ```
71
+
72
+ ## Releasing
73
+
74
+ This gem is configured for public release on RubyGems.org.
75
+
76
+ After RubyGems Trusted Publishing is configured for this repository, publishing
77
+ a GitHub release runs the `Release` workflow and publishes the gem.
78
+
79
+ ```sh
80
+ gem build brotli_splice.gemspec
81
+ gem push brotli_splice-*.gem
82
+ ```
83
+
84
+ Alternatively, use Bundler's release task after tagging the version:
85
+
86
+ ```sh
87
+ bundle exec rake release
88
+ ```
89
+
90
+ For local extension development without installing the gem:
91
+
92
+ ```sh
93
+ cd ext/brotli_splice
94
+ ruby extconf.rb
95
+ make
96
+ cd ../..
97
+ ruby -Ilib test_ext.rb
98
+ ```
99
+
100
+ ## API
101
+
102
+ ### `BrotliSplice.encode(html, secret_offset, secret_length, quality: 11) → Hash`
103
+
104
+ Compress `html` with a spliceable slot at the given byte position.
105
+
106
+ **Parameters:**
107
+ - `html` — the full HTML string (binary encoding)
108
+ - `secret_offset` — byte offset where the replaceable section starts
109
+ - `secret_length` — total byte length of the replaceable section (must be > 2)
110
+ - `quality:` — Brotli quality level, 0–11 (default: 11)
111
+
112
+ **Returns** a Hash:
113
+ ```ruby
114
+ {
115
+ data: String, # the Brotli-compressed stream
116
+ secret_offset: Integer, # byte offset of the replaceable data within the compressed stream
117
+ secret_length: Integer, # replaceable byte count (= secret_length - 2)
118
+ context_suffix: String, # the 2 fixed context bytes ("\r\n")
119
+ }
120
+ ```
121
+
122
+ ### `BrotliSplice.replace(compressed_data, new_secret, secret_offset, secret_length) → String`
123
+
124
+ Replace the secret in a compressed stream. Returns a new string with the
125
+ swapped bytes. This is a pure `memcpy` — no compression or decompression.
126
+
127
+ **Parameters:**
128
+ - `compressed_data` — the Brotli stream from `encode`
129
+ - `new_secret` — replacement content, must be exactly `secret_length` bytes
130
+ - `secret_offset` — from the `encode` result
131
+ - `secret_length` — from the `encode` result
132
+
133
+ ## Example
134
+
135
+ ```ruby
136
+ require "brotli_splice"
137
+ require "brotli" # for verification
138
+
139
+ html = "<html>...TOKEN_PLACEHOLDER_HERE...</html>"
140
+ token_offset = html.index("TOKEN_PLACEHOLDER_HERE")
141
+ token_length = "TOKEN_PLACEHOLDER_HERE".bytesize + 2 # +2 for context suffix
142
+
143
+ # Compress once
144
+ result = BrotliSplice.encode(html, token_offset, token_length, quality: 11)
145
+
146
+ # Per-request: swap in the real token (must be result[:secret_length] bytes)
147
+ real_token = "user-specific-secret".ljust(result[:secret_length], "\0")
148
+ response = BrotliSplice.replace(result[:data], real_token,
149
+ result[:secret_offset], result[:secret_length])
150
+
151
+ # The response is a valid Brotli stream ready to send
152
+ decoded = Brotli.inflate(response)
153
+ # => "<html>...user-specific-secret\0\0\r\n...</html>"
154
+ ```
155
+
156
+ ## Decoded output
157
+
158
+ The decoded stream is the original HTML with the secret region replaced by:
159
+
160
+ ```
161
+ [replaceable bytes (secret_length - 2)] [context suffix "\r\n"]
162
+ ```
163
+
164
+ The 2-byte context suffix replaces the last 2 bytes of the original secret
165
+ region. Design your placeholder to account for this (e.g. make it 2 bytes
166
+ longer, or place the suffix where a line break is natural).
167
+
168
+ ## Limitations
169
+
170
+ - The replaceable slot must be ≤ 65534 bytes (64KB minus the 2-byte context)
171
+ - The replacement must be **exactly** `secret_length` bytes (no length changes)
172
+ - The last 2 bytes of the original secret region become `\r\n` in the output
173
+ - Only one spliceable slot per stream (encode with multiple slots would require
174
+ chaining encoders)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/brotli_splice/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "brotli_splice"
7
+ spec.version = BrotliSplice::VERSION
8
+ spec.summary = "Create Brotli streams with fixed-length spliceable slots"
9
+ spec.description = <<~DESC
10
+ BrotliSplice is a Ruby C extension that creates Brotli-compressed streams
11
+ containing a fixed-length uncompressed slot. The slot can be overwritten
12
+ with a simple byte copy, making it useful for injecting fixed-size secrets
13
+ or tokens into pre-compressed HTML responses.
14
+ DESC
15
+
16
+ spec.authors = ["Shopify"]
17
+ spec.email = ["gems@shopify.com"]
18
+ spec.homepage = "https://github.com/Shopify/brotli_splice"
19
+ spec.license = "MIT"
20
+
21
+ spec.required_ruby_version = ">= 3.1"
22
+ spec.require_paths = ["lib"]
23
+ spec.extensions = ["ext/brotli_splice/extconf.rb"]
24
+
25
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
26
+ spec.metadata["homepage_uri"] = spec.homepage
27
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/main"
28
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
29
+ spec.metadata["rubygems_mfa_required"] = "true"
30
+
31
+ spec.files = Dir.chdir(__dir__) do
32
+ Dir[
33
+ "lib/**/*.rb",
34
+ "ext/brotli_splice/**/*.{c,h,rb}",
35
+ "README.md",
36
+ "LICENSE.md",
37
+ "brotli_splice.gemspec",
38
+ ].reject { |file| File.directory?(file) }
39
+ end
40
+ end
@@ -0,0 +1,197 @@
1
+ /*
2
+ * BrotliSplice — Ruby C extension for spliceable Brotli encoding.
3
+ *
4
+ * Produces a Brotli stream with a fixed-length uncompressed slot whose
5
+ * raw bytes can be overwritten without re-encoding the rest.
6
+ *
7
+ * Uses the standard Brotli streaming API with BROTLI_PARAM_STREAM_OFFSET.
8
+ */
9
+
10
+ #include <ruby.h>
11
+ #include <ruby/encoding.h>
12
+ #include <brotli/encode.h>
13
+ #include <brotli/decode.h>
14
+ #include <string.h>
15
+
16
+ static VALUE mBrotliSplice;
17
+ static VALUE eBrotliSpliceError;
18
+
19
+ #define CTX_LEN 2
20
+ static const uint8_t CTX_BYTES[CTX_LEN] = { '\r', '\n' };
21
+
22
+ /* ── Streaming encoder helper ─────────────────────────────────── */
23
+ static size_t encoder_feed(BrotliEncoderState *s, BrotliEncoderOperation op,
24
+ const uint8_t *in, size_t in_len,
25
+ uint8_t *out, size_t out_cap) {
26
+ size_t avail_in = in_len, avail_out = out_cap, total = 0;
27
+ const uint8_t *next_in = in;
28
+ uint8_t *next_out = out;
29
+ if (!BrotliEncoderCompressStream(s, op,
30
+ &avail_in, &next_in, &avail_out, &next_out, &total)) {
31
+ return 0;
32
+ }
33
+ return out_cap - avail_out;
34
+ }
35
+
36
+ /* ── Build uncompressed meta-block (ISLAST=0) ─────────────────── */
37
+ static size_t make_uncompressed_block(uint8_t *out,
38
+ const uint8_t *data, size_t len) {
39
+ if (len == 0 || len > 65536) return 0;
40
+ uint32_t r = (uint32_t)(len - 1);
41
+ uint32_t nibbles = 0;
42
+ if (len > (1u << 16)) nibbles = (len > (1u << 20)) ? 2 : 1;
43
+ uint32_t bits = (nibbles << 1) | (r << 3) | (1u << (19 + 4 * nibbles));
44
+ out[0] = (uint8_t)bits;
45
+ out[1] = (uint8_t)(bits >> 8);
46
+ out[2] = (uint8_t)(bits >> 16);
47
+ size_t hdr = 3;
48
+ if (nibbles == 2) { out[3] = (uint8_t)(bits >> 24); hdr = 4; }
49
+ memcpy(out + hdr, data, len);
50
+ return hdr + len;
51
+ }
52
+
53
+ /*
54
+ * call-seq:
55
+ * BrotliSplice.encode(html, secret_offset, secret_length, quality: 11) -> Hash
56
+ *
57
+ * Brotli-encode +html+ with a spliceable slot at the given position.
58
+ *
59
+ * The slot's last 2 bytes are reserved as a fixed context suffix ("\r\n")
60
+ * and cannot be replaced. The replaceable portion is +secret_length - 2+
61
+ * bytes.
62
+ *
63
+ * Returns:
64
+ * { data: String, # the Brotli-encoded stream
65
+ * secret_offset: Integer, # byte offset of the replaceable data in the stream
66
+ * secret_length: Integer, # replaceable byte count (secret_length - 2)
67
+ * context_suffix: String } # the 2 fixed context bytes ("\r\n")
68
+ */
69
+ static VALUE rb_brotli_splice_encode(int argc, VALUE *argv, VALUE self) {
70
+ VALUE rb_html, rb_sec_off, rb_sec_len, rb_opts;
71
+ rb_scan_args(argc, argv, "3:", &rb_html, &rb_sec_off, &rb_sec_len, &rb_opts);
72
+
73
+ Check_Type(rb_html, T_STRING);
74
+ const uint8_t *html = (const uint8_t *)RSTRING_PTR(rb_html);
75
+ size_t html_len = RSTRING_LEN(rb_html);
76
+ size_t sec_off = NUM2SIZET(rb_sec_off);
77
+ size_t sec_total = NUM2SIZET(rb_sec_len);
78
+
79
+ if (sec_total <= CTX_LEN)
80
+ rb_raise(eBrotliSpliceError, "secret_length must be > %d", CTX_LEN);
81
+ if (sec_off + sec_total > html_len)
82
+ rb_raise(eBrotliSpliceError, "secret_offset + secret_length exceeds html size");
83
+
84
+ int quality = 11;
85
+ if (!NIL_P(rb_opts)) {
86
+ VALUE q = rb_hash_aref(rb_opts, ID2SYM(rb_intern("quality")));
87
+ if (!NIL_P(q)) quality = NUM2INT(q);
88
+ }
89
+
90
+ /* Split into part1 / secret_body / context / part3 */
91
+ const uint8_t *part1 = html;
92
+ size_t p1_len = sec_off;
93
+
94
+ size_t sec_body = sec_total - CTX_LEN;
95
+ const uint8_t *secret = html + sec_off; /* secret_body bytes */
96
+
97
+ const uint8_t *part3 = html + sec_off + sec_total;
98
+ size_t p3_len = html_len - sec_off - sec_total;
99
+
100
+ /* Allocate output buffer */
101
+ size_t out_cap = html_len + 65536;
102
+ uint8_t *out = xmalloc(out_cap);
103
+ size_t pos = 0;
104
+
105
+ /* ── Chunk 1: compress part1 with FLUSH ──────────────────────── */
106
+ BrotliEncoderState *enc1 = BrotliEncoderCreateInstance(NULL, NULL, NULL);
107
+ if (!enc1) { xfree(out); rb_raise(eBrotliSpliceError, "encoder creation failed"); }
108
+ BrotliEncoderSetParameter(enc1, BROTLI_PARAM_QUALITY, quality);
109
+ BrotliEncoderSetParameter(enc1, BROTLI_PARAM_LGWIN, 22);
110
+
111
+ size_t c1 = encoder_feed(enc1, BROTLI_OPERATION_FLUSH,
112
+ part1, p1_len, out + pos, out_cap - pos);
113
+ BrotliEncoderDestroyInstance(enc1);
114
+ if (c1 == 0 && p1_len > 0) {
115
+ xfree(out);
116
+ rb_raise(eBrotliSpliceError, "chunk1 encoding failed");
117
+ }
118
+ pos += c1;
119
+
120
+ /* ── Chunk 2: uncompressed meta-block for secret body ────────── */
121
+ size_t sec_data_offset = pos + 3; /* 3-byte header for ≤64KB */
122
+ size_t c2 = make_uncompressed_block(out + pos, secret, sec_body);
123
+ if (c2 == 0) { xfree(out); rb_raise(eBrotliSpliceError, "chunk2 failed"); }
124
+ pos += c2;
125
+
126
+ /* ── Chunk 3: context bytes + part3, STREAM_OFFSET ───────────── */
127
+ BrotliEncoderState *enc2 = BrotliEncoderCreateInstance(NULL, NULL, NULL);
128
+ if (!enc2) { xfree(out); rb_raise(eBrotliSpliceError, "encoder2 creation failed"); }
129
+ BrotliEncoderSetParameter(enc2, BROTLI_PARAM_QUALITY, quality);
130
+ BrotliEncoderSetParameter(enc2, BROTLI_PARAM_LGWIN, 22);
131
+ BrotliEncoderSetParameter(enc2, BROTLI_PARAM_STREAM_OFFSET,
132
+ (uint32_t)(p1_len + sec_body));
133
+
134
+ size_t c3_in_len = CTX_LEN + p3_len;
135
+ uint8_t *c3_in = xmalloc(c3_in_len);
136
+ memcpy(c3_in, CTX_BYTES, CTX_LEN);
137
+ memcpy(c3_in + CTX_LEN, part3, p3_len);
138
+
139
+ size_t c3 = encoder_feed(enc2, BROTLI_OPERATION_FINISH,
140
+ c3_in, c3_in_len, out + pos, out_cap - pos);
141
+ BrotliEncoderDestroyInstance(enc2);
142
+ xfree(c3_in);
143
+ if (c3 == 0) { xfree(out); rb_raise(eBrotliSpliceError, "chunk3 encoding failed"); }
144
+ pos += c3;
145
+
146
+ /* Build result */
147
+ VALUE data = rb_str_new((char *)out, pos);
148
+ rb_enc_associate(data, rb_ascii8bit_encoding());
149
+ xfree(out);
150
+
151
+ VALUE result = rb_hash_new();
152
+ rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
153
+ rb_hash_aset(result, ID2SYM(rb_intern("secret_offset")), SIZET2NUM(sec_data_offset));
154
+ rb_hash_aset(result, ID2SYM(rb_intern("secret_length")), SIZET2NUM(sec_body));
155
+ rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")),
156
+ rb_str_new((const char *)CTX_BYTES, CTX_LEN));
157
+ return result;
158
+ }
159
+
160
+ /*
161
+ * call-seq:
162
+ * BrotliSplice.replace(compressed_data, new_secret, secret_offset, secret_length) -> String
163
+ *
164
+ * Replace the secret in a compressed stream. +new_secret+ must be
165
+ * exactly +secret_length+ bytes. Returns a new string.
166
+ */
167
+ static VALUE rb_brotli_splice_replace(VALUE self,
168
+ VALUE rb_data, VALUE rb_secret,
169
+ VALUE rb_offset, VALUE rb_length) {
170
+ Check_Type(rb_data, T_STRING);
171
+ Check_Type(rb_secret, T_STRING);
172
+ size_t offset = NUM2SIZET(rb_offset);
173
+ size_t length = NUM2SIZET(rb_length);
174
+
175
+ if ((size_t)RSTRING_LEN(rb_secret) != length)
176
+ rb_raise(eBrotliSpliceError,
177
+ "secret length %ld != expected %zu",
178
+ RSTRING_LEN(rb_secret), length);
179
+
180
+ if (offset + length > (size_t)RSTRING_LEN(rb_data))
181
+ rb_raise(eBrotliSpliceError, "offset+length exceeds data size");
182
+
183
+ VALUE result = rb_str_dup(rb_data);
184
+ rb_str_modify(result);
185
+ memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(rb_secret), length);
186
+ return result;
187
+ }
188
+
189
+ void Init_brotli_splice(void) {
190
+ mBrotliSplice = rb_define_module("BrotliSplice");
191
+ eBrotliSpliceError = rb_define_class_under(mBrotliSplice, "Error", rb_eRuntimeError);
192
+
193
+ rb_define_singleton_method(mBrotliSplice, "encode",
194
+ rb_brotli_splice_encode, -1);
195
+ rb_define_singleton_method(mBrotliSplice, "replace",
196
+ rb_brotli_splice_replace, 4);
197
+ }
@@ -0,0 +1,14 @@
1
+ require "mkmf"
2
+
3
+ # Find brotli headers and libraries (Homebrew or system)
4
+ dir_config("brotli",
5
+ ["/opt/homebrew/include", "/usr/local/include"],
6
+ ["/opt/homebrew/lib", "/usr/local/lib"])
7
+
8
+ abort "missing brotli/encode.h" unless have_header("brotli/encode.h")
9
+ abort "missing brotli/decode.h" unless have_header("brotli/decode.h")
10
+ abort "missing libbrotlienc" unless have_library("brotlienc")
11
+ abort "missing libbrotlidec" unless have_library("brotlidec")
12
+ abort "missing libbrotlicommon" unless have_library("brotlicommon")
13
+
14
+ create_makefile("brotli_splice/brotli_splice")
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrotliSplice
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "brotli_splice/version"
4
+
5
+ begin
6
+ require "brotli_splice/brotli_splice"
7
+ rescue LoadError => error
8
+ begin
9
+ require_relative "../ext/brotli_splice/brotli_splice"
10
+ rescue LoadError
11
+ raise error
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brotli_splice
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Shopify
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ BrotliSplice is a Ruby C extension that creates Brotli-compressed streams
15
+ containing a fixed-length uncompressed slot. The slot can be overwritten
16
+ with a simple byte copy, making it useful for injecting fixed-size secrets
17
+ or tokens into pre-compressed HTML responses.
18
+ email:
19
+ - gems@shopify.com
20
+ executables: []
21
+ extensions:
22
+ - ext/brotli_splice/extconf.rb
23
+ extra_rdoc_files: []
24
+ files:
25
+ - LICENSE.md
26
+ - README.md
27
+ - brotli_splice.gemspec
28
+ - ext/brotli_splice/brotli_splice.c
29
+ - ext/brotli_splice/extconf.rb
30
+ - lib/brotli_splice.rb
31
+ - lib/brotli_splice/version.rb
32
+ homepage: https://github.com/Shopify/brotli_splice
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ allowed_push_host: https://rubygems.org
37
+ homepage_uri: https://github.com/Shopify/brotli_splice
38
+ source_code_uri: https://github.com/Shopify/brotli_splice/tree/main
39
+ changelog_uri: https://github.com/Shopify/brotli_splice/releases
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.5.22
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Create Brotli streams with fixed-length spliceable slots
60
+ test_files: []