Skip to main content

tocat_api/
pipeline.rs

1//! Composition: chains of [`Plugin`] stages, and the registry that builds them.
2//!
3//! # Cost model
4//!
5//! A chunk is threaded through the stages of a segment by reference. A stage
6//! that only observes (`ctx.pass_through()`) does not cause a copy: the next
7//! stage (and ultimately the socket) is handed the original read buffer. Only
8//! a stage that rewrites bytes materialises them, and even then the pipeline
9//! ping-pongs between two buffers it owns rather than allocating.
10//!
11//! So a pipeline of N observers costs N virtual calls per chunk and zero
12//! copies, which is why running one is not much worse than not running one.
13//!
14//! # Framing
15//!
16//! A chunk off the wire is one unit: one call to the stage below, one write at
17//! the far end. A stage that calls [`Ctx::boundary`] says it wants what it
18//! emitted delivered as several, which is how a stage like `block` cuts a
19//! stream into fixed-size records rather than merely accumulating them.
20//!
21//! Units cost something, so nothing pays for them unless it asked. A stage
22//! that never calls `boundary` leaves the boundary list empty, which reads as
23//! "one unit" everywhere and allocates nothing. Below a stage that did, every
24//! stage is called once per unit, so unit counts multiply down a chain: this
25//! is the reason `boundary` exists as an explicit request rather than
26//! something inferred from a stage emitting more than once.
27//!
28//! Passing through is still free under a framing stage. A stage that hands
29//! every unit back untouched copies nothing and answers [`Emit::Passthrough`],
30//! exactly as it would on an unframed chunk; the copy starts at the first unit
31//! it rewrites, drops or reframes.
32//!
33//! # Ticks
34//!
35//! A stage may also ask to be called on a schedule. The pipeline owns the
36//! schedules (one deadline per ticking stage) and the host owns the timer
37//! that asks whether any of them have come due. Splitting it that way means
38//! the host wakes at one period (the shortest any stage asked for) rather than
39//! holding a timer per stage, and a pipeline with nothing ticking costs
40//! nothing at all: [`Pipeline::tick_interval`] returns `None` and no timer is
41//! created.
42
43use std::{
44    collections::BTreeMap,
45    fmt,
46    sync::Arc,
47    time::{Duration, Instant},
48};
49
50use crate::{
51    Direction, PluginSpec,
52    channel::HostBuilder,
53    error::{PluginError, Result},
54    normalize,
55    plugin::{
56        Boundaries, BuildCtx, Ctx, EffectSink, Emission, Emit, Execution, ExternalStage, Needs,
57        PipelineMeta, Plugin, PluginFactory, Stage, StageInfo,
58    },
59};
60
61const EMPTY: &[u8] = &[];
62
63/// The framing of an unframed emission: one unit, covering everything.
64const NO_BOUNDS: &[usize] = &[];
65
66/// Which buffer the live bytes are sitting in.
67#[derive(Clone, Copy, PartialEq, Eq)]
68enum Slot {
69    /// The caller's read buffer: nothing has been rewritten yet.
70    Input,
71    A,
72    B,
73}
74
75/// What came out of a pipeline, and how it is framed.
76///
77/// [`bytes`](Self::bytes) is the whole emission and [`units`](Self::units) is
78/// the pieces the segment asked for it to be delivered in. Unless a stage
79/// called [`Ctx::boundary`] there is exactly one unit and the two say the same
80/// thing, so a sink with no framing of its own (a byte stream, a file) can
81/// write `bytes` in one call and ignore units entirely.
82///
83/// Where units are observable they matter: on a datagram sink one unit is one
84/// message, and across a detached boundary one unit is one parcel and so one
85/// call to the segment below.
86#[derive(Debug, Clone, Copy)]
87pub struct Emitted<'p> {
88    bytes: &'p [u8],
89    /// One offset per unit, each the end of its unit.
90    ///
91    /// Empty means unframed. Otherwise the last offset is always `bytes.len()`
92    /// and the offsets ascend, so every byte belongs to exactly one unit and
93    /// none can be dropped by a sink that iterates units.
94    bounds: &'p [usize],
95}
96
97impl<'p> Emitted<'p> {
98    /// One unit covering all of `bytes`, which is what an unframed chunk is.
99    #[must_use]
100    pub const fn whole(bytes: &'p [u8]) -> Self {
101        Self {
102            bytes,
103            bounds: NO_BOUNDS,
104        }
105    }
106
107    /// Nothing reached the end of the pipeline.
108    #[must_use]
109    pub const fn empty() -> Self {
110        Self::whole(EMPTY)
111    }
112
113    /// Everything emitted, concatenated.
114    #[must_use]
115    pub const fn bytes(&self) -> &'p [u8] {
116        self.bytes
117    }
118
119    #[must_use]
120    pub const fn is_empty(&self) -> bool {
121        self.bytes.is_empty()
122    }
123
124    #[must_use]
125    pub const fn len(&self) -> usize {
126        self.bytes.len()
127    }
128
129    /// Each unit, in order. Exactly one unit when nothing declared any.
130    ///
131    /// Takes `self` by value so the iterator borrows the pipeline rather than
132    /// this handle, which is what lets a caller iterate units while the
133    /// emission is in flight.
134    pub fn units(self) -> impl Iterator<Item = &'p [u8]> {
135        let Self { bytes, bounds } = self;
136
137        // Nothing declared any framing, so the whole emission is the one unit.
138        // An empty emission has no units at all rather than one empty one.
139        let unframed = bounds.is_empty() && !bytes.is_empty();
140
141        units(bytes, bounds).chain(unframed.then_some(bytes))
142    }
143}
144
145/// What one stage reads and what it writes into, borrowed out of a
146/// [`Buffers`], along with the slot the writes land in.
147type Halves<'b> = (Slot, &'b [u8], &'b [usize], &'b mut Emission);
148
149/// The two halves a segment ping-pongs between.
150///
151/// Bytes and boundaries travel together, which is why each half is a whole
152/// [`Emission`] rather than a buffer: a stage that rewrites its input writes
153/// into the far half and declares the framing of what it wrote there, and a
154/// stage that passes through leaves both where they are.
155#[derive(Default)]
156struct Buffers {
157    a: Emission,
158    b: Emission,
159}
160
161impl Buffers {
162    /// What a stage reads and what it writes into, as disjoint borrows, plus
163    /// the slot it wrote into so the caller cannot pick a different one.
164    ///
165    /// The destination is always the half the live bytes are not in, so a
166    /// stage never reads and writes the same buffer. The first stage of a
167    /// chunk reads the caller's buffer, which leaves both halves free.
168    fn borrow<'b>(&'b mut self, live: Slot, input: &'b [u8]) -> Halves<'b> {
169        match live {
170            Slot::Input => (Slot::A, input, NO_BOUNDS, &mut self.a),
171            Slot::A => (Slot::B, self.a.bytes(), self.a.bounds(), &mut self.b),
172            Slot::B => (Slot::A, self.b.bytes(), self.b.bounds(), &mut self.a),
173        }
174    }
175
176    /// The bytes currently live, and their framing.
177    fn live<'b>(&'b self, live: Slot, input: &'b [u8]) -> (&'b [u8], &'b [usize]) {
178        match live {
179            Slot::Input => (input, NO_BOUNDS),
180            Slot::A => (self.a.bytes(), self.a.bounds()),
181            Slot::B => (self.b.bytes(), self.b.bounds()),
182        }
183    }
184}
185
186/// An ordered chain of stages that run inline on one task.
187pub struct Pipeline {
188    /// Pipeline metadata
189    meta: PipelineMeta,
190    /// Pipeline stages
191    stages: Vec<Box<dyn Plugin>>,
192    /// Display names, parallel to `stages`.
193    ///
194    /// Kept in their own vector rather than beside each plugin because
195    /// `self.stages[i]` and `self.names[i]` are then disjoint fields, which
196    /// the borrow checker will let the hot loop touch at the same time.
197    names: Vec<String>,
198    /// The ping-pong halves the stages rewrite into.
199    bufs: Buffers,
200    /// One entry per stage that asked to be ticked, in stage order.
201    ticks: Vec<Schedule>,
202}
203
204/// When a ticking stage is next owed a call.
205struct Schedule {
206    stage: usize,
207    period: Duration,
208    next: Instant,
209}
210
211/// What arrives at one stage: the bytes above it, how they are framed, and
212/// whether the stream has ended.
213///
214/// The first two are what [`Emitted`] carries, because on every stage but the
215/// first they *are* what the stage above emitted. `bounds` frames `bytes` the
216/// same way: empty is one unit, which is the shape of every chunk off the wire
217/// and of everything a stage that never calls [`Ctx::boundary`] produces.
218struct Input<'a> {
219    pub bytes: &'a [u8],
220    pub bounds: &'a [usize],
221    pub eof: bool,
222}
223
224/// Where one stage's output goes, and who it belongs to.
225///
226/// Everything [`Ctx::new`] needs except the payload, which is why it is built
227/// once per stage and turned into a [`Ctx`] once per unit. Host-side only: a
228/// plugin never sees this, and it is not one of the `*Ctx` types handed to
229/// one.
230struct Wiring<'a> {
231    meta: &'a PipelineMeta,
232    stage: &'a str,
233    dst: &'a mut Emission,
234    sink: &'a mut dyn EffectSink,
235}
236
237impl Wiring<'_> {
238    /// The context for one call, over these outputs and those bytes.
239    ///
240    /// Borrows rather than consumes, so the caller still has an emission to
241    /// inspect once the stage has returned.
242    fn ctx<'b>(&'b mut self, bytes: &'b [u8]) -> Ctx<'b> {
243        Ctx::new(self.meta, self.stage, bytes, self.dst, self.sink)
244    }
245}
246
247impl Pipeline {
248    #[must_use]
249    pub fn new(meta: PipelineMeta, stages: Vec<Box<dyn Plugin>>) -> Self {
250        let names = stages.iter().map(|s| s.name().to_string()).collect();
251        Self::with_names(meta, stages, names)
252    }
253
254    /// As [`Pipeline::new`], but with display names that may differ from the
255    /// plugin names: aliases, or `#n` suffixes for repeated plugins.
256    #[must_use]
257    pub fn with_names(
258        meta: PipelineMeta,
259        stages: Vec<Box<dyn Plugin>>,
260        names: Vec<String>,
261    ) -> Self {
262        debug_assert_eq!(stages.len(), names.len());
263
264        // Asked once, here, so the per-chunk path never touches it. A zero
265        // period would spin the host's timer, so it reads as "no ticks" (the
266        // same answer as `None`), which is what a stage configured with an
267        // interval of zero means by it.
268        let start = Instant::now();
269        let ticks = stages
270            .iter()
271            .enumerate()
272            .filter_map(|(stage, plugin)| {
273                let period = plugin.tick_interval().filter(|p| !p.is_zero())?;
274
275                Some(Schedule {
276                    stage,
277                    period,
278                    next: start + period,
279                })
280            })
281            .collect();
282
283        Self {
284            meta,
285            stages,
286            names,
287            bufs: Buffers::default(),
288            ticks,
289        }
290    }
291
292    #[must_use]
293    pub fn meta(&self) -> &PipelineMeta {
294        &self.meta
295    }
296
297    #[must_use]
298    pub fn is_empty(&self) -> bool {
299        self.stages.is_empty()
300    }
301
302    #[must_use]
303    pub fn len(&self) -> usize {
304        self.stages.len()
305    }
306
307    pub fn stage_names(&self) -> impl Iterator<Item = &str> {
308        self.names.iter().map(String::as_str)
309    }
310
311    /// How often the host should ask this pipeline for ticks, or `None` when
312    /// no stage wants any.
313    ///
314    /// The shortest period any stage asked for. A stage that wanted a longer
315    /// one is simply not due on most of those wakeups, which is cheaper than a
316    /// timer each.
317    #[must_use]
318    pub fn tick_interval(&self) -> Option<Duration> {
319        self.ticks.iter().map(|schedule| schedule.period).min()
320    }
321
322    /// The next stage owed a tick at `now`, with its schedule advanced.
323    fn due(&mut self, now: Instant) -> Option<usize> {
324        let schedule = self
325            .ticks
326            .iter_mut()
327            .find(|schedule| schedule.next <= now)?;
328
329        schedule.next += schedule.period;
330
331        // Slept through several periods: resume from now rather than firing
332        // once for each one we missed. A stage that wants to know how long it
333        // was actually away measures it itself.
334        if schedule.next <= now {
335            schedule.next = now + schedule.period;
336        }
337
338        Some(schedule.stage)
339    }
340
341    /// Restart one stage's schedule, as [`Ctx::rearm`] asked.
342    ///
343    /// The next tick falls a full period from now rather than from wherever
344    /// the cadence had reached, which turns the interval a stage asked for
345    /// into a delay it can rely on: a stage that starts holding bytes says so,
346    /// and hears back an interval later rather than at the next wakeup that
347    /// happens to be due.
348    ///
349    /// The clock is read here and only here, so a pipeline in which nothing
350    /// ever asks never touches it on the per-chunk path. A stage that asked
351    /// for no ticks has no schedule and is silently ignored.
352    fn rearm(&mut self, stage: usize) {
353        if let Some(schedule) = self.ticks.iter_mut().find(|s| s.stage == stage) {
354            schedule.next = Instant::now() + schedule.period;
355        }
356    }
357
358    /// Give one due stage its tick, and return what reached the end of the
359    /// pipeline.
360    ///
361    /// `None` means nothing was due. Call it again until it says so: two
362    /// stages can come due on the same wakeup, and each one's output has to be
363    /// written before the next runs.
364    ///
365    /// Unlike [`process`](Pipeline::process) and [`finish`](Pipeline::finish)
366    /// this does not run every stage. A tick belongs to one of them, and what
367    /// it emits cascades through the stages *below* it only, the ones above
368    /// are upstream of a chunk that did not come from them.
369    pub fn tick<'p>(
370        &'p mut self,
371        now: Instant,
372        sink: &mut dyn EffectSink,
373    ) -> Result<Option<Emitted<'p>>> {
374        let Some(index) = self.due(now) else {
375            return Ok(None);
376        };
377
378        run_tick(
379            &mut self.stages[index],
380            &self.meta,
381            &self.names[index],
382            &mut self.bufs.a,
383            sink,
384        )?;
385
386        // A stage may restart its own schedule from a tick as readily as from
387        // a chunk, which is how one that has just given up waiting says so.
388        if self.bufs.a.rearm_requested() {
389            self.rearm(index);
390        }
391
392        // The overwhelming case: an observer that reports and forwards
393        // nothing. No stage below it needs to hear about that.
394        if self.bufs.a.bytes().is_empty() {
395            return Ok(Some(Emitted::empty()));
396        }
397
398        self.drive(EMPTY, index + 1, Slot::A, false, sink).map(Some)
399    }
400
401    /// The first stage that must not carry datagrams, if any.
402    #[must_use]
403    pub fn datagram_hazard(&self) -> Option<&str> {
404        self.stages
405            .iter()
406            .zip(&self.names)
407            .find(|(stage, _)| !stage.boundaries().preserves_messages())
408            .map(|(_, name)| name.as_str())
409    }
410
411    /// What every stage does to boundaries and what it needs of them, in order.
412    ///
413    /// Read once at build time. A [`Chain`] folds these across its segments to
414    /// answer whether each requiring stage got what it asked for.
415    pub fn declarations(&self) -> impl Iterator<Item = Declaration<'_>> {
416        self.stages
417            .iter()
418            .zip(&self.names)
419            .map(|(stage, name)| Declaration {
420                stage: name.as_str(),
421                boundaries: stage.boundaries(),
422                needs: stage.needs(),
423            })
424    }
425
426    /// Push one chunk through every stage.
427    ///
428    /// The result borrows `input` directly when every stage passed it through,
429    /// and carries the framing of whatever reframed it otherwise.
430    pub fn process<'p>(
431        &'p mut self,
432        input: &'p [u8],
433        sink: &mut dyn EffectSink,
434    ) -> Result<Emitted<'p>> {
435        self.drive(input, 0, Slot::Input, false, sink)
436    }
437
438    /// Signal EOF, cascading each stage's final bytes through the ones below.
439    pub fn finish<'p>(&'p mut self, sink: &mut dyn EffectSink) -> Result<Emitted<'p>> {
440        self.drive(EMPTY, 0, Slot::Input, true, sink)
441    }
442
443    /// Thread bytes through `self.stages[from..]`.
444    ///
445    /// `live` says where they start out: [`Slot::Input`] for the caller's
446    /// buffer, or a half of the segment's own, which is how a tick cascades
447    /// into the stages below the one that fired.
448    fn drive<'p>(
449        &'p mut self,
450        input: &'p [u8],
451        from: usize,
452        live: Slot,
453        eof: bool,
454        sink: &mut dyn EffectSink,
455    ) -> Result<Emitted<'p>> {
456        let mut live = live;
457
458        for index in from..self.stages.len() {
459            let (slot, src, src_bounds, dst) = self.bufs.borrow(live, input);
460
461            let mut wiring = Wiring {
462                meta: &self.meta,
463                stage: &self.names[index],
464                dst,
465                sink,
466            };
467
468            let chunk = Input {
469                bytes: src,
470                bounds: src_bounds,
471                eof,
472            };
473
474            run(&mut *self.stages[index], &mut wiring, chunk)?;
475
476            let emitted = dst.emit();
477            let rearm = dst.rearm_requested();
478
479            if rearm {
480                self.rearm(index);
481            }
482
483            if emitted != Emit::Passthrough {
484                live = slot;
485            }
486
487            // A swallowed chunk cannot become bytes again further down. At end
488            // of stream there is no early exit, because every stage still has
489            // to hear about it.
490            if !eof && self.bufs.live(live, input).0.is_empty() {
491                return Ok(Emitted::empty());
492            }
493        }
494
495        let (bytes, bounds) = self.bufs.live(live, input);
496
497        Ok(Emitted { bytes, bounds })
498    }
499}
500
501/// Leaves [`Emit::Passthrough`] on the emission only when nothing was copied,
502/// so the caller can go on handing the source buffer down the chain.
503///
504/// Takes `&mut dyn Plugin` rather than the `Box` it lives in: the box is the
505/// caller's storage and says nothing here.
506fn run(plugin: &mut dyn Plugin, wiring: &mut Wiring, input: Input) -> Result<()> {
507    wiring.dst.reset();
508
509    if input.bounds.is_empty() {
510        {
511            let mut ctx = wiring.ctx(input.bytes);
512
513            if input.eof {
514                // Every stage but the first is handed whatever the one above
515                // it produced on its way out.
516                if !input.bytes.is_empty() {
517                    plugin.on_bytes(&mut ctx, input.bytes)?;
518                }
519
520                plugin.on_eof(&mut ctx)?;
521            } else {
522                plugin.on_bytes(&mut ctx, input.bytes)?;
523            }
524        }
525
526        // Only when the stage declared framing of its own. Closing an unframed
527        // emission would allocate on the hot path to say what empty says.
528        if !wiring.dst.bounds().is_empty() {
529            wiring.dst.close();
530        }
531
532        return Ok(());
533    }
534
535    let mut copied = false;
536
537    for (index, unit) in units(input.bytes, input.bounds).enumerate() {
538        wiring.dst.next_unit();
539
540        {
541            let mut ctx = wiring.ctx(unit);
542            plugin.on_bytes(&mut ctx, unit)?;
543        }
544
545        if !copied {
546            if wiring.dst.emit() == Emit::Passthrough {
547                // Nothing but the source buffer's own bytes so far, so there
548                // is still nothing to copy.
549                continue;
550            }
551
552            // The first unit this stage did not hand back verbatim, so the
553            // ones before it have to become real bytes now.
554            materialise(input.bytes, input.bounds, index, wiring.dst);
555            copied = true;
556        }
557
558        if wiring.dst.emit() == Emit::Passthrough {
559            wiring.dst.out.extend_from_slice(unit);
560        }
561
562        wiring.dst.close();
563    }
564
565    if input.eof {
566        wiring.dst.next_unit();
567
568        {
569            let mut ctx = wiring.ctx(EMPTY);
570            plugin.on_eof(&mut ctx)?;
571        }
572
573        // An epilogue is bytes of its own, so it forces the copy that the
574        // units before it avoided.
575        if !copied && !wiring.dst.bytes().is_empty() {
576            materialise(input.bytes, input.bounds, input.bounds.len(), wiring.dst);
577            copied = true;
578        }
579
580        if copied {
581            wiring.dst.close();
582        }
583    }
584
585    wiring.dst.emit = if copied {
586        Emit::Buffered
587    } else {
588        Emit::Passthrough
589    };
590
591    Ok(())
592}
593
594/// Execute a stage's tick. There is no input, so anything it wants downstream
595/// it has to write.
596fn run_tick(
597    plugin: &mut Box<dyn Plugin>,
598    meta: &PipelineMeta,
599    stage: &str,
600    dst: &mut Emission,
601    sink: &mut dyn EffectSink,
602) -> Result<()> {
603    dst.reset();
604
605    {
606        let mut ctx = Ctx::new(meta, stage, EMPTY, dst, sink);
607        plugin.on_tick(&mut ctx)?;
608    }
609
610    if !dst.bounds().is_empty() {
611        dst.close();
612    }
613
614    Ok(())
615}
616
617/// Walk a buffer's units, given the offsets that frame it.
618fn units<'a>(bytes: &'a [u8], bounds: &'a [usize]) -> impl Iterator<Item = &'a [u8]> {
619    let mut start = 0;
620
621    bounds.iter().map(move |&end| {
622        let unit = &bytes[start..end];
623        start = end;
624        unit
625    })
626}
627
628/// Copy the units a stage handed back verbatim in front of whatever it has
629/// already written for the unit that broke the run.
630///
631/// At most once per stage per call, and only for a stage that was both handed
632/// framed bytes and did something other than pass them through. A pipeline
633/// with no framing in it never reaches this, and neither does an observer
634/// sitting under one.
635fn materialise(input: &[u8], in_bounds: &[usize], done: usize, dst: &mut Emission) {
636    let prefix = if done == 0 { 0 } else { in_bounds[done - 1] };
637
638    if prefix == 0 {
639        return;
640    }
641
642    dst.out.splice(0..0, input[..prefix].iter().copied());
643
644    // What the stage wrote is now further along by the length of the prefix,
645    // and the prefix's own units come first.
646    for bound in dst.bounds.iter_mut() {
647        *bound += prefix;
648    }
649
650    dst.bounds.splice(0..0, in_bounds[..done].iter().copied());
651}
652
653impl fmt::Debug for Pipeline {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        f.debug_struct("Pipeline")
656            .field("direction", &self.meta.direction)
657            .field("stages", &self.stage_names().collect::<Vec<_>>())
658            .finish()
659    }
660}
661
662/// One link in a chain: either stages the host calls inline, or a subprocess
663/// it feeds and drains.
664#[expect(
665    clippy::large_enum_variant,
666    reason = "destructured once per segment per connection"
667)]
668#[derive(Debug)]
669pub enum Segment {
670    Inline(Pipeline),
671    Process(ExternalStage),
672}
673
674/// What one stage declared about message boundaries, and what it is called.
675#[derive(Debug, Clone, Copy, PartialEq, Eq)]
676pub struct Declaration<'a> {
677    pub stage: &'a str,
678    pub boundaries: Boundaries,
679    pub needs: Needs,
680}
681
682/// Which way a stage was looking when its requirement went unmet.
683#[derive(Debug, Clone, Copy, PartialEq, Eq)]
684pub enum Side {
685    /// The stage needs whole messages arriving.
686    Upstream,
687    /// The stage needs the units it emits to survive.
688    Downstream,
689}
690
691impl Side {
692    /// How to describe the far end in a message about this side.
693    #[must_use]
694    pub fn endpoint_role(self) -> &'static str {
695        match self {
696            Self::Upstream => "source",
697            Self::Downstream => "destination",
698        }
699    }
700}
701
702/// A stage placed where it cannot work.
703///
704/// `cause` names the stage that destroyed the boundaries, or `None` when
705/// nothing in the chain did and the endpoint itself is the byte stream.
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub struct BoundaryFault<'a> {
708    pub stage: &'a str,
709    pub side: Side,
710    pub cause: Option<&'a str>,
711}
712
713/// Everything that runs on one direction, split into segments.
714///
715/// One segment is the common common case and runs entirely on the reading task.
716/// A stage declared `Detached` starts a new segment, which the host runs on its
717/// own task behind a bounded channel; subsequent inline stages join that
718/// segment rather than spawning more.
719#[derive(Debug)]
720pub struct Chain {
721    meta: PipelineMeta,
722    segments: Vec<Segment>,
723}
724
725impl Chain {
726    #[must_use]
727    pub fn new(meta: PipelineMeta, segments: Vec<Segment>) -> Self {
728        Self { meta, segments }
729    }
730
731    #[must_use]
732    pub fn meta(&self) -> &PipelineMeta {
733        &self.meta
734    }
735
736    /// No stages at all: the host should use its plain copy path.
737    #[must_use]
738    pub fn is_empty(&self) -> bool {
739        self.segments.is_empty()
740    }
741
742    #[must_use]
743    pub fn segments(&self) -> &[Segment] {
744        &self.segments
745    }
746
747    #[must_use]
748    pub fn into_segments(self) -> Vec<Segment> {
749        self.segments
750    }
751
752    /// The first stage on this chain that must not carry datagrams, if any.
753    ///
754    /// A subprocess never can: its stdin and stdout are byte streams, so
755    /// message boundaries are gone the moment bytes cross the pipe.
756    #[must_use]
757    pub fn datagram_hazard(&self) -> Option<&str> {
758        self.segments().iter().find_map(|segment| match segment {
759            Segment::Inline(pipeline) => pipeline.datagram_hazard(),
760            Segment::Process(external) => Some(external.name.as_str()),
761        })
762    }
763
764    /// Every stage's boundary declaration, flattened across segments.
765    ///
766    /// A detached subprocess is [`Boundaries::Fuse`] whatever it runs: its
767    /// stdin and stdout are byte streams, so units do not cross the pipe.
768    #[must_use]
769    pub fn declarations(&self) -> Vec<Declaration<'_>> {
770        self.segments
771            .iter()
772            .flat_map(|segment| match segment {
773                Segment::Inline(pipeline) => pipeline.declarations().collect::<Vec<_>>(),
774                Segment::Process(external) => vec![Declaration {
775                    stage: external.name.as_str(),
776                    boundaries: Boundaries::Fuse,
777                    needs: Needs::Nothing,
778                }],
779            })
780            .collect()
781    }
782
783    /// Every stage on this chain whose requirement the path does not meet.
784    ///
785    /// Each requiring stage is checked by walking away from it towards the end
786    /// it named. The scan stops at the first stage that settles the question:
787    /// a `frame` below satisfies a downstream requirement however many stages
788    /// fuse under it, an `unframe` above satisfies an upstream one, and a stage
789    /// that does not carry units in the direction of travel is the fault. A
790    /// scan that reaches the end of the chain is answered by the endpoint,
791    /// which carries messages only when it is a datagram endpoint.
792    ///
793    /// Both flags are oriented for this chain's direction: `upstream` is the
794    /// endpoint it reads from and `downstream` the one it writes to.
795    #[must_use]
796    pub fn boundary_faults(
797        &self,
798        upstream_datagram: bool,
799        downstream_datagram: bool,
800    ) -> Vec<BoundaryFault<'_>> {
801        let declarations = self.declarations();
802        let mut faults = Vec::new();
803
804        for (index, declaration) in declarations.iter().enumerate() {
805            if declaration.needs.downstream() {
806                let cause = declarations[index + 1..]
807                    .iter()
808                    .find(|below| !below.boundaries.passes_downstream())
809                    .map(|below| {
810                        if below.boundaries.satisfies_downstream() {
811                            None
812                        } else {
813                            Some(below.stage)
814                        }
815                    });
816
817                match cause {
818                    // Nothing below settles it, so the endpoint answers.
819                    None if !downstream_datagram => faults.push(BoundaryFault {
820                        stage: declaration.stage,
821                        side: Side::Downstream,
822                        cause: None,
823                    }),
824                    Some(Some(stage)) => faults.push(BoundaryFault {
825                        stage: declaration.stage,
826                        side: Side::Downstream,
827                        cause: Some(stage),
828                    }),
829                    _ => {}
830                }
831            }
832
833            if declaration.needs.upstream() {
834                let cause = declarations[..index]
835                    .iter()
836                    .rev()
837                    .find(|above| !above.boundaries.passes_upstream())
838                    .map(|above| {
839                        if above.boundaries.satisfies_upstream() {
840                            None
841                        } else {
842                            Some(above.stage)
843                        }
844                    });
845
846                match cause {
847                    None if !upstream_datagram => faults.push(BoundaryFault {
848                        stage: declaration.stage,
849                        side: Side::Upstream,
850                        cause: None,
851                    }),
852                    Some(Some(stage)) => faults.push(BoundaryFault {
853                        stage: declaration.stage,
854                        side: Side::Upstream,
855                        cause: Some(stage),
856                    }),
857                    _ => {}
858                }
859            }
860        }
861
862        faults
863    }
864
865    #[must_use]
866    pub fn stage_names(&self) -> Vec<&str> {
867        self.segments
868            .iter()
869            .flat_map(|segment| match segment {
870                Segment::Inline(pipeline) => pipeline.stage_names().collect::<Vec<_>>(),
871                Segment::Process(external) => vec![external.name.as_str()],
872            })
873            .collect()
874    }
875}
876
877/// Every plugin this binary knows how to build.
878#[derive(Default)]
879pub struct Registry {
880    factories: BTreeMap<String, Arc<dyn PluginFactory>>,
881}
882
883impl Registry {
884    #[must_use]
885    pub fn new() -> Self {
886        Self::default()
887    }
888
889    pub fn register(&mut self, factory: impl PluginFactory) -> &mut Self {
890        self.register_arc(Arc::new(factory))
891    }
892
893    pub fn register_arc(&mut self, factory: Arc<dyn PluginFactory>) -> &mut Self {
894        self.factories.insert(normalize(factory.name()), factory);
895        self
896    }
897
898    #[must_use]
899    pub fn get(&self, name: &str) -> Option<&Arc<dyn PluginFactory>> {
900        self.factories.get(&normalize(name))
901    }
902
903    pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn PluginFactory>> {
904        self.factories.values()
905    }
906
907    /// The names plugins call themselves, not the normalized keys they are
908    /// stored under: these are shown to people, in `--list-plugins` and in the
909    /// suggestions on an unknown plugin.
910    pub fn names(&self) -> impl Iterator<Item = &str> {
911        // Keys are normalized, but name() preserves the original
912        self.factories.values().map(|f| f.name())
913    }
914
915    /// Build the chain for one direction.
916    ///
917    /// Entries that do not apply to `meta.direction` are skipped. On the
918    /// sink-to-source path the survivors are mirrored, so a declaration
919    /// `[a, b]` nests as `a(b(payload))` in both directions.
920    pub fn build(
921        &self,
922        specs: &[PluginSpec],
923        meta: &PipelineMeta,
924        host: &mut dyn HostBuilder,
925    ) -> Result<Chain> {
926        let mut selected: Vec<&PluginSpec> = specs
927            .iter()
928            .filter(|spec| spec.direction.contains(meta.direction))
929            .collect();
930
931        if meta.direction == Direction::SinkToSource {
932            selected.reverse();
933        }
934
935        let display = display_names(&selected);
936
937        // Neighbours, as seen on this path: the upstream endpoint, every
938        // stage, then the downstream endpoint. Stage `i` sits at `i + 1`.
939        let mut labels = Vec::with_capacity(display.len() + 2);
940        labels.push(meta.upstream().to_string());
941        labels.extend(display.iter().cloned());
942        labels.push(meta.downstream().to_string());
943
944        let total = selected.len();
945        let mut segments: Vec<Segment> = Vec::new();
946        let mut draft: Option<SegmentDraft> = None;
947
948        for (index, spec) in selected.iter().enumerate() {
949            let factory = self
950                .get(&spec.name)
951                .ok_or_else(|| PluginError::unknown(&spec.name, self.names()))?
952                .clone();
953
954            let execution = match spec.detach {
955                Some(true) => Execution::Detached,
956                Some(false) => Execution::Inline,
957                None => factory.execution(),
958            };
959
960            let stage_info = StageInfo {
961                index,
962                total,
963                name: &display[index],
964                upstream: &labels[index],
965                downstream: &labels[index + 2],
966            };
967
968            let mut ctx = BuildCtx::new(&spec.name, &spec.config, meta, stage_info, host);
969
970            match factory.build(&mut ctx)? {
971                Stage::Filter(plugin) => {
972                    // A detached stage starts a new segment; so does the first
973                    // stage after a subprocess, since it cannot run inside one.
974                    if draft.is_none() || execution == Execution::Detached {
975                        if let Some(ready) = draft.take() {
976                            segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
977                        }
978                        draft = Some(SegmentDraft::default());
979                    }
980
981                    draft
982                        .as_mut()
983                        .expect("a draft was just ensured")
984                        .push(plugin, display[index].clone());
985                }
986                Stage::External(external) => {
987                    if spec.detach == Some(false) {
988                        return Err(PluginError::config(
989                            &spec.name,
990                            "runs as a subprocess and always has its own task; `detach = false` \
991                             cannot be honoured",
992                        ));
993                    }
994
995                    if let Some(ready) = draft.take() {
996                        segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
997                    }
998
999                    segments.push(Segment::Process(external));
1000                }
1001            }
1002        }
1003
1004        if let Some(ready) = draft.take() {
1005            segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
1006        }
1007
1008        Ok(Chain::new(meta.clone(), segments))
1009    }
1010
1011    /// Build both directions at once. Returns `(source_to_sink,
1012    /// sink_to_source)`.
1013    ///
1014    /// Both share one `host`, which is how identical side-channel targets end
1015    /// up sharing a single writer.
1016    pub fn build_pair(
1017        &self,
1018        specs: &[PluginSpec],
1019        source: &str,
1020        sink: &str,
1021        peer: Option<&str>,
1022        host: &mut dyn HostBuilder,
1023    ) -> Result<(Chain, Chain)> {
1024        let forward = PipelineMeta::new(Direction::SourceToSink, source, sink).with_peer(peer);
1025        let reverse = PipelineMeta {
1026            direction: Direction::SinkToSource,
1027            ..forward.clone()
1028        };
1029
1030        Ok((
1031            self.build(specs, &forward, host)?,
1032            self.build(specs, &reverse, host)?,
1033        ))
1034    }
1035}
1036
1037/// A segment being assembled: stages and their display names, kept together so
1038/// the two vectors cannot drift out of step.
1039#[derive(Default)]
1040struct SegmentDraft {
1041    stages: Vec<Box<dyn Plugin>>,
1042    names: Vec<String>,
1043}
1044
1045impl SegmentDraft {
1046    fn push(&mut self, plugin: Box<dyn Plugin>, name: String) {
1047        self.stages.push(plugin);
1048        self.names.push(name);
1049    }
1050
1051    fn into_pipeline(self, meta: PipelineMeta) -> Pipeline {
1052        Pipeline::with_names(meta, self.stages, self.names)
1053    }
1054}
1055
1056/// Display name per stage: the `as` alias when given, else the plugin name,
1057/// with `#n` appended when a name would otherwise appear twice on one path.
1058fn display_names(specs: &[&PluginSpec]) -> Vec<String> {
1059    let base: Vec<&str> = specs
1060        .iter()
1061        .map(|spec| spec.alias.as_deref().unwrap_or(spec.name.as_str()))
1062        .collect();
1063
1064    let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
1065    for name in &base {
1066        *seen.entry(name).or_insert(0) += 1;
1067    }
1068
1069    let mut used: BTreeMap<&str, usize> = BTreeMap::new();
1070    base.iter()
1071        .map(|name| {
1072            if seen.get(name).copied().unwrap_or(0) > 1 {
1073                let n = used.entry(name).or_insert(0);
1074                *n += 1;
1075                format!("{name}#{n}")
1076            } else {
1077                (*name).to_string()
1078            }
1079        })
1080        .collect()
1081}
1082
1083impl fmt::Debug for Registry {
1084    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1085        f.debug_struct("Registry")
1086            .field("plugins", &self.names().collect::<Vec<_>>())
1087            .finish()
1088    }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094    use crate::{
1095        ChannelId, DirectionSpec,
1096        plugin::{LogLevel, StderrMode},
1097    };
1098
1099    #[derive(Default)]
1100    struct Recorder {
1101        writes: Vec<(ChannelId, Vec<u8>)>,
1102        logs: Vec<String>,
1103    }
1104
1105    impl EffectSink for Recorder {
1106        fn write(&mut self, channel: ChannelId, bytes: &[u8]) {
1107            self.writes.push((channel, bytes.to_vec()));
1108        }
1109
1110        fn log(&mut self, _level: LogLevel, stage: &str, message: &str) {
1111            self.logs.push(format!("{stage}: {message}"));
1112        }
1113    }
1114
1115    /// Observes without touching the payload.
1116    struct Observer;
1117
1118    impl Plugin for Observer {
1119        fn name(&self) -> &str {
1120            "observer"
1121        }
1122
1123        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1124            ctx.side_write(ChannelId(0), input);
1125            ctx.pass_through();
1126            Ok(())
1127        }
1128    }
1129
1130    struct Upper;
1131
1132    impl Plugin for Upper {
1133        fn name(&self) -> &str {
1134            "upper"
1135        }
1136
1137        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1138            let upper: Vec<u8> = input.iter().map(u8::to_ascii_uppercase).collect();
1139            ctx.forward(&upper);
1140            Ok(())
1141        }
1142    }
1143
1144    /// Declares boundaries and needs, and does nothing else. The scans read
1145    /// only what a stage declared, so a fault test needs no bytes at all.
1146    struct Declares {
1147        name: &'static str,
1148        boundaries: Boundaries,
1149        needs: Needs,
1150    }
1151
1152    impl Declares {
1153        fn boxed(name: &'static str, boundaries: Boundaries, needs: Needs) -> Box<dyn Plugin> {
1154            Box::new(Self {
1155                name,
1156                boundaries,
1157                needs,
1158            })
1159        }
1160    }
1161
1162    impl Plugin for Declares {
1163        fn name(&self) -> &str {
1164            self.name
1165        }
1166
1167        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1168            ctx.pass_through();
1169            Ok(())
1170        }
1171
1172        fn boundaries(&self) -> Boundaries {
1173            self.boundaries
1174        }
1175
1176        fn needs(&self) -> Needs {
1177            self.needs
1178        }
1179    }
1180
1181    /// One inline segment of declaring stages, named after what they declare.
1182    fn declaring(stages: Vec<Box<dyn Plugin>>) -> Chain {
1183        let names = stages.iter().map(|s| s.name().to_owned()).collect();
1184
1185        Chain::new(
1186            meta(),
1187            vec![Segment::Inline(Pipeline::with_names(meta(), stages, names))],
1188        )
1189    }
1190
1191    /// Buffers everything, emits it reversed at EOF.
1192    #[derive(Default)]
1193    struct Reverse(Vec<u8>);
1194
1195    impl Plugin for Reverse {
1196        fn name(&self) -> &str {
1197            "reverse"
1198        }
1199
1200        fn on_bytes(&mut self, _ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1201            self.0.extend_from_slice(input);
1202            Ok(())
1203        }
1204
1205        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1206            let mut buf = std::mem::take(&mut self.0);
1207            buf.reverse();
1208            ctx.forward(&buf);
1209            Ok(())
1210        }
1211    }
1212
1213    /// Emits on every tick, the way a keepalive would.
1214    struct Beacon(Duration);
1215
1216    impl Plugin for Beacon {
1217        fn name(&self) -> &str {
1218            "beacon"
1219        }
1220
1221        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1222            ctx.pass_through();
1223            Ok(())
1224        }
1225
1226        fn tick_interval(&self) -> Option<Duration> {
1227            Some(self.0)
1228        }
1229
1230        fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1231            ctx.forward(b"ping");
1232            Ok(())
1233        }
1234    }
1235
1236    /// Wants ticks but never emits on one. The shape almost every ticking
1237    /// stage actually has.
1238    struct Quiet(Duration);
1239
1240    impl Plugin for Quiet {
1241        fn name(&self) -> &str {
1242            "quiet"
1243        }
1244
1245        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1246            ctx.pass_through();
1247            Ok(())
1248        }
1249
1250        fn tick_interval(&self) -> Option<Duration> {
1251            Some(self.0)
1252        }
1253
1254        fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1255            ctx.log(LogLevel::Info, "still here");
1256            Ok(())
1257        }
1258    }
1259
1260    /// Cuts what it is given into fixed-size units, the way `block` does.
1261    /// Anything left over is emitted at EOF.
1262    struct Chop {
1263        size: usize,
1264        held: Vec<u8>,
1265    }
1266
1267    impl Chop {
1268        fn new(size: usize) -> Self {
1269            Self {
1270                size,
1271                held: Vec::new(),
1272            }
1273        }
1274    }
1275
1276    impl Plugin for Chop {
1277        fn name(&self) -> &str {
1278            "chop"
1279        }
1280
1281        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1282            self.held.extend_from_slice(input);
1283
1284            while self.held.len() >= self.size {
1285                let rest = self.held.split_off(self.size);
1286                ctx.forward(&self.held);
1287                ctx.boundary();
1288                self.held = rest;
1289            }
1290
1291            Ok(())
1292        }
1293
1294        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1295            if !self.held.is_empty() {
1296                let held = std::mem::take(&mut self.held);
1297                ctx.forward(&held);
1298                ctx.boundary();
1299            }
1300
1301            Ok(())
1302        }
1303    }
1304
1305    /// Drops every unit whose first byte is `skip`, and passes the rest
1306    /// through. Enough to break the all-passthrough run part way along.
1307    struct Sieve(u8);
1308
1309    impl Plugin for Sieve {
1310        fn name(&self) -> &str {
1311            "sieve"
1312        }
1313
1314        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1315            if input.first() == Some(&self.0) {
1316                ctx.drop_chunk();
1317            } else {
1318                ctx.pass_through();
1319            }
1320
1321            Ok(())
1322        }
1323    }
1324
1325    /// Passes everything through and writes an epilogue at end of stream, the
1326    /// way a codec closing a frame does.
1327    struct Trailer(&'static [u8]);
1328
1329    impl Plugin for Trailer {
1330        fn name(&self) -> &str {
1331            "trailer"
1332        }
1333
1334        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1335            ctx.pass_through();
1336            Ok(())
1337        }
1338
1339        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1340            ctx.forward(self.0);
1341            Ok(())
1342        }
1343    }
1344
1345    /// Passes everything through and restarts its own schedule on the way,
1346    /// the way a stage that has just started holding bytes does.
1347    struct Restart(Option<Duration>);
1348
1349    impl Plugin for Restart {
1350        fn name(&self) -> &str {
1351            "restart"
1352        }
1353
1354        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1355            ctx.rearm();
1356            ctx.pass_through();
1357            Ok(())
1358        }
1359
1360        fn tick_interval(&self) -> Option<Duration> {
1361            self.0
1362        }
1363    }
1364
1365    fn meta() -> PipelineMeta {
1366        PipelineMeta::new(Direction::SourceToSink, "src", "sink")
1367    }
1368
1369    /// Comfortably past any schedule set at construction.
1370    fn later() -> Instant {
1371        Instant::now() + Duration::from_secs(3600)
1372    }
1373
1374    /// Every unit of an emission, for comparing against a literal.
1375    fn parts<'a>(emitted: &Emitted<'a>) -> Vec<&'a [u8]> {
1376        emitted.units().collect()
1377    }
1378
1379    #[test]
1380    fn empty_pipeline_returns_the_input_slice() {
1381        let mut p = Pipeline::new(meta(), Vec::new());
1382        let mut sink = Recorder::default();
1383        let input = b"hello";
1384
1385        let out = p.process(input, &mut sink).unwrap();
1386        assert!(std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()));
1387    }
1388
1389    #[test]
1390    fn observers_never_copy_the_payload() {
1391        let mut p = Pipeline::new(meta(), vec![Box::new(Observer), Box::new(Observer)]);
1392        let mut sink = Recorder::default();
1393        let input = b"payload";
1394
1395        let out = p.process(input, &mut sink).unwrap();
1396
1397        assert!(
1398            std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()),
1399            "a chain of observers must hand the original buffer downstream",
1400        );
1401        assert_eq!(sink.writes.len(), 2);
1402    }
1403
1404    #[test]
1405    fn stages_chain_in_order() {
1406        let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Reverse::default())]);
1407        let mut sink = Recorder::default();
1408
1409        assert!(p.process(b"ab", &mut sink).unwrap().is_empty());
1410        assert!(p.process(b"cd", &mut sink).unwrap().is_empty());
1411        assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"DCBA");
1412    }
1413
1414    #[test]
1415    fn repeated_plugins_get_distinct_display_names() {
1416        let specs = [
1417            PluginSpec::new("tee", DirectionSpec::Both),
1418            PluginSpec::new("tee", DirectionSpec::Both).named("audit"),
1419            PluginSpec::new("tee", DirectionSpec::Both),
1420        ];
1421        let refs: Vec<&PluginSpec> = specs.iter().collect();
1422
1423        assert_eq!(display_names(&refs), ["tee#1", "audit", "tee#2"]);
1424    }
1425
1426    #[test]
1427    fn a_pipeline_with_nothing_ticking_has_no_schedule() {
1428        let mut p = Pipeline::new(meta(), vec![Box::new(Observer)]);
1429        let mut sink = Recorder::default();
1430
1431        assert_eq!(p.tick_interval(), None, "so the host builds no timer");
1432        assert!(p.tick(later(), &mut sink).unwrap().is_none());
1433    }
1434
1435    /// One timer for the segment, at the shortest period asked for; the stage
1436    /// that wanted the longer one is simply not due on most wakeups.
1437    #[test]
1438    fn the_schedule_is_the_shortest_period_asked_for() {
1439        let p = Pipeline::new(
1440            meta(),
1441            vec![
1442                Box::new(Quiet(Duration::from_secs(30))),
1443                Box::new(Beacon(Duration::from_secs(5))),
1444            ],
1445        );
1446
1447        assert_eq!(p.tick_interval(), Some(Duration::from_secs(5)));
1448    }
1449
1450    #[test]
1451    fn a_tick_cascades_through_the_stages_below_it() {
1452        let mut p = Pipeline::new(
1453            meta(),
1454            vec![Box::new(Beacon(Duration::from_secs(60))), Box::new(Upper)],
1455        );
1456        let mut sink = Recorder::default();
1457
1458        assert!(
1459            p.tick(Instant::now(), &mut sink).unwrap().is_none(),
1460            "not due yet",
1461        );
1462
1463        let now = later();
1464        assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"PING");
1465        assert!(
1466            p.tick(now, &mut sink).unwrap().is_none(),
1467            "one turn per stage per wakeup, however far behind the schedule is",
1468        );
1469    }
1470
1471    /// The common case has to stay free: a stage that only reports must not
1472    /// push an empty chunk at everything below it.
1473    #[test]
1474    fn a_silent_tick_does_not_disturb_the_stages_below() {
1475        let mut p = Pipeline::new(
1476            meta(),
1477            vec![Box::new(Quiet(Duration::from_secs(60))), Box::new(Observer)],
1478        );
1479        let mut sink = Recorder::default();
1480
1481        assert!(p.tick(later(), &mut sink).unwrap().unwrap().is_empty());
1482        assert!(sink.writes.is_empty(), "the observer below never ran");
1483        assert_eq!(sink.logs, ["quiet: still here"]);
1484    }
1485
1486    /// Ticking is orthogonal to the data path: a stage above the beacon must
1487    /// not see its output, and the payload must be unaffected.
1488    #[test]
1489    fn ticks_and_chunks_do_not_interfere() {
1490        let mut p = Pipeline::new(
1491            meta(),
1492            vec![
1493                Box::new(Observer),
1494                Box::new(Beacon(Duration::from_secs(60))),
1495            ],
1496        );
1497        let mut sink = Recorder::default();
1498
1499        assert_eq!(
1500            p.tick(later(), &mut sink).unwrap().unwrap().bytes(),
1501            b"ping"
1502        );
1503        assert!(
1504            sink.writes.is_empty(),
1505            "the observer sits above the beacon and saw nothing",
1506        );
1507
1508        assert_eq!(
1509            p.process(b"payload", &mut sink).unwrap().bytes(),
1510            b"payload"
1511        );
1512        assert_eq!(sink.writes, [(ChannelId(0), b"payload".to_vec())]);
1513    }
1514
1515    #[test]
1516    fn two_stages_due_at_once_each_get_a_turn() {
1517        let mut p = Pipeline::new(
1518            meta(),
1519            vec![
1520                Box::new(Beacon(Duration::from_secs(60))),
1521                Box::new(Quiet(Duration::from_secs(60))),
1522            ],
1523        );
1524        let mut sink = Recorder::default();
1525        let now = later();
1526
1527        assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"ping");
1528        assert!(p.tick(now, &mut sink).unwrap().unwrap().is_empty());
1529        assert!(p.tick(now, &mut sink).unwrap().is_none());
1530    }
1531
1532    #[test]
1533    fn transform_then_observe_keeps_the_transformed_bytes() {
1534        let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Observer)]);
1535        let mut sink = Recorder::default();
1536
1537        assert_eq!(p.process(b"hi", &mut sink).unwrap().bytes(), b"HI");
1538        assert_eq!(sink.writes[0].1, b"HI".to_vec());
1539    }
1540
1541    /// Nothing asked for framing, so there is one unit and it is everything.
1542    #[test]
1543    fn an_unframed_emission_is_one_unit() {
1544        let mut p = Pipeline::new(meta(), vec![Box::new(Upper)]);
1545        let mut sink = Recorder::default();
1546
1547        let out = p.process(b"hi", &mut sink).unwrap();
1548        assert_eq!(parts(&out), [b"HI".as_slice()]);
1549    }
1550
1551    #[test]
1552    fn an_empty_emission_has_no_units() {
1553        assert!(Emitted::empty().units().next().is_none());
1554    }
1555
1556    /// The whole point of `boundary`: several units out of one call, and they
1557    /// stay separate rather than fusing into the concatenation.
1558    #[test]
1559    fn a_stage_can_emit_several_units_from_one_chunk() {
1560        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2))]);
1561        let mut sink = Recorder::default();
1562
1563        let out = p.process(b"abcdef", &mut sink).unwrap();
1564
1565        assert_eq!(out.bytes(), b"abcdef", "the bytes are still the bytes");
1566        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
1567    }
1568
1569    /// A stage under a framing stage is called once per unit, not once per
1570    /// chunk, and its output stays framed the same way.
1571    #[test]
1572    fn framing_survives_a_stage_that_rewrites_it() {
1573        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Upper)]);
1574        let mut sink = Recorder::default();
1575
1576        let out = p.process(b"abcdef", &mut sink).unwrap();
1577        assert_eq!(parts(&out), [b"AB".as_slice(), b"CD", b"EF"]);
1578    }
1579
1580    /// The cost model has to hold under framing too: an observer below a
1581    /// framing stage sees each unit and still copies nothing.
1582    #[test]
1583    fn an_observer_under_a_framing_stage_still_copies_nothing() {
1584        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Observer)]);
1585        let mut sink = Recorder::default();
1586
1587        let out = p.process(b"abcdef", &mut sink).unwrap();
1588
1589        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
1590        assert_eq!(
1591            sink.writes.len(),
1592            3,
1593            "the observer was called once per unit, not once per chunk",
1594        );
1595    }
1596
1597    /// The copy starts at the first unit that is not handed back verbatim, and
1598    /// the units before it have to survive that.
1599    #[test]
1600    fn units_passed_through_before_a_drop_are_kept() {
1601        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'c'))]);
1602        let mut sink = Recorder::default();
1603
1604        let out = p.process(b"abcdef", &mut sink).unwrap();
1605
1606        assert_eq!(out.bytes(), b"abef");
1607        assert_eq!(parts(&out), [b"ab".as_slice(), b"ef"]);
1608    }
1609
1610    /// Same again with the drop first, which is the case where there is no
1611    /// prefix to keep.
1612    #[test]
1613    fn dropping_the_first_unit_keeps_the_rest() {
1614        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'a'))]);
1615        let mut sink = Recorder::default();
1616
1617        let out = p.process(b"abcdef", &mut sink).unwrap();
1618
1619        assert_eq!(parts(&out), [b"cd".as_slice(), b"ef"]);
1620    }
1621
1622    /// A stage that emits at EOF after passing every unit through has to force
1623    /// the copy it had been avoiding, or its epilogue would arrive alone.
1624    #[test]
1625    fn an_epilogue_after_a_run_of_passthroughs_keeps_both() {
1626        let mut p = Pipeline::new(
1627            meta(),
1628            vec![Box::new(Chop::new(4)), Box::new(Trailer(b"!"))],
1629        );
1630        let mut sink = Recorder::default();
1631
1632        let out = p.process(b"abcdef", &mut sink).unwrap();
1633        assert_eq!(parts(&out), [b"abcd".as_slice()]);
1634
1635        let out = p.finish(&mut sink).unwrap();
1636        assert_eq!(out.bytes(), b"ef!");
1637        assert_eq!(parts(&out), [b"ef".as_slice(), b"!"]);
1638    }
1639
1640    /// A short tail is held until EOF, and arrives as a unit of its own.
1641    #[test]
1642    fn a_short_final_unit_is_emitted_at_end_of_stream() {
1643        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4))]);
1644        let mut sink = Recorder::default();
1645
1646        let out = p.process(b"abcdef", &mut sink).unwrap();
1647        assert_eq!(parts(&out), [b"abcd".as_slice()]);
1648
1649        let out = p.finish(&mut sink).unwrap();
1650        assert_eq!(parts(&out), [b"ef".as_slice()]);
1651    }
1652
1653    /// Two framing stages compose: the second reframes what the first handed
1654    /// it, one unit at a time.
1655    #[test]
1656    fn framing_stages_compose() {
1657        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4)), Box::new(Chop::new(2))]);
1658        let mut sink = Recorder::default();
1659
1660        let out = p.process(b"abcdefgh", &mut sink).unwrap();
1661        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef", b"gh"]);
1662    }
1663
1664    /// Without a rearm the schedule is a cadence: it advances from wherever
1665    /// it had reached, which says nothing about how long the stage has
1666    /// actually been waiting. A rearm makes the interval mean "from now".
1667    #[test]
1668    fn a_stage_can_restart_its_own_schedule() {
1669        let period = Duration::from_secs(600);
1670        let mut p = Pipeline::new(meta(), vec![Box::new(Restart(Some(period)))]);
1671        let mut sink = Recorder::default();
1672        let start = Instant::now();
1673
1674        // Fires, which moves the cadence on to two periods from construction.
1675        assert!(
1676            p.tick(start + period + Duration::from_secs(100), &mut sink)
1677                .unwrap()
1678                .is_some(),
1679        );
1680        assert!(
1681            p.tick(start + period + Duration::from_secs(200), &mut sink)
1682                .unwrap()
1683                .is_none(),
1684            "the cadence has moved past this",
1685        );
1686
1687        p.process(b"payload", &mut sink).unwrap();
1688
1689        assert!(
1690            p.tick(start + period + Duration::from_secs(300), &mut sink)
1691                .unwrap()
1692                .is_some(),
1693            "the chunk restarted the schedule, so a period from now is due \
1694             again well before the cadence would have come round",
1695        );
1696    }
1697
1698    #[test]
1699    fn rearming_a_stage_that_asked_for_no_ticks_does_nothing() {
1700        let mut p = Pipeline::new(meta(), vec![Box::new(Restart(None))]);
1701        let mut sink = Recorder::default();
1702
1703        assert_eq!(
1704            p.process(b"payload", &mut sink).unwrap().bytes(),
1705            b"payload"
1706        );
1707        assert!(p.tick(later(), &mut sink).unwrap().is_none());
1708    }
1709
1710    /// Buffering below a framing stage still works: the stage sees each unit
1711    /// and answers whenever it has something to say.
1712    #[test]
1713    fn a_buffering_stage_under_a_framing_stage_holds_across_units() {
1714        let mut p = Pipeline::new(
1715            meta(),
1716            vec![Box::new(Chop::new(2)), Box::new(Reverse::default())],
1717        );
1718        let mut sink = Recorder::default();
1719
1720        assert!(p.process(b"abcd", &mut sink).unwrap().is_empty());
1721        assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"dcba");
1722    }
1723    // ------------------------------------------------------------------
1724    // Boundary declarations
1725    //
1726    // Two scans, one per side, each walking away from the requiring stage
1727    // until something settles the question. `frame` seals and settles a
1728    // downstream scan; `unframe` splits and settles an upstream one; anything
1729    // that fuses is the fault; reaching the end hands the question to the
1730    // endpoint.
1731
1732    fn seal() -> Box<dyn Plugin> {
1733        Declares::boxed("frame", Boundaries::Seal, Needs::Nothing)
1734    }
1735
1736    fn split() -> Box<dyn Plugin> {
1737        Declares::boxed("unframe", Boundaries::Split, Needs::Nothing)
1738    }
1739
1740    fn fuse() -> Box<dyn Plugin> {
1741        Declares::boxed("compress", Boundaries::Fuse, Needs::Nothing)
1742    }
1743
1744    fn preserve() -> Box<dyn Plugin> {
1745        Declares::boxed("hash", Boundaries::Preserve, Needs::Nothing)
1746    }
1747
1748    fn needs_below() -> Box<dyn Plugin> {
1749        Declares::boxed("needs-below", Boundaries::Preserve, Needs::Downstream)
1750    }
1751
1752    fn needs_above() -> Box<dyn Plugin> {
1753        Declares::boxed("needs-above", Boundaries::Preserve, Needs::Upstream)
1754    }
1755
1756    #[test]
1757    fn a_datagram_endpoint_answers_a_requirement_by_itself() {
1758        assert!(
1759            declaring(vec![needs_below()])
1760                .boundary_faults(false, true)
1761                .is_empty(),
1762            "a datagram sink carries whatever units it was handed",
1763        );
1764        assert!(
1765            declaring(vec![needs_above()])
1766                .boundary_faults(true, false)
1767                .is_empty(),
1768            "a datagram source delivers whole messages",
1769        );
1770    }
1771
1772    #[test]
1773    fn a_byte_endpoint_does_not_and_says_which_one() {
1774        let binding = declaring(vec![needs_below()]);
1775        let faults = binding.boundary_faults(true, false);
1776
1777        assert_eq!(faults.len(), 1);
1778        assert_eq!(faults[0].stage, "needs-below");
1779        assert_eq!(faults[0].side, Side::Downstream);
1780        assert_eq!(faults[0].cause, None, "no stage broke it; the endpoint did");
1781
1782        let binding = declaring(vec![needs_above()]);
1783        let faults = binding.boundary_faults(false, true);
1784
1785        assert_eq!(faults.len(), 1);
1786        assert_eq!(faults[0].side, Side::Upstream);
1787    }
1788
1789    /// The composition the `frame` pair exists for, and the reason a
1790    /// downstream scan stops at a seal rather than folding past it.
1791    #[test]
1792    fn framing_satisfies_a_requirement_a_stream_endpoint_would_not() {
1793        let chain = declaring(vec![needs_below(), seal()]);
1794        assert!(chain.boundary_faults(false, false).is_empty());
1795
1796        let chain = declaring(vec![split(), needs_above()]);
1797        assert!(chain.boundary_faults(false, false).is_empty());
1798    }
1799
1800    /// A seal below covers everything under it, because the boundary is in the
1801    /// payload by then and no later stage can lose it.
1802    #[test]
1803    fn a_seal_covers_a_fuse_beneath_it() {
1804        let chain = declaring(vec![needs_below(), seal(), fuse()]);
1805
1806        assert!(chain.boundary_faults(false, false).is_empty());
1807    }
1808
1809    /// The same two stages the other way round, which is the mistake this
1810    /// check exists to catch: the units are gone before the seal sees them.
1811    #[test]
1812    fn a_fuse_before_the_seal_is_a_fault_that_names_it() {
1813        let binding = declaring(vec![needs_below(), fuse(), seal()]);
1814        let faults = binding.boundary_faults(false, false);
1815
1816        assert_eq!(faults.len(), 1);
1817        assert_eq!(faults[0].stage, "needs-below");
1818        assert_eq!(faults[0].cause, Some("compress"));
1819    }
1820
1821    /// Passing stages are walked through rather than counted against.
1822    #[test]
1823    fn preserving_stages_do_not_settle_a_scan_either_way() {
1824        let chain = declaring(vec![split(), preserve(), needs_above(), preserve(), seal()]);
1825
1826        assert!(chain.boundary_faults(false, false).is_empty());
1827    }
1828
1829    /// A split below a requiring stage re-cuts the bytes by the sender's
1830    /// framing, so the units from above do not reach the far end.
1831    #[test]
1832    fn a_split_below_does_not_carry_units_from_above() {
1833        let binding = declaring(vec![needs_below(), split()]);
1834        let faults = binding.boundary_faults(false, false);
1835
1836        assert_eq!(faults.len(), 1);
1837        assert_eq!(faults[0].cause, Some("unframe"));
1838    }
1839
1840    /// Scanning upwards, a seal is transparent: it emits one unit for every
1841    /// unit it was given, so what is above it still arrives.
1842    #[test]
1843    fn a_seal_above_is_transparent_to_an_upstream_scan() {
1844        let chain = declaring(vec![split(), seal(), needs_above()]);
1845        assert!(chain.boundary_faults(false, false).is_empty());
1846
1847        let binding = declaring(vec![seal(), needs_above()]);
1848        let faults = binding.boundary_faults(false, false);
1849        assert_eq!(
1850            faults.len(),
1851            1,
1852            "nothing above the seal supplies boundaries"
1853        );
1854        assert_eq!(faults[0].cause, None);
1855    }
1856
1857    /// A subprocess is a pair of byte streams whatever it runs, so it fuses.
1858    #[test]
1859    fn a_detached_process_fuses() {
1860        let chain = Chain::new(
1861            meta(),
1862            vec![
1863                Segment::Inline(Pipeline::with_names(
1864                    meta(),
1865                    vec![needs_below()],
1866                    vec!["needs-below".to_owned()],
1867                )),
1868                Segment::Process(ExternalStage {
1869                    argv: vec!["cat".to_owned()],
1870                    shell: false,
1871                    stderr: StderrMode::Log,
1872                    name: "process".to_owned(),
1873                }),
1874            ],
1875        );
1876
1877        let faults = chain.boundary_faults(true, true);
1878
1879        assert_eq!(faults.len(), 1);
1880        assert_eq!(faults[0].cause, Some("process"));
1881    }
1882
1883    /// The warning and the error are separate questions over the same
1884    /// declarations: sealing rewrites a datagram without splitting it.
1885    #[test]
1886    fn only_fusing_and_splitting_are_datagram_hazards() {
1887        assert_eq!(declaring(vec![preserve()]).datagram_hazard(), None);
1888        assert_eq!(declaring(vec![seal()]).datagram_hazard(), None);
1889        assert_eq!(declaring(vec![fuse()]).datagram_hazard(), Some("compress"));
1890        assert_eq!(declaring(vec![split()]).datagram_hazard(), Some("unframe"));
1891    }
1892}