Skip to main content

zencodec_io/
row_encode.rs

1//! Owned row-source encoding with policy before final-destination effects.
2//!
3//! Finish-first is the default offer. It stages compressed bytes under the
4//! supplied storage budget, releases the source and encoder, then writes.
5//! Live output is offered only when begin reports streaming; it does not imply
6//! a memory bound. No native workspace or source allocation bound is inferred.
7//! Failures consume the operation, retain the selected decision and report
8//! confirmed destination appends. Partial output is never retried.
9//!
10//! ```
11//! use zencodec::{encode::Encoder, operation::PolicySet, storage::StorageBudget};
12//! use zencodec_io::row_encode::{self, Options, Raster};
13//! use zenpixels::PixelSlice;
14//! fn save<E: Encoder, W: std::io::Write>(
15//!     encoder: E, pixels: PixelSlice<'_>, writer: W,
16//! ) -> Result<row_encode::Completed, Box<dyn core::error::Error>> {
17//!     Ok(row_encode::encode(
18//!         encoder, Raster::new(pixels), writer, PolicySet::new(),
19//!         Options::new(StorageBudget::new(64 * 1024 * 1024, 4096), 64 * 1024 * 1024),
20//!         None,
21//!     )?)
22//! }
23//! ```
24use crate::destination::{Destination, Forward};
25use alloc::{boxed::Box, collections::VecDeque, sync::Arc};
26use core::{
27    error::Error,
28    fmt,
29    num::{NonZeroU32, NonZeroUsize},
30};
31
32use std::{
33    fs::File,
34    io::{self, Read, Seek, SeekFrom, Write},
35};
36use zencodec::output_patch::OutputAccess;
37use zencodec::{
38    At,
39    encode::{
40        DynEncodeJob, DynEncoder, EncodeDeclaration, EncodeJob, EncodeOutput, EncodePlan,
41        EncodePlanError, Encoder, InputRequirement,
42    },
43    operation::{
44        OperationPolicy, OperationReport, OutputSchedule, OutputScheduleFacts, OutputScheduleOffer,
45        OutputSource, PolicySet, StagingFacts, StagingKind, resolve_output_schedule,
46    },
47    storage::{OwnedStorage, StorageBudget},
48};
49use zenpixels::{ColorContext, PixelDescriptor, PixelSlice, PixelSliceMut, sample::ExactDepth};
50
51mod planned;
52
53/// Fixed sample declaration for this operation. Color conversion is not implicit.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Description {
56    /// Width in samples.
57    pub width: u32,
58    /// Total rows.
59    pub height: u32,
60    /// Complete sample descriptor accepted by the configured encoder.
61    pub descriptor: PixelDescriptor,
62    /// Original color context carried on every source and encoder row view.
63    pub color_context: Option<Arc<ColorContext>>,
64    /// Optional assertion about current canonical samples, checked before push.
65    /// `Some` is strict: unsupported layouts refuse before startup, including
66    /// full-storage CMYK claims. `None` does not restrict sample depth.
67    /// It does not prove the encoder preserves samples or bound native memory.
68    pub exact_depth: Option<ExactDepth>,
69}
70impl Description {
71    /// Construct a declaration; execution checks dimensions and size arithmetic.
72    pub const fn new(width: u32, height: u32, descriptor: PixelDescriptor) -> Self {
73        Self {
74            width,
75            height,
76            descriptor,
77            color_context: None,
78            exact_depth: None,
79        }
80    }
81    /// Attach the actual sample context without changing the descriptor.
82    pub fn with_color_context(mut self, context: Arc<ColorContext>) -> Self {
83        self.color_context = Some(context);
84        self
85    }
86
87    /// State a sample-grid claim; execution verifies each active row before push.
88    /// Supported layouts follow [`EncodeDeclaration::exact_depth`]. A claim
89    /// changes admission: it must be verifiable even if the encoder ignores it.
90    pub const fn with_exact_depth(mut self, exact: ExactDepth) -> Self {
91        self.exact_depth = Some(exact);
92        self
93    }
94}
95/// A fixed-interpretation source, retained across policy and read on demand.
96pub trait Source {
97    /// Original source error; retained in the operation's error chain.
98    type Error: Error + Send + Sync + 'static;
99    /// Fixed declaration. Changing it during read fails before the next encode.
100    fn description(&self) -> Description;
101    /// Acquisition mechanism, not a retained-memory guarantee.
102    fn mechanism(&self) -> OutputSource {
103        OutputSource::PullRows
104    }
105    /// Return a real immutable raster view retained for the entire operation.
106    /// The default supplies no borrow. The driver uses this returned storage,
107    /// never `mechanism()` or a capability flag, as exact replay evidence.
108    fn borrowed_pixels(&self) -> Option<PixelSlice<'_>> {
109        None
110    }
111    /// Whether ordered traversals can restart at row zero without reconstructing
112    /// this source. Defaults to forward-only. This proves no sample identity;
113    /// positional replay is checked with per-row digests and optional identity.
114    fn supports_positional_replay(&self) -> bool {
115        false
116    }
117    /// Optional opaque identity/version of this retained source. A stable value
118    /// is weaker than sample-exact replay. The driver also checks row digests.
119    fn replay_identity(&self) -> Result<Option<[u64; 4]>, Self::Error> {
120        Ok(None)
121    }
122    /// Fill every active sample of the requested consecutive rows, honoring
123    /// destination stride. No reading beyond the declared image is requested.
124    /// A late interpretation change must fail, never relabel earlier samples.
125    /// `None` adds no per-call control; any control retained by the source
126    /// remains active.
127    fn read_rows(
128        &mut self,
129        first: u32,
130        rows: PixelSliceMut<'_>,
131        pulse: Option<&dyn zencodec::Pulse>,
132    ) -> Result<(), Self::Error>;
133}
134/// Borrow a complete raster through the same row-source interface.
135/// An attached exact-depth claim is preserved and verified before each push.
136/// Unsupported claimed layouts refuse before startup; this is stricter than
137/// using an otherwise identical unclaimed view.
138pub struct Raster<'a>(PixelSlice<'a>);
139impl<'a> Raster<'a> {
140    /// Retain the caller's raster borrow; source padding is not copied.
141    pub const fn new(pixels: PixelSlice<'a>) -> Self {
142        Self(pixels)
143    }
144}
145impl Source for Raster<'_> {
146    type Error = zencodec::CodecError;
147    fn description(&self) -> Description {
148        let mut desc = Description::new(self.0.width(), self.0.rows(), self.0.descriptor());
149        desc.color_context = self.0.color_context().cloned();
150        desc.exact_depth = self.0.exact_depth();
151        desc
152    }
153    fn mechanism(&self) -> OutputSource {
154        OutputSource::BorrowedRaster
155    }
156    fn borrowed_pixels(&self) -> Option<PixelSlice<'_>> {
157        Some(self.0.clone())
158    }
159    fn read_rows(
160        &mut self,
161        first: u32,
162        mut rows: PixelSliceMut<'_>,
163        pulse: Option<&dyn zencodec::Pulse>,
164    ) -> Result<(), Self::Error> {
165        let pulse = pulse.unwrap_or(&zencodec::NoPulse);
166        if rows.width() != self.0.width()
167            || rows.descriptor() != self.0.descriptor()
168            || rows.color_context() != self.0.color_context()
169            || first
170                .checked_add(rows.rows())
171                .is_none_or(|end| end > self.0.rows())
172        {
173            return Err(zencodec::CodecError::new(
174                None,
175                zencodec::InvalidKind::Buffer.into(),
176            ));
177        }
178        for y in 0..rows.rows() {
179            pulse
180                .check()
181                .map_err(|r| zencodec::CodecError::new(None, r.into()))?;
182            rows.row_mut(y).copy_from_slice(self.0.row(first + y));
183        }
184        Ok(())
185    }
186}
187/// Managed payload storage and output limits. Neither bounds native allocations
188/// or queue bookkeeping.
189pub struct Options {
190    storage: StorageBudget,
191    max_output_bytes: u64,
192    batch_rows: NonZeroU32,
193    preset: PolicySet,
194    disk: Option<Disk>,
195    raw_disk: Option<planned::RawDisk>,
196}
197impl Options {
198    /// Row buffers and compressed staging share this admission pool.
199    /// Zero output allowance is a real limit; it is not an unlimited sentinel.
200    pub fn new(storage: StorageBudget, max_output_bytes: u64) -> Self {
201        Self {
202            storage,
203            max_output_bytes,
204            batch_rows: NonZeroU32::new(16).unwrap(),
205            preset: PolicySet::new(),
206            disk: None,
207            raw_disk: None,
208        }
209    }
210    /// Stage finish-first output in an owned private scratch file at its end.
211    /// Existing prefixes are preserved. The output limit also caps disk payload.
212    /// Replay admits at most `replay_bytes` from the shared storage budget.
213    /// Spool writes may block with native state alive. The caller owns path,
214    /// permissions and deletion; aliases/external mutation are unsupported.
215    /// No fallback, file creation or deletion occurs. Live selection ignores
216    /// the scratch file and writes directly to the final destination.
217    pub fn with_disk_staging(
218        mut self,
219        mut file: File,
220        replay_bytes: NonZeroUsize,
221    ) -> io::Result<Self> {
222        let origin = file.stream_position()?;
223        if file.seek(SeekFrom::End(0))? != origin {
224            return Err(io::Error::new(
225                io::ErrorKind::InvalidInput,
226                "scratch must start at its end",
227            ));
228        }
229        self.disk = Some(Disk {
230            file,
231            origin,
232            replay_bytes,
233        });
234        Ok(self)
235    }
236    /// Offer a distinct owned scratch file for raw replay, never the compressed
237    /// output spool. Starts at the file end and preserves its prefix. The caller
238    /// owns path, permissions and deletion and must not alias this with output.
239    /// Raw disk bytes are capped separately; RAM digests use the storage budget.
240    /// Replay is hash-verified, not an exact private immutable snapshot.
241    pub fn with_raw_disk_staging(mut self, mut file: File, max_bytes: u64) -> io::Result<Self> {
242        let origin = file.stream_position()?;
243        if file.seek(SeekFrom::End(0))? != origin {
244            return Err(io::Error::new(
245                io::ErrorKind::InvalidInput,
246                "raw scratch must start at its end",
247            ));
248        }
249        self.raw_disk = Some(planned::RawDisk {
250            file,
251            origin,
252            max_bytes,
253        });
254        Ok(self)
255    }
256    /// Maximum rows per push for a `Rows` plan. Replay strips are native-owned
257    /// and follow the native encoder's requests; this does not bound their size.
258    pub fn with_batch_rows(mut self, rows: NonZeroU32) -> Self {
259        self.batch_rows = rows;
260        self
261    }
262    /// Built-in policy used when the supplied callback returns UsePreset.
263    /// During migration, these requirements and the policy argument
264    /// both apply. A bound policy supplies preferences in place of these.
265    #[deprecated(note = "Pass PolicySet or BoundPolicy as the operation policy argument")]
266    pub fn with_policy_set(mut self, preset: PolicySet) -> Self {
267        self.preset = preset;
268        self
269    }
270}
271/// Operation boundary that failed.
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum Stage {
275    /// Validate declarations, begin the encoder and admit row storage.
276    Prepare,
277    /// Select the complete acquisition/output route (output only for raw encoders).
278    Policy,
279    /// Request source samples or detect changed interpretation.
280    Read,
281    /// Encode rows or admit pending compressed storage.
282    Encode,
283    /// Finish and release the encoder.
284    Finish,
285    /// Flush or read staged compressed output, or admit its replay buffer.
286    Replay,
287    /// Write or flush the final destination.
288    Write,
289    /// Seek or overwrite final patches, including restoration to the output end.
290    Patch,
291    /// Construct and start the admitted job, or reject a changed native plan.
292    Start,
293}
294/// Driver contract refusal.
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296#[non_exhaustive]
297pub enum Refusal {
298    /// Empty geometry is not a raster.
299    Empty,
300    /// Size arithmetic overflow.
301    Overflow,
302    /// Source declaration changed after preparation.
303    SourceChanged,
304    /// Total encoded bytes exceed the supplied output allowance.
305    OutputLimit,
306    /// Layout requires unavailable or unannounced seek-back writes.
307    Patches,
308    /// Destination access changed while policy was deciding.
309    DestinationChanged,
310    /// Encoder requests are out of order, inconsistent or outside the declaration.
311    RequestOrder {
312        /// Zero-based native pass named by the request.
313        pass: u32,
314        /// First requested row, or zero when rejecting a new pass.
315        first: u32,
316    },
317    /// The encoder exceeded its admitted native pass bound or final pass.
318    PassLimit {
319        /// Zero-based native pass that could not be admitted.
320        pass: u32,
321    },
322    /// Completion or a new pass arrived before the current traversal completed.
323    IncompletePass {
324        /// Current or requested zero-based native pass; zero before any fill.
325        pass: u32,
326    },
327    /// A pull did not perform exactly one successful fill, or swallowed a failure.
328    PullProtocol,
329    /// Samples or identity changed on a repeated source/disk traversal.
330    ReplayChanged {
331        /// Zero-based traversal on which a discrepancy was detected.
332        pass: u32,
333        /// Row whose digest differed, or the next row at an identity check.
334        first: u32,
335    },
336    /// Final-pass-only output appeared before its final traversal started.
337    EarlyOutput {
338        /// Last started pass; `None` means output appeared before the first fill.
339        pass: Option<u32>,
340    },
341    /// No executable input route could reserve its required managed storage.
342    NoInputRoute,
343}
344impl fmt::Display for Refusal {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        write!(f, "row encoding refused: {self:?}")
347    }
348}
349impl Error for Refusal {}
350
351/// Actual samples contradict the declared grid. The complete strip is refused.
352/// Earlier live bytes remain irrevocable; no finish or alternate route is tried.
353#[derive(Clone, Copy, Debug, PartialEq, Eq)]
354pub struct ExactDepthContradiction {
355    /// Absolute zero-based first row containing an off-grid active sample.
356    /// This can be later than the first row of the refused strip.
357    pub first_row: u32,
358    /// The asserted grid that did not hold for that row.
359    pub claim: ExactDepth,
360}
361impl fmt::Display for ExactDepthContradiction {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        write!(
364            f,
365            "row samples contradict exact-depth claim at row {}",
366            self.first_row
367        )
368    }
369}
370impl Error for ExactDepthContradiction {}
371type Cause = Box<dyn Error + Send + Sync>;
372/// Terminal error. Default formatting omits backend payloads and source data.
373pub struct Failure {
374    /// Boundary that failed.
375    pub stage: Stage,
376    /// Confirmed destination appends, including partial writes.
377    pub emitted: u64,
378    /// Confirmed overwritten bytes, separate from appended extent.
379    pub patched_bytes: u64,
380    /// Bounded policy history, including after later source/codec/I/O failure.
381    pub report: Box<OperationReport>,
382    cause: Cause,
383}
384impl fmt::Debug for Failure {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        f.debug_struct("RowEncodeFailure")
387            .field("stage", &self.stage)
388            .field("emitted", &self.emitted)
389            .field("patched_bytes", &self.patched_bytes)
390            .field("report", &self.report)
391            .finish_non_exhaustive()
392    }
393}
394impl fmt::Display for Failure {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        write!(
397            f,
398            "row encoding failed during {:?} after {} appended bytes",
399            self.stage, self.emitted
400        )
401    }
402}
403impl Error for Failure {
404    fn source(&self) -> Option<&(dyn Error + 'static)> {
405        Some(self.cause.as_ref())
406    }
407}
408/// Successful destination completion and bounded decision history.
409#[derive(Debug)]
410pub struct Completed {
411    /// Confirmed appended bytes.
412    pub emitted: u64,
413    /// Confirmed overwritten bytes, separate from appended extent.
414    pub patched_bytes: u64,
415    /// Executed schedule.
416    pub schedule: OutputSchedule,
417    /// Decisions made before source reads and final output writes.
418    pub report: OperationReport,
419}
420fn check(pulse: &dyn zencodec::Pulse) -> Result<(), Cause> {
421    pulse
422        .check()
423        .map_err(|r| At::wrap(zencodec::CodecError::new(None, r.into())).into())
424}
425fn validate_source<S: Source>(
426    source: &S,
427    desc: &Description,
428    mechanism: OutputSource,
429) -> Result<(), Cause> {
430    if source.description() != *desc || source.mechanism() != mechanism {
431        return Err(Refusal::SourceChanged.into());
432    }
433    Ok(())
434}
435fn count(total: &mut u64, len: usize, limit: u64) -> Result<(), Cause> {
436    let next = total.checked_add(len as u64).ok_or(Refusal::Overflow)?;
437    if next > limit {
438        return Err(Refusal::OutputLimit.into());
439    }
440    *total = next;
441    Ok(())
442}
443fn write_all(
444    writer: &mut impl Write,
445    mut bytes: &[u8],
446    emitted: &mut u64,
447    pulse: &dyn zencodec::Pulse,
448) -> Result<(), Cause> {
449    while !bytes.is_empty() {
450        check(pulse)?;
451        match writer.write(bytes) {
452            Ok(0) => return Err(io::Error::from(io::ErrorKind::WriteZero).into()),
453            Ok(n) if n <= bytes.len() => {
454                *emitted += n as u64;
455                bytes = &bytes[n..];
456            }
457            Ok(_) => {
458                return Err(io::Error::new(
459                    io::ErrorKind::InvalidData,
460                    "writer returned an invalid count",
461                )
462                .into());
463            }
464            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
465            Err(e) => return Err(e.into()),
466        }
467    }
468    check(pulse)
469}
470struct Disk {
471    file: File,
472    origin: u64,
473    replay_bytes: NonZeroUsize,
474}
475impl Disk {
476    fn append(&mut self, bytes: &[u8], pulse: &dyn zencodec::Pulse) -> Result<(), Cause> {
477        let mut written = 0;
478        write_all(&mut self.file, bytes, &mut written, pulse)
479    }
480    fn replay(
481        &mut self,
482        total: u64,
483        storage: &StorageBudget,
484        writer: &mut impl Write,
485        emitted: &mut u64,
486        boundary: &mut Stage,
487        pulse: &dyn zencodec::Pulse,
488    ) -> Result<(), Cause> {
489        *boundary = Stage::Replay;
490        check(pulse)?;
491        self.file.flush()?;
492        self.file.seek(SeekFrom::Start(self.origin))?;
493        let size = total.min(self.replay_bytes.get() as u64) as usize;
494        let mut buffer = storage.bytes(size)?;
495        let mut remaining = total;
496        while remaining != 0 {
497            *boundary = Stage::Replay;
498            check(pulse)?;
499            let n = remaining.min(size as u64) as usize;
500            self.file.read_exact(&mut buffer.as_mut_slice()[..n])?;
501            check(pulse)?;
502            *boundary = Stage::Write;
503            write_all(writer, &buffer.as_slice()[..n], emitted, pulse)?;
504            remaining -= n as u64;
505        }
506        Ok(())
507    }
508}
509fn stage(
510    bytes: &[u8],
511    storage: &StorageBudget,
512    packets: &mut VecDeque<OwnedStorage<u8>>,
513) -> Result<(), Cause> {
514    if !bytes.is_empty() {
515        packets.try_reserve(1)?;
516        let mut owned = storage.bytes(bytes.len())?;
517        owned.as_mut_slice().copy_from_slice(bytes);
518        packets.push_back(owned);
519    }
520    Ok(())
521}
522/// Encode a retained fixed-interpretation source through a prepared schedule.
523///
524/// Default policy chooses finish-first. No row-level refusal falls back to
525/// whole-image allocation. Policy runs once before reading source rows or
526/// publishing the header. The encoder, source, storage and writer stay retained
527/// across that decision. Unknown total wait retention is reported honestly.
528///
529/// Finish-first admits compressed chunks as they appear and drops source,
530/// row buffer and encoder before destination writes. Source I/O and explicit
531/// staging can still wait while native state is alive. Live mode pauses source
532/// reads during synchronous writes; this alone proves no native memory bound.
533/// Cancellation is checked around native calls and each writer operation; it
534/// cannot interrupt a blocking OS write. No retries or implicit finish on drop.
535/// `None` adds no per-call control. Controls retained by the encoder or source
536/// and all configured policy and resource requirements still apply.
537pub fn encode<E: Encoder, S: Source, W: Write, R: OperationPolicy>(
538    encoder: E,
539    source: S,
540    writer: W,
541    resolver: R,
542    options: Options,
543    pulse: Option<&dyn zencodec::Pulse>,
544) -> Result<Completed, At<Failure>> {
545    encode_to_destination(encoder, source, Forward(writer), resolver, options, pulse)
546}
547
548/// Encode to an actual forward or seek-back destination. Layout admission occurs
549/// before policy, source reads and output publication. Patches are fully checked
550/// before final tail writes. I/O failure can leave partial overwrites; their
551/// confirmed byte count is distinct from appended extent. No automatic spooling
552/// converts a forward destination into a seekable one.
553pub fn encode_to_destination<E: Encoder, S: Source, D: Destination, R: OperationPolicy>(
554    encoder: E,
555    source: S,
556    writer: D,
557    resolver: R,
558    options: Options,
559    pulse: Option<&dyn zencodec::Pulse>,
560) -> Result<Completed, At<Failure>> {
561    run_rows(
562        LegacyStartup(encoder),
563        source,
564        writer,
565        resolver,
566        options,
567        pulse,
568    )
569}
570
571/// Encode a configured job to a writer, admitting its plan before native startup.
572///
573/// Unlike the raw-encoder adapter, this retains the lightweight job across policy
574/// and supplies current color before header creation. `Rows` pushes strips;
575/// `Replay` fills native-owned strips. Unsupported preflight and whole-raster
576/// input refuse before encoder construction; there is no fallback.
577/// Pass a `PolicySet` or `BoundPolicy` for mandatory requirements. Evidence
578/// unavailable from the generic job stays unknown, including metadata/fidelity.
579/// The complete checked request-to-destination convenience is a separate layer.
580pub fn encode_job<J: EncodeJob, S: Source, W: Write, R: OperationPolicy>(
581    job: J,
582    source: S,
583    writer: W,
584    resolver: R,
585    options: Options,
586    pulse: Option<&dyn zencodec::Pulse>,
587) -> Result<Completed, At<Failure>>
588where
589    J::Enc: Encoder,
590{
591    encode_job_to_destination(job, source, Forward(writer), resolver, options, pulse)
592}
593
594/// Execute a retained job's declared row route against actual destination access.
595///
596/// Sequence: admit preparation workspace, plan without native startup, admit
597/// the route policy, revalidate the retained
598/// source/destination, reserve driver rows, construct once, begin with current
599/// color, compare the complete plan, then read/publish. A refusal or startup
600/// failure never retries another route. Strict unknown workspace/wait evidence
601/// refuses; byte timing alone never establishes either bound.
602///
603/// This is an adapter primitive for an already configured job. It does not
604/// select registry configurations or certify descriptive metadata treatment.
605pub fn encode_job_to_destination<J: EncodeJob, S: Source, D: Destination, R: OperationPolicy>(
606    job: J,
607    source: S,
608    writer: D,
609    resolver: R,
610    options: Options,
611    pulse: Option<&dyn zencodec::Pulse>,
612) -> Result<Completed, At<Failure>>
613where
614    J::Enc: Encoder,
615{
616    planned::run(
617        PlannedStartup(job),
618        source,
619        writer,
620        resolver,
621        options,
622        pulse,
623    )
624}
625
626/// Run an already configured registry job through the planned row driver.
627///
628/// This is the erased-job counterpart of [`encode_job`]. The retained job plans
629/// before policy and constructs its encoder only after admission. Native errors
630/// keep their concrete causes; no typed-job shim or policy error conversion is
631/// required from the caller. Job evidence unavailable from these generic traits
632/// stays unknown; strict metadata, range and workspace requirements refuse.
633pub fn encode_dyn_job<S: Source, W: Write, R: OperationPolicy>(
634    job: Box<dyn DynEncodeJob>,
635    source: S,
636    writer: W,
637    resolver: R,
638    options: Options,
639    pulse: Option<&dyn zencodec::Pulse>,
640) -> Result<Completed, At<Failure>> {
641    encode_dyn_job_to_destination(job, source, Forward(writer), resolver, options, pulse)
642}
643
644/// Run a retained erased job against a forward or seek-back destination.
645///
646/// Admission, source revalidation, declared startup, output limits, staging and
647/// partial-publication reporting are identical to [`encode_job_to_destination`].
648/// Only construction and native calls use the existing object-safe codec traits.
649pub fn encode_dyn_job_to_destination<S: Source, D: Destination, R: OperationPolicy>(
650    job: Box<dyn DynEncodeJob>,
651    source: S,
652    writer: D,
653    resolver: R,
654    options: Options,
655    pulse: Option<&dyn zencodec::Pulse>,
656) -> Result<Completed, At<Failure>> {
657    planned::run(
658        DynPlannedStartup(job),
659        source,
660        writer,
661        resolver,
662        options,
663        pulse,
664    )
665}
666
667fn validate_row_plan(
668    plan: EncodePlan,
669    declaration: &EncodeDeclaration<'_>,
670) -> Result<EncodePlan, Cause> {
671    plan.validate(declaration)?;
672    if matches!(plan.input, InputRequirement::WholeRaster) {
673        return Err(EncodePlanError::InputProtocol.into());
674    }
675    Ok(plan)
676}
677
678trait RowStartup {
679    type Enc: Encoder;
680    // Preserve legacy allocation/start ordering on the existing raw adapter.
681    const EARLY_BUFFER: bool;
682    fn plan(&mut self, declaration: &EncodeDeclaration<'_>) -> Result<EncodePlan, Cause>;
683    fn start(
684        self,
685        declaration: &EncodeDeclaration<'_>,
686        plan: &EncodePlan,
687    ) -> Result<Self::Enc, Cause>;
688}
689struct LegacyStartup<E>(E);
690impl<E: Encoder> RowStartup for LegacyStartup<E> {
691    type Enc = E;
692    const EARLY_BUFFER: bool = true;
693    fn plan(&mut self, declaration: &EncodeDeclaration<'_>) -> Result<EncodePlan, Cause> {
694        let timing = self.0.begin_output(
695            declaration.width,
696            declaration.height,
697            declaration.descriptor,
698            declaration.access,
699        )?;
700        Ok(EncodePlan::rows(timing))
701    }
702    fn start(self, _: &EncodeDeclaration<'_>, _: &EncodePlan) -> Result<E, Cause> {
703        Ok(self.0)
704    }
705}
706struct PlannedStartup<J>(J);
707impl<J: EncodeJob> RowStartup for PlannedStartup<J>
708where
709    J::Enc: Encoder,
710{
711    type Enc = J::Enc;
712    const EARLY_BUFFER: bool = false;
713    fn plan(&mut self, declaration: &EncodeDeclaration<'_>) -> Result<EncodePlan, Cause> {
714        validate_row_plan(self.0.plan_encode(declaration)?, declaration)
715    }
716    fn start(
717        self,
718        declaration: &EncodeDeclaration<'_>,
719        plan: &EncodePlan,
720    ) -> Result<Self::Enc, Cause> {
721        let mut encoder = self.0.encoder()?;
722        let observed = encoder.begin_declared(declaration)?;
723        plan.validate_started(&observed)?;
724        Ok(encoder)
725    }
726}
727
728struct DynPlannedStartup(Box<dyn DynEncodeJob>);
729impl RowStartup for DynPlannedStartup {
730    type Enc = DynRows;
731    const EARLY_BUFFER: bool = false;
732    fn plan(&mut self, declaration: &EncodeDeclaration<'_>) -> Result<EncodePlan, Cause> {
733        validate_row_plan(self.0.plan_encode(declaration)?, declaration)
734    }
735    fn start(
736        self,
737        declaration: &EncodeDeclaration<'_>,
738        plan: &EncodePlan,
739    ) -> Result<Self::Enc, Cause> {
740        let mut encoder = self.0.into_encoder()?;
741        let observed = encoder.begin_declared(declaration)?;
742        plan.validate_started(&observed)?;
743        Ok(DynRows(encoder))
744    }
745}
746
747// Private, already-started executor for the common loop. It does not pretend to
748// be a typed EncodeJob: that trait's checked setters have narrower error types.
749struct DynRows(Box<dyn DynEncoder>);
750impl Encoder for DynRows {
751    type Error = zencodec::decode::ErasedError;
752    fn reject(operation: zencodec::UnsupportedOperation) -> Self::Error {
753        Self::Error::new(Box::new(operation))
754    }
755    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error> {
756        self.0.encode(pixels).map_err(Self::Error::new)
757    }
758    fn pending_output(&self) -> &[u8] {
759        self.0.pending_output()
760    }
761    fn consume_output(&mut self, n: usize) {
762        self.0.consume_output(n);
763    }
764    fn push_rows(&mut self, pixels: PixelSlice<'_>) -> Result<(), Self::Error> {
765        self.0.push_rows(pixels).map_err(Self::Error::new)
766    }
767    fn pull_rows(
768        &mut self,
769        rows: &mut dyn zencodec::encode::EncodeRows,
770        pulse: Option<&dyn zencodec::Pulse>,
771    ) -> Result<zencodec::encode::PullProgress, Self::Error> {
772        self.0.pull_rows(rows, pulse).map_err(Self::Error::new)
773    }
774    fn finish(self) -> Result<EncodeOutput, Self::Error> {
775        self.0.finish().map_err(Self::Error::new)
776    }
777    fn finish_output(
778        self,
779    ) -> Result<zencodec::output_patch::OutputFinish<EncodeOutput>, Self::Error> {
780        self.0.finish_output().map_err(Self::Error::new)
781    }
782}
783
784fn run_rows<P: RowStartup, S: Source, D: Destination, R: OperationPolicy>(
785    mut startup: P,
786    mut source: S,
787    mut writer: D,
788    mut resolver: R,
789    mut options: Options,
790    pulse: Option<&dyn zencodec::Pulse>,
791) -> Result<Completed, At<Failure>> {
792    let control = pulse.unwrap_or(&zencodec::NoPulse);
793    let mut boundary = Stage::Prepare;
794    let mut emitted = 0;
795    let mut patched_bytes = 0;
796    let mut report = OperationReport::default();
797    let bound_requirements = resolver.requirements();
798    let entry_requirements = options.preset;
799    let run = || -> Result<OutputSchedule, Cause> {
800        check(control)?;
801        // This adapter has no qualified native preparation/workspace bound.
802        // Refuse before asking the job to prepare any state. These checks do
803        // not discharge the unchanged requirements at final route selection.
804        boundary = Stage::Policy;
805        entry_requirements.check_workspace(None)?;
806        if let Some(required) = bound_requirements {
807            required.check_workspace(None)?;
808        }
809        boundary = Stage::Prepare;
810        let desc = source.description();
811        let mechanism = source.mechanism();
812        if desc.width == 0 || desc.height == 0 {
813            return Err(Refusal::Empty.into());
814        }
815        let stride = (desc.width as usize)
816            .checked_mul(desc.descriptor.bytes_per_pixel())
817            .ok_or(Refusal::Overflow)?;
818        let rows = options.batch_rows.get().min(desc.height);
819        let len = stride.checked_mul(rows as usize).ok_or(Refusal::Overflow)?;
820        let early_buffer = if P::EARLY_BUFFER {
821            Some(options.storage.bytes(len)?)
822        } else {
823            None
824        };
825        let access = if writer.can_patch() {
826            OutputAccess::SeekBack
827        } else {
828            OutputAccess::Forward
829        };
830        let mut declaration =
831            EncodeDeclaration::new(desc.width, desc.height, desc.descriptor).with_access(access);
832        if let Some(color) = &desc.color_context {
833            declaration = declaration.with_color(color);
834        }
835        if let Some(exact) = desc.exact_depth {
836            declaration = declaration.with_exact_depth(exact);
837        }
838        // Includes the legacy path, whose plan() may start native headers.
839        // This validates structure only; planning must not read source samples.
840        declaration.validate_exact_depth()?;
841        let plan = startup.plan(&declaration)?;
842        let timing = plan.timing;
843        if timing.patches_at_finish() && !writer.can_patch() {
844            return Err(Refusal::Patches.into());
845        }
846        check(control)?;
847        let mut finish_first = OutputScheduleOffer::new(OutputSchedule::FinishFirst);
848        finish_first.staging = Some(StagingFacts::new(
849            if options.disk.is_some() {
850                StagingKind::Disk
851            } else {
852                StagingKind::Memory
853            },
854            0,
855            if options.disk.is_some() {
856                options.max_output_bytes
857            } else {
858                options.storage.max_bytes() as u64
859            },
860        ));
861        let offers = [finish_first, OutputScheduleOffer::new(OutputSchedule::Live)];
862        let offers = &offers[..if timing.streams() { 2 } else { 1 }];
863        boundary = Stage::Policy;
864        let (index, record) = match resolve_output_schedule(
865            OutputScheduleFacts::new(timing, mechanism),
866            offers,
867            &mut resolver,
868            options.preset,
869            pulse,
870        ) {
871            Ok(selected) => selected,
872            Err(error) => {
873                report.record(error.error().record());
874                return Err(error.into());
875            }
876        };
877        report.record(record);
878        let schedule = offers[index].schedule;
879        check(control)?;
880        boundary = Stage::Read;
881        validate_source(&source, &desc, mechanism)?;
882        let current_access = if writer.can_patch() {
883            OutputAccess::SeekBack
884        } else {
885            OutputAccess::Forward
886        };
887        if current_access != access {
888            return Err(Refusal::DestinationChanged.into());
889        }
890        boundary = Stage::Prepare;
891        let mut buffer = match early_buffer {
892            Some(buffer) => buffer,
893            None => options.storage.bytes(len)?,
894        };
895        check(control)?;
896        boundary = Stage::Start;
897        let mut encoder = startup.start(&declaration, &plan)?;
898        // Exact carrier evidence can own ICC bytes. Release it before final
899        // destination waits; the executing encoder owns its admitted metadata.
900        drop(plan);
901        let mut staged = VecDeque::new();
902        let mut produced = 0;
903        let mut y = 0;
904        loop {
905            check(control)?;
906            boundary = Stage::Read;
907            validate_source(&source, &desc, mechanism)?;
908            // The final pending bytes belong to finish. Do not introduce an
909            // avoidable writer wait just before releasing native/source state.
910            if y == desc.height {
911                break;
912            }
913            boundary = Stage::Encode;
914            let pending = encoder.pending_output();
915            count(&mut produced, pending.len(), options.max_output_bytes)?;
916            if schedule == OutputSchedule::Live {
917                boundary = Stage::Write;
918                write_all(&mut writer, pending, &mut emitted, control)?;
919            } else {
920                boundary = Stage::Encode;
921                if let Some(disk) = &mut options.disk {
922                    disk.origin.checked_add(produced).ok_or(Refusal::Overflow)?;
923                    disk.append(pending, control)?;
924                } else {
925                    stage(pending, &options.storage, &mut staged)?;
926                }
927            }
928            encoder.consume_output(pending.len());
929            let count = rows.min(desc.height - y);
930            let size = stride
931                .checked_mul(count as usize)
932                .ok_or(Refusal::Overflow)?;
933            boundary = Stage::Read;
934            validate_source(&source, &desc, mechanism)?;
935            let mut destination = PixelSliceMut::new(
936                &mut buffer.as_mut_slice()[..size],
937                desc.width,
938                count,
939                stride,
940                desc.descriptor,
941            )?;
942            if let Some(context) = &desc.color_context {
943                destination = destination.with_color_context(context.clone());
944            }
945            source.read_rows(y, destination, pulse)?;
946            check(control)?;
947            validate_source(&source, &desc, mechanism)?;
948            boundary = Stage::Encode;
949            let mut pixels = PixelSlice::new(
950                &buffer.as_slice()[..size],
951                desc.width,
952                count,
953                stride,
954                desc.descriptor,
955            )?;
956            if let Some(exact) = desc.exact_depth {
957                // Check untagged rows written through the mutable source view.
958                // Per-row checks retain the first contradictory absolute row;
959                // nothing in this strip reaches the encoder until all pass.
960                for row in 0..pixels.rows() {
961                    check(control)?;
962                    if !exact.holds_for(&pixels.sub_rows(row, 1))? {
963                        return Err(ExactDepthContradiction {
964                            first_row: y + row,
965                            claim: exact,
966                        }
967                        .into());
968                    }
969                }
970                // A Pulse callback during verification can also mutate a
971                // source declaration. Keep that mutation from reaching push.
972                boundary = Stage::Read;
973                validate_source(&source, &desc, mechanism)?;
974                boundary = Stage::Encode;
975                pixels = pixels.with_exact_depth(exact)?;
976            }
977            // Grid verification needs no color context. Attach it only after
978            // scanning so its per-row borrowed views need no Arc clone/drop.
979            if let Some(context) = &desc.color_context {
980                pixels = pixels.with_color_context(context.clone());
981            }
982            encoder.push_rows(pixels)?;
983            y += count;
984        }
985        drop(source);
986        drop(buffer);
987        drop(desc);
988        boundary = Stage::Finish;
989        check(control)?;
990        let finished = encoder.finish_output()?;
991        check(control)?;
992        finished.validate(produced)?;
993        if !finished.patches.is_empty() && (!writer.can_patch() || !timing.patches_at_finish()) {
994            return Err(Refusal::Patches.into());
995        }
996        let patches = finished.patches;
997        let rest = finished.tail;
998        count(&mut produced, rest.len(), options.max_output_bytes)?;
999        if schedule == OutputSchedule::FinishFirst {
1000            if let Some(disk) = &mut options.disk {
1001                disk.origin.checked_add(produced).ok_or(Refusal::Overflow)?;
1002                disk.append(rest.data(), control)?;
1003                drop(rest);
1004                disk.replay(
1005                    produced,
1006                    &options.storage,
1007                    &mut writer,
1008                    &mut emitted,
1009                    &mut boundary,
1010                    control,
1011                )?;
1012            } else {
1013                stage(rest.data(), &options.storage, &mut staged)?;
1014                drop(rest);
1015                boundary = Stage::Write;
1016                while let Some(bytes) = staged.pop_front() {
1017                    write_all(&mut writer, bytes.as_slice(), &mut emitted, control)?;
1018                }
1019            }
1020        } else {
1021            boundary = Stage::Write;
1022            write_all(&mut writer, rest.data(), &mut emitted, control)?;
1023        }
1024        check(control)?;
1025        if !patches.is_empty() {
1026            boundary = Stage::Patch;
1027            for patch in patches {
1028                check(control)?;
1029                writer.position(patch.offset)?;
1030                write_all(&mut writer, &patch.bytes, &mut patched_bytes, control)?;
1031            }
1032            check(control)?;
1033            writer.position(produced)?;
1034        }
1035        boundary = Stage::Write;
1036        check(control)?;
1037        writer.flush()?;
1038        check(control)?;
1039        Ok(schedule)
1040    };
1041    match run() {
1042        Ok(schedule) => Ok(Completed {
1043            emitted,
1044            patched_bytes,
1045            schedule,
1046            report,
1047        }),
1048        Err(cause) => Err(At::wrap(Failure {
1049            stage: boundary,
1050            emitted,
1051            patched_bytes,
1052            report: Box::new(report),
1053            cause,
1054        })),
1055    }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use super::*;
1061    #[test]
1062    fn raster_honors_both_strides_and_refuses_context_mismatch_before_copy() {
1063        let input = [1, 2, 3, 99, 98, 97, 4, 5, 6];
1064        let context = Arc::new(ColorContext::from_cicp(zencodec::Cicp::SRGB));
1065        let pixels = PixelSlice::new(&input, 1, 2, 6, PixelDescriptor::RGB8_SRGB)
1066            .unwrap()
1067            .with_color_context(context.clone());
1068        let mut source = Raster::new(pixels);
1069        let mut output = [77; 12];
1070        let view = PixelSliceMut::new(&mut output, 1, 2, 9, PixelDescriptor::RGB8_SRGB).unwrap();
1071        assert!(
1072            source
1073                .read_rows(0, view, Some(&zencodec::how_far::NoPulse))
1074                .is_err()
1075        );
1076        assert_eq!(output, [77; 12]);
1077        let view = PixelSliceMut::new(&mut output, 1, 2, 9, PixelDescriptor::RGB8_SRGB)
1078            .unwrap()
1079            .with_color_context(context);
1080        source
1081            .read_rows(0, view, Some(&zencodec::how_far::NoPulse))
1082            .unwrap();
1083        assert_eq!(output, [1, 2, 3, 77, 77, 77, 77, 77, 77, 4, 5, 6]);
1084    }
1085}
1086
1087#[cfg(test)]
1088mod patch_tests {
1089    use super::*;
1090    use crate::destination::SeekBack;
1091    use zencodec::{
1092        encode::{EncodeOutput, OutputTiming},
1093        output_patch::{OutputFinish, Patch, PatchError},
1094    };
1095    struct Patching {
1096        pending: bool,
1097        bad: bool,
1098    }
1099    impl Encoder for Patching {
1100        type Error = io::Error;
1101        fn reject(op: zencodec::UnsupportedOperation) -> Self::Error {
1102            io::Error::other(op)
1103        }
1104        fn encode(self, _: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error> {
1105            Err(io::Error::other("negotiated path only"))
1106        }
1107        fn begin_output(
1108            &mut self,
1109            _: u32,
1110            _: u32,
1111            _: PixelDescriptor,
1112            _: OutputAccess,
1113        ) -> Result<OutputTiming, Self::Error> {
1114            self.pending = true;
1115            Ok(OutputTiming::streaming(0).with_patches())
1116        }
1117        fn pending_output(&self) -> &[u8] {
1118            if self.pending { &[0, 0] } else { &[] }
1119        }
1120        fn consume_output(&mut self, _: usize) {
1121            self.pending = false;
1122        }
1123        fn push_rows(&mut self, _: PixelSlice<'_>) -> Result<(), Self::Error> {
1124            Ok(())
1125        }
1126        fn finish_output(self) -> Result<OutputFinish<EncodeOutput>, Self::Error> {
1127            Ok(OutputFinish::new(
1128                EncodeOutput::new(alloc::vec![3], zencodec::ImageFormat::Unknown),
1129                alloc::vec![Patch {
1130                    offset: if self.bad { u64::MAX } else { 0 },
1131                    bytes: alloc::vec![1, 2],
1132                }],
1133            ))
1134        }
1135    }
1136    struct Short {
1137        cursor: io::Cursor<alloc::vec::Vec<u8>>,
1138        patch: bool,
1139        fail: bool,
1140    }
1141    impl Write for Short {
1142        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1143            if self.patch && self.fail && self.cursor.position() == 7 {
1144                return Err(io::ErrorKind::BrokenPipe.into());
1145            }
1146            self.cursor.write(&bytes[..bytes.len().min(1)])
1147        }
1148        fn flush(&mut self) -> io::Result<()> {
1149            Ok(())
1150        }
1151    }
1152    impl Seek for Short {
1153        fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
1154            if matches!(from, SeekFrom::Start(6)) {
1155                self.patch = true;
1156            }
1157            self.cursor.seek(from)
1158        }
1159    }
1160    fn source() -> Raster<'static> {
1161        Raster::new(PixelSlice::new(&[7, 8, 9], 1, 1, 3, PixelDescriptor::RGB8_SRGB).unwrap())
1162    }
1163    fn options() -> Options {
1164        Options::new(StorageBudget::new(1024, 16), 1024)
1165    }
1166    #[test]
1167    fn cancellation_during_patch_or_restore_prevents_later_effects() {
1168        use core::sync::atomic::{AtomicBool, Ordering::Relaxed};
1169        use zencodec::how_far::{Child, Execution, PhaseSpec, PlanError, Report};
1170        struct Cancel(AtomicBool);
1171        impl zencodec::enough::Stop for Cancel {
1172            fn check(&self) -> Result<(), zencodec::enough::StopReason> {
1173                if self.0.load(Relaxed) {
1174                    Err(zencodec::enough::StopReason::Cancelled)
1175                } else {
1176                    Ok(())
1177                }
1178            }
1179        }
1180        impl Report for Cancel {
1181            fn advance(&self, _: u64) {}
1182        }
1183        impl zencodec::Pulse for Cancel {
1184            fn split(
1185                &self,
1186                _: Execution,
1187                _: &[PhaseSpec<'_>],
1188            ) -> Result<alloc::vec::Vec<Child<'_>>, PlanError> {
1189                Err(PlanError::Unsupported)
1190            }
1191        }
1192        struct Writer<'a> {
1193            cursor: io::Cursor<alloc::vec::Vec<u8>>,
1194            pulse: &'a Cancel,
1195            positions: usize,
1196            flushes: usize,
1197            cancel_on_patch: bool,
1198            cancel_on_restore: bool,
1199        }
1200        impl Write for Writer<'_> {
1201            fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1202                let n = self.cursor.write(bytes)?;
1203                if self.positions == 2 && self.cancel_on_patch {
1204                    self.pulse.0.store(true, Relaxed);
1205                }
1206                Ok(n)
1207            }
1208            fn flush(&mut self) -> io::Result<()> {
1209                self.flushes += 1;
1210                Ok(())
1211            }
1212        }
1213        impl Seek for Writer<'_> {
1214            fn seek(&mut self, at: SeekFrom) -> io::Result<u64> {
1215                self.positions += 1;
1216                let pos = self.cursor.seek(at)?;
1217                if self.positions == 3 && self.cancel_on_restore {
1218                    self.pulse.0.store(true, Relaxed);
1219                }
1220                Ok(pos)
1221            }
1222        }
1223        for schedule in [OutputSchedule::FinishFirst, OutputSchedule::Live] {
1224            for mode in 0..3 {
1225                let pulse = Cancel(AtomicBool::new(false));
1226                let mut writer = Writer {
1227                    cursor: io::Cursor::new(alloc::vec::Vec::new()),
1228                    pulse: &pulse,
1229                    positions: 0,
1230                    flushes: 0,
1231                    cancel_on_patch: mode == 0,
1232                    cancel_on_restore: mode == 1,
1233                };
1234                let result = encode_to_destination(
1235                    Patching {
1236                        pending: false,
1237                        bad: false,
1238                    },
1239                    source(),
1240                    SeekBack::new(&mut writer).unwrap(),
1241                    PolicySet::new().require_output_schedule(schedule),
1242                    options(),
1243                    Some(&pulse),
1244                );
1245                assert_eq!(writer.cursor.get_ref(), &[1, 2, 3]);
1246                if mode == 2 {
1247                    let completed = result.unwrap();
1248                    assert_eq!((completed.emitted, completed.patched_bytes), (3, 2));
1249                    assert_eq!((writer.positions, writer.flushes), (3, 1));
1250                } else {
1251                    let error = result.unwrap_err();
1252                    assert_eq!(
1253                        zencodec::find_cause::<zencodec::CodecError>(&error)
1254                            .unwrap()
1255                            .category(),
1256                        zencodec::ErrorCategory::Stopped(zencodec::enough::StopReason::Cancelled)
1257                    );
1258                    assert_eq!((error.error().emitted, error.error().patched_bytes), (3, 2));
1259                    assert_eq!(writer.flushes, 0);
1260                    assert_eq!(writer.positions, if mode == 0 { 2 } else { 3 });
1261                }
1262            }
1263        }
1264    }
1265    #[test]
1266    fn incompatible_destination_refuses_before_policy_or_any_output() {
1267        let mut bytes = alloc::vec::Vec::new();
1268        let error =
1269            encode(
1270                Patching {
1271                    pending: false,
1272                    bad: false,
1273                },
1274                source(),
1275                &mut bytes,
1276                |_: zencodec::operation::Question<'_>| -> Result<
1277                    zencodec::operation::Decision,
1278                    zencodec::At<core::convert::Infallible>,
1279                > { panic!("must refuse before policy") },
1280                options(),
1281                Some(&zencodec::how_far::NoPulse),
1282            )
1283            .unwrap_err();
1284        assert!(bytes.is_empty());
1285        assert_eq!(
1286            zencodec::find_cause::<Refusal>(&error),
1287            Some(&Refusal::Patches)
1288        );
1289        assert_eq!(error.error().emitted, 0);
1290    }
1291    #[test]
1292    fn patches_preflight_all_ranges_and_report_partial_overwrites_separately() {
1293        for schedule in [OutputSchedule::FinishFirst, OutputSchedule::Live] {
1294            for (bad, fail) in [(false, false), (true, false), (false, true)] {
1295                let mut writer = Short {
1296                    cursor: io::Cursor::new(b"prefix".to_vec()),
1297                    patch: false,
1298                    fail,
1299                };
1300                writer.cursor.set_position(6);
1301                let result = encode_to_destination(
1302                    Patching {
1303                        pending: false,
1304                        bad,
1305                    },
1306                    source(),
1307                    SeekBack::new(&mut writer).unwrap(),
1308                    PolicySet::new().require_output_schedule(schedule),
1309                    options(),
1310                    Some(&zencodec::how_far::NoPulse),
1311                );
1312                assert_eq!(&writer.cursor.get_ref()[..6], b"prefix");
1313                if bad {
1314                    let error = result.unwrap_err();
1315                    assert_eq!(
1316                        zencodec::find_cause::<PatchError>(&error),
1317                        Some(&PatchError::OutOfRange)
1318                    );
1319                    let prior = if schedule == OutputSchedule::Live {
1320                        2
1321                    } else {
1322                        0
1323                    };
1324                    assert_eq!(error.error().emitted, prior);
1325                    assert_eq!(writer.cursor.get_ref().len(), 6 + prior as usize);
1326                    assert_eq!(error.error().patched_bytes, 0);
1327                } else if fail {
1328                    let error = result.unwrap_err();
1329                    assert_eq!(error.error().stage, Stage::Patch);
1330                    assert_eq!(error.error().emitted, 3);
1331                    assert_eq!(error.error().patched_bytes, 1);
1332                    assert_eq!(
1333                        zencodec::find_cause::<io::Error>(&error).unwrap().kind(),
1334                        io::ErrorKind::BrokenPipe
1335                    );
1336                    assert_eq!(&writer.cursor.get_ref()[6..], &[1, 0, 3]);
1337                } else {
1338                    let completed = result.unwrap();
1339                    assert_eq!(completed.emitted, 3);
1340                    assert_eq!(completed.patched_bytes, 2);
1341                    assert_eq!(writer.cursor.position(), 9);
1342                    assert_eq!(&writer.cursor.get_ref()[6..], &[1, 2, 3]);
1343                }
1344            }
1345        }
1346    }
1347}