Skip to main content

composefs_splitdirfdstream/
transport.rs

1//! Source-agnostic FD-transport mechanics for the splitdirfdstream wire protocol.
2//!
3//! This module contains the pure, Storage/Layer-independent helpers that govern
4//! *how* a set of file descriptors is partitioned into transport frames and how
5//! the sparse dirfd-slot layout is computed.  Both the `composefs-storage`
6//! layer-transfer producer and future producers (e.g. a composefs-repo producer)
7//! can share these primitives without pulling in any higher-level dependencies.
8//!
9//! # Public API
10//!
11//! | Item | Role |
12//! |------|------|
13//! | [`MAX_FDS_PER_FRAME`] | Enforced per-frame fd cap (240, safely below 253). |
14//! | [`seed_from_id`] | Derive a deterministic `u64` seed from an opaque string id. |
15//! | [`open_devnull`] | Open `/dev/null` for use as a dummy fd. |
16//! | [`FdLimitError`] | Error from [`split_fds_into_frames`] when `more=false` overflows. |
17//! | [`LayerFdLayout`] | Complete assembled wire layout from [`build_layer_fd_layout`]. |
18//! | [`build_layer_fd_layout`] | Assemble the full sparse dirfd+keepalive+lifetime fd array. |
19//! | [`split_fds_into_frames`] | Partition fds into frames, enforcing `more=false` cap. |
20//! | [`spawn_self_reaping_producer`] | Spawn the 3-phase keepalive-lock producer task (requires `tokio` feature). |
21//!
22//! The producer-side seed jitter (sparse-slot placement, frame count, extra
23//! lifetime fds) is computed by crate-internal helpers (`sparse_dir_slots`,
24//! `compute_n_frames`, `n_extra_lifetime_fds`); the consumer never recomputes
25//! it, since every `FileBackedData` chunk carries an explicit `dirfd_index`.
26
27use std::os::fd::OwnedFd;
28
29use rand::seq::SliceRandom as _;
30use rand_pcg::Pcg64;
31use rand_pcg::rand_core::SeedableRng as _;
32
33// ── Constants ────────────────────────────────────────────────────────────────
34
35/// Our enforced per-frame fd cap.
36///
37/// Kept safely below the hard kernel limit of 253 fds per `sendmsg(SCM_RIGHTS)`
38/// call.  Every transport frame carries at most this many fds.
39pub const MAX_FDS_PER_FRAME: usize = 240;
40
41// ── Frame splitting ───────────────────────────────────────────────────────────
42
43/// Partition `fds` into `n_frames` contiguous batches as evenly as possible.
44///
45/// The first `fds.len() % n_frames` batches receive one extra fd (so all
46/// batches are non-empty when `n_frames <= fds.len()`).  Ordering is preserved:
47/// concatenating the returned batches in order reconstructs the original vec.
48///
49/// # Panics
50/// Panics if `n_frames == 0` or `n_frames > fds.len()`.
51pub(crate) fn split_into_frames(fds: Vec<OwnedFd>, n_frames: usize) -> Vec<Vec<OwnedFd>> {
52    assert!(n_frames > 0 && n_frames <= fds.len());
53    let fd_count = fds.len();
54    let base = fd_count / n_frames;
55    let remainder = fd_count % n_frames;
56    let mut result = Vec::with_capacity(n_frames);
57    let mut it = fds.into_iter();
58    for i in 0..n_frames {
59        let batch_size = base + if i < remainder { 1 } else { 0 };
60        result.push(it.by_ref().take(batch_size).collect());
61    }
62    result
63}
64
65// ── Seed derivation ───────────────────────────────────────────────────────────
66
67/// Derive a deterministic `u64` seed from an opaque string identifier.
68///
69/// Uses the first 8 bytes of SHA-256(id) interpreted as little-endian u64.
70/// The result is stable across runs for the same id string.
71pub fn seed_from_id(id: &str) -> u64 {
72    let hash = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), id.as_bytes())
73        .expect("SHA-256 hashing should not fail");
74    u64::from_le_bytes(hash[..8].try_into().expect("sha256 is at least 8 bytes"))
75}
76
77// ── Frame-count helpers ───────────────────────────────────────────────────────
78
79/// Compute the *hash-derived* minimum frame count for `fd_count` FDs keyed by `seed`.
80///
81/// - If `fd_count < 3`: returns `max(fd_count, 1)` (never more frames than FDs,
82///   but always at least one so `split_into_frames` has a valid divisor).  This
83///   branch is effectively dead: a real transport array is always ≥4 FDs
84///   (pipe + ≥2 dirfd slots + keepalive).
85/// - Otherwise: `max(3, seed % (fd_count + 1))` clamped to `fd_count`.
86///
87/// This guarantees ≥3 frames whenever there are ≥3 FDs, which exercises the
88/// multi-frame path in tests while keeping the arithmetic deterministic.
89///
90/// **This is the hash-min component only.**  The call site additionally enforces
91/// a per-frame cap ([`MAX_FDS_PER_FRAME`]) by taking `hash_min.max(cap_min)` where
92/// `cap_min = ceil(fd_count / MAX_FDS_PER_FRAME)`.  For small layers
93/// (≤`MAX_FDS_PER_FRAME` fds) `cap_min = 1` so the hash-min always wins,
94/// preserving test-hardening behaviour.
95pub(crate) fn n_frames_for(seed: u64, fd_count: usize) -> usize {
96    if fd_count < 3 {
97        return fd_count.max(1);
98    }
99    let raw = (seed % (fd_count as u64 + 1)) as usize;
100    raw.max(3).min(fd_count)
101}
102
103/// Compute the actual number of transport frames for a `more=true` call.
104///
105/// Returns the larger of:
106/// - the hash-derived minimum ([`n_frames_for`]`(seed, fd_count)`, ≥3 for real
107///   layers), which exercises multi-frame paths in tests; and
108/// - `ceil(fd_count / MAX_FDS_PER_FRAME)`, the minimum needed so every frame
109///   carries at most [`MAX_FDS_PER_FRAME`] fds (kernel SCM_RIGHTS cap).
110///
111/// The result is additionally clamped to `[1, fd_count]` so `split_into_frames`
112/// never receives an out-of-range value.
113///
114/// Proved invariant: `fd_count.div_ceil(n) <= MAX_FDS_PER_FRAME` for the
115/// returned `n`, because `n >= ceil(fd_count / MAX_FDS_PER_FRAME)`.
116pub(crate) fn compute_n_frames(seed: u64, fd_count: usize) -> usize {
117    let hash_min = n_frames_for(seed, fd_count);
118    let cap_min = fd_count.div_ceil(MAX_FDS_PER_FRAME);
119    hash_min.max(cap_min).min(fd_count).max(1)
120}
121
122// ── Dummy-fd helpers ──────────────────────────────────────────────────────────
123
124/// Open `/dev/null` as an opaque dummy fd.
125///
126/// Returns a plain [`std::io::Result`] so callers can map it to their own error type.
127pub fn open_devnull() -> std::io::Result<OwnedFd> {
128    rustix::fs::open(
129        c"/dev/null",
130        rustix::fs::OFlags::RDONLY,
131        rustix::fs::Mode::empty(),
132    )
133    .map_err(std::io::Error::from)
134}
135
136/// Open an anonymous `memfd` as an opaque dummy fd.
137///
138/// Returns a plain [`std::io::Result`] so callers can map it to their own error type.
139pub(crate) fn open_memfd() -> std::io::Result<OwnedFd> {
140    rustix::fs::memfd_create(
141        c"composefs-layer-transfer-dummy",
142        rustix::fs::MemfdFlags::CLOEXEC,
143    )
144    .map_err(std::io::Error::from)
145}
146
147// ── Sparse-slot layout ────────────────────────────────────────────────────────
148
149/// Describes the sparse dirfd-slot layout produced by [`sparse_dir_slots`].
150///
151/// The `dirfds` array passed to the consumer has length `total_slots` and contains
152/// real diff-dir fds at `real_slot_indices[i]` (for chain layer `i`) and dummy fds
153/// at all other positions.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub(crate) struct SparseLayout {
156    /// Total number of slots in the dirfds array (real + dummy).
157    pub(crate) total_slots: usize,
158    /// Ascending slot indices assigned to real layers.
159    ///
160    /// `real_slot_indices[i]` is the slot index for chain layer `i`.
161    pub(crate) real_slot_indices: Vec<u32>,
162    /// Slot indices that are dummy (complement of `real_slot_indices` in `0..total_slots`).
163    pub(crate) dummy_slot_indices: Vec<u32>,
164}
165
166/// Compute the sparse slot assignment for `n_real` real layers using `seed`.
167///
168/// This is a pure function (no I/O) that encodes the deterministic slot-placement
169/// algorithm:
170///
171/// 1. `n_dummy = (seed >> 8) % (n_real + 1) + 1`  (at least 1 dummy).
172/// 2. `total_slots = n_real + n_dummy`.
173/// 3. Deterministically shuffle `0..total_slots` with a `seed`-seeded PCG RNG,
174///    take the first `n_real` as real-layer slot indices, sort them ascending.
175/// 4. Dummy slots are `0..total_slots` minus the real indices.
176///
177/// The shuffle uses [`Pcg64`], whose algorithm is stable across `rand_pcg`
178/// releases, so the placement is reproducible from the seed (useful for tests).
179/// Reproducibility is *not* a correctness requirement: the consumer is driven
180/// entirely by the explicit `dirfd_index` in each `FileBackedData` chunk and
181/// never recomputes this layout.
182///
183/// The returned [`SparseLayout`] records `total_slots`, `real_slot_indices`
184/// (ascending, length `n_real`), and `dummy_slot_indices` (ascending).
185pub(crate) fn sparse_dir_slots(n_real: usize, seed: u64) -> SparseLayout {
186    // ── 1/2: total slot count (at least 1 dummy) ─────────────────────────────
187    let n_dummy: usize = ((seed >> 8) % (n_real as u64 + 1) + 1) as usize;
188    let total_slots = n_real + n_dummy;
189
190    // ── 3: select real-layer slot positions ──────────────────────────────────
191    // Deterministically shuffle all slot indices from the seed, take the first
192    // n_real as the real-layer positions, then sort them ascending.
193    let mut rng = Pcg64::seed_from_u64(seed);
194    let mut shuffled_slots: Vec<u32> = (0..total_slots as u32).collect();
195    shuffled_slots.shuffle(&mut rng);
196    let mut real_indices: Vec<u32> = shuffled_slots[..n_real].to_vec();
197    real_indices.sort_unstable();
198
199    // ── 4: compute dummy indices (complement) ─────────────────────────────────
200    let real_set: std::collections::HashSet<u32> = real_indices.iter().copied().collect();
201    let dummy_indices: Vec<u32> = (0..total_slots as u32)
202        .filter(|i| !real_set.contains(i))
203        .collect();
204
205    SparseLayout {
206        total_slots,
207        real_slot_indices: real_indices,
208        dummy_slot_indices: dummy_indices,
209    }
210}
211
212// ── Lifetime dummy count ──────────────────────────────────────────────────────
213
214/// Compute the number of extra opaque lifetime dummy fds for a given seed.
215///
216/// Returns 0, 1, or 2 — derived as `(seed >> 16) % 3`.  These are additional
217/// dummy fds (beyond the keepalive pipe write-end) that the client must hold open
218/// until it has finished reading the splitdirfdstream, then close.  Alternating
219/// `/dev/null` and `memfd` fd kinds exercise transparent client pass-through.
220pub(crate) fn n_extra_lifetime_fds(seed: u64) -> usize {
221    ((seed >> 16) % 3) as usize
222}
223
224// ── High-level fd-layout helpers ─────────────────────────────────────────────
225
226/// Error returned by [`split_fds_into_frames`] when `more=false` and the fd
227/// count exceeds [`MAX_FDS_PER_FRAME`].
228///
229/// The caller must map this to their interface error and instruct the client to
230/// retry with `more=true`.
231#[derive(Debug, thiserror::Error)]
232#[error("fd count {fd_count} exceeds per-frame limit {max_per_frame}; retry with more=true")]
233pub struct FdLimitError {
234    /// Total number of fds that would be sent.
235    pub fd_count: usize,
236    /// The per-frame cap that was exceeded.
237    pub max_per_frame: usize,
238}
239
240/// Assembled FD layout ready to wire to the client in one or more transport frames.
241///
242/// This is the output of [`build_layer_fd_layout`]: a complete description of
243/// every fd that will be sent across the socket, split into two groups:
244///
245/// * The **wire region** — everything that travels to the client:
246///   - `fds_all[0]` — pipe read end (carries the `splitdirfdstream` bytes).
247///   - `fds_all[1..=dir_count]` — dirfds region (sparse: real dirs + dummies).
248///   - `fds_all[dir_count+1..]` — lifetime region (keepalive write + extras).
249/// * The **keepalive read end** — retained on the server; when the client drops
250///   the keepalive write end the server sees EOF and may release resources.
251///
252/// The `real_indices` slice maps chain layer `i` → slot index within the
253/// *dirfds region* where that layer's real diff-dir fd sits.  Pass this to the
254/// producer (via [`build_layer_fd_layout`]'s return value) so it can write the
255/// correct `dirfd_index` into each `FileBackedData` chunk.
256#[derive(Debug)]
257pub struct LayerFdLayout {
258    /// All fds to send to the client, ordered as described above.
259    pub fds_all: Vec<OwnedFd>,
260    /// Number of slots in the dirfds region (`dir_count` in reply and in the
261    /// wire layout: `fds_all[1..=dir_count]`).
262    pub dir_count: u32,
263    /// Slot index within the dirfds region for each real layer (ascending).
264    ///
265    /// `real_indices[i]` is the `dirfd_index` the producer must use for chain
266    /// layer `i`.
267    pub real_indices: Vec<u32>,
268    /// Server-side read end of the keepalive pipe.
269    ///
270    /// Hold this `OwnedFd` until the client has finished consuming the layer
271    /// (or until the request is complete if keepalive is not needed for resource
272    /// management).  When it drops, the client's matching write end sees EOF.
273    pub keepalive_read: OwnedFd,
274}
275
276/// Build the complete wire fd layout for a producer serving `n_real` real diff
277/// directories.
278///
279/// This pure-ish (only I/O: `pipe`, `/dev/null`, `memfd`) function:
280///
281/// 1. Calls [`sparse_dir_slots`] to compute the sparse placement.
282/// 2. Opens dummy fds (`/dev/null` alternating with `memfd`) for gap slots.
283/// 3. Builds the dirfds region by interleaving real and dummy fds in slot order.
284/// 4. Creates a keepalive pipe; the write end goes into `fds_all`, the read end
285///    is returned in [`LayerFdLayout::keepalive_read`].
286/// 5. Adds `n_extra_lifetime_fds(seed)` extra dummy lifetime fds.
287/// 6. Prepends the `pipe_read` fd so `fds_all[0]` is always the data pipe.
288///
289/// The caller is responsible for:
290/// * Passing the correct `real_fds` (in chain order) — the first real_fd gets
291///   slot `real_indices[0]`, the second gets `real_indices[1]`, etc.
292/// * Spawning the producer with `write_fd` (not returned here; the caller opens
293///   the pipe and passes `write_fd` to the producer separately).
294///
295/// # Arguments
296/// * `pipe_read` — Read end of the data pipe (will become `fds_all[0]`).
297/// * `real_fds` — Pre-opened real diff-directory file descriptors, one per
298///   chain layer, in chain order.
299/// * `seed` — Deterministic seed (see [`seed_from_id`]).
300///
301/// # Errors
302/// Returns `io::Error` if any dummy-fd or keepalive-pipe creation fails.
303pub fn build_layer_fd_layout(
304    pipe_read: OwnedFd,
305    real_fds: Vec<OwnedFd>,
306    seed: u64,
307) -> std::io::Result<LayerFdLayout> {
308    let n_real = real_fds.len();
309    let layout = sparse_dir_slots(n_real, seed);
310    let total_slots = layout.total_slots;
311
312    // ── Build dirfds region: slot i = real fd or dummy fd ────────────────────
313    let mut dirfd_region: Vec<Option<OwnedFd>> = (0..total_slots).map(|_| None).collect();
314
315    // Place real fds at their assigned slots.
316    for (chain_idx, real_fd) in real_fds.into_iter().enumerate() {
317        let slot = layout.real_slot_indices[chain_idx] as usize;
318        dirfd_region[slot] = Some(real_fd);
319    }
320
321    // Fill dummy slots, alternating /dev/null and memfd.
322    for (dummy_ordinal, &dummy_slot) in layout.dummy_slot_indices.iter().enumerate() {
323        let dummy_fd = if dummy_ordinal % 2 == 0 {
324            open_devnull()?
325        } else {
326            open_memfd()?
327        };
328        dirfd_region[dummy_slot as usize] = Some(dummy_fd);
329    }
330
331    // Unwrap: every slot was filled above.
332    let dirfd_region: Vec<OwnedFd> = dirfd_region.into_iter().map(|o| o.unwrap()).collect();
333
334    // ── Keepalive pipe ────────────────────────────────────────────────────────
335    let (keepalive_read, keepalive_write) =
336        rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC).map_err(std::io::Error::from)?;
337
338    // ── Extra lifetime dummy fds ──────────────────────────────────────────────
339    let n_extra = n_extra_lifetime_fds(seed);
340    let mut extra_lifetime: Vec<OwnedFd> = Vec::with_capacity(n_extra);
341    for i in 0..n_extra {
342        let fd = if i % 2 == 0 {
343            open_devnull()?
344        } else {
345            open_memfd()?
346        };
347        extra_lifetime.push(fd);
348    }
349
350    // ── Assemble fds_all ──────────────────────────────────────────────────────
351    //   index 0              : pipe read
352    //   index 1..=dir_count  : dirfds region
353    //   index dir_count+1    : keepalive write end
354    //   remaining            : extra lifetime dummies
355    let dir_count = total_slots as u32;
356    let mut fds_all: Vec<OwnedFd> = Vec::with_capacity(1 + total_slots + 1 + n_extra);
357    fds_all.push(pipe_read);
358    fds_all.extend(dirfd_region);
359    fds_all.push(keepalive_write);
360    fds_all.extend(extra_lifetime);
361
362    Ok(LayerFdLayout {
363        fds_all,
364        dir_count,
365        real_indices: layout.real_slot_indices,
366        keepalive_read,
367    })
368}
369
370/// Partition a complete fd array into transport frames.
371///
372/// When `more` is `true` the fds are split across `compute_n_frames(seed, n)`
373/// batches (≥3 for test hardening, cap-limited to ≤[`MAX_FDS_PER_FRAME`] per
374/// frame).
375///
376/// When `more` is `false` all fds must fit in a single frame.  If the total
377/// count exceeds [`MAX_FDS_PER_FRAME`] the fds are returned intact inside an
378/// [`Err(FdLimitError)`] so they can be dropped cleanly by the caller before
379/// returning an error reply.
380///
381/// # Returns
382/// `Ok(Vec<Vec<OwnedFd>>)` — one inner vec per transport frame (1…N frames).
383/// `Err(FdLimitError { fds, .. })` — the original fds are returned so the
384/// caller can drop them; the error contains the count and cap for a diagnostic.
385pub fn split_fds_into_frames(
386    fds: Vec<OwnedFd>,
387    seed: u64,
388    more: bool,
389) -> Result<Vec<Vec<OwnedFd>>, (Vec<OwnedFd>, FdLimitError)> {
390    let fd_count = fds.len();
391    if !more && fd_count > MAX_FDS_PER_FRAME {
392        return Err((
393            fds,
394            FdLimitError {
395                fd_count,
396                max_per_frame: MAX_FDS_PER_FRAME,
397            },
398        ));
399    }
400    let n = if more {
401        compute_n_frames(seed, fd_count)
402    } else {
403        1
404    };
405    Ok(split_into_frames(fds, n))
406}
407
408// ─────────────────────────────────────────────────────────────────────────────
409// Self-reaping producer task (optional: requires `tokio` feature)
410// ─────────────────────────────────────────────────────────────────────────────
411
412/// Spawn a self-reaping blocking producer task that implements the 3-phase
413/// keepalive-lock protocol used by both the repo and cstor `GetLayer`
414/// implementations.
415///
416/// # Phases
417///
418/// 1. **Produce**: call `produce(write_fd_as_File)`.  When `produce` returns the
419///    write end of the data pipe drops, signalling data-EOF to the consumer.
420/// 2. **Keepalive wait**: read from `keepalive_read` until EOF.  The consumer
421///    drops its keepalive write end after it finishes draining the data pipe,
422///    so this phase acts as a rendezvous before releasing any held resources.
423/// 3. **Release**: drop `guard`.  For the repo path `guard` is `()` (no-op);
424///    for the cstor path `guard` is a `LayerStoreLock` whose drop releases the
425///    shared flock.
426///
427/// # Ordering guarantee
428///
429/// Phase 2 begins **after** `write_fd` is dropped (end of Phase 1), so the
430/// data pipe is always at EOF before we start waiting on the keepalive.  The
431/// reverse would cause a deadlock: if the consumer must drain to EOF to drop
432/// the keepalive but the producer never finishes, the pipe never EOF.
433///
434/// # Arguments
435///
436/// * `write_fd` — Write end of the data pipe; moved into the closure and
437///   dropped at the end of Phase 1.
438/// * `keepalive_read` — Read end of the keepalive pipe; retained until Phase 2
439///   EOF, then dropped.
440/// * `guard` — Opaque lifetime guard released in Phase 3 (e.g. a lock).
441/// * `produce` — Called with a `std::fs::File` wrapping `write_fd`.  Should
442///   write the complete `splitdirfdstream` into the file and return.  Errors
443///   are logged as warnings; a short/corrupt stream will be detected later by
444///   the consumer's integrity check.
445#[cfg(feature = "tokio")]
446pub fn spawn_self_reaping_producer(
447    write_fd: OwnedFd,
448    keepalive_read: OwnedFd,
449    guard: impl Send + 'static,
450    produce: impl FnOnce(std::fs::File) + Send + 'static,
451) {
452    tokio::task::spawn_blocking(move || {
453        // Phase 1: produce into write_fd; drop write_fd (data-pipe EOF).
454        {
455            let wf = std::fs::File::from(write_fd);
456            produce(wf);
457            // write_fd drops here → data-pipe EOF visible to consumer.
458        }
459
460        // Phase 2: wait for consumer to signal completion via keepalive EOF.
461        let mut buf = [0u8; 64];
462        loop {
463            match rustix::io::read(&keepalive_read, &mut buf) {
464                Ok(0) => break,                    // EOF — consumer dropped its write end
465                Ok(_) => {}                        // unexpected bytes — drain and continue
466                Err(rustix::io::Errno::INTR) => {} // EINTR — retry
467                Err(_) => break,                   // other error — bail
468            }
469        }
470
471        // Phase 3: release guard (e.g. drop the shared flock).
472        drop(guard);
473    });
474}
475
476// ─────────────────────────────────────────────────────────────────────────────
477// Tests
478// ─────────────────────────────────────────────────────────────────────────────
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    /// Seed for "child-layer-001" (SHA-256("child-layer-001")[0..8] as LE-u64).
485    ///
486    /// Verified: `seed_from_id("child-layer-001") == LAYER_SEED`.
487    const LAYER_SEED: u64 = 9_551_015_030_439_334_514;
488
489    /// `compute_n_frames` must ensure every frame holds ≤ MAX_FDS_PER_FRAME fds
490    /// for a large synthetic fd count where the hash-min alone would be too small.
491    #[test]
492    fn test_compute_n_frames_caps_large_fd_count() {
493        // Use a seed whose hash-min is small (e.g. seed=0 → raw=0 → max(3,0)=3).
494        let seed: u64 = 0;
495        // Choose fd_count large enough that 3 frames would exceed 240 each.
496        // ceil(1000 / 3) = 334 > 240, so cap_min must win.
497        let fd_count: usize = 1000;
498
499        let n = compute_n_frames(seed, fd_count);
500
501        // Invariant: every frame carries at most MAX_FDS_PER_FRAME fds.
502        let max_frame_size = fd_count.div_ceil(n);
503        assert!(
504            max_frame_size <= MAX_FDS_PER_FRAME,
505            "frame size {max_frame_size} exceeds cap {MAX_FDS_PER_FRAME} (n={n}, fd_count={fd_count})",
506        );
507
508        // Also verify n is large enough: ceil(1000/240) = 5.
509        let cap_min = fd_count.div_ceil(MAX_FDS_PER_FRAME);
510        assert!(n >= cap_min, "n={n} < cap_min={cap_min}",);
511    }
512
513    /// For small fd counts (≤ MAX_FDS_PER_FRAME) the hash-min should still
514    /// dominate so test-hardening (≥3 frames for real layers) is preserved.
515    #[test]
516    fn test_compute_n_frames_small_fd_count_preserves_hash_min() {
517        // The existing test layer has 9 fds → cap_min=1, hash_min=4.
518        let n = compute_n_frames(LAYER_SEED, 9);
519        // EXPECTED_N_FRAMES = 4 for "child-layer-001"
520        assert_eq!(n, 4, "hash_min should win for small fd counts");
521
522        // Any fd count ≤ MAX_FDS_PER_FRAME has cap_min=1; hash_min (≥3) always wins.
523        for fd_count in 3..=MAX_FDS_PER_FRAME {
524            let n = compute_n_frames(LAYER_SEED, fd_count);
525            let max_frame_size = fd_count.div_ceil(n);
526            assert!(
527                max_frame_size <= MAX_FDS_PER_FRAME,
528                "frame size {max_frame_size} > cap for fd_count={fd_count}",
529            );
530        }
531    }
532
533    /// `compute_n_frames` invariant holds across a sweep of large fd counts.
534    #[test]
535    fn test_compute_n_frames_cap_invariant_sweep() {
536        let seeds: &[u64] = &[0, 1, 42, LAYER_SEED, u64::MAX];
537        let fd_counts: &[usize] = &[
538            MAX_FDS_PER_FRAME,
539            MAX_FDS_PER_FRAME + 1,
540            MAX_FDS_PER_FRAME * 2,
541            MAX_FDS_PER_FRAME * 5,
542            1000,
543            5000,
544        ];
545        for &seed in seeds {
546            for &fd_count in fd_counts {
547                let n = compute_n_frames(seed, fd_count);
548                let max_frame_size = fd_count.div_ceil(n);
549                assert!(
550                    max_frame_size <= MAX_FDS_PER_FRAME,
551                    "invariant violated: seed={seed} fd_count={fd_count} n={n} \
552                     max_frame_size={max_frame_size} cap={MAX_FDS_PER_FRAME}",
553                );
554            }
555        }
556    }
557
558    /// A non-streaming (`more=false`) call that would exceed MAX_FDS_PER_FRAME
559    /// must be detected by the over-cap check before the producer is spawned.
560    ///
561    /// We test the pure decision logic (`!more && fd_count > MAX_FDS_PER_FRAME`)
562    /// without opening real fds, keeping the test deterministic and cheap.
563    #[test]
564    fn test_more_false_over_cap_decision() {
565        // Verify the threshold is exactly MAX_FDS_PER_FRAME.
566        assert!(
567            !(!false && MAX_FDS_PER_FRAME > MAX_FDS_PER_FRAME),
568            "fd_count == cap should NOT trigger error",
569        );
570        assert!(
571            !false && (MAX_FDS_PER_FRAME + 1) > MAX_FDS_PER_FRAME,
572            "fd_count == cap+1 MUST trigger error",
573        );
574
575        // Simulate: if !more and fd_count > MAX_FDS_PER_FRAME → error.
576        let should_error =
577            |more: bool, fd_count: usize| -> bool { !more && fd_count > MAX_FDS_PER_FRAME };
578
579        assert!(
580            !should_error(true, MAX_FDS_PER_FRAME + 1),
581            "more=true should never error on fd count"
582        );
583        assert!(
584            !should_error(false, MAX_FDS_PER_FRAME),
585            "more=false at exactly cap should not error"
586        );
587        assert!(
588            should_error(false, MAX_FDS_PER_FRAME + 1),
589            "more=false over cap must error"
590        );
591        assert!(
592            should_error(false, 1000),
593            "more=false with 1000 fds must error"
594        );
595    }
596
597    /// `seed_from_id` must match the precomputed value for "child-layer-001".
598    #[test]
599    fn test_seed_from_id_child_layer() {
600        assert_eq!(
601            seed_from_id("child-layer-001"),
602            LAYER_SEED,
603            "seed_from_id must match precomputed SHA-256-derived value"
604        );
605    }
606
607    /// `sparse_dir_slots` must produce a valid, seed-reproducible layout.
608    ///
609    /// The dummy count derives from the seed independently of placement, so for
610    /// "child-layer-001" (seed = 9551015030439334514): `n_dummy = (seed >> 8) %
611    /// (2+1) + 1 = 3`, hence `total_slots = 5`.  Real-slot *placement* comes from
612    /// a `Pcg64` shuffle: we assert the structural invariants plus determinism
613    /// (same seed → same layout) rather than pinning the exact slot indices.
614    #[test]
615    fn test_sparse_dir_slots_known_layout() {
616        let layout = sparse_dir_slots(2, LAYER_SEED);
617
618        // Seed-derived counts (independent of the shuffle).
619        assert_eq!(layout.total_slots, 5, "total_slots must be 5");
620        assert_eq!(layout.real_slot_indices.len(), 2, "two real layers");
621        assert_eq!(layout.dummy_slot_indices.len(), 3, "three dummy slots");
622
623        // Structural invariants: both lists ascending, disjoint, and together
624        // covering exactly 0..total_slots.
625        assert!(
626            layout.real_slot_indices.windows(2).all(|w| w[0] < w[1]),
627            "real_slot_indices ascending"
628        );
629        assert!(
630            layout.dummy_slot_indices.windows(2).all(|w| w[0] < w[1]),
631            "dummy_slot_indices ascending"
632        );
633        let mut all: Vec<u32> = layout
634            .real_slot_indices
635            .iter()
636            .chain(&layout.dummy_slot_indices)
637            .copied()
638            .collect();
639        all.sort_unstable();
640        assert_eq!(
641            all,
642            (0..layout.total_slots as u32).collect::<Vec<_>>(),
643            "real and dummy slots must partition 0..total_slots"
644        );
645
646        // Determinism: the Pcg64 shuffle is reproducible from the seed.
647        assert_eq!(
648            sparse_dir_slots(2, LAYER_SEED),
649            layout,
650            "same seed must yield the same layout"
651        );
652    }
653
654    /// `n_extra_lifetime_fds` must return 2 for "child-layer-001"'s seed.
655    #[test]
656    fn test_n_extra_lifetime_fds_child_layer() {
657        assert_eq!(
658            n_extra_lifetime_fds(LAYER_SEED),
659            2,
660            "n_extra_lifetime_fds must be 2 for child-layer-001 seed"
661        );
662    }
663}