qubit-codec 0.17.4

Core codec traits and buffer conversion primitives for Rust
Documentation
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Policy hooks used by buffered decoder engines.

use core::num::NonZeroUsize;

use super::decode_context::DecodeContext;
use super::decode_incomplete_action::DecodeIncompleteAction;
use super::decode_invalid_action::DecodeInvalidAction;
use crate::CapacityError;
use crate::Codec;
use crate::TranscodeDecodeErrorOf;

/// Policy hooks for [`crate::engine::TranscodeDecodeEngine`].
///
/// Hooks own policy state, such as malformed-input replacement behavior. The
/// engine passes the codec into hook methods when policy code needs codec
/// metadata.
///
/// Implement this trait when a buffered decoder needs policy decisions after
/// the low-level codec reports an error. The engine handles input/output cursor
/// bookkeeping, output-capacity checks, and successful one-value decodes; hooks
/// decide whether invalid or EOF-incomplete input should be skipped, replaced,
/// or returned as an error.
///
/// The hook receives a [`DecodeContext`] with absolute input/output cursors, so
/// errors can include useful positions without duplicating engine arithmetic.
/// Stateful hooks may also use [`finish_hooks`](Self::finish_hooks) to emit
/// final values after the caller has supplied all input and completed EOF
/// decoding.
///
/// # Examples
///
/// This hook replaces malformed units with `b'?'` and otherwise lets the engine
/// keep decoding. At EOF, implementations may also override
/// [`Self::handle_incomplete_decode`] to repair or discard a trailing partial
/// value.
///
/// ```rust
/// use core::convert::Infallible;
/// use core::num::NonZeroUsize;
///
/// use qubit_codec::Codec;
/// use qubit_codec::DecodeFailure;
/// use qubit_codec::TranscodeDecodeError;
/// use qubit_codec::engine::DecodeContext;
/// use qubit_codec::engine::DecodeInvalidAction;
/// use qubit_codec::engine::TranscodeDecodeHooks;
///
/// #[derive(Clone, Copy)]
/// struct MyCodec;
///
/// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// enum MyDecodeError {
///     Malformed { consumed: NonZeroUsize },
/// }
///
/// impl Codec for MyCodec {
///     type Value = u8;
///     type Unit = u8;
///     type DecodeError = MyDecodeError;
///     type EncodeError = Infallible;
///
///     const MIN_UNITS_PER_VALUE: usize = 1;
///     const MAX_ENCODE_UNITS_PER_VALUE: usize = 1;
///     const MAX_DECODE_UNITS_PER_VALUE: usize = 1;
///
///     unsafe fn decode(
///         &mut self,
///         input: &[u8],
///         index: usize,
///     ) -> Result<(u8, NonZeroUsize), DecodeFailure<Self::DecodeError>> {
///         match input[index] {
///             0xff => Err(DecodeFailure::invalid(
///                 MyDecodeError::Malformed {
///                     consumed: NonZeroUsize::MIN,
///                 },
///                 NonZeroUsize::MIN,
///             )),
///             value => Ok((value, NonZeroUsize::MIN)),
///         }
///     }
///
///     unsafe fn encode(
///         &mut self,
///         value: &u8,
///         output: &mut [u8],
///         index: usize,
///     ) -> Result<usize, Self::EncodeError> {
///         output[index] = *value;
///         Ok(1)
///     }
/// }
///
/// #[derive(Default)]
/// struct ReplacementHooks;
///
/// impl TranscodeDecodeHooks<MyCodec> for ReplacementHooks {
///     fn handle_invalid_decode(
///         &mut self,
///         _codec: &mut MyCodec,
///         error: &MyDecodeError,
///         consumed: Option<NonZeroUsize>,
///         _context: DecodeContext,
///     ) -> Result<DecodeInvalidAction<u8>, TranscodeDecodeError<MyDecodeError>> {
///         match error {
///             MyDecodeError::Malformed { .. } => {
///                 Ok(DecodeInvalidAction::Emit {
///                     value: b'?',
///                     consumed: consumed.expect("codec reported malformed width"),
///                 })
///             }
///         }
///     }
/// }
///
/// let mut hooks = ReplacementHooks::default();
/// let mut codec = MyCodec;
/// let context = DecodeContext::new(0, 0, 0, 0, 1);
/// let error = MyDecodeError::Malformed { consumed: NonZeroUsize::MIN };
/// let action = hooks.handle_invalid_decode(
///     &mut codec, &error, Some(NonZeroUsize::MIN), context,
/// ).expect("replacement policy accepts malformed input");
/// assert!(matches!(action, DecodeInvalidAction::Emit { value: b'?', consumed }
///     if consumed == NonZeroUsize::MIN));
/// ```
///
/// # Type Parameters
///
/// - `C`: Low-level codec owned by the engine.
pub trait TranscodeDecodeHooks<C>
where
    C: Codec,
{
    /// Returns an upper bound for decoded values produced from `input_len`
    /// units.
    ///
    /// This bound covers the streaming decode phase, including conservative
    /// capacity preflight in streaming and EOF operations, as required by
    /// [`crate::Transcoder::max_transcode_output_len`]. It describes required
    /// writable capacity, not only values actually emitted. It must not include
    /// codec reset values, codec finish values, or hook finish output.
    /// Streaming actions can emit at most one value for the current input
    /// event or skip it; an emitted replacement consumes a non-zero number of
    /// source units. The hook API has no separate operation for draining
    /// hook-owned output without processing input. Use a custom
    /// [`crate::Transcoder`] for streaming behavior beyond this action model,
    /// such as one-to-many output or draining output without new input. The
    /// bound may depend on immutable hook and codec configuration, but must
    /// cover every reachable transient hook and codec state.
    ///
    /// The bound must cover the preflight required by
    /// [`handle_invalid_decode`](Self::handle_invalid_decode) and
    /// [`handle_incomplete_decode`](Self::handle_incomplete_decode), even when
    /// the chosen action skips input and emits no value. Zero actual output
    /// does not imply zero required writable capacity. See the
    /// [trait example](#examples) for the hook action model.
    ///
    /// The default implementation returns `input_len`, which remains
    /// conservative when a replacement policy consumes fewer units than a
    /// valid codec value. Override it when codec or hook policy can produce a
    /// larger streaming output bound than this default.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `input_len`: Number of source units the caller plans to decode.
    ///
    /// # Returns
    ///
    /// Returns a conservative upper bound for streaming output.
    ///
    /// # Errors
    ///
    /// Returns [`CapacityError::OutputLengthOverflow`] when the bound cannot
    /// be represented as `usize`.
    #[inline]
    fn max_transcode_output_len(&self, _codec: &C, input_len: usize) -> Result<usize, CapacityError> {
        Ok(input_len)
    }

    /// Returns an upper bound for values emitted by finishing hook-owned state.
    ///
    /// `finish` never receives more input. Implementations must report the
    /// maximum output that could be derived from hook-owned state in any
    /// reachable transient stream state, not only what happens to remain when
    /// this method is called.
    ///
    /// The default implementation returns `0`. Override it when
    /// [`finish_hooks`](Self::finish_hooks) can write trailers, delayed
    /// replacements, or any other hook-owned final output. Do not include
    /// [`Codec::MAX_DECODE_FINISH_VALUES`]; the engine adds the codec finish
    /// bound separately.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    ///
    /// # Returns
    ///
    /// Returns the finite, transient-state-independent final-output upper
    /// bound.
    #[must_use]
    #[inline]
    fn max_finish_output_len(&self, _codec: &C) -> usize {
        0
    }

    /// Handles a codec-domain invalid decode error during `transcode`.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `error`: Invalid domain error returned by the codec.
    /// - `consumed`: Known invalid input units that may be consumed by
    ///   non-strict policies, or `None` when the codec cannot state an invalid
    ///   span length.
    /// - `context`: Decode attempt context.
    ///
    /// # Returns
    ///
    /// Returns the action selected by this hook policy.
    ///
    /// Returned consuming actions must not consume more than
    /// `context.available()` input units.
    ///
    /// # Errors
    ///
    /// Returns [`crate::TranscodeDecodeError`] when the policy rejects the
    /// input with a codec-domain error.
    ///
    /// # Panics
    ///
    /// The engine panics if a consuming action exceeds
    /// `context.available()` input units.
    fn handle_invalid_decode(
        &mut self,
        codec: &mut C,
        error: &C::DecodeError,
        consumed: Option<NonZeroUsize>,
        context: DecodeContext,
    ) -> Result<DecodeInvalidAction<C::Value>, TranscodeDecodeErrorOf<C>>;

    /// Handles a codec value left incomplete after end of input.
    ///
    /// The engine calls this method only from `transcode_eof`. `source` is
    /// `Some` only when the codec returned an incomplete failure carrying a
    /// source, such as through
    /// [`crate::DecodeFailure::incomplete_with_source`]. It is `None` when
    /// the available tail is shorter than [`Codec::MIN_UNITS_PER_VALUE`]
    /// and the codec was not called, or when the codec was called but
    /// returned [`crate::DecodeFailure::incomplete`] without a source.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `source`: Optional domain error explaining the incomplete tail.
    /// - `required_total`: Minimum source units required to retry decoding.
    /// - `context`: Decode attempt context.
    ///
    /// # Returns
    ///
    /// Returns the EOF policy action. `Skip` and `Emit` consume every source
    /// unit available in `context`. At confirmed EOF,
    /// [`crate::engine::TranscodeDecodeEngine`] requires one writable value
    /// slot before invoking this method, even when the eventual action is
    /// `Skip` or `Reject`; a capacity shortage does not invoke the hook or
    /// change its state. [`crate::engine::TranscodeConvertEngine`] may invoke
    /// the hook without target capacity: `Skip` can consume the tail, while
    /// `Emit` can retain a pending value for encoding. A retry drains that
    /// pending value without invoking the hook again for the same tail.
    ///
    /// # Errors
    ///
    /// Returns [`crate::TranscodeDecodeError`] when policy evaluation fails.
    #[inline]
    fn handle_incomplete_decode(
        &mut self,
        _codec: &mut C,
        _source: Option<&C::DecodeError>,
        _required_total: NonZeroUsize,
        _context: DecodeContext,
    ) -> Result<DecodeIncompleteAction<C::Value>, TranscodeDecodeErrorOf<C>> {
        Ok(DecodeIncompleteAction::Reject)
    }

    /// Runs hook-owned cleanup as part of stream reset.
    ///
    /// Called before [`Codec::decode_reset`]
    /// writes its own reset output. Stateless hooks may use the default no-op.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    #[inline]
    fn reset_hooks(&mut self, _codec: &mut C) {}

    /// Finishes hook-owned state and writes any retained output.
    ///
    /// The default implementation is a no-op for stateless decode hooks.
    /// Stateful hooks may emit final values such as checksums, reset markers,
    /// or other trailer data. The caller must provide at least
    /// [`TranscodeDecodeHooks::max_finish_output_len`] writable slots from
    /// `output_index`. Implementations must not write beyond that declared
    /// final-output bound.
    ///
    /// Called after [`Codec::decode_finish`] has
    /// written its own finish output.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `output`: Output value slice visible to the hook.
    /// - `output_index`: Absolute output value index where writing starts.
    ///
    /// # Returns
    ///
    /// Returns the number of values written by finalization. This count must
    /// not exceed [`TranscodeDecodeHooks::max_finish_output_len`].
    ///
    /// # Errors
    ///
    /// Returns a transcode decode error when hook-owned state cannot be
    /// finalized.
    #[inline]
    fn finish_hooks(
        &mut self,
        _codec: &mut C,
        _output: &mut [C::Value],
        _output_index: usize,
    ) -> Result<usize, TranscodeDecodeErrorOf<C>> {
        Ok(0)
    }
}