brotli_splice 0.1.1 → 0.2.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: '0810be973e8e1ad5dd1b28a4733754bd2ec87f6e5d5c5c3e6a29d44589e505ab'
4
- data.tar.gz: cc459d36cbf640e4fb47d426dbce7dcb68f4458cf6daf49080e7fa9d67d5e0bd
3
+ metadata.gz: 8c2ff7c74f1280217916777270114a52fefb8a6c7c67899db9a6f2017d978e2c
4
+ data.tar.gz: 8a4c7335d85ef0364713704e2ee18924596742401f15d63359dba3182a54c5f5
5
5
  SHA512:
6
- metadata.gz: 8af6e37050b29d440cf25ae1f97ea502d2dffa513516e8592216fd16664a3a68c8796121704dde5f10618eea4353ba39a9a117820f35de34a281a24775913b72
7
- data.tar.gz: 5742e2a3183a99e2965d8dd2e8dbe3e3f93c8c7ea1cbe6e2bcfb065bf3ab82c5eb88b7f32590a4f36b98a6b18bf17025fa14600260e2334b2b797e1c0ece604f
6
+ metadata.gz: 34fdb4f6643d3924e66214fad37b678806ed3d93333f2e340ec324b5403d1b87b5260909659ab8365e8faaaf4ac5274c28c10a62ea48245a10a768a591d1356e
7
+ data.tar.gz: ba10b2081093749fac4f52d5c58b4a513181b04a1a99e7ce67d9c50a29b1669b4ed39030dafcb8581caa29a3fff77565a4418e16df26bb86512840b6902c24f9
data/README.md CHANGED
@@ -99,6 +99,24 @@ ruby -Ilib test_ext.rb
99
99
 
100
100
  ## API
101
101
 
102
+ ### `BrotliSplice::Encoder`
103
+
104
+ Use the stateful encoder when the full document is not available up front:
105
+
106
+ ```ruby
107
+ encoder = BrotliSplice::Encoder.new(quality: 5, lgwin: 22)
108
+ compressed = encoder.write("<html><head>".b)
109
+ compressed << encoder.write("<title>Store</title>".b)
110
+ compressed << encoder.slot("live-token\r\n".b)
111
+ compressed << encoder.finish("</head><body>...</body></html>".b)
112
+ ```
113
+
114
+ `write` flushes and byte-aligns each chunk. `slot` may be called once. Its input must end in the fixed `"\r\n"` context suffix, which is not replaceable. `finish` closes the stream and can include a final chunk. Strings are treated as byte sequences regardless of their Ruby encoding.
115
+
116
+ After `slot`, `slot_offset` and `slot_length` identify the raw replaceable bytes in the combined compressed output. `context_suffix` returns `"\r\n"`.
117
+
118
+ Call `close` when abandoning an unfinished encoder. It releases native state without producing output and is safe to call more than once or after `finish`. Once an encoder is closed, finished, or fails after consuming input, later encoding operations raise `BrotliSplice::Error`.
119
+
102
120
  ### `BrotliSplice.encode(html, secret_offset, secret_length, quality: 11) → Hash`
103
121
 
104
122
  Compress `html` with a spliceable slot at the given byte position.
@@ -167,7 +185,7 @@ longer, or place the suffix where a line break is natural).
167
185
 
168
186
  ## Limitations
169
187
 
170
- - The replaceable slot must be 65534 bytes (64KB minus the 2-byte context)
188
+ - The replaceable slot must be no larger than 65536 bytes, plus the 2-byte context suffix
171
189
  - The replacement must be **exactly** `secret_length` bytes (no length changes)
172
190
  - The last 2 bytes of the original secret region become `\r\n` in the output
173
191
  - Only one spliceable slot per stream (encode with multiple slots would require
@@ -1,197 +1,494 @@
1
1
  /*
2
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
3
  */
9
4
 
10
5
  #include <ruby.h>
11
6
  #include <ruby/encoding.h>
12
7
  #include <brotli/encode.h>
13
- #include <brotli/decode.h>
8
+ #include <stddef.h>
9
+ #include <stdint.h>
10
+ #include <stdlib.h>
14
11
  #include <string.h>
15
12
 
16
13
  static VALUE mBrotliSplice;
14
+ static VALUE cEncoder;
17
15
  static VALUE eBrotliSpliceError;
18
16
 
19
17
  #define CTX_LEN 2
18
+ #define OUTPUT_CHUNK_SIZE 16384
20
19
  static const uint8_t CTX_BYTES[CTX_LEN] = { '\r', '\n' };
21
20
 
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;
21
+ typedef enum {
22
+ ENCODER_UNINITIALIZED,
23
+ ENCODER_ACTIVE,
24
+ ENCODER_FINISHED,
25
+ ENCODER_CLOSED,
26
+ ENCODER_FAILED,
27
+ } encoder_lifecycle_t;
28
+
29
+ typedef struct {
30
+ BrotliEncoderState *encoder;
31
+ int quality;
32
+ int lgwin;
33
+ size_t uncompressed_bytes;
34
+ size_t bytes_written;
35
+ size_t slot_offset;
36
+ size_t slot_length;
37
+ size_t native_bytes;
38
+ int has_slot;
39
+ encoder_lifecycle_t lifecycle;
40
+ } splice_encoder_t;
41
+
42
+ typedef union {
43
+ struct {
44
+ size_t allocation_size;
45
+ } tracked;
46
+ max_align_t alignment;
47
+ } allocation_header_t;
48
+
49
+ static void *encoder_alloc_native(void *opaque, size_t size) {
50
+ splice_encoder_t *encoder = opaque;
51
+ if (size > SIZE_MAX - sizeof(allocation_header_t)) return NULL;
52
+
53
+ size_t allocation_size = sizeof(allocation_header_t) + size;
54
+ allocation_header_t *header = malloc(allocation_size);
55
+ if (!header) return NULL;
56
+ if (encoder->native_bytes > SIZE_MAX - allocation_size) {
57
+ free(header);
58
+ return NULL;
32
59
  }
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;
60
+
61
+ header->tracked.allocation_size = allocation_size;
62
+ encoder->native_bytes += allocation_size;
63
+ return header + 1;
51
64
  }
52
65
 
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");
66
+ static void encoder_free_native(void *opaque, void *address) {
67
+ if (!address) return;
68
+
69
+ splice_encoder_t *encoder = opaque;
70
+ allocation_header_t *header = ((allocation_header_t *)address) - 1;
71
+ encoder->native_bytes -= header->tracked.allocation_size;
72
+ free(header);
73
+ }
74
+
75
+ static void encoder_destroy_native(splice_encoder_t *encoder) {
76
+ if (!encoder->encoder) return;
83
77
 
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);
78
+ BrotliEncoderDestroyInstance(encoder->encoder);
79
+ encoder->encoder = NULL;
80
+ }
81
+
82
+ static void encoder_terminalize(splice_encoder_t *encoder, encoder_lifecycle_t lifecycle) {
83
+ encoder_destroy_native(encoder);
84
+ encoder->lifecycle = lifecycle;
85
+ }
86
+
87
+ static void encoder_free(void *pointer) {
88
+ splice_encoder_t *encoder = pointer;
89
+ encoder_destroy_native(encoder);
90
+ xfree(encoder);
91
+ }
92
+
93
+ static size_t encoder_memsize(const void *pointer) {
94
+ if (!pointer) return 0;
95
+
96
+ const splice_encoder_t *encoder = pointer;
97
+ if (encoder->native_bytes > SIZE_MAX - sizeof(splice_encoder_t)) return SIZE_MAX;
98
+ return sizeof(splice_encoder_t) + encoder->native_bytes;
99
+ }
100
+
101
+ static const rb_data_type_t encoder_type = {
102
+ "BrotliSplice::Encoder",
103
+ {NULL, encoder_free, encoder_memsize},
104
+ NULL,
105
+ NULL,
106
+ RUBY_TYPED_FREE_IMMEDIATELY,
107
+ };
108
+
109
+ static VALUE encoder_alloc(VALUE klass) {
110
+ splice_encoder_t *encoder;
111
+ VALUE object = TypedData_Make_Struct(klass, splice_encoder_t, &encoder_type, encoder);
112
+ memset(encoder, 0, sizeof(*encoder));
113
+ return object;
114
+ }
115
+
116
+ static BrotliEncoderState *new_encoder(splice_encoder_t *encoder, int quality, int lgwin, uint32_t stream_offset) {
117
+ BrotliEncoderState *state = BrotliEncoderCreateInstance(encoder_alloc_native, encoder_free_native, encoder);
118
+ if (!state) rb_raise(eBrotliSpliceError, "encoder creation failed");
119
+
120
+ if (!BrotliEncoderSetParameter(state, BROTLI_PARAM_QUALITY, (uint32_t)quality) ||
121
+ !BrotliEncoderSetParameter(state, BROTLI_PARAM_LGWIN, (uint32_t)lgwin) ||
122
+ (stream_offset > 0 && !BrotliEncoderSetParameter(state, BROTLI_PARAM_STREAM_OFFSET, stream_offset))) {
123
+ BrotliEncoderDestroyInstance(state);
124
+ rb_raise(eBrotliSpliceError, "invalid encoder parameters");
88
125
  }
89
126
 
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");
127
+ return state;
128
+ }
129
+
130
+ static void check_string(VALUE string) {
131
+ Check_Type(string, T_STRING);
132
+ }
133
+
134
+ static VALUE binary_string(const uint8_t *bytes, size_t length) {
135
+ VALUE result = rb_str_new((const char *)bytes, length);
136
+ rb_enc_associate(result, rb_ascii8bit_encoding());
137
+ return result;
138
+ }
139
+
140
+ static VALUE encoder_feed(BrotliEncoderState *state, BrotliEncoderOperation operation,
141
+ const uint8_t *input, size_t input_length) {
142
+ VALUE output = rb_str_buf_new(64);
143
+ size_t available_in = input_length;
144
+ const uint8_t *next_in = input;
145
+
146
+ for (;;) {
147
+ uint8_t buffer[OUTPUT_CHUNK_SIZE];
148
+ size_t available_out = sizeof(buffer);
149
+ uint8_t *next_out = buffer;
150
+ size_t before_in = available_in;
151
+
152
+ if (!BrotliEncoderCompressStream(
153
+ state,
154
+ operation,
155
+ &available_in,
156
+ &next_in,
157
+ &available_out,
158
+ &next_out,
159
+ NULL)) {
160
+ rb_raise(eBrotliSpliceError, "encoding failed");
161
+ }
162
+
163
+ size_t produced = sizeof(buffer) - available_out;
164
+ if (produced > 0) rb_str_cat(output, (const char *)buffer, produced);
165
+
166
+ if (operation == BROTLI_OPERATION_FINISH) {
167
+ if (BrotliEncoderIsFinished(state)) break;
168
+ } else if (available_in == 0 && !BrotliEncoderHasMoreOutput(state)) {
169
+ break;
170
+ }
171
+
172
+ if (before_in == available_in && produced == 0) {
173
+ rb_raise(eBrotliSpliceError, "encoder made no progress");
174
+ }
175
+ }
176
+
177
+ rb_enc_associate(output, rb_ascii8bit_encoding());
178
+ return output;
179
+ }
180
+
181
+ static void make_uncompressed_header(uint8_t output[3], size_t length) {
182
+ uint32_t value = (uint32_t)(length - 1);
183
+ uint32_t bits = (value << 3) | (1u << 19);
184
+ output[0] = (uint8_t)bits;
185
+ output[1] = (uint8_t)(bits >> 8);
186
+ output[2] = (uint8_t)(bits >> 16);
187
+ }
188
+
189
+ static VALUE encoder_initialize(int argc, VALUE *argv, VALUE self) {
190
+ VALUE options;
191
+ rb_scan_args(argc, argv, "0:", &options);
192
+
193
+ splice_encoder_t *encoder;
194
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
195
+
196
+ if (encoder->lifecycle != ENCODER_UNINITIALIZED) {
197
+ rb_raise(eBrotliSpliceError, "encoder is already initialized");
198
+ }
199
+
200
+ int quality_value = 11;
201
+ int lgwin_value = 22;
202
+ if (!NIL_P(options)) {
203
+ VALUE quality = rb_hash_aref(options, ID2SYM(rb_intern("quality")));
204
+ VALUE lgwin = rb_hash_aref(options, ID2SYM(rb_intern("lgwin")));
205
+ if (!NIL_P(quality)) quality_value = NUM2INT(quality);
206
+ if (!NIL_P(lgwin)) lgwin_value = NUM2INT(lgwin);
207
+ }
208
+ if (quality_value < 0 || quality_value > 11) {
209
+ rb_raise(eBrotliSpliceError, "quality must be between 0 and 11");
210
+ }
211
+ if (lgwin_value < 10 || lgwin_value > 24) {
212
+ rb_raise(eBrotliSpliceError, "lgwin must be between 10 and 24");
213
+ }
214
+ if (encoder->lifecycle != ENCODER_UNINITIALIZED) {
215
+ rb_raise(eBrotliSpliceError, "encoder is already initialized");
117
216
  }
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);
217
+
218
+ encoder->quality = quality_value;
219
+ encoder->lgwin = lgwin_value;
220
+ encoder->encoder = new_encoder(encoder, encoder->quality, encoder->lgwin, 0);
221
+ encoder->lifecycle = ENCODER_ACTIVE;
222
+ return self;
223
+ }
224
+
225
+ static void ensure_writable(splice_encoder_t *encoder) {
226
+ switch (encoder->lifecycle) {
227
+ case ENCODER_ACTIVE:
228
+ return;
229
+ case ENCODER_FINISHED:
230
+ rb_raise(eBrotliSpliceError, "encoder is finished");
231
+ case ENCODER_CLOSED:
232
+ rb_raise(eBrotliSpliceError, "encoder is closed");
233
+ case ENCODER_FAILED:
234
+ rb_raise(eBrotliSpliceError, "encoder failed");
235
+ case ENCODER_UNINITIALIZED:
236
+ rb_raise(eBrotliSpliceError, "encoder is not initialized");
237
+ }
238
+ }
239
+
240
+ static VALUE encoder_initialize_copy(VALUE self, VALUE original) {
241
+ rb_raise(rb_eTypeError, "BrotliSplice::Encoder cannot be copied");
242
+ }
243
+
244
+ typedef struct {
245
+ splice_encoder_t *encoder;
246
+ VALUE bytes;
247
+ } encoder_operation_t;
248
+
249
+ static VALUE run_encoder_operation(
250
+ splice_encoder_t *encoder,
251
+ VALUE (*operation)(VALUE),
252
+ encoder_operation_t *arguments
253
+ ) {
254
+ int state = 0;
255
+ VALUE output = rb_protect(operation, (VALUE)arguments, &state);
256
+ if (state) {
257
+ encoder_terminalize(encoder, ENCODER_FAILED);
258
+ rb_jump_tag(state);
259
+ }
260
+ return output;
261
+ }
262
+
263
+ static VALUE encoder_write_operation(VALUE opaque) {
264
+ encoder_operation_t *arguments = (encoder_operation_t *)opaque;
265
+ splice_encoder_t *encoder = arguments->encoder;
266
+ VALUE bytes = arguments->bytes;
267
+ VALUE output = encoder_feed(
268
+ encoder->encoder,
269
+ BROTLI_OPERATION_FLUSH,
270
+ (const uint8_t *)RSTRING_PTR(bytes),
271
+ (size_t)RSTRING_LEN(bytes));
272
+ encoder->uncompressed_bytes += (size_t)RSTRING_LEN(bytes);
273
+ encoder->bytes_written += (size_t)RSTRING_LEN(output);
274
+ return output;
275
+ }
276
+
277
+ static VALUE encoder_write(VALUE self, VALUE bytes) {
278
+ splice_encoder_t *encoder;
279
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
280
+ ensure_writable(encoder);
281
+ check_string(bytes);
282
+
283
+ encoder_operation_t arguments = {encoder, bytes};
284
+ return run_encoder_operation(encoder, encoder_write_operation, &arguments);
285
+ }
286
+
287
+ static VALUE encoder_slot_operation(VALUE opaque) {
288
+ encoder_operation_t *arguments = (encoder_operation_t *)opaque;
289
+ splice_encoder_t *encoder = arguments->encoder;
290
+ VALUE bytes = arguments->bytes;
291
+ size_t total_length = (size_t)RSTRING_LEN(bytes);
292
+ size_t body_length = total_length - CTX_LEN;
293
+ size_t max_stream_offset = ((size_t)1) << encoder->lgwin;
294
+ uint32_t stream_offset = (uint32_t)(
295
+ encoder->uncompressed_bytes + body_length > max_stream_offset
296
+ ? max_stream_offset
297
+ : encoder->uncompressed_bytes + body_length);
298
+
299
+ VALUE output = encoder_feed(
300
+ encoder->encoder,
301
+ BROTLI_OPERATION_FLUSH,
302
+ NULL,
303
+ 0);
304
+ size_t flushed_length = (size_t)RSTRING_LEN(output);
305
+
306
+ BrotliEncoderState *next_encoder = new_encoder(encoder, encoder->quality, encoder->lgwin, stream_offset);
307
+ encoder_destroy_native(encoder);
308
+ encoder->encoder = next_encoder;
309
+
310
+ uint8_t header[3];
311
+ make_uncompressed_header(header, body_length);
312
+ rb_str_cat(output, (const char *)header, sizeof(header));
313
+ rb_str_cat(output, RSTRING_PTR(bytes), body_length);
314
+
315
+ VALUE context_output = encoder_feed(
316
+ encoder->encoder,
317
+ BROTLI_OPERATION_PROCESS,
318
+ CTX_BYTES,
319
+ CTX_LEN);
320
+ rb_str_concat(output, context_output);
321
+
322
+ encoder->slot_offset = encoder->bytes_written + flushed_length + sizeof(header);
323
+ encoder->slot_length = body_length;
324
+ encoder->has_slot = 1;
325
+ encoder->bytes_written += flushed_length + sizeof(header) + body_length + (size_t)RSTRING_LEN(context_output);
326
+ encoder->uncompressed_bytes += body_length + CTX_LEN;
327
+ return output;
328
+ }
329
+
330
+ static VALUE encoder_slot(VALUE self, VALUE bytes) {
331
+ splice_encoder_t *encoder;
332
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
333
+ ensure_writable(encoder);
334
+ check_string(bytes);
335
+
336
+ if (encoder->has_slot) rb_raise(eBrotliSpliceError, "slot may only be written once");
337
+
338
+ size_t total_length = (size_t)RSTRING_LEN(bytes);
339
+ if (total_length <= CTX_LEN) rb_raise(eBrotliSpliceError, "slot must be longer than 2 bytes");
340
+
341
+ size_t body_length = total_length - CTX_LEN;
342
+ if (body_length > 65536) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
343
+ if (memcmp(RSTRING_PTR(bytes) + body_length, CTX_BYTES, CTX_LEN) != 0) {
344
+ rb_raise(eBrotliSpliceError, "slot must end with the context suffix \\r\\n");
345
+ }
346
+
347
+ encoder_operation_t arguments = {encoder, bytes};
348
+ return run_encoder_operation(encoder, encoder_slot_operation, &arguments);
349
+ }
350
+
351
+ static VALUE encoder_finish_operation(VALUE opaque) {
352
+ encoder_operation_t *arguments = (encoder_operation_t *)opaque;
353
+ splice_encoder_t *encoder = arguments->encoder;
354
+ VALUE bytes = arguments->bytes;
355
+ VALUE output = encoder_feed(
356
+ encoder->encoder,
357
+ BROTLI_OPERATION_FINISH,
358
+ (const uint8_t *)RSTRING_PTR(bytes),
359
+ (size_t)RSTRING_LEN(bytes));
360
+ encoder->uncompressed_bytes += (size_t)RSTRING_LEN(bytes);
361
+ encoder->bytes_written += (size_t)RSTRING_LEN(output);
362
+ encoder_terminalize(encoder, ENCODER_FINISHED);
363
+ return output;
364
+ }
365
+
366
+ static VALUE encoder_finish(int argc, VALUE *argv, VALUE self) {
367
+ VALUE bytes;
368
+ rb_scan_args(argc, argv, "01", &bytes);
369
+ if (NIL_P(bytes)) bytes = binary_string(NULL, 0);
370
+
371
+ splice_encoder_t *encoder;
372
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
373
+ ensure_writable(encoder);
374
+ check_string(bytes);
375
+
376
+ encoder_operation_t arguments = {encoder, bytes};
377
+ return run_encoder_operation(encoder, encoder_finish_operation, &arguments);
378
+ }
379
+
380
+ static VALUE encoder_close(VALUE self) {
381
+ splice_encoder_t *encoder;
382
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
383
+ if (encoder->lifecycle == ENCODER_ACTIVE || encoder->lifecycle == ENCODER_UNINITIALIZED) {
384
+ encoder_terminalize(encoder, ENCODER_CLOSED);
385
+ }
386
+ return Qnil;
387
+ }
388
+
389
+ static VALUE encoder_slot_offset(VALUE self) {
390
+ splice_encoder_t *encoder;
391
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
392
+ return encoder->has_slot ? SIZET2NUM(encoder->slot_offset) : Qnil;
393
+ }
394
+
395
+ static VALUE encoder_slot_length(VALUE self) {
396
+ splice_encoder_t *encoder;
397
+ TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
398
+ return encoder->has_slot ? SIZET2NUM(encoder->slot_length) : Qnil;
399
+ }
400
+
401
+ static VALUE encoder_context_suffix(VALUE self) {
402
+ return binary_string(CTX_BYTES, CTX_LEN);
403
+ }
404
+
405
+ typedef struct {
406
+ VALUE encoder;
407
+ VALUE prefix;
408
+ VALUE slot;
409
+ VALUE suffix;
410
+ } encode_operation_t;
411
+
412
+ static VALUE encode_operation(VALUE opaque) {
413
+ encode_operation_t *arguments = (encode_operation_t *)opaque;
414
+ VALUE data = rb_funcall(arguments->encoder, rb_intern("write"), 1, arguments->prefix);
415
+ rb_str_concat(data, rb_funcall(arguments->encoder, rb_intern("slot"), 1, arguments->slot));
416
+ rb_str_concat(data, rb_funcall(arguments->encoder, rb_intern("finish"), 1, arguments->suffix));
150
417
 
151
418
  VALUE result = rb_hash_new();
152
419
  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));
420
+ rb_hash_aset(result, ID2SYM(rb_intern("secret_offset")), rb_funcall(arguments->encoder, rb_intern("slot_offset"), 0));
421
+ rb_hash_aset(result, ID2SYM(rb_intern("secret_length")), rb_funcall(arguments->encoder, rb_intern("slot_length"), 0));
422
+ rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")), encoder_context_suffix(arguments->encoder));
157
423
  return result;
158
424
  }
159
425
 
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))
426
+ static VALUE close_encoder_ensure(VALUE encoder_value) {
427
+ return encoder_close(encoder_value);
428
+ }
429
+
430
+ static VALUE rb_brotli_splice_encode(int argc, VALUE *argv, VALUE self) {
431
+ VALUE html, secret_offset_value, secret_length_value, options;
432
+ rb_scan_args(argc, argv, "3:", &html, &secret_offset_value, &secret_length_value, &options);
433
+ Check_Type(html, T_STRING);
434
+
435
+ size_t secret_offset = NUM2SIZET(secret_offset_value);
436
+ size_t secret_length = NUM2SIZET(secret_length_value);
437
+ size_t html_length = (size_t)RSTRING_LEN(html);
438
+ if (secret_length <= CTX_LEN) rb_raise(eBrotliSpliceError, "secret_length must be > %d", CTX_LEN);
439
+ if (secret_offset > html_length || secret_length > html_length - secret_offset) {
440
+ rb_raise(eBrotliSpliceError, "secret_offset + secret_length exceeds html size");
441
+ }
442
+
443
+ VALUE prefix = binary_string((const uint8_t *)RSTRING_PTR(html), secret_offset);
444
+ VALUE slot = binary_string((const uint8_t *)RSTRING_PTR(html) + secret_offset, secret_length);
445
+ VALUE suffix = binary_string(
446
+ (const uint8_t *)RSTRING_PTR(html) + secret_offset + secret_length,
447
+ html_length - secret_offset - secret_length);
448
+ rb_str_modify(slot);
449
+ memcpy(RSTRING_PTR(slot) + secret_length - CTX_LEN, CTX_BYTES, CTX_LEN);
450
+
451
+ VALUE encoder = rb_class_new_instance_kw(NIL_P(options) ? 0 : 1, NIL_P(options) ? NULL : &options, cEncoder, RB_PASS_KEYWORDS);
452
+ encode_operation_t arguments = {encoder, prefix, slot, suffix};
453
+ return rb_ensure(encode_operation, (VALUE)&arguments, close_encoder_ensure, encoder);
454
+ }
455
+
456
+ static VALUE rb_brotli_splice_replace(VALUE self, VALUE data, VALUE secret,
457
+ VALUE offset_value, VALUE length_value) {
458
+ Check_Type(data, T_STRING);
459
+ Check_Type(secret, T_STRING);
460
+ size_t offset = NUM2SIZET(offset_value);
461
+ size_t length = NUM2SIZET(length_value);
462
+
463
+ if ((size_t)RSTRING_LEN(secret) != length) {
464
+ rb_raise(eBrotliSpliceError, "secret length %ld != expected %zu", RSTRING_LEN(secret), length);
465
+ }
466
+ if (offset > (size_t)RSTRING_LEN(data) || length > (size_t)RSTRING_LEN(data) - offset) {
181
467
  rb_raise(eBrotliSpliceError, "offset+length exceeds data size");
468
+ }
182
469
 
183
- VALUE result = rb_str_dup(rb_data);
470
+ VALUE result = rb_str_dup(data);
184
471
  rb_str_modify(result);
185
- memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(rb_secret), length);
472
+ memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(secret), length);
186
473
  return result;
187
474
  }
188
475
 
189
476
  void Init_brotli_splice(void) {
190
477
  mBrotliSplice = rb_define_module("BrotliSplice");
191
478
  eBrotliSpliceError = rb_define_class_under(mBrotliSplice, "Error", rb_eRuntimeError);
479
+ cEncoder = rb_define_class_under(mBrotliSplice, "Encoder", rb_cObject);
480
+
481
+ rb_define_alloc_func(cEncoder, encoder_alloc);
482
+ rb_define_method(cEncoder, "initialize", encoder_initialize, -1);
483
+ rb_define_method(cEncoder, "initialize_copy", encoder_initialize_copy, 1);
484
+ rb_define_method(cEncoder, "write", encoder_write, 1);
485
+ rb_define_method(cEncoder, "slot", encoder_slot, 1);
486
+ rb_define_method(cEncoder, "finish", encoder_finish, -1);
487
+ rb_define_method(cEncoder, "close", encoder_close, 0);
488
+ rb_define_method(cEncoder, "slot_offset", encoder_slot_offset, 0);
489
+ rb_define_method(cEncoder, "slot_length", encoder_slot_length, 0);
490
+ rb_define_method(cEncoder, "context_suffix", encoder_context_suffix, 0);
192
491
 
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);
492
+ rb_define_singleton_method(mBrotliSplice, "encode", rb_brotli_splice_encode, -1);
493
+ rb_define_singleton_method(mBrotliSplice, "replace", rb_brotli_splice_replace, 4);
197
494
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrotliSplice
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brotli_splice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
11
+ date: 2026-09-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  BrotliSplice is a Ruby C extension that creates Brotli-compressed streams