1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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
117
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! **The piece-count ceiling** — the one place this crate refuses a request that
//! would exhaust memory instead of attempting it.
//!
//! Every parallel path here turns *one* size parameter into *many* independent
//! pieces: `cfg.segment` cuts an object into segments ([`crate::split`]),
//! `member` cuts a buffer into gzip members ([`crate::gzip`]). Both are
//! `len.div_ceil(piece)`, and both are **amplifying**: the caller passes two
//! small integers and the crate allocates a structure proportional to their
//! quotient. A `piece` of 1 against a multi-megabyte input asks for millions of
//! independently framed, independently compressed, independently checksummed
//! pieces.
//!
//! That is not a hypothetical. On 2026-08-03 a test crossing `len = 3_000_000`
//! with `member = 1` allocated ~497 GB on a 499 GB box, the kernel OOM killer
//! fired and took unrelated services down with it. The test was the trigger; the
//! bug was that the library said yes.
//!
//! # The measurement the ceiling is derived from
//!
//! Peak RSS of `gzip_compress_members(&[b'x'; n], &Config::default(), 1)` — i.e.
//! `n` one-byte members — on oden (32 cores, glibc malloc), release build:
//!
//! | pieces | peak RSS |
//! |---|---|
//! | 100_000 | 0.45 GB |
//! | 250_000 | 0.87 GB |
//! | 300_000 | 0.78 GB |
//! | 400_000 | **13.0 GB** |
//! | 800_000 | **21.0 GB** (address-space capped) |
//! | 3_000_000 | ~497 GB uncapped — the OOM |
//!
//! The cost is not merely linear-with-a-big-constant. Below ~3x10^5 pieces the
//! peak stays under a gigabyte; above it the allocator churn from one encoder
//! pair per unit turns the curve superlinear and, past the cap, unpredictable.
//! So the ceiling is set at the last power of two that is still measurably on
//! the flat part of that curve: [`MAX_PIECES`] = 2^18 = 262_144.
//!
//! # Why an error and not a clamp
//!
//! Clamping — quietly raising `piece` until the count fits — would be the
//! friendlier-looking fix and it is the wrong one. This crate's headline
//! guarantee is that the same `(input, Config)` produces byte-identical output
//! at any worker count, and the segment plan is what carries that guarantee. A
//! silent clamp changes the plan, therefore changes the output bytes, and does
//! it *without the caller ever learning* — turning a loud failure into a
//! reproducibility hole. An `Err` costs the caller one line and tells them
//! exactly what to pass instead.
//!
//! # What is NOT bounded here, and why
//!
//! The number of *objects* handed to [`crate::compress_objects`] is not capped.
//! That count is not amplification: the caller already holds every one of those
//! buffers in memory, so the plan is a constant factor over storage the caller
//! has already paid for. Only counts derived from a size *parameter* are
//! checked.
use crateDeflateError;
/// The largest number of independently framed pieces any one request may ask
/// for: **262_144** (2^18).
///
/// Measured headroom, so this is not an arbitrary round number:
///
/// - at the default `segment` of 1 MiB it admits a **256 GiB** object;
/// - at a 256 KiB gzip member it admits a **64 GiB** buffer;
/// - even at a deliberately small 64 KiB segment it admits **16 GiB**.
///
/// Every one of those is larger than any `&[u8]` this crate is realistically
/// handed, while the measured peak RSS at the ceiling stays under 1 GB. See the
/// module docs for the sweep.
pub const MAX_PIECES: usize = 1 << 18;
/// How many pieces of `piece` bytes an input of `total` bytes is cut into.
///
/// An empty input is one (empty) piece, not zero: a zlib stream with no blocks
/// and a gzip file with no members are both malformed, so every path here emits
/// at least one. `piece` is floored at 1 so a `0` argument cannot divide by
/// zero — but note that `piece = 0` and `piece = 1` then ask for the same
/// (rejected) `total` pieces.
/// Return the piece count for `(total, piece)`, or refuse it.
///
/// `what` names the caller's axis ("gzip members", "split segments") so the
/// message is actionable without a backtrace. The error carries the request as
/// asked *and* the smallest `piece` that would have been accepted.
/// The smallest piece size that keeps `total` bytes under [`MAX_PIECES`]. What
/// the error tells the caller to pass instead.