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