brotli_splice 0.1.1 → 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 +59 -6
- data/brotli_splice.gemspec +1 -1
- data/ext/brotli_splice/brotli_splice.c +632 -158
- 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
|
|
|
@@ -99,6 +104,24 @@ ruby -Ilib test_ext.rb
|
|
|
99
104
|
|
|
100
105
|
## API
|
|
101
106
|
|
|
107
|
+
### `BrotliSplice::Encoder`
|
|
108
|
+
|
|
109
|
+
Use the stateful encoder when the full document is not available up front:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
encoder = BrotliSplice::Encoder.new(quality: 5, lgwin: 22)
|
|
113
|
+
compressed = encoder.write("<html><head>".b)
|
|
114
|
+
compressed << encoder.write("<title>Store</title>".b)
|
|
115
|
+
compressed << encoder.slot("live-token\r\n".b)
|
|
116
|
+
compressed << encoder.finish("</head><body>...</body></html>".b)
|
|
117
|
+
```
|
|
118
|
+
|
|
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.
|
|
120
|
+
|
|
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"`.
|
|
122
|
+
|
|
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`.
|
|
124
|
+
|
|
102
125
|
### `BrotliSplice.encode(html, secret_offset, secret_length, quality: 11) → Hash`
|
|
103
126
|
|
|
104
127
|
Compress `html` with a spliceable slot at the given byte position.
|
|
@@ -119,6 +142,28 @@ Compress `html` with a spliceable slot at the given byte position.
|
|
|
119
142
|
}
|
|
120
143
|
```
|
|
121
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
|
+
|
|
122
167
|
### `BrotliSplice.replace(compressed_data, new_secret, secret_offset, secret_length) → String`
|
|
123
168
|
|
|
124
169
|
Replace the secret in a compressed stream. Returns a new string with the
|
|
@@ -130,6 +175,15 @@ swapped bytes. This is a pure `memcpy` — no compression or decompression.
|
|
|
130
175
|
- `secret_offset` — from the `encode` result
|
|
131
176
|
- `secret_length` — from the `encode` result
|
|
132
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
|
+
|
|
133
187
|
## Example
|
|
134
188
|
|
|
135
189
|
```ruby
|
|
@@ -167,8 +221,7 @@ longer, or place the suffix where a line break is natural).
|
|
|
167
221
|
|
|
168
222
|
## Limitations
|
|
169
223
|
|
|
170
|
-
-
|
|
224
|
+
- Each replaceable slot must be no larger than 65536 bytes, plus the 2-byte context suffix
|
|
171
225
|
- The replacement must be **exactly** `secret_length` bytes (no length changes)
|
|
172
|
-
- The last 2 bytes of
|
|
173
|
-
-
|
|
174
|
-
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
|
|
@@ -1,197 +1,671 @@
|
|
|
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 <
|
|
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
|
+
/* An uncompressed meta-block header stores MLEN - 1 in 16 bits. */
|
|
19
|
+
#define MAX_SLOT_BODY_LEN 65536
|
|
20
|
+
#define OUTPUT_CHUNK_SIZE 16384
|
|
20
21
|
static const uint8_t CTX_BYTES[CTX_LEN] = { '\r', '\n' };
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
23
|
+
typedef enum {
|
|
24
|
+
ENCODER_UNINITIALIZED,
|
|
25
|
+
ENCODER_ACTIVE,
|
|
26
|
+
ENCODER_FINISHED,
|
|
27
|
+
ENCODER_CLOSED,
|
|
28
|
+
ENCODER_FAILED,
|
|
29
|
+
} encoder_lifecycle_t;
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
size_t offset;
|
|
33
|
+
size_t length;
|
|
34
|
+
} slot_position_t;
|
|
35
|
+
|
|
36
|
+
typedef struct {
|
|
37
|
+
BrotliEncoderState *encoder;
|
|
38
|
+
int quality;
|
|
39
|
+
int lgwin;
|
|
40
|
+
size_t uncompressed_bytes;
|
|
41
|
+
size_t bytes_written;
|
|
42
|
+
slot_position_t *slots;
|
|
43
|
+
size_t slot_count;
|
|
44
|
+
size_t slot_capacity;
|
|
45
|
+
size_t native_bytes;
|
|
46
|
+
encoder_lifecycle_t lifecycle;
|
|
47
|
+
} splice_encoder_t;
|
|
48
|
+
|
|
49
|
+
typedef union {
|
|
50
|
+
struct {
|
|
51
|
+
size_t allocation_size;
|
|
52
|
+
} tracked;
|
|
53
|
+
max_align_t alignment;
|
|
54
|
+
} allocation_header_t;
|
|
55
|
+
|
|
56
|
+
static void *encoder_alloc_native(void *opaque, size_t size) {
|
|
57
|
+
splice_encoder_t *encoder = opaque;
|
|
58
|
+
if (size > SIZE_MAX - sizeof(allocation_header_t)) return NULL;
|
|
59
|
+
|
|
60
|
+
size_t allocation_size = sizeof(allocation_header_t) + size;
|
|
61
|
+
allocation_header_t *header = malloc(allocation_size);
|
|
62
|
+
if (!header) return NULL;
|
|
63
|
+
if (encoder->native_bytes > SIZE_MAX - allocation_size) {
|
|
64
|
+
free(header);
|
|
65
|
+
return NULL;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
header->tracked.allocation_size = allocation_size;
|
|
69
|
+
encoder->native_bytes += allocation_size;
|
|
70
|
+
return header + 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static void encoder_free_native(void *opaque, void *address) {
|
|
74
|
+
if (!address) return;
|
|
75
|
+
|
|
76
|
+
splice_encoder_t *encoder = opaque;
|
|
77
|
+
allocation_header_t *header = ((allocation_header_t *)address) - 1;
|
|
78
|
+
encoder->native_bytes -= header->tracked.allocation_size;
|
|
79
|
+
free(header);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static void encoder_destroy_native(splice_encoder_t *encoder) {
|
|
83
|
+
if (!encoder->encoder) return;
|
|
84
|
+
|
|
85
|
+
BrotliEncoderDestroyInstance(encoder->encoder);
|
|
86
|
+
encoder->encoder = NULL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static void encoder_terminalize(splice_encoder_t *encoder, encoder_lifecycle_t lifecycle) {
|
|
90
|
+
encoder_destroy_native(encoder);
|
|
91
|
+
encoder->lifecycle = lifecycle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static void encoder_free(void *pointer) {
|
|
95
|
+
splice_encoder_t *encoder = pointer;
|
|
96
|
+
encoder_destroy_native(encoder);
|
|
97
|
+
xfree(encoder->slots);
|
|
98
|
+
xfree(encoder);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static size_t encoder_memsize(const void *pointer) {
|
|
102
|
+
if (!pointer) return 0;
|
|
103
|
+
|
|
104
|
+
const splice_encoder_t *encoder = pointer;
|
|
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;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static const rb_data_type_t encoder_type = {
|
|
111
|
+
"BrotliSplice::Encoder",
|
|
112
|
+
{NULL, encoder_free, encoder_memsize},
|
|
113
|
+
NULL,
|
|
114
|
+
NULL,
|
|
115
|
+
RUBY_TYPED_FREE_IMMEDIATELY,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
static VALUE encoder_alloc(VALUE klass) {
|
|
119
|
+
splice_encoder_t *encoder;
|
|
120
|
+
VALUE object = TypedData_Make_Struct(klass, splice_encoder_t, &encoder_type, encoder);
|
|
121
|
+
memset(encoder, 0, sizeof(*encoder));
|
|
122
|
+
return object;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static BrotliEncoderState *new_encoder(splice_encoder_t *encoder, int quality, int lgwin, uint32_t stream_offset) {
|
|
126
|
+
BrotliEncoderState *state = BrotliEncoderCreateInstance(encoder_alloc_native, encoder_free_native, encoder);
|
|
127
|
+
if (!state) rb_raise(eBrotliSpliceError, "encoder creation failed");
|
|
128
|
+
|
|
129
|
+
if (!BrotliEncoderSetParameter(state, BROTLI_PARAM_QUALITY, (uint32_t)quality) ||
|
|
130
|
+
!BrotliEncoderSetParameter(state, BROTLI_PARAM_LGWIN, (uint32_t)lgwin) ||
|
|
131
|
+
(stream_offset > 0 && !BrotliEncoderSetParameter(state, BROTLI_PARAM_STREAM_OFFSET, stream_offset))) {
|
|
132
|
+
BrotliEncoderDestroyInstance(state);
|
|
133
|
+
rb_raise(eBrotliSpliceError, "invalid encoder parameters");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static void check_string(VALUE string) {
|
|
140
|
+
Check_Type(string, T_STRING);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static VALUE binary_string(const uint8_t *bytes, size_t length) {
|
|
144
|
+
VALUE result = rb_str_new((const char *)bytes, length);
|
|
145
|
+
rb_enc_associate(result, rb_ascii8bit_encoding());
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static VALUE encoder_feed(BrotliEncoderState *state, BrotliEncoderOperation operation,
|
|
150
|
+
const uint8_t *input, size_t input_length) {
|
|
151
|
+
VALUE output = rb_str_buf_new(64);
|
|
152
|
+
size_t available_in = input_length;
|
|
153
|
+
const uint8_t *next_in = input;
|
|
154
|
+
|
|
155
|
+
for (;;) {
|
|
156
|
+
uint8_t buffer[OUTPUT_CHUNK_SIZE];
|
|
157
|
+
size_t available_out = sizeof(buffer);
|
|
158
|
+
uint8_t *next_out = buffer;
|
|
159
|
+
size_t before_in = available_in;
|
|
160
|
+
|
|
161
|
+
if (!BrotliEncoderCompressStream(
|
|
162
|
+
state,
|
|
163
|
+
operation,
|
|
164
|
+
&available_in,
|
|
165
|
+
&next_in,
|
|
166
|
+
&available_out,
|
|
167
|
+
&next_out,
|
|
168
|
+
NULL)) {
|
|
169
|
+
rb_raise(eBrotliSpliceError, "encoding failed");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
size_t produced = sizeof(buffer) - available_out;
|
|
173
|
+
if (produced > 0) rb_str_cat(output, (const char *)buffer, produced);
|
|
174
|
+
|
|
175
|
+
if (operation == BROTLI_OPERATION_FINISH) {
|
|
176
|
+
if (BrotliEncoderIsFinished(state)) break;
|
|
177
|
+
} else if (available_in == 0 && !BrotliEncoderHasMoreOutput(state)) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (before_in == available_in && produced == 0) {
|
|
182
|
+
rb_raise(eBrotliSpliceError, "encoder made no progress");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
rb_enc_associate(output, rb_ascii8bit_encoding());
|
|
187
|
+
return output;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static void make_uncompressed_header(uint8_t output[3], size_t length) {
|
|
191
|
+
uint32_t value = (uint32_t)(length - 1);
|
|
192
|
+
uint32_t bits = (value << 3) | (1u << 19);
|
|
193
|
+
output[0] = (uint8_t)bits;
|
|
194
|
+
output[1] = (uint8_t)(bits >> 8);
|
|
195
|
+
output[2] = (uint8_t)(bits >> 16);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static VALUE encoder_initialize(int argc, VALUE *argv, VALUE self) {
|
|
199
|
+
VALUE options;
|
|
200
|
+
rb_scan_args(argc, argv, "0:", &options);
|
|
201
|
+
|
|
202
|
+
splice_encoder_t *encoder;
|
|
203
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
204
|
+
|
|
205
|
+
if (encoder->lifecycle != ENCODER_UNINITIALIZED) {
|
|
206
|
+
rb_raise(eBrotliSpliceError, "encoder is already initialized");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
int quality_value = 11;
|
|
210
|
+
int lgwin_value = 22;
|
|
211
|
+
if (!NIL_P(options)) {
|
|
212
|
+
VALUE quality = rb_hash_aref(options, ID2SYM(rb_intern("quality")));
|
|
213
|
+
VALUE lgwin = rb_hash_aref(options, ID2SYM(rb_intern("lgwin")));
|
|
214
|
+
if (!NIL_P(quality)) quality_value = NUM2INT(quality);
|
|
215
|
+
if (!NIL_P(lgwin)) lgwin_value = NUM2INT(lgwin);
|
|
216
|
+
}
|
|
217
|
+
if (quality_value < 0 || quality_value > 11) {
|
|
218
|
+
rb_raise(eBrotliSpliceError, "quality must be between 0 and 11");
|
|
219
|
+
}
|
|
220
|
+
if (lgwin_value < 10 || lgwin_value > 24) {
|
|
221
|
+
rb_raise(eBrotliSpliceError, "lgwin must be between 10 and 24");
|
|
222
|
+
}
|
|
223
|
+
if (encoder->lifecycle != ENCODER_UNINITIALIZED) {
|
|
224
|
+
rb_raise(eBrotliSpliceError, "encoder is already initialized");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
encoder->quality = quality_value;
|
|
228
|
+
encoder->lgwin = lgwin_value;
|
|
229
|
+
encoder->encoder = new_encoder(encoder, encoder->quality, encoder->lgwin, 0);
|
|
230
|
+
encoder->lifecycle = ENCODER_ACTIVE;
|
|
231
|
+
return self;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static void ensure_writable(splice_encoder_t *encoder) {
|
|
235
|
+
switch (encoder->lifecycle) {
|
|
236
|
+
case ENCODER_ACTIVE:
|
|
237
|
+
return;
|
|
238
|
+
case ENCODER_FINISHED:
|
|
239
|
+
rb_raise(eBrotliSpliceError, "encoder is finished");
|
|
240
|
+
case ENCODER_CLOSED:
|
|
241
|
+
rb_raise(eBrotliSpliceError, "encoder is closed");
|
|
242
|
+
case ENCODER_FAILED:
|
|
243
|
+
rb_raise(eBrotliSpliceError, "encoder failed");
|
|
244
|
+
case ENCODER_UNINITIALIZED:
|
|
245
|
+
rb_raise(eBrotliSpliceError, "encoder is not initialized");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static VALUE encoder_initialize_copy(VALUE self, VALUE original) {
|
|
250
|
+
rb_raise(rb_eTypeError, "BrotliSplice::Encoder cannot be copied");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
typedef struct {
|
|
254
|
+
splice_encoder_t *encoder;
|
|
255
|
+
VALUE bytes;
|
|
256
|
+
} encoder_operation_t;
|
|
257
|
+
|
|
258
|
+
static VALUE run_encoder_operation(
|
|
259
|
+
splice_encoder_t *encoder,
|
|
260
|
+
VALUE (*operation)(VALUE),
|
|
261
|
+
encoder_operation_t *arguments
|
|
262
|
+
) {
|
|
263
|
+
int state = 0;
|
|
264
|
+
VALUE output = rb_protect(operation, (VALUE)arguments, &state);
|
|
265
|
+
if (state) {
|
|
266
|
+
encoder_terminalize(encoder, ENCODER_FAILED);
|
|
267
|
+
rb_jump_tag(state);
|
|
268
|
+
}
|
|
269
|
+
return output;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static VALUE encoder_write_operation(VALUE opaque) {
|
|
273
|
+
encoder_operation_t *arguments = (encoder_operation_t *)opaque;
|
|
274
|
+
splice_encoder_t *encoder = arguments->encoder;
|
|
275
|
+
VALUE bytes = arguments->bytes;
|
|
276
|
+
VALUE output = encoder_feed(
|
|
277
|
+
encoder->encoder,
|
|
278
|
+
BROTLI_OPERATION_FLUSH,
|
|
279
|
+
(const uint8_t *)RSTRING_PTR(bytes),
|
|
280
|
+
(size_t)RSTRING_LEN(bytes));
|
|
281
|
+
encoder->uncompressed_bytes += (size_t)RSTRING_LEN(bytes);
|
|
282
|
+
encoder->bytes_written += (size_t)RSTRING_LEN(output);
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static VALUE encoder_write(VALUE self, VALUE bytes) {
|
|
287
|
+
splice_encoder_t *encoder;
|
|
288
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
289
|
+
ensure_writable(encoder);
|
|
290
|
+
check_string(bytes);
|
|
291
|
+
|
|
292
|
+
encoder_operation_t arguments = {encoder, bytes};
|
|
293
|
+
return run_encoder_operation(encoder, encoder_write_operation, &arguments);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static VALUE encoder_slot_operation(VALUE opaque) {
|
|
297
|
+
encoder_operation_t *arguments = (encoder_operation_t *)opaque;
|
|
298
|
+
splice_encoder_t *encoder = arguments->encoder;
|
|
299
|
+
VALUE bytes = arguments->bytes;
|
|
300
|
+
size_t total_length = (size_t)RSTRING_LEN(bytes);
|
|
301
|
+
size_t body_length = total_length - CTX_LEN;
|
|
302
|
+
size_t max_stream_offset = ((size_t)1) << encoder->lgwin;
|
|
303
|
+
uint32_t stream_offset = (uint32_t)(
|
|
304
|
+
encoder->uncompressed_bytes + body_length > max_stream_offset
|
|
305
|
+
? max_stream_offset
|
|
306
|
+
: encoder->uncompressed_bytes + body_length);
|
|
307
|
+
|
|
308
|
+
VALUE output = encoder_feed(
|
|
309
|
+
encoder->encoder,
|
|
310
|
+
BROTLI_OPERATION_FLUSH,
|
|
311
|
+
NULL,
|
|
312
|
+
0);
|
|
313
|
+
size_t flushed_length = (size_t)RSTRING_LEN(output);
|
|
314
|
+
|
|
315
|
+
BrotliEncoderState *next_encoder = new_encoder(encoder, encoder->quality, encoder->lgwin, stream_offset);
|
|
316
|
+
encoder_destroy_native(encoder);
|
|
317
|
+
encoder->encoder = next_encoder;
|
|
318
|
+
|
|
319
|
+
uint8_t header[3];
|
|
320
|
+
make_uncompressed_header(header, body_length);
|
|
321
|
+
rb_str_cat(output, (const char *)header, sizeof(header));
|
|
322
|
+
rb_str_cat(output, RSTRING_PTR(bytes), body_length);
|
|
323
|
+
|
|
324
|
+
VALUE context_output = encoder_feed(
|
|
325
|
+
encoder->encoder,
|
|
326
|
+
BROTLI_OPERATION_PROCESS,
|
|
327
|
+
CTX_BYTES,
|
|
328
|
+
CTX_LEN);
|
|
329
|
+
rb_str_concat(output, context_output);
|
|
330
|
+
|
|
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++;
|
|
334
|
+
encoder->bytes_written += flushed_length + sizeof(header) + body_length + (size_t)RSTRING_LEN(context_output);
|
|
335
|
+
encoder->uncompressed_bytes += body_length + CTX_LEN;
|
|
336
|
+
return output;
|
|
337
|
+
}
|
|
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
|
+
|
|
351
|
+
static VALUE encoder_slot(VALUE self, VALUE bytes) {
|
|
352
|
+
splice_encoder_t *encoder;
|
|
353
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
354
|
+
ensure_writable(encoder);
|
|
355
|
+
check_string(bytes);
|
|
356
|
+
|
|
357
|
+
size_t total_length = (size_t)RSTRING_LEN(bytes);
|
|
358
|
+
if (total_length <= CTX_LEN) rb_raise(eBrotliSpliceError, "slot must be longer than 2 bytes");
|
|
359
|
+
|
|
360
|
+
size_t body_length = total_length - CTX_LEN;
|
|
361
|
+
if (body_length > MAX_SLOT_BODY_LEN) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
|
|
362
|
+
if (memcmp(RSTRING_PTR(bytes) + body_length, CTX_BYTES, CTX_LEN) != 0) {
|
|
363
|
+
rb_raise(eBrotliSpliceError, "slot must end with the context suffix \\r\\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
reserve_slot(encoder);
|
|
367
|
+
|
|
368
|
+
encoder_operation_t arguments = {encoder, bytes};
|
|
369
|
+
return run_encoder_operation(encoder, encoder_slot_operation, &arguments);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static VALUE encoder_finish_operation(VALUE opaque) {
|
|
373
|
+
encoder_operation_t *arguments = (encoder_operation_t *)opaque;
|
|
374
|
+
splice_encoder_t *encoder = arguments->encoder;
|
|
375
|
+
VALUE bytes = arguments->bytes;
|
|
376
|
+
VALUE output = encoder_feed(
|
|
377
|
+
encoder->encoder,
|
|
378
|
+
BROTLI_OPERATION_FINISH,
|
|
379
|
+
(const uint8_t *)RSTRING_PTR(bytes),
|
|
380
|
+
(size_t)RSTRING_LEN(bytes));
|
|
381
|
+
encoder->uncompressed_bytes += (size_t)RSTRING_LEN(bytes);
|
|
382
|
+
encoder->bytes_written += (size_t)RSTRING_LEN(output);
|
|
383
|
+
encoder_terminalize(encoder, ENCODER_FINISHED);
|
|
384
|
+
return output;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
static VALUE encoder_finish(int argc, VALUE *argv, VALUE self) {
|
|
388
|
+
VALUE bytes;
|
|
389
|
+
rb_scan_args(argc, argv, "01", &bytes);
|
|
390
|
+
if (NIL_P(bytes)) bytes = binary_string(NULL, 0);
|
|
391
|
+
|
|
392
|
+
splice_encoder_t *encoder;
|
|
393
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
394
|
+
ensure_writable(encoder);
|
|
395
|
+
check_string(bytes);
|
|
396
|
+
|
|
397
|
+
encoder_operation_t arguments = {encoder, bytes};
|
|
398
|
+
return run_encoder_operation(encoder, encoder_finish_operation, &arguments);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
static VALUE encoder_close(VALUE self) {
|
|
402
|
+
splice_encoder_t *encoder;
|
|
403
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
404
|
+
if (encoder->lifecycle == ENCODER_ACTIVE || encoder->lifecycle == ENCODER_UNINITIALIZED) {
|
|
405
|
+
encoder_terminalize(encoder, ENCODER_CLOSED);
|
|
406
|
+
}
|
|
407
|
+
return Qnil;
|
|
51
408
|
}
|
|
52
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
|
+
|
|
414
|
+
static VALUE encoder_slot_offset(VALUE self) {
|
|
415
|
+
splice_encoder_t *encoder;
|
|
416
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
417
|
+
const slot_position_t *slot = last_slot(encoder);
|
|
418
|
+
return slot ? SIZET2NUM(slot->offset) : Qnil;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
static VALUE encoder_slot_length(VALUE self) {
|
|
422
|
+
splice_encoder_t *encoder;
|
|
423
|
+
TypedData_Get_Struct(self, splice_encoder_t, &encoder_type, encoder);
|
|
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;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
static VALUE encoder_context_suffix(VALUE self) {
|
|
443
|
+
return binary_string(CTX_BYTES, CTX_LEN);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
typedef struct {
|
|
447
|
+
VALUE encoder;
|
|
448
|
+
VALUE pieces;
|
|
449
|
+
} encode_operation_t;
|
|
450
|
+
|
|
53
451
|
/*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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")
|
|
452
|
+
* Pieces alternate between compressed chunks and slots, starting and ending
|
|
453
|
+
* with a compressed chunk: [chunk, slot, chunk, slot, ..., chunk].
|
|
68
454
|
*/
|
|
455
|
+
static VALUE encode_operation(VALUE opaque) {
|
|
456
|
+
encode_operation_t *arguments = (encode_operation_t *)opaque;
|
|
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;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static VALUE close_encoder_ensure(VALUE encoder_value) {
|
|
468
|
+
return encoder_close(encoder_value);
|
|
469
|
+
}
|
|
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
|
+
|
|
69
502
|
static VALUE rb_brotli_splice_encode(int argc, VALUE *argv, VALUE self) {
|
|
70
|
-
VALUE
|
|
71
|
-
rb_scan_args(argc, argv, "3:", &
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
size_t
|
|
76
|
-
size_t
|
|
77
|
-
|
|
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)
|
|
503
|
+
VALUE html, secret_offset_value, secret_length_value, options;
|
|
504
|
+
rb_scan_args(argc, argv, "3:", &html, &secret_offset_value, &secret_length_value, &options);
|
|
505
|
+
Check_Type(html, T_STRING);
|
|
506
|
+
|
|
507
|
+
size_t secret_offset = NUM2SIZET(secret_offset_value);
|
|
508
|
+
size_t secret_length = NUM2SIZET(secret_length_value);
|
|
509
|
+
size_t html_length = (size_t)RSTRING_LEN(html);
|
|
510
|
+
if (secret_length <= CTX_LEN) rb_raise(eBrotliSpliceError, "secret_length must be > %d", CTX_LEN);
|
|
511
|
+
if (secret_offset > html_length || secret_length > html_length - secret_offset) {
|
|
82
512
|
rb_raise(eBrotliSpliceError, "secret_offset + secret_length exceeds html size");
|
|
513
|
+
}
|
|
514
|
+
if (secret_length - CTX_LEN > MAX_SLOT_BODY_LEN) rb_raise(eBrotliSpliceError, "slot body must not exceed 64 KB");
|
|
83
515
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
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);
|
|
150
520
|
|
|
151
521
|
VALUE result = rb_hash_new();
|
|
152
522
|
rb_hash_aset(result, ID2SYM(rb_intern("data")), data);
|
|
153
|
-
rb_hash_aset(result, ID2SYM(rb_intern("secret_offset")),
|
|
154
|
-
rb_hash_aset(result, ID2SYM(rb_intern("secret_length")),
|
|
155
|
-
rb_hash_aset(result, ID2SYM(rb_intern("context_suffix")),
|
|
156
|
-
rb_str_new((const char *)CTX_BYTES, CTX_LEN));
|
|
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));
|
|
157
526
|
return result;
|
|
158
527
|
}
|
|
159
528
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
size_t
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
rb_raise(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
static VALUE rb_brotli_splice_replace(VALUE self, VALUE data, VALUE secret,
|
|
577
|
+
VALUE offset_value, VALUE length_value) {
|
|
578
|
+
Check_Type(data, T_STRING);
|
|
579
|
+
Check_Type(secret, T_STRING);
|
|
580
|
+
size_t offset = NUM2SIZET(offset_value);
|
|
581
|
+
size_t length = NUM2SIZET(length_value);
|
|
582
|
+
|
|
583
|
+
if ((size_t)RSTRING_LEN(secret) != length) {
|
|
584
|
+
rb_raise(eBrotliSpliceError, "secret length %ld != expected %zu", RSTRING_LEN(secret), length);
|
|
585
|
+
}
|
|
586
|
+
if (offset > (size_t)RSTRING_LEN(data) || length > (size_t)RSTRING_LEN(data) - offset) {
|
|
181
587
|
rb_raise(eBrotliSpliceError, "offset+length exceeds data size");
|
|
588
|
+
}
|
|
182
589
|
|
|
183
|
-
VALUE result = rb_str_dup(
|
|
590
|
+
VALUE result = rb_str_dup(data);
|
|
184
591
|
rb_str_modify(result);
|
|
185
|
-
memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(
|
|
592
|
+
memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(secret), length);
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
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);
|
|
186
647
|
return result;
|
|
187
648
|
}
|
|
188
649
|
|
|
189
650
|
void Init_brotli_splice(void) {
|
|
190
651
|
mBrotliSplice = rb_define_module("BrotliSplice");
|
|
191
652
|
eBrotliSpliceError = rb_define_class_under(mBrotliSplice, "Error", rb_eRuntimeError);
|
|
653
|
+
cEncoder = rb_define_class_under(mBrotliSplice, "Encoder", rb_cObject);
|
|
654
|
+
|
|
655
|
+
rb_define_alloc_func(cEncoder, encoder_alloc);
|
|
656
|
+
rb_define_method(cEncoder, "initialize", encoder_initialize, -1);
|
|
657
|
+
rb_define_method(cEncoder, "initialize_copy", encoder_initialize_copy, 1);
|
|
658
|
+
rb_define_method(cEncoder, "write", encoder_write, 1);
|
|
659
|
+
rb_define_method(cEncoder, "slot", encoder_slot, 1);
|
|
660
|
+
rb_define_method(cEncoder, "finish", encoder_finish, -1);
|
|
661
|
+
rb_define_method(cEncoder, "close", encoder_close, 0);
|
|
662
|
+
rb_define_method(cEncoder, "slot_offset", encoder_slot_offset, 0);
|
|
663
|
+
rb_define_method(cEncoder, "slot_length", encoder_slot_length, 0);
|
|
664
|
+
rb_define_method(cEncoder, "slots", encoder_slots, 0);
|
|
665
|
+
rb_define_method(cEncoder, "context_suffix", encoder_context_suffix, 0);
|
|
192
666
|
|
|
193
|
-
rb_define_singleton_method(mBrotliSplice, "encode",
|
|
194
|
-
|
|
195
|
-
rb_define_singleton_method(mBrotliSplice, "replace",
|
|
196
|
-
|
|
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);
|
|
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);
|
|
197
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:
|