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 encoder engines.

use super::encode_context::EncodeContext;
use super::encode_unencodable_action::EncodeUnencodableAction;
use crate::CapacityError;
use crate::Codec;
use crate::TranscodeEncodeErrorOf;

/// Policy hooks for [`crate::engine::TranscodeEncodeEngine`].
///
/// Hooks own policy state, such as replacement or ignore behavior, but not the
/// codec or engine cursor state. The engine owns the normal one-value encode
/// operation and calls hooks only when policy decisions are needed.
///
/// Implement this trait when a buffered encoder needs policy decisions around
/// individual values while reusing the common engine loop. Examples include
/// rejecting unsupported values with adapter-level context, skipping
/// unsupported values, replacing them with encodable values, resetting
/// hook-owned state in [`reset_hooks`](Self::reset_hooks), or emitting final
/// state in [`finish_hooks`](Self::finish_hooks).
///
/// The engine calls
/// [`handle_unencodable_encode`](Self::handle_unencodable_encode) only after
/// [`Codec::can_encode_value`] returns `false` for the current input
/// value. Encodable values are encoded directly by the engine with
/// [`Codec::encode_len`] and [`Codec::encode`].
///
/// # Examples
///
/// This hook reports unsupported values as adapter-level errors.
///
/// ```rust
/// use core::convert::Infallible;
/// use core::num::NonZeroUsize;
///
/// use qubit_codec::Codec;
/// use qubit_codec::DecodeFailure;
/// use qubit_codec::TranscodeEncodeErrorOf;
/// use qubit_codec::engine::EncodeContext;
/// use qubit_codec::engine::EncodeUnencodableAction;
/// use qubit_codec::engine::TranscodeEncodeEngine;
/// use qubit_codec::engine::TranscodeEncodeHooks;
///
/// #[derive(Clone, Copy)]
/// struct ByteCodec;
///
/// impl Codec for ByteCodec {
///     type Value = u8;
///     type Unit = u8;
///     type DecodeError = Infallible;
///     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>> {
///         Ok((input[index], NonZeroUsize::MIN))
///     }
///
///     unsafe fn encode(
///         &mut self,
///         value: &u8,
///         output: &mut [u8],
///         index: usize,
///     ) -> Result<usize, Self::EncodeError> {
///         output[index] = *value;
///         Ok(1)
///     }
/// }
///
/// struct StrictHooks;
///
/// impl<C> TranscodeEncodeHooks<C> for StrictHooks
/// where
///     C: Codec,
/// {
///     fn handle_unencodable_encode(
///         &mut self,
///         _codec: &mut C,
///         _context: &EncodeContext<'_, C::Value>,
///     ) -> Result<EncodeUnencodableAction<C::Value>, TranscodeEncodeErrorOf<C>> {
///         Ok(EncodeUnencodableAction::Reject)
///     }
/// }
///
/// let codec = ByteCodec;
/// let hooks = StrictHooks;
/// let bound = hooks.max_transcode_output_len(&codec, 1).unwrap();
/// assert!(bound >= ByteCodec::MAX_ENCODE_UNITS_PER_VALUE);
/// let mut engine = TranscodeEncodeEngine::new(codec, hooks);
/// engine.reset(&mut [], 0).unwrap();
/// let mut output = [0_u8; 1];
/// let progress = engine.transcode(&[42], 0, &mut output, 0).unwrap();
/// assert_eq!(progress.read(), 1);
/// assert_eq!(progress.written(), 1);
/// assert_eq!(output, [42]);
/// ```
///
/// # Type Parameters
///
/// - `C`: Low-level codec owned by the engine.
pub trait TranscodeEncodeHooks<C>
where
    C: Codec,
{
    /// Returns the maximum output units needed for `input_len` values.
    ///
    /// This bound covers the streaming encode 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 units actually emitted. It must not include
    /// codec reset output, codec finish output, or hook finish output.
    /// It may depend on immutable hook and codec configuration, but it must
    /// cover every reachable transient hook and codec state. Each streaming
    /// action handles one current input value by skipping it or encoding at
    /// most one replacement; the hook API has no separate operation for
    /// draining hook-owned output without 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.
    ///
    /// Before [`handle_unencodable_encode`](Self::handle_unencodable_encode),
    /// the engine may preflight [`Codec::MAX_ENCODE_UNITS_PER_VALUE`], even
    /// when the selected action is `Skip` or `Reject` and emits no units.
    /// Thus the bound must leave room for that preflight; zero actual output
    /// does not imply zero required writable capacity. See the
    /// [trait example](#examples) for a capacity check before constructing the
    /// engine.
    ///
    /// The default implementation multiplies `input_len` by
    /// [`Codec::MAX_ENCODE_UNITS_PER_VALUE`]. This bound is valid for direct
    /// encoding, skipped unencodable values, and single-value replacements.
    /// Override it when the codec's streaming output bound requires a larger
    /// value.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `input_len`: Number of input values the caller plans to encode.
    ///
    /// # 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> {
        input_len
            .checked_mul(C::MAX_ENCODE_UNITS_PER_VALUE)
            .ok_or(CapacityError::OutputLengthOverflow)
    }

    /// Returns an upper bound for units emitted by finishing hook-owned state.
    ///
    /// `finish` never receives more input values. 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, checksums,
    /// delayed replacements, or any other hook-owned final output. Do not
    /// include [`Codec::MAX_ENCODE_FINISH_UNITS`]; 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.
    #[inline]
    #[must_use]
    fn max_finish_output_len(&self, _codec: &C) -> usize {
        0
    }

    /// Handles one unencodable input value.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `context`: Encode context for the input value that
    ///   [`Codec::can_encode_value`] rejected.
    ///
    /// # Returns
    ///
    /// Returns the action selected by this hook policy.
    ///
    /// Replacement actions must contain a value that the same codec can encode.
    /// The engine treats unencodable replacements as hook contract violations
    /// and panics.
    ///
    /// Before invoking this method, the engine requires writable capacity for
    /// [`Codec::MAX_ENCODE_UNITS_PER_VALUE`]. This conservative bound also
    /// applies when the selected action is `Skip` or `Reject`; it may exceed
    /// the replacement's actual encoded width. A capacity shortage does not
    /// invoke the hook, so hook state is not changed by output backpressure.
    ///
    /// # Errors
    ///
    /// Returns [`crate::TranscodeEncodeError`] when the policy rejects the
    /// value with a codec-domain error.
    fn handle_unencodable_encode(
        &mut self,
        codec: &mut C,
        context: &EncodeContext<'_, C::Value>,
    ) -> Result<EncodeUnencodableAction<C::Value>, TranscodeEncodeErrorOf<C>>;

    /// Runs hook-owned cleanup as part of stream reset.
    ///
    /// Called before [`Codec::encode_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 units.
    ///
    /// The default implementation is a no-op for stateless encode hooks.
    /// Stateful hooks may emit final units such as reset sequences, checksums,
    /// or trailers. The caller must provide at least
    /// [`TranscodeEncodeHooks::max_finish_output_len`] writable units from
    /// `output_index`. Implementations must not write beyond that declared
    /// final-output bound.
    ///
    /// Called after [`Codec::encode_finish`] has
    /// written its own finish output.
    ///
    /// # Parameters
    ///
    /// - `codec`: Low-level codec owned by the engine.
    /// - `output`: Output unit slice visible to the hook.
    /// - `output_index`: Absolute output unit index where writing starts.
    ///
    /// # Returns
    ///
    /// Returns the number of units written by finalization. This count must not
    /// exceed [`TranscodeEncodeHooks::max_finish_output_len`].
    ///
    /// # Errors
    ///
    /// Returns a transcode encode error when hook-owned state cannot be
    /// finalized.
    #[inline]
    fn finish_hooks(
        &mut self,
        _codec: &mut C,
        _output: &mut [C::Unit],
        _output_index: usize,
    ) -> Result<usize, TranscodeEncodeErrorOf<C>> {
        Ok(0)
    }
}