brotli_splice 0.2.0 → 0.3.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/README.md +43 -8
- data/brotli_splice.gemspec +1 -1
- data/ext/brotli_splice/brotli_splice.c +213 -36
- data/lib/brotli_splice/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce9fb6c98f2b365b24bad75eb7f1ae7bfef2f86c025a87f27e04d5bf5f8732ee
|
|
4
|
+
data.tar.gz: 5b98a2d69c15302930662ea35ce53b1a987a180e388473083d1c2f89dc59f532
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 122de9b45887622ec860896ea6a7b5ad263f2c0ad5ed78296abc7e53beeaae019b740c86d33c6a88337692fc5bdcacc3673d2ea3b63fdd02e1b98a444daa6a5a
|
|
7
|
+
data.tar.gz: c5a7e0fc26794e77b74fd412c9c26a2350500d1529f90b93fd3add154b2145d231491864c35bf0a1ae0091b3a09a76ae53ee833736d5583df6d2c4d73245823f
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# BrotliSplice
|
|
2
2
|
|
|
3
|
-
A Ruby C extension that produces Brotli-compressed streams with
|
|
4
|
-
**spliceable
|
|
3
|
+
A Ruby C extension that produces Brotli-compressed streams with fixed-length
|
|
4
|
+
**spliceable slots** — regions whose raw bytes can be overwritten without
|
|
5
5
|
decompressing or re-encoding the rest of the stream.
|
|
6
6
|
|
|
7
7
|
## Use case
|
|
@@ -33,6 +33,11 @@ The last 2 bytes of the secret slot are reserved as a fixed **context suffix**
|
|
|
33
33
|
uses the preceding 2 bytes as literal context when compressing part3. The
|
|
34
34
|
replaceable portion is therefore `secret_length - 2` bytes.
|
|
35
35
|
|
|
36
|
+
A stream can hold several slots. Each slot repeats the same layout: the encoder
|
|
37
|
+
flushes, writes the slot as an uncompressed meta-block, and continues with a
|
|
38
|
+
fresh encoder at the new stream offset. Every slot has its own context suffix
|
|
39
|
+
and adds its own share of the overhead described below.
|
|
40
|
+
|
|
36
41
|
Compression overhead vs standard Brotli is small — typically 100–200 bytes
|
|
37
42
|
for realistic HTML documents. Two sources of overhead:
|
|
38
43
|
|
|
@@ -111,9 +116,9 @@ compressed << encoder.slot("live-token\r\n".b)
|
|
|
111
116
|
compressed << encoder.finish("</head><body>...</body></html>".b)
|
|
112
117
|
```
|
|
113
118
|
|
|
114
|
-
`write` flushes and byte-aligns each chunk. `slot` may be called
|
|
119
|
+
`write` flushes and byte-aligns each chunk. `slot` may be called any number of times. 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
120
|
|
|
116
|
-
After `slot`, `slot_offset` and `slot_length` identify the raw replaceable bytes in the combined compressed output. `context_suffix` returns `"\r\n"`.
|
|
121
|
+
After `slot`, `slot_offset` and `slot_length` identify the raw replaceable bytes of the most recent slot in the combined compressed output. `slots` lists every slot in stream order as `{ offset:, length: }` hashes. `context_suffix` returns `"\r\n"`.
|
|
117
122
|
|
|
118
123
|
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
124
|
|
|
@@ -137,6 +142,28 @@ Compress `html` with a spliceable slot at the given byte position.
|
|
|
137
142
|
}
|
|
138
143
|
```
|
|
139
144
|
|
|
145
|
+
### `BrotliSplice.encode_slots(html, slots, quality: 11) → Hash`
|
|
146
|
+
|
|
147
|
+
Compress `html` with a spliceable slot at each given byte range. With one
|
|
148
|
+
range, `data` is identical to the output of `encode`.
|
|
149
|
+
|
|
150
|
+
**Parameters:**
|
|
151
|
+
- `html` — the full HTML string (binary encoding)
|
|
152
|
+
- `slots` — `[offset, length]` byte ranges in ascending order. Ranges may touch
|
|
153
|
+
but must not overlap, and each length must be > 2
|
|
154
|
+
- `quality:` — Brotli quality level, 0–11 (default: 11)
|
|
155
|
+
|
|
156
|
+
**Returns** a Hash:
|
|
157
|
+
```ruby
|
|
158
|
+
{
|
|
159
|
+
data: String, # the Brotli-compressed stream
|
|
160
|
+
slots: Array, # one { offset:, length: } Hash per range, in order:
|
|
161
|
+
# the replaceable bytes within the compressed stream
|
|
162
|
+
# (length = range length - 2)
|
|
163
|
+
context_suffix: String, # the 2 fixed context bytes ("\r\n")
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
140
167
|
### `BrotliSplice.replace(compressed_data, new_secret, secret_offset, secret_length) → String`
|
|
141
168
|
|
|
142
169
|
Replace the secret in a compressed stream. Returns a new string with the
|
|
@@ -148,6 +175,15 @@ swapped bytes. This is a pure `memcpy` — no compression or decompression.
|
|
|
148
175
|
- `secret_offset` — from the `encode` result
|
|
149
176
|
- `secret_length` — from the `encode` result
|
|
150
177
|
|
|
178
|
+
### `BrotliSplice.replace_slots(compressed_data, replacements) → String`
|
|
179
|
+
|
|
180
|
+
Replace several slots while copying `compressed_data` only once.
|
|
181
|
+
`replacements` is an Array of `[new_secret, secret_offset, secret_length]`
|
|
182
|
+
entries, with offsets and lengths from `encode_slots` or `Encoder#slots`.
|
|
183
|
+
Entries must be in ascending offset order and must not overlap, as those methods
|
|
184
|
+
return them; they may touch. Every entry is checked before any byte is copied,
|
|
185
|
+
so an invalid entry raises `BrotliSplice::Error` and returns nothing.
|
|
186
|
+
|
|
151
187
|
## Example
|
|
152
188
|
|
|
153
189
|
```ruby
|
|
@@ -185,8 +221,7 @@ longer, or place the suffix where a line break is natural).
|
|
|
185
221
|
|
|
186
222
|
## Limitations
|
|
187
223
|
|
|
188
|
-
-
|
|
224
|
+
- Each replaceable slot must be no larger than 65536 bytes, plus the 2-byte context suffix
|
|
189
225
|
- The replacement must be **exactly** `secret_length` bytes (no length changes)
|
|
190
|
-
- The last 2 bytes of
|
|
191
|
-
-
|
|
192
|
-
chaining encoders)
|
|
226
|
+
- The last 2 bytes of each original slot region become `\r\n` in the output
|
|
227
|
+
- Slots must not overlap
|
data/brotli_splice.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.summary = "Create Brotli streams with fixed-length spliceable slots"
|
|
9
9
|
spec.description = <<~DESC
|
|
10
10
|
BrotliSplice is a Ruby C extension that creates Brotli-compressed streams
|
|
11
|
-
containing
|
|
11
|
+
containing fixed-length uncompressed slots. Each slot can be overwritten
|
|
12
12
|
with a simple byte copy, making it useful for injecting fixed-size secrets
|
|
13
13
|
or tokens into pre-compressed HTML responses.
|
|
14
14
|
DESC
|
|
@@ -15,6 +15,8 @@ static VALUE cEncoder;
|
|
|
15
15
|
static VALUE eBrotliSpliceError;
|
|
16
16
|
|
|
17
17
|
#define CTX_LEN 2
|
|
18
|
+
/* An uncompressed meta-block header stores MLEN - 1 in 16 bits. */
|
|
19
|
+
#define MAX_SLOT_BODY_LEN 65536
|
|
18
20
|
#define OUTPUT_CHUNK_SIZE 16384
|
|
19
21
|
static const uint8_t CTX_BYTES[CTX_LEN] = { '\r', '\n' };
|
|
20
22
|
|
|
@@ -26,16 +28,21 @@ typedef enum {
|
|
|
26
28
|
ENCODER_FAILED,
|
|
27
29
|
} encoder_lifecycle_t;
|
|
28
30
|
|
|
31
|
+
typedef struct {
|
|
32
|
+
size_t offset;
|
|
33
|
+
size_t length;
|
|
34
|
+
} slot_position_t;
|
|
35
|
+
|
|
29
36
|
typedef struct {
|
|
30
37
|
BrotliEncoderState *encoder;
|
|
31
38
|
int quality;
|
|
32
39
|
int lgwin;
|
|
33
40
|
size_t uncompressed_bytes;
|
|
34
41
|
size_t bytes_written;
|
|
35
|
-
|
|
36
|
-
size_t
|
|
42
|
+
slot_position_t *slots;
|
|
43
|
+
size_t slot_count;
|
|
44
|
+
size_t slot_capacity;
|
|
37
45
|
size_t native_bytes;
|
|
38
|
-
int has_slot;
|
|
39
46
|
encoder_lifecycle_t lifecycle;
|
|
40
47
|
} splice_encoder_t;
|
|
41
48
|
|
|
@@ -87,6 +94,7 @@ static void encoder_terminalize(splice_encoder_t *encoder, encoder_lifecycle_t l
|
|
|
87
94
|
static void encoder_free(void *pointer) {
|
|
88
95
|
splice_encoder_t *encoder = pointer;
|
|
89
96
|
encoder_destroy_native(encoder);
|
|
97
|
+
xfree(encoder->slots);
|
|
90
98
|
xfree(encoder);
|
|
91
99
|
}
|
|
92
100
|
|
|
@@ -94,8 +102,9 @@ static size_t encoder_memsize(const void *pointer) {
|
|
|
94
102
|
if (!pointer) return 0;
|
|
95
103
|
|
|
96
104
|
const splice_encoder_t *encoder = pointer;
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
size_t fixed_size = sizeof(splice_encoder_t) + encoder->slot_capacity * sizeof(slot_position_t);
|
|
106
|
+
if (encoder->native_bytes > SIZE_MAX - fixed_size) return SIZE_MAX;
|
|
107
|
+
return fixed_size + encoder->native_bytes;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
static const rb_data_type_t encoder_type = {
|
|
@@ -319,31 +328,43 @@ static VALUE encoder_slot_operation(VALUE opaque) {
|
|
|
319
328
|
CTX_LEN);
|
|
320
329
|
rb_str_concat(output, context_output);
|
|
321
330
|
|
|
322
|
-
encoder->
|
|
323
|
-
encoder->
|
|
324
|
-
encoder->
|
|
331
|
+
encoder->slots[encoder->slot_count].offset = encoder->bytes_written + flushed_length + sizeof(header);
|
|
332
|
+
encoder->slots[encoder->slot_count].length = body_length;
|
|
333
|
+
encoder->slot_count++;
|
|
325
334
|
encoder->bytes_written += flushed_length + sizeof(header) + body_length + (size_t)RSTRING_LEN(context_output);
|
|
326
335
|
encoder->uncompressed_bytes += body_length + CTX_LEN;
|
|
327
336
|
return output;
|
|
328
337
|
}
|
|
329
338
|
|
|
339
|
+
/*
|
|
340
|
+
* Grows the slot list before any stream bytes are produced, so a failed
|
|
341
|
+
* allocation leaves the encoder usable and the stream unchanged.
|
|
342
|
+
*/
|
|
343
|
+
static void reserve_slot(splice_encoder_t *encoder) {
|
|
344
|
+
if (encoder->slot_count < encoder->slot_capacity) return;
|
|
345
|
+
|
|
346
|
+
size_t capacity = encoder->slot_capacity ? encoder->slot_capacity * 2 : 4;
|
|
347
|
+
REALLOC_N(encoder->slots, slot_position_t, capacity);
|
|
348
|
+
encoder->slot_capacity = capacity;
|
|
349
|
+
}
|
|
350
|
+
|
|
330
351
|
static VALUE encoder_slot(VALUE self, VALUE bytes) {
|
|
331
352
|
splice_encoder_t *encoder;
|
|
332
353
|
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
333
354
|
ensure_writable(encoder);
|
|
334
355
|
check_string(bytes);
|
|
335
356
|
|
|
336
|
-
if (encoder->has_slot) rb_raise(eBrotliSpliceError, "slot may only be written once");
|
|
337
|
-
|
|
338
357
|
size_t total_length = (size_t)RSTRING_LEN(bytes);
|
|
339
358
|
if (total_length <= CTX_LEN) rb_raise(eBrotliSpliceError, "slot must be longer than 2 bytes");
|
|
340
359
|
|
|
341
360
|
size_t body_length = total_length - CTX_LEN;
|
|
342
|
-
if (body_length >
|
|
361
|
+
if (body_length > MAX_SLOT_BODY_LEN) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
|
|
343
362
|
if (memcmp(RSTRING_PTR(bytes) + body_length, CTX_BYTES, CTX_LEN) != 0) {
|
|
344
363
|
rb_raise(eBrotliSpliceError, "slot must end with the context suffix \\r\\n");
|
|
345
364
|
}
|
|
346
365
|
|
|
366
|
+
reserve_slot(encoder);
|
|
367
|
+
|
|
347
368
|
encoder_operation_t arguments = {encoder, bytes};
|
|
348
369
|
return run_encoder_operation(encoder, encoder_slot_operation, &arguments);
|
|
349
370
|
}
|
|
@@ -386,16 +407,36 @@ static VALUE encoder_close(VALUE self) {
|
|
|
386
407
|
return Qnil;
|
|
387
408
|
}
|
|
388
409
|
|
|
410
|
+
static const slot_position_t *last_slot(const splice_encoder_t *encoder) {
|
|
411
|
+
return encoder->slot_count ? &encoder->slots[encoder->slot_count - 1] : NULL;
|
|
412
|
+
}
|
|
413
|
+
|
|
389
414
|
static VALUE encoder_slot_offset(VALUE self) {
|
|
390
415
|
splice_encoder_t *encoder;
|
|
391
416
|
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
392
|
-
|
|
417
|
+
const slot_position_t *slot = last_slot(encoder);
|
|
418
|
+
return slot ? SIZET2NUM(slot->offset) : Qnil;
|
|
393
419
|
}
|
|
394
420
|
|
|
395
421
|
static VALUE encoder_slot_length(VALUE self) {
|
|
396
422
|
splice_encoder_t *encoder;
|
|
397
423
|
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
398
|
-
|
|
424
|
+
const slot_position_t *slot = last_slot(encoder);
|
|
425
|
+
return slot ? SIZET2NUM(slot->length) : Qnil;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static VALUE encoder_slots(VALUE self) {
|
|
429
|
+
splice_encoder_t *encoder;
|
|
430
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
431
|
+
|
|
432
|
+
VALUE slots = rb_ary_new_capa((long)encoder->slot_count);
|
|
433
|
+
for (size_t index = 0; index < encoder->slot_count; index++) {
|
|
434
|
+
VALUE slot = rb_hash_new();
|
|
435
|
+
rb_hash_aset(slot, ID2SYM(rb_intern("offset")), SIZET2NUM(encoder->slots[index].offset));
|
|
436
|
+
rb_hash_aset(slot, ID2SYM(rb_intern("length")), SIZET2NUM(encoder->slots[index].length));
|
|
437
|
+
rb_ary_push(slots, slot);
|
|
438
|
+
}
|
|
439
|
+
return slots;
|
|
399
440
|
}
|
|
400
441
|
|
|
401
442
|
static VALUE encoder_context_suffix(VALUE self) {
|
|
@@ -404,29 +445,60 @@ static VALUE encoder_context_suffix(VALUE self) {
|
|
|
404
445
|
|
|
405
446
|
typedef struct {
|
|
406
447
|
VALUE encoder;
|
|
407
|
-
VALUE
|
|
408
|
-
VALUE slot;
|
|
409
|
-
VALUE suffix;
|
|
448
|
+
VALUE pieces;
|
|
410
449
|
} encode_operation_t;
|
|
411
450
|
|
|
451
|
+
/*
|
|
452
|
+
* Pieces alternate between compressed chunks and slots, starting and ending
|
|
453
|
+
* with a compressed chunk: [chunk, slot, chunk, slot, ..., chunk].
|
|
454
|
+
*/
|
|
412
455
|
static VALUE encode_operation(VALUE opaque) {
|
|
413
456
|
encode_operation_t *arguments = (encode_operation_t *)opaque;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")), encoder_context_suffix(arguments->encoder));
|
|
423
|
-
return result;
|
|
457
|
+
long last = RARRAY_LEN(arguments->pieces) - 1;
|
|
458
|
+
VALUE data = binary_string(NULL, 0);
|
|
459
|
+
for (long index = 0; index < last; index += 2) {
|
|
460
|
+
rb_str_concat(data, rb_funcall(arguments->encoder, rb_intern("write"), 1, RARRAY_AREF(arguments->pieces, index)));
|
|
461
|
+
rb_str_concat(data, rb_funcall(arguments->encoder, rb_intern("slot"), 1, RARRAY_AREF(arguments->pieces, index + 1)));
|
|
462
|
+
}
|
|
463
|
+
rb_str_concat(data, rb_funcall(arguments->encoder, rb_intern("finish"), 1, RARRAY_AREF(arguments->pieces, last)));
|
|
464
|
+
return data;
|
|
424
465
|
}
|
|
425
466
|
|
|
426
467
|
static VALUE close_encoder_ensure(VALUE encoder_value) {
|
|
427
468
|
return encoder_close(encoder_value);
|
|
428
469
|
}
|
|
429
470
|
|
|
471
|
+
static VALUE new_encoder_with_options(VALUE options) {
|
|
472
|
+
return rb_class_new_instance_kw(NIL_P(options) ? 0 : 1, NIL_P(options) ? NULL : &options, cEncoder, RB_PASS_KEYWORDS);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static VALUE encode_pieces(VALUE encoder, VALUE pieces) {
|
|
476
|
+
encode_operation_t arguments = {encoder, pieces};
|
|
477
|
+
return rb_ensure(encode_operation, (VALUE)&arguments, close_encoder_ensure, encoder);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/*
|
|
481
|
+
* `bounds` holds validated, ascending, non-overlapping [offset, length] pairs.
|
|
482
|
+
* Each slot copy ends in the context suffix, which `Encoder#slot` requires.
|
|
483
|
+
*/
|
|
484
|
+
static VALUE slot_pieces(VALUE html, const size_t *bounds, long count) {
|
|
485
|
+
VALUE pieces = rb_ary_new_capa(count * 2 + 1);
|
|
486
|
+
size_t position = 0;
|
|
487
|
+
for (long index = 0; index < count; index++) {
|
|
488
|
+
size_t offset = bounds[index * 2];
|
|
489
|
+
size_t length = bounds[index * 2 + 1];
|
|
490
|
+
rb_ary_push(pieces, binary_string((const uint8_t *)RSTRING_PTR(html) + position, offset - position));
|
|
491
|
+
|
|
492
|
+
VALUE slot = binary_string((const uint8_t *)RSTRING_PTR(html) + offset, length);
|
|
493
|
+
rb_str_modify(slot);
|
|
494
|
+
memcpy(RSTRING_PTR(slot) + length - CTX_LEN, CTX_BYTES, CTX_LEN);
|
|
495
|
+
rb_ary_push(pieces, slot);
|
|
496
|
+
position = offset + length;
|
|
497
|
+
}
|
|
498
|
+
rb_ary_push(pieces, binary_string((const uint8_t *)RSTRING_PTR(html) + position, (size_t)RSTRING_LEN(html) - position));
|
|
499
|
+
return pieces;
|
|
500
|
+
}
|
|
501
|
+
|
|
430
502
|
static VALUE rb_brotli_splice_encode(int argc, VALUE *argv, VALUE self) {
|
|
431
503
|
VALUE html, secret_offset_value, secret_length_value, options;
|
|
432
504
|
rb_scan_args(argc, argv, "3:", &html, &secret_offset_value, &secret_length_value, &options);
|
|
@@ -439,18 +511,66 @@ static VALUE rb_brotli_splice_encode(int argc, VALUE *argv, VALUE self) {
|
|
|
439
511
|
if (secret_offset > html_length || secret_length > html_length - secret_offset) {
|
|
440
512
|
rb_raise(eBrotliSpliceError, "secret_offset + secret_length exceeds html size");
|
|
441
513
|
}
|
|
514
|
+
if (secret_length - CTX_LEN > MAX_SLOT_BODY_LEN) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
|
|
442
515
|
|
|
443
|
-
|
|
444
|
-
VALUE
|
|
445
|
-
VALUE
|
|
446
|
-
|
|
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);
|
|
516
|
+
size_t bounds[2] = {secret_offset, secret_length};
|
|
517
|
+
VALUE pieces = slot_pieces(html, bounds, 1);
|
|
518
|
+
VALUE encoder = new_encoder_with_options(options);
|
|
519
|
+
VALUE data = encode_pieces(encoder, pieces);
|
|
450
520
|
|
|
451
|
-
VALUE
|
|
452
|
-
|
|
453
|
-
|
|
521
|
+
VALUE result = rb_hash_new();
|
|
522
|
+
rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
|
|
523
|
+
rb_hash_aset(result, ID2SYM(rb_intern("secret_offset")), encoder_slot_offset(encoder));
|
|
524
|
+
rb_hash_aset(result, ID2SYM(rb_intern("secret_length")), encoder_slot_length(encoder));
|
|
525
|
+
rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")), encoder_context_suffix(encoder));
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
static VALUE rb_brotli_splice_encode_slots(int argc, VALUE *argv, VALUE self) {
|
|
530
|
+
VALUE html, slots, options;
|
|
531
|
+
rb_scan_args(argc, argv, "2:", &html, &slots, &options);
|
|
532
|
+
Check_Type(html, T_STRING);
|
|
533
|
+
Check_Type(slots, T_ARRAY);
|
|
534
|
+
|
|
535
|
+
/*
|
|
536
|
+
* Coerce every pair before validating any of them: numeric coercion can run
|
|
537
|
+
* Ruby code that changes `html`.
|
|
538
|
+
*/
|
|
539
|
+
long count = RARRAY_LEN(slots);
|
|
540
|
+
VALUE bounds_buffer;
|
|
541
|
+
size_t *bounds = ALLOCV_N(size_t, bounds_buffer, (size_t)count * 2);
|
|
542
|
+
for (long index = 0; index < count; index++) {
|
|
543
|
+
VALUE slot = rb_ary_entry(slots, index);
|
|
544
|
+
Check_Type(slot, T_ARRAY);
|
|
545
|
+
if (RARRAY_LEN(slot) != 2) rb_raise(rb_eArgError, "each slot must be an [offset, length] pair");
|
|
546
|
+
bounds[index * 2] = NUM2SIZET(rb_ary_entry(slot, 0));
|
|
547
|
+
bounds[index * 2 + 1] = NUM2SIZET(rb_ary_entry(slot, 1));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
size_t html_length = (size_t)RSTRING_LEN(html);
|
|
551
|
+
size_t previous_end = 0;
|
|
552
|
+
for (long index = 0; index < count; index++) {
|
|
553
|
+
size_t offset = bounds[index * 2];
|
|
554
|
+
size_t length = bounds[index * 2 + 1];
|
|
555
|
+
if (length <= CTX_LEN) rb_raise(eBrotliSpliceError, "slot length must be > %d", CTX_LEN);
|
|
556
|
+
if (length - CTX_LEN > MAX_SLOT_BODY_LEN) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
|
|
557
|
+
if (offset > html_length || length > html_length - offset) {
|
|
558
|
+
rb_raise(eBrotliSpliceError, "slot offset + length exceeds html size");
|
|
559
|
+
}
|
|
560
|
+
if (offset < previous_end) rb_raise(eBrotliSpliceError, "slots must be in ascending order and must not overlap");
|
|
561
|
+
previous_end = offset + length;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
VALUE pieces = slot_pieces(html, bounds, count);
|
|
565
|
+
ALLOCV_END(bounds_buffer);
|
|
566
|
+
VALUE encoder = new_encoder_with_options(options);
|
|
567
|
+
VALUE data = encode_pieces(encoder, pieces);
|
|
568
|
+
|
|
569
|
+
VALUE result = rb_hash_new();
|
|
570
|
+
rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
|
|
571
|
+
rb_hash_aset(result, ID2SYM(rb_intern("slots")), encoder_slots(encoder));
|
|
572
|
+
rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")), encoder_context_suffix(encoder));
|
|
573
|
+
return result;
|
|
454
574
|
}
|
|
455
575
|
|
|
456
576
|
static VALUE rb_brotli_splice_replace(VALUE self, VALUE data, VALUE secret,
|
|
@@ -473,6 +593,60 @@ static VALUE rb_brotli_splice_replace(VALUE self, VALUE data, VALUE secret,
|
|
|
473
593
|
return result;
|
|
474
594
|
}
|
|
475
595
|
|
|
596
|
+
static VALUE rb_brotli_splice_replace_slots(VALUE self, VALUE data, VALUE replacements) {
|
|
597
|
+
Check_Type(data, T_STRING);
|
|
598
|
+
Check_Type(replacements, T_ARRAY);
|
|
599
|
+
|
|
600
|
+
/*
|
|
601
|
+
* Coerce every entry before validating any of them: numeric coercion can run
|
|
602
|
+
* Ruby code that changes `data` or a secret.
|
|
603
|
+
*/
|
|
604
|
+
long count = RARRAY_LEN(replacements);
|
|
605
|
+
VALUE secrets = rb_ary_new_capa(count);
|
|
606
|
+
VALUE bounds_buffer;
|
|
607
|
+
size_t *bounds = ALLOCV_N(size_t, bounds_buffer, (size_t)count * 2);
|
|
608
|
+
for (long index = 0; index < count; index++) {
|
|
609
|
+
VALUE replacement = rb_ary_entry(replacements, index);
|
|
610
|
+
Check_Type(replacement, T_ARRAY);
|
|
611
|
+
if (RARRAY_LEN(replacement) != 3) {
|
|
612
|
+
rb_raise(rb_eArgError, "each replacement must be a [secret, offset, length] triple");
|
|
613
|
+
}
|
|
614
|
+
VALUE secret = rb_ary_entry(replacement, 0);
|
|
615
|
+
Check_Type(secret, T_STRING);
|
|
616
|
+
rb_ary_push(secrets, secret);
|
|
617
|
+
bounds[index * 2] = NUM2SIZET(rb_ary_entry(replacement, 1));
|
|
618
|
+
bounds[index * 2 + 1] = NUM2SIZET(rb_ary_entry(replacement, 2));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
size_t data_length = (size_t)RSTRING_LEN(data);
|
|
622
|
+
size_t previous_end = 0;
|
|
623
|
+
for (long index = 0; index < count; index++) {
|
|
624
|
+
VALUE secret = RARRAY_AREF(secrets, index);
|
|
625
|
+
size_t offset = bounds[index * 2];
|
|
626
|
+
size_t length = bounds[index * 2 + 1];
|
|
627
|
+
if ((size_t)RSTRING_LEN(secret) != length) {
|
|
628
|
+
rb_raise(eBrotliSpliceError, "secret length %ld != expected %zu", RSTRING_LEN(secret), length);
|
|
629
|
+
}
|
|
630
|
+
if (offset > data_length || length > data_length - offset) {
|
|
631
|
+
rb_raise(eBrotliSpliceError, "offset+length exceeds data size");
|
|
632
|
+
}
|
|
633
|
+
if (offset < previous_end) {
|
|
634
|
+
rb_raise(eBrotliSpliceError, "replacements must be in ascending order and must not overlap");
|
|
635
|
+
}
|
|
636
|
+
previous_end = offset + length;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
VALUE result = rb_str_dup(data);
|
|
640
|
+
rb_str_modify(result);
|
|
641
|
+
for (long index = 0; index < count; index++) {
|
|
642
|
+
VALUE secret = RARRAY_AREF(secrets, index);
|
|
643
|
+
memcpy(RSTRING_PTR(result) + bounds[index * 2], RSTRING_PTR(secret), bounds[index * 2 + 1]);
|
|
644
|
+
}
|
|
645
|
+
ALLOCV_END(bounds_buffer);
|
|
646
|
+
RB_GC_GUARD(secrets);
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
|
|
476
650
|
void Init_brotli_splice(void) {
|
|
477
651
|
mBrotliSplice = rb_define_module("BrotliSplice");
|
|
478
652
|
eBrotliSpliceError = rb_define_class_under(mBrotliSplice, "Error", rb_eRuntimeError);
|
|
@@ -487,8 +661,11 @@ void Init_brotli_splice(void) {
|
|
|
487
661
|
rb_define_method(cEncoder, "close", encoder_close, 0);
|
|
488
662
|
rb_define_method(cEncoder, "slot_offset", encoder_slot_offset, 0);
|
|
489
663
|
rb_define_method(cEncoder, "slot_length", encoder_slot_length, 0);
|
|
664
|
+
rb_define_method(cEncoder, "slots", encoder_slots, 0);
|
|
490
665
|
rb_define_method(cEncoder, "context_suffix", encoder_context_suffix, 0);
|
|
491
666
|
|
|
492
667
|
rb_define_singleton_method(mBrotliSplice, "encode", rb_brotli_splice_encode, -1);
|
|
668
|
+
rb_define_singleton_method(mBrotliSplice, "encode_slots", rb_brotli_splice_encode_slots, -1);
|
|
493
669
|
rb_define_singleton_method(mBrotliSplice, "replace", rb_brotli_splice_replace, 4);
|
|
670
|
+
rb_define_singleton_method(mBrotliSplice, "replace_slots", rb_brotli_splice_replace_slots, 2);
|
|
494
671
|
}
|
metadata
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brotli_splice
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
BrotliSplice is a Ruby C extension that creates Brotli-compressed streams
|
|
15
|
-
containing
|
|
15
|
+
containing fixed-length uncompressed slots. Each slot can be overwritten
|
|
16
16
|
with a simple byte copy, making it useful for injecting fixed-size secrets
|
|
17
17
|
or tokens into pre-compressed HTML responses.
|
|
18
18
|
email:
|