choreo-daemon 0.2.0

Agentic coding assistant — daemon, TUI, and bridges
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! zstd codec for `session_turns` value blobs.
//!
//! The schema-2 contract is "a standard zstd frame around the MessagePack
//! serialization of a [`Turn`](choreo_proto::Turn)". The codec is implemented
//! by `structured-zstd` (pure Rust, no libzstd C build); numeric compression
//! levels map onto C zstd numbering, so [`COMPRESSION_LEVEL`] keeps the tuning
//! that was benchmarked against the C library. Kept as its own module so the
//! storage plumbing in `super` (schema, tables, migrations) does not carry the
//! codec details, its fixtures, or its tests.

use std::io;
use std::io::Read;

use super::db_err;

// ── Codec constants ──────────────────────────────────────────────────────────

/// zstd compression level for `session_turns` values. Level 6 was chosen by
/// benchmarking against the production database (10 k+ real turns, ~160 MB of
/// raw MessagePack): it captures ~85% of the available compression (ratio
/// 3.66→3.83) while keeping encode fast (~100 MB/s; a median 6 KB turn encodes
/// in ~60 µs) and the one-time 1→2 migration quick (~1.7 s on the measured DB).
/// Those figures were measured on the C libzstd; the pure-Rust codec
/// (structured-zstd, which maps numeric levels 1–22 onto C zstd numbering) is
/// roughly comparable or faster on small payloads and a bit slower on large
/// ones, but still µs-scale per turn, so level 6 remains the sweet spot:
/// decode is flat across levels, levels above 9 add <1% ratio while tripling
/// encode cost (12+ is ~18× slower for ~0.01 more ratio), so 6 balances ratio
/// and write-path cost. Tuning this is a constant, not a design change — the
/// codec is concrete (zstd frame format) and the on-disk contract is
/// "zstd-compressed MessagePack", independent of the implementation.
const COMPRESSION_LEVEL: i32 = 6;

/// Maximum number of bytes a single `session_turns` value may expand to when a
/// zstd frame is decompressed. A zstd frame advertises its uncompressed size in
/// its header, and `Decoder`/`decode_all` allocate that much on faith — a
/// corrupt or malicious row could therefore claim an absurd size and pin the
/// daemon's memory. We cap decompression and treat an over-limit frame as
/// undecodable (the caller skips it with a warning, same as any corrupt row).
/// Set comfortably above any legitimate turn payload: conversation text + tool
/// output + reasoning artifacts.
const MAX_TURN_DECODED_BYTES: u64 = 256 * 1024 * 1024; // 256 MiB

/// The 4-byte little-endian magic that prefixes every zstd frame
/// (0xFD2FB528). It is zstd's *inherent* frame marker, not a wrapper tag we
/// add — we use it purely to tell an already-compressed row from a legacy
/// raw-MessagePack row, which is what makes the 1→2 migration safe to re-run
/// after a crash (idempotency the migration framework requires).
///
/// It is unambiguous: a legacy `Turn` always serializes to a named-MessagePack
/// map, so its first byte is a map header (0x80..0x8f, the 13-field marker is
/// 0x8D) — never 0x28 (a positive fixint). A zstd frame starts with 0x28.
pub(super) const ZSTD_FRAME_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];

// ── Turn value compression ────────────────────────────────────────────────────

/// Encode a MessagePack-encoded `Turn` with zstd at [`COMPRESSION_LEVEL`] into
/// one complete, standard zstd frame. Compression is applied to the WHOLE
/// serialized blob (never per-field): zstd matches redundancy across the entire
/// buffer, so it also compresses the MessagePack framing overhead (field keys,
/// headers) on top of the string payloads. Pure-Rust `structured-zstd`; numeric
/// levels match C zstd numbering, so [`COMPRESSION_LEVEL`] keeps its tuned
/// meaning. Infallible for our purposes — any input can be framed — so we
/// return the `Vec` directly rather than a `Result`.
pub(super) fn zstd_encode(payload: &[u8]) -> Vec<u8> {
    structured_zstd::encoding::compress_slice_to_vec(
        payload,
        structured_zstd::encoding::CompressionLevel::from_level(COMPRESSION_LEVEL),
    )
}

/// Recover the original MessagePack bytes from a zstd frame, bounded by
/// [`MAX_TURN_DECODED_BYTES`] (see [`zstd_decode_with_limit`]).
pub(super) fn zstd_decode(blob: &[u8]) -> io::Result<Vec<u8>> {
    zstd_decode_with_limit(blob, MAX_TURN_DECODED_BYTES)
}

/// Decompress a zstd frame into a buffer of at most `limit` bytes, then reject
/// the blob unless it was consumed to EOF.
///
/// Two defenses, both load-bearing:
///
/// 1. **Bounded decode.** A zstd frame advertises its uncompressed size in its
///    header, and a one-shot decoder would allocate that much on faith — a
///    corrupt or malicious row could claim an absurd size and pin the daemon's
///    memory. The streaming decoder's own `read_to_end` fast path pre-sizes its
///    output from the DECLARED size before validating the block data, so we
///    must read through a `Take` cap: `Read::take` forces the generic
///    block-at-a-time read loop, which decodes only as many bytes as actually
///    arrive. Reads stop at `limit + 1` bytes total, and a buffer that reached
///    the cap is rejected as undecodable. Never drop the `.take()` wrapper —
///    doing so re-opens the allocation-on-faith hole for a crafted row.
/// 2. **Strict consumption.** A well-formed row is exactly ONE frame. The
///    generic read loop stops at the end of the first frame and ignores
///    whatever follows, so checking the source cursor reached true EOF turns
///    trailing bytes or a second concatenated frame (a corrupt or foreign row)
///    into a hard error instead of silently returning the truncated first
///    frame. The old C libzstd decoder refused those at EOF too; this restores
///    that fail-loud parity.
///
/// The limit is a parameter so the guard is unit-testable without allocating a
/// huge buffer; production callers use [`MAX_TURN_DECODED_BYTES`].
pub(super) fn zstd_decode_with_limit(blob: &[u8], limit: u64) -> io::Result<Vec<u8>> {
    let decoder = structured_zstd::decoding::StreamingDecoder::new(std::io::Cursor::new(blob))
        .map_err(|e| db_err(format!("zstd decode turn (init): {e}")))?;
    let mut buf = Vec::new();
    // `Read::take` truncates the read at limit+1 bytes, so we never allocate an
    // attacker-claimed size; buf ending at (or over) the limit means the frame
    // expands further than we're willing to accept. Reading through `Take` also
    // bypasses the decoder's pre-sizing read_to_end fast path (see the doc
    // comment above).
    let mut capped = decoder.take(limit + 1);
    capped
        .read_to_end(&mut buf)
        .map_err(|e| db_err(format!("zstd decode turn: {e}")))?;
    if buf.len() as u64 > limit {
        return Err(db_err(format!(
            "turn frame expands past {limit} bytes; refusing"
        )));
    }
    // The bounded read stops at the end of the first frame; anything left over
    // means the row is not exactly one frame (trailing garbage or a second
    // concatenated frame) — refuse it rather than silently truncating.
    let consumed = capped.into_inner().into_inner().position();
    if consumed != blob.len() as u64 {
        return Err(db_err(format!(
            "turn frame left {} trailing bytes unconsumed; refusing",
            blob.len() as u64 - consumed
        )));
    }
    Ok(buf)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn zstd_round_trip_and_bounded_decode() {
        // Compressible payload: compresses well, so the stored frame is smaller
        // than the source (the point of the schema-2 codec) and the framing is
        // exactly a zstd frame (magic prefix).
        let payload = b"hello world hello world redundant redundant ".repeat(64);
        let compressed = zstd_encode(&payload);
        assert!(
            compressed.starts_with(&ZSTD_FRAME_MAGIC),
            "encoded blob must be a zstd frame"
        );
        assert!(
            compressed.len() < payload.len(),
            "compressible turn text must shrink after zstd"
        );

        // Round trip: decode recovers the exact original bytes.
        let decoded = zstd_decode(&compressed).unwrap();
        assert_eq!(decoded, payload);

        // Incompressible payload still round-trips (frame may not shrink, but
        // must decode back losslessly).
        let randomish: Vec<u8> = (0u8..=255).cycle().take(4096).collect();
        let c2 = zstd_encode(&randomish);
        assert_eq!(zstd_decode(&c2).unwrap(), randomish);
    }

    #[test]
    fn zstd_bounded_decode_rejects_oversized_frame() {
        // A small, highly-compressible payload that expands well past a small
        // limit must be rejected rather than fully allocated — the
        // decompression-bomb guard (production cap: MAX_TURN_DECODED_BYTES).
        let compressed = zstd_encode(&b"x".repeat(10_000));
        // limit 100 ≪ 10 kB expanded size ⇒ undecodable.
        assert!(zstd_decode_with_limit(&compressed, 100).is_err());
        // A limit large enough for the payload still decodes.
        assert_eq!(
            zstd_decode_with_limit(&compressed, 1_000_000).unwrap(),
            b"x".repeat(10_000)
        );
    }

    // --- Legacy-compat fixtures ---------------------------------------------
    //
    // Schema-2 databases hold zstd frames written by the old C libzstd
    // (`zstd` crate, level 6). The pure-Rust decoder must recover those
    // byte-for-byte. These frames were generated once with libzstd level 6 via
    // a throwaway probe crate and embedded as constants so the test suite needs
    // no C dependency. If they ever need regenerating, compress the same
    // payloads with the `zstd` crate's `encode_all(.., 6)`.

    /// C-libzstd level-6 frame of `b"hello world hello world redundant redundant ".repeat(64)` (2816 B → 42 B).
    const CZSTD_FIXTURE_REPETITIVE: &[u8] = &[
        0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x58, 0x0D, 0x01, 0x00, 0xA8, 0x68, 0x65, 0x6C, 0x6C, 0x6F,
        0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x20, 0x72, 0x65, 0x64, 0x75, 0x6E, 0x64, 0x61, 0x6E,
        0x74, 0x03, 0x00, 0xD1, 0x7A, 0xCA, 0x83, 0x96, 0xF8, 0x9C, 0x2B, 0x49,
    ];

    /// C-libzstd level-6 frame of `(0u8..=255).cycle().take(4096)` (4096 B → 275 B).
    const CZSTD_FIXTURE_RANDOMISH: &[u8] = &[
        0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x58, 0x55, 0x08, 0x00, 0x04, 0x10, 0x00, 0x01, 0x02, 0x03,
        0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12,
        0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21,
        0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30,
        0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
        0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E,
        0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D,
        0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C,
        0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B,
        0x7C, 0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A,
        0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
        0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8,
        0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
        0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6,
        0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5,
        0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4,
        0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3,
        0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x01, 0x00, 0x00,
        0xFD, 0x1E, 0xF0, 0xD7, 0x14,
    ];

    /// Deterministic stand-in for a MessagePack-encoded turn: prose + JSON tool
    /// output + markdown, ~15 KB with heavy internal repetition (the shape of
    /// real `session_turns` values). Same LCG and word list used to produce
    /// [`CZSTD_FIXTURE_TURNLIKE`], so the expected bytes match exactly.
    fn turn_like_fixture_payload() -> Vec<u8> {
        let mut state: u64 = 0x5EED_CAFE;
        let mut next = move || {
            state = state
                .wrapping_mul(6364136223846793005)
                .wrapping_add(1442695040888963407);
            (state >> 33) as usize
        };
        let words = [
            "refactor",
            "migration",
            "compression",
            "decoder",
            "session",
            "message",
            "transaction",
            "daemon",
            "client",
            "protocol",
            "schema",
            "buffer",
            "channel",
            "stream",
            "payload",
            "deadlock",
            "lock",
            "timeout",
            "retry",
        ];
        let mut out = String::new();
        for block in 0..60 {
            out.push_str(&format!(
                "user: What about the {} in block {block}?\n",
                words[next() % words.len()]
            ));
            out.push_str("assistant: The ");
            for _ in 0..next() % 10 + 3 {
                out.push_str(words[next() % words.len()]);
                out.push(' ');
            }
            out.push_str("needs care.\n");
            out.push_str("```json\n{\"tool\":\"read_file\",\"block\":42,\"status\":\"ok\",\"ms\":123}\n```\n");
            out.push_str("- [ ] verify\n### Analysis\n> bounded decode protects us.\n\n");
        }
        out.into_bytes()
    }

    /// C-libzstd level-6 frame of the turn-like payload (15111 B → 1365 B).
    const CZSTD_FIXTURE_TURNLIKE: &[u8] = &[
        0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x58, 0x65, 0x2A, 0x00, 0x16, 0x17, 0x46, 0x22, 0x40, 0x6B,
        0xDC, 0xCB, 0xE6, 0x4F, 0xA9, 0x18, 0xDE, 0x5A, 0x0D, 0x8E, 0x5C, 0x8A, 0x24, 0xD1, 0x92,
        0xAE, 0x4E, 0x53, 0xB7, 0xF6, 0x63, 0x13, 0x11, 0xCA, 0xE8, 0x59, 0x3A, 0x19, 0x9E, 0xA4,
        0x11, 0x01, 0x39, 0x00, 0x3F, 0x00, 0x3B, 0x00, 0xEA, 0x78, 0x19, 0xE9, 0xF6, 0x7B, 0x46,
        0x7E, 0xFE, 0xA9, 0x47, 0xED, 0x4F, 0x8D, 0xD6, 0xF1, 0x3C, 0x9B, 0xFF, 0x69, 0x63, 0x3B,
        0xD7, 0x51, 0x90, 0xCD, 0x47, 0x7D, 0x7F, 0xDC, 0x15, 0x19, 0x50, 0x24, 0xC3, 0x63, 0x88,
        0x91, 0xF1, 0xC9, 0xC8, 0xD1, 0x2A, 0xCC, 0x75, 0x7C, 0xBE, 0xC4, 0x0C, 0xCD, 0x57, 0x87,
        0x41, 0x36, 0xBD, 0x2E, 0x05, 0x08, 0x64, 0x6A, 0x3C, 0xBD, 0x0D, 0x89, 0x90, 0x07, 0x09,
        0x80, 0x38, 0x8E, 0x83, 0x3C, 0x34, 0xB6, 0x1A, 0xE5, 0x91, 0x4A, 0x28, 0x06, 0x56, 0x8D,
        0x72, 0xE9, 0xF5, 0x5D, 0x25, 0x98, 0x6B, 0x94, 0x4A, 0xA8, 0x85, 0x9E, 0x46, 0x02, 0xD4,
        0x6D, 0xD5, 0x28, 0x88, 0xF9, 0x95, 0x03, 0x12, 0xD9, 0x17, 0x1C, 0xC7, 0x41, 0x02, 0xB6,
        0xF5, 0xA5, 0xC3, 0xDD, 0x98, 0xD0, 0xC6, 0x18, 0xDB, 0x1F, 0xF7, 0x81, 0x5E, 0x3F, 0x22,
        0x7E, 0xCD, 0x7A, 0x1B, 0x0D, 0x89, 0xA1, 0xAD, 0xF0, 0x40, 0xFC, 0x58, 0x41, 0xDD, 0x37,
        0x8A, 0xFC, 0xB8, 0xAF, 0x2F, 0xA8, 0x0C, 0x4D, 0xFA, 0x40, 0x90, 0x00, 0x97, 0xD2, 0xFF,
        0xED, 0x73, 0x3F, 0x72, 0x21, 0xBF, 0x85, 0x09, 0x17, 0x46, 0x89, 0x19, 0x29, 0x80, 0xF8,
        0xE3, 0x13, 0xA4, 0x91, 0x20, 0x11, 0x88, 0x4C, 0xA2, 0x2C, 0x12, 0x77, 0x49, 0x4B, 0x96,
        0x22, 0x61, 0xDC, 0xE2, 0xDC, 0xA2, 0x39, 0xB6, 0xDC, 0xD4, 0xAF, 0x70, 0x4C, 0x5B, 0x94,
        0x45, 0x32, 0xF5, 0xBB, 0xC6, 0x5A, 0x53, 0x34, 0x66, 0x61, 0x0A, 0x73, 0xCD, 0x18, 0x5B,
        0x8C, 0x31, 0x8B, 0xE2, 0x6B, 0x35, 0xB6, 0xD6, 0x62, 0x16, 0xE5, 0x25, 0xDE, 0xE8, 0xF9,
        0x45, 0xE2, 0xA0, 0x2E, 0x6C, 0x53, 0x7F, 0x81, 0xDC, 0xA8, 0x31, 0xDB, 0x24, 0xC9, 0xFE,
        0xCF, 0x01, 0x61, 0x84, 0xC4, 0x18, 0xE3, 0x38, 0xB5, 0xD4, 0x03, 0x12, 0x88, 0x70, 0x80,
        0x48, 0x4C, 0x32, 0x1C, 0x43, 0x61, 0xCC, 0x32, 0x43, 0x44, 0x02, 0x19, 0x91, 0x40, 0x44,
        0x62, 0x24, 0xDE, 0x0F, 0x05, 0x32, 0xD7, 0xA3, 0x44, 0xF7, 0xA0, 0x23, 0x9B, 0x43, 0xBC,
        0x18, 0xC2, 0x74, 0x6F, 0x09, 0x29, 0x9B, 0xE2, 0x2B, 0xB8, 0xFB, 0x88, 0x54, 0x3C, 0x00,
        0xEC, 0xA9, 0xD8, 0x6F, 0x62, 0x61, 0xB0, 0xDE, 0x20, 0xFB, 0xE0, 0x16, 0x70, 0xA0, 0x7F,
        0xFE, 0xA3, 0xBB, 0xC2, 0x4D, 0xB6, 0xA8, 0x53, 0xF7, 0x4D, 0x17, 0x2C, 0xE5, 0x58, 0x83,
        0xED, 0xF8, 0xA2, 0x3E, 0x61, 0x5D, 0x5C, 0xC2, 0x2A, 0xF4, 0xF4, 0x1D, 0xAB, 0xDE, 0xF1,
        0xAD, 0xE5, 0xFC, 0x2E, 0xC5, 0xA4, 0xC0, 0x73, 0x66, 0x3B, 0x6E, 0x13, 0xBA, 0x8F, 0xBA,
        0xD1, 0xC6, 0xF8, 0x73, 0x46, 0xCA, 0x2E, 0x3B, 0x08, 0x04, 0x90, 0xCE, 0x56, 0xB5, 0xB8,
        0x91, 0xBE, 0x75, 0xE0, 0x78, 0xFF, 0x9E, 0xDB, 0xD7, 0x58, 0x06, 0x7C, 0x99, 0x4A, 0x3A,
        0x58, 0x8B, 0xB8, 0x92, 0xA1, 0xA6, 0x07, 0x04, 0x7F, 0x46, 0x72, 0x32, 0x6B, 0x4D, 0xB9,
        0x63, 0x1F, 0x83, 0x49, 0x18, 0xFA, 0xBB, 0x81, 0x10, 0x13, 0xBB, 0x19, 0x55, 0x6E, 0x97,
        0xC6, 0xA3, 0x37, 0xD4, 0x50, 0x70, 0x49, 0x5A, 0x21, 0x97, 0x44, 0x21, 0x3E, 0xFD, 0x73,
        0x97, 0x95, 0x76, 0xF1, 0x1A, 0xC8, 0x0F, 0xA4, 0xA0, 0xF8, 0xBF, 0x30, 0x92, 0x6B, 0xAC,
        0xAD, 0xE6, 0x9D, 0x05, 0xF1, 0x2A, 0xA0, 0x34, 0xFF, 0x73, 0x81, 0x81, 0x4C, 0xEC, 0xC3,
        0x83, 0x48, 0xCE, 0x8F, 0xE2, 0xE3, 0x38, 0xB9, 0x11, 0x1B, 0x06, 0x6C, 0x23, 0xEC, 0x89,
        0x11, 0xAC, 0xE3, 0x0D, 0x3E, 0xD2, 0x14, 0xD8, 0x84, 0x44, 0x11, 0x91, 0x7E, 0x69, 0x42,
        0xA9, 0xAB, 0x2E, 0x14, 0x23, 0x0E, 0x88, 0x54, 0x7B, 0x76, 0x07, 0x91, 0xF8, 0x1F, 0x2B,
        0x85, 0x19, 0x35, 0x18, 0x1F, 0xF9, 0xC9, 0xC7, 0xE7, 0x82, 0x5E, 0x3F, 0x90, 0x2E, 0x60,
        0x3E, 0x78, 0xEF, 0x47, 0xD3, 0x23, 0x87, 0xEF, 0xF7, 0x88, 0xE7, 0x51, 0x5D, 0x01, 0x62,
        0x09, 0x5A, 0x2E, 0x0C, 0x43, 0xB3, 0x15, 0xFF, 0x02, 0x36, 0x7B, 0x4A, 0xCB, 0x81, 0x7B,
        0x3E, 0xFE, 0xDD, 0x77, 0xA3, 0x84, 0xF1, 0xB2, 0x1A, 0xDF, 0xB5, 0xC6, 0x02, 0x8A, 0x31,
        0xB3, 0xBC, 0xA8, 0x98, 0x54, 0x03, 0xE5, 0x45, 0x05, 0x47, 0x1C, 0x0F, 0xA0, 0x13, 0x41,
        0x97, 0x9D, 0x21, 0x0D, 0xBE, 0xBC, 0x58, 0xDE, 0x68, 0x33, 0x1A, 0x93, 0x23, 0x8C, 0x10,
        0x92, 0x40, 0x70, 0x69, 0x89, 0x88, 0x02, 0x86, 0x60, 0x06, 0x7B, 0x8B, 0x9C, 0x15, 0x12,
        0xD5, 0x99, 0x0B, 0x08, 0x97, 0x17, 0x3D, 0x1F, 0x8E, 0x9E, 0x87, 0x20, 0xEB, 0x48, 0x34,
        0xB4, 0x2E, 0x7B, 0x80, 0x36, 0x3F, 0x52, 0xCA, 0x46, 0x48, 0x1E, 0xED, 0x0A, 0x3D, 0x96,
        0xE3, 0x91, 0xE8, 0x6C, 0xDE, 0x2E, 0x4D, 0x1F, 0xFD, 0xB5, 0x44, 0xF2, 0x10, 0xC6, 0x89,
        0xDC, 0x22, 0xFD, 0x02, 0x1F, 0x46, 0x22, 0xE8, 0xB4, 0x62, 0x7C, 0x61, 0x81, 0x56, 0xF0,
        0x91, 0xAB, 0xA1, 0x0F, 0x47, 0x2B, 0x58, 0x17, 0xA9, 0xB7, 0x49, 0x91, 0x40, 0x61, 0x72,
        0xEB, 0x70, 0xA8, 0x4B, 0x25, 0xAD, 0x3E, 0xCA, 0x17, 0xCD, 0x21, 0x3B, 0xFD, 0x51, 0x5C,
        0xD1, 0x00, 0xF8, 0x2A, 0x4A, 0x35, 0x4F, 0xFC, 0x82, 0x3E, 0x63, 0xE6, 0x48, 0x43, 0xC1,
        0xAE, 0x7B, 0x8F, 0xCE, 0xD5, 0x38, 0x90, 0xF4, 0x84, 0x3E, 0x01, 0xC4, 0xE1, 0x4C, 0xA8,
        0xD1, 0x05, 0x99, 0x65, 0xF0, 0xDC, 0x1D, 0x0D, 0x98, 0x7A, 0x75, 0x82, 0x9A, 0x3A, 0x5B,
        0x8C, 0xEA, 0xDD, 0x33, 0x1A, 0x95, 0x4A, 0x41, 0x6F, 0xF5, 0xD9, 0x93, 0xB6, 0x49, 0x47,
        0xE3, 0xCD, 0x8A, 0x58, 0x14, 0x66, 0x29, 0xBB, 0xE0, 0xC9, 0x74, 0xC6, 0x0D, 0x10, 0xB4,
        0xF1, 0x93, 0xAF, 0xC1, 0x47, 0x04, 0x6F, 0x12, 0x49, 0xE8, 0xDB, 0x49, 0xD9, 0xF8, 0x43,
        0x49, 0x26, 0x9A, 0x12, 0x54, 0x1A, 0x8E, 0xEE, 0x39, 0xCB, 0xBE, 0x48, 0x8C, 0x22, 0xFF,
        0xB8, 0xCE, 0x4A, 0xCE, 0x17, 0xB5, 0x19, 0xDC, 0x38, 0xBF, 0xBD, 0xE6, 0x98, 0x0F, 0x19,
        0xAB, 0x5D, 0x1B, 0xCC, 0x23, 0xD8, 0x12, 0x48, 0xCC, 0x96, 0x26, 0x1C, 0x23, 0xD7, 0xFD,
        0xA7, 0x7B, 0xC4, 0x0F, 0x1B, 0xC5, 0x7B, 0x6D, 0x30, 0xEC, 0x00, 0xB0, 0xB1, 0xDD, 0xA5,
        0x17, 0x32, 0xC2, 0xBA, 0xAC, 0x3B, 0xEC, 0x88, 0xE4, 0x30, 0x91, 0x8D, 0x24, 0x84, 0x64,
        0x9E, 0x44, 0xB7, 0xB2, 0x13, 0xB7, 0x54, 0x4F, 0x9D, 0x28, 0x30, 0x07, 0x9A, 0x8C, 0x60,
        0x37, 0xC2, 0x88, 0x39, 0x0E, 0x3F, 0x2D, 0x65, 0x94, 0xD9, 0xA9, 0x55, 0x03, 0x37, 0x0F,
        0x70, 0xFF, 0x7A, 0x5D, 0x19, 0xC3, 0xDA, 0x1E, 0x7C, 0x35, 0xE7, 0xB4, 0xE6, 0x39, 0x73,
        0xAA, 0x0F, 0xED, 0x13, 0x15, 0x02, 0x08, 0x3C, 0x37, 0x3A, 0x69, 0xEE, 0x49, 0xFB, 0x98,
        0xD1, 0x93, 0xED, 0x37, 0xBF, 0x16, 0xC9, 0x6A, 0x7E, 0x48, 0xF4, 0x95, 0xB8, 0x46, 0x77,
        0x45, 0xC2, 0x4B, 0xA2, 0x82, 0x5F, 0x3C, 0x15, 0xAD, 0xB6, 0x34, 0xCC, 0xB9, 0x6B, 0xFC,
        0xEC, 0x36, 0x65, 0xA8, 0xC2, 0xF7, 0x39, 0xCA, 0x8C, 0x37, 0x64, 0x06, 0x15, 0xDA, 0x2A,
        0x68, 0x60, 0xFD, 0xAF, 0xE9, 0x3D, 0x17, 0xDF, 0xBC, 0xFA, 0xA2, 0x5E, 0xB6, 0xF9, 0xB3,
        0x02, 0x8F, 0x9B, 0x4B, 0xA0, 0x6A, 0xD3, 0xEE, 0xD5, 0x73, 0x17, 0x13, 0x09, 0x19, 0x95,
        0x94, 0xC6, 0xE6, 0xCC, 0xD7, 0xBD, 0xF3, 0xB0, 0x29, 0x19, 0x14, 0x16, 0xAB, 0xC8, 0x66,
        0xB9, 0xAD, 0x77, 0x9B, 0x35, 0xB0, 0x90, 0x34, 0xEC, 0xCF, 0xBE, 0x3D, 0xAA, 0x3C, 0x07,
        0x1A, 0x90, 0x83, 0x30, 0xF0, 0x76, 0xBD, 0x22, 0xF1, 0x4B, 0x41, 0x4E, 0x3D, 0x4F, 0xE6,
        0x25, 0x90, 0x6F, 0xDC, 0xAA, 0xEE, 0x40, 0xE1, 0x84, 0x34, 0xA8, 0x97, 0x3A, 0x53, 0x09,
        0x58, 0x67, 0x72, 0xB4, 0xD1, 0xB9, 0x03, 0x31, 0xFE, 0x11, 0xAA, 0x87, 0xC7, 0xF7, 0xCC,
        0xC8, 0xC6, 0x66, 0xB0, 0xBD, 0x03, 0x85, 0x50, 0xC5, 0x97, 0x72, 0xCD, 0x20, 0x0D, 0xCA,
        0x35, 0x49, 0x40, 0x33, 0x22, 0xC9, 0xAC, 0x26, 0x37, 0x1F, 0xEE, 0x8C, 0x78, 0xDE, 0xFB,
        0x1E, 0x68, 0x9C, 0xD4, 0xC6, 0x4E, 0xE6, 0x98, 0x14, 0xE6, 0x15, 0x3D, 0x09, 0x4D, 0x04,
        0x83, 0xC3, 0xAE, 0xB4, 0x15, 0xE1, 0x83, 0x58, 0xF2, 0x2D, 0x20, 0x1C, 0x1A, 0xBB, 0x92,
        0x56, 0x4B, 0x97, 0x0E, 0xCB, 0xC7, 0xDD, 0x61, 0xD4, 0x10, 0x3E, 0x73, 0x1A, 0x73, 0x9C,
        0x4D, 0x06, 0x41, 0x75, 0x37, 0x24, 0xF8, 0x04, 0xF8, 0xF2, 0x4F, 0x14, 0xDE, 0xD4, 0x7B,
        0xF8, 0x5F, 0x63, 0x07, 0x2B, 0xAF, 0x48, 0x48, 0x27, 0xFB, 0xDD, 0x4E, 0x50, 0x34, 0x0D,
        0xCA, 0xF2, 0xC6, 0xCB, 0x1C, 0x73, 0xBE, 0x18, 0x1F, 0x44, 0xBC, 0xF5, 0x5C, 0x06, 0x0E,
        0xAB, 0xC7, 0xCA, 0x7E, 0x23, 0xF0, 0x58, 0x79, 0x95, 0xE7, 0xA0, 0x93, 0x5A, 0xB0, 0xCB,
        0x19, 0x1C, 0x62, 0x1A, 0xEA, 0x43, 0xDB, 0xD4, 0x5D, 0x28, 0xB9, 0xAC, 0x6A, 0x12, 0xAE,
        0x2F, 0x35, 0xA0, 0x03, 0x56, 0x0A, 0x64, 0x15, 0xB0, 0x8A, 0x08, 0x11, 0x20, 0x7F, 0xB2,
        0xBD, 0x48, 0x62, 0xD4, 0x4C, 0x48, 0x62, 0xF3, 0x9F, 0x67, 0x94, 0x51, 0x03, 0x46, 0x36,
        0xE6, 0xE6, 0x8C, 0x7B, 0x44, 0xFF, 0x3E, 0x87, 0xA9, 0x6C, 0x6E, 0x21, 0xE1, 0xC2, 0x1A,
        0x06, 0x79, 0xDC, 0xD9, 0x8E, 0xC6, 0x80, 0x2D, 0x30, 0xE6, 0xB3, 0x40, 0x3F, 0xCC, 0x42,
        0xB9, 0xD9, 0x2F, 0x9A, 0x6E, 0x0A, 0x7E, 0xAD, 0xAD, 0xA5, 0xBA, 0x78, 0x4D, 0x59, 0x15,
    ];

    #[test]
    fn zstd_decodes_c_libzstd_frames() {
        // Legacy compat: schema-2 DBs hold frames written by the old C libzstd
        // (COMPRESSION_LEVEL=6). The pure-Rust decoder must recover them
        // byte-for-byte. Each fixture must also carry the standard zstd magic
        // (guards against a copy/paste error in the embedded bytes).
        for fixture in [
            CZSTD_FIXTURE_REPETITIVE,
            CZSTD_FIXTURE_RANDOMISH,
            CZSTD_FIXTURE_TURNLIKE,
        ] {
            assert!(fixture.starts_with(&ZSTD_FRAME_MAGIC));
        }

        let repetitive = b"hello world hello world redundant redundant ".repeat(64);
        assert_eq!(repetitive.len(), 2816);
        assert_eq!(zstd_decode(CZSTD_FIXTURE_REPETITIVE).unwrap(), repetitive);

        let randomish: Vec<u8> = (0u8..=255).cycle().take(4096).collect();
        assert_eq!(zstd_decode(CZSTD_FIXTURE_RANDOMISH).unwrap(), randomish);

        let turnlike = turn_like_fixture_payload();
        assert_eq!(zstd_decode(CZSTD_FIXTURE_TURNLIKE).unwrap(), turnlike);
    }

    #[test]
    fn zstd_decode_rejects_trailing_bytes() {
        // Defense 2 (strict consumption): a row that is not exactly one frame
        // must be refused, not silently truncated to the leading frame.
        let payload = b"hello world hello world redundant redundant ".repeat(64);
        let frame = zstd_encode(&payload);

        // A valid frame followed by trailing garbage — the old C decoder
        // refused this at EOF ("unsupported format").
        let mut with_tail = frame.clone();
        with_tail.extend_from_slice(b"GARBAGE");
        assert!(zstd_decode(&with_tail).is_err());

        // Two concatenated frames in one row (an old-crate style double write)
        // must also be refused rather than returning only the first frame.
        let mut double = frame.clone();
        double.extend_from_slice(&frame);
        assert!(zstd_decode(&double).is_err());
    }
}