qubit_codec/transcode/transcode_progress.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8use core::num::NonZeroUsize;
9
10use super::TranscodeContractError;
11use super::TranscodeStatus;
12
13/// Counts how much work a [`crate::Transcoder`] completed before
14/// returning.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct TranscodeProgress {
17 /// Stop reason reported by the transcoder.
18 status: TranscodeStatus,
19 /// Number of input units consumed from the requested input index.
20 read: usize,
21 /// Number of output units written from the requested output index.
22 written: usize,
23}
24
25impl TranscodeProgress {
26 /// Creates a progress value.
27 ///
28 /// # Parameters
29 ///
30 /// - `status`: The reason conversion stopped.
31 /// - `read`: Number of input units consumed from the call's input index.
32 /// - `written`: Number of output units written from the call's output
33 /// index.
34 ///
35 /// # Returns
36 ///
37 /// Returns a progress value carrying the supplied counters.
38 #[inline(always)]
39 #[must_use]
40 pub const fn new(status: TranscodeStatus, read: usize, written: usize) -> Self {
41 Self { status, read, written }
42 }
43
44 /// Creates a completed progress value.
45 ///
46 /// # Parameters
47 ///
48 /// - `read`: Number of consumed input units.
49 /// - `written`: Number of produced output units.
50 ///
51 /// # Returns
52 ///
53 /// Returns a progress value whose status is [`TranscodeStatus::Complete`].
54 #[inline(always)]
55 #[must_use]
56 pub const fn complete(read: usize, written: usize) -> Self {
57 Self::new(TranscodeStatus::Complete, read, written)
58 }
59
60 /// Creates progress that stopped because more input is needed.
61 ///
62 /// # Parameters
63 ///
64 /// - `required`: Current minimum total input units required before retrying
65 /// from the current input position. A later retry may raise this lower
66 /// bound.
67 /// - `read`: Number of consumed input units.
68 /// - `written`: Number of produced output units.
69 ///
70 /// # Returns
71 ///
72 /// Returns a progress value with [`TranscodeStatus::NeedInput`].
73 #[inline(always)]
74 #[must_use]
75 pub const fn need_input(required: NonZeroUsize, read: usize, written: usize) -> Self {
76 Self::new(TranscodeStatus::need_input(required), read, written)
77 }
78
79 /// Creates progress that stopped because more output capacity is needed.
80 ///
81 /// # Parameters
82 ///
83 /// - `required`: Total output units required from the current output
84 /// position.
85 /// - `read`: Number of consumed input units.
86 /// - `written`: Number of produced output units.
87 ///
88 /// # Returns
89 ///
90 /// Returns a progress value with [`TranscodeStatus::NeedOutput`].
91 #[inline(always)]
92 #[must_use]
93 pub const fn need_output(required: NonZeroUsize, read: usize, written: usize) -> Self {
94 Self::new(TranscodeStatus::need_output(required), read, written)
95 }
96
97 /// Returns the status that stopped conversion.
98 ///
99 /// # Returns
100 ///
101 /// Returns the stored [`TranscodeStatus`].
102 #[inline(always)]
103 #[must_use]
104 pub const fn status(self) -> TranscodeStatus {
105 self.status
106 }
107
108 /// Returns whether conversion consumed all currently supplied input.
109 ///
110 /// # Returns
111 ///
112 /// Returns `true` when the stored status is
113 /// [`TranscodeStatus::Complete`].
114 #[inline(always)]
115 #[must_use]
116 pub const fn is_complete(self) -> bool {
117 matches!(self.status, TranscodeStatus::Complete)
118 }
119
120 /// Returns whether conversion stopped because more input is needed.
121 ///
122 /// # Returns
123 ///
124 /// Returns `true` when the stored status is
125 /// [`TranscodeStatus::NeedInput`].
126 #[inline(always)]
127 #[must_use]
128 pub const fn is_need_input(self) -> bool {
129 matches!(self.status, TranscodeStatus::NeedInput { .. })
130 }
131
132 /// Returns whether conversion stopped because more output capacity is
133 /// needed.
134 ///
135 /// # Returns
136 ///
137 /// Returns `true` when the stored status is
138 /// [`TranscodeStatus::NeedOutput`].
139 #[inline(always)]
140 #[must_use]
141 pub const fn is_need_output(self) -> bool {
142 matches!(self.status, TranscodeStatus::NeedOutput { .. })
143 }
144
145 /// Returns the number of input units consumed by the call.
146 ///
147 /// # Returns
148 ///
149 /// Returns a count relative to the input index passed to the conversion
150 /// call.
151 #[inline(always)]
152 #[must_use]
153 pub const fn read(self) -> usize {
154 self.read
155 }
156
157 /// Returns the number of output units written by the call.
158 ///
159 /// # Returns
160 ///
161 /// Returns a count relative to the output index passed to the conversion
162 /// call.
163 #[inline(always)]
164 #[must_use]
165 pub const fn written(self) -> usize {
166 self.written
167 }
168
169 /// Validates this progress against the call bounds supplied to a
170 /// transcoder.
171 ///
172 /// Buffered drivers should call this before using [`Self::read`] or
173 /// [`Self::written`] to advance unchecked input or output cursors. The
174 /// method checks relative counters, unsatisfied `NeedInput` / `NeedOutput`
175 /// requirements, and the `Complete` invariant that all visible input was
176 /// consumed.
177 ///
178 /// This is a contract checker, not a semantic recovery policy. It performs
179 /// the same validation in every build profile. Drivers that advance unsafe
180 /// cursors may convert failures into their own error type, while
181 /// convenience helpers may assert that validation succeeds. Custom
182 /// [`crate::Transcoder`] implementations must therefore return progress
183 /// that satisfies the documented contract in all builds.
184 ///
185 /// # Parameters
186 ///
187 /// - `input_index`: Input index originally passed to the transcoder.
188 /// - `available_input`: Number of input units visible from `input_index`.
189 /// - `output_index`: Output index originally passed to the transcoder.
190 /// - `available_output`: Number of output slots visible from
191 /// `output_index`.
192 ///
193 /// # Returns
194 ///
195 /// Returns `Ok(())` when progress is internally consistent with the
196 /// supplied call bounds.
197 ///
198 /// # Errors
199 ///
200 /// Returns [`TranscodeContractError`] when a custom transcoder reports
201 /// counters or missing-capacity requirements that do not match the buffers
202 /// supplied by the caller.
203 pub fn validate(
204 &self,
205 input_index: usize,
206 available_input: usize,
207 output_index: usize,
208 available_output: usize,
209 ) -> Result<(), TranscodeContractError> {
210 if self.read > available_input {
211 return Err(TranscodeContractError::OverRead {
212 read: self.read,
213 available: available_input,
214 });
215 }
216 if self.written > available_output {
217 return Err(TranscodeContractError::OverWritten {
218 written: self.written,
219 available: available_output,
220 });
221 }
222
223 input_index
224 .checked_add(self.read)
225 .ok_or(TranscodeContractError::ProgressIndexOverflow {
226 index: input_index,
227 advanced: self.read,
228 })?;
229 output_index
230 .checked_add(self.written)
231 .ok_or(TranscodeContractError::ProgressIndexOverflow {
232 index: output_index,
233 advanced: self.written,
234 })?;
235 match self.status {
236 TranscodeStatus::Complete => {
237 if self.read != available_input {
238 return Err(TranscodeContractError::CompleteWithRemainingInput {
239 read: self.read,
240 available: available_input,
241 });
242 }
243 Ok(())
244 }
245 TranscodeStatus::NeedInput { required } => {
246 let available = available_input - self.read;
247 if required.get() <= available {
248 return Err(TranscodeContractError::SatisfiedNeed {
249 required: required.get(),
250 available,
251 });
252 }
253 Ok(())
254 }
255 TranscodeStatus::NeedOutput { required } => {
256 let available = available_output - self.written;
257 if required.get() <= available {
258 return Err(TranscodeContractError::SatisfiedNeed {
259 required: required.get(),
260 available,
261 });
262 }
263 Ok(())
264 }
265 }
266 }
267}