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