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 +4 -4
- data/README.md +19 -1
- data/ext/brotli_splice/brotli_splice.c +455 -158
- data/lib/brotli_splice/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c2ff7c74f1280217916777270114a52fefb8a6c7c67899db9a6f2017d978e2c
|
|
4
|
+
data.tar.gz: 8a4c7335d85ef0364713704e2ee18924596742401f15d63359dba3182a54c5f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 <
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
VALUE
|
|
148
|
-
|
|
149
|
-
|
|
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")),
|
|
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));
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
470
|
+
VALUE result = rb_str_dup(data);
|
|
184
471
|
rb_str_modify(result);
|
|
185
|
-
memcpy(RSTRING_PTR(result) + offset, RSTRING_PTR(
|
|
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
|
-
|
|
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
|
}
|
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.
|
|
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-
|
|
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
|