qubit_redact/http/body_capture.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Checked borrowed input for HTTP body redaction.
9
10use std::fmt;
11
12use super::BodyCaptureError;
13
14/// Borrowed HTTP body bytes with truthful source-length metadata.
15///
16/// # Type Parameters
17///
18/// * `'a` - Lifetime of the borrowed body bytes.
19#[must_use]
20#[derive(Clone, Copy, PartialEq, Eq)]
21pub struct BodyCapture<'a> {
22 /// Source bytes available to the redactor before its hard input budget.
23 bytes: &'a [u8],
24 /// Exact total source length, or `None` when omitted length is unknown.
25 total_len: Option<usize>,
26 /// Whether the source already omitted bytes before reaching the redactor.
27 source_truncated: bool,
28}
29
30impl fmt::Debug for BodyCapture<'_> {
31 /// Formats safe capture metadata without exposing body bytes.
32 ///
33 /// # Parameters
34 ///
35 /// * `formatter` - Destination formatting context.
36 ///
37 /// # Returns
38 ///
39 /// The formatter result from writing the safe metadata.
40 ///
41 /// # Errors
42 ///
43 /// Returns [`fmt::Error`] when the destination rejects a write.
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 formatter
46 .debug_struct("BodyCapture")
47 .field("bytes", &"<redacted>")
48 .field("captured_len", &self.bytes.len())
49 .field("total_len", &self.total_len)
50 .field("omitted_len", &self.omitted_len())
51 .field("source_truncated", &self.source_truncated)
52 .finish()
53 }
54}
55
56impl<'a> BodyCapture<'a> {
57 /// Creates a capture containing the complete source body.
58 ///
59 /// # Parameters
60 ///
61 /// * `bytes` - Complete source body bytes.
62 ///
63 /// # Returns
64 ///
65 /// A capture whose total length equals the borrowed slice length.
66 pub const fn complete(bytes: &'a [u8]) -> Self {
67 Self {
68 bytes,
69 total_len: Some(bytes.len()),
70 source_truncated: false,
71 }
72 }
73
74 /// Captures at most `max_bytes` from a complete source body.
75 ///
76 /// # Parameters
77 ///
78 /// * `bytes` - Complete source body bytes.
79 /// * `max_bytes` - Maximum prefix length to expose to the redactor.
80 ///
81 /// # Returns
82 ///
83 /// A complete capture when the body fits, otherwise a truncated prefix
84 /// carrying the exact total source length. A zero limit captures an empty
85 /// prefix of non-empty input.
86 #[inline]
87 pub fn prefix(bytes: &'a [u8], max_bytes: usize) -> Self {
88 let captured_len = bytes.len().min(max_bytes);
89 if captured_len == bytes.len() {
90 Self::complete(bytes)
91 } else {
92 Self {
93 bytes: &bytes[..captured_len],
94 total_len: Some(bytes.len()),
95 source_truncated: true,
96 }
97 }
98 }
99
100 /// Creates a capture known to omit an unknown number of source bytes.
101 ///
102 /// # Parameters
103 ///
104 /// * `bytes` - Captured prefix of the source body.
105 ///
106 /// # Returns
107 ///
108 /// An infallible truncated capture whose complete source length is
109 /// unknown.
110 pub const fn truncated_unknown(bytes: &'a [u8]) -> Self {
111 Self {
112 bytes,
113 total_len: None,
114 source_truncated: true,
115 }
116 }
117
118 /// Creates a capture known to omit source bytes.
119 ///
120 /// # Parameters
121 ///
122 /// * `bytes` - Captured prefix of the source body.
123 /// * `total_len` - Exact complete source length, or `None` when unknown.
124 ///
125 /// # Returns
126 ///
127 /// A checked truncated capture.
128 ///
129 /// # Errors
130 ///
131 /// Returns [`BodyCaptureError::InvalidTotalLength`] when a known total is
132 /// less than or equal to the captured slice length.
133 #[inline]
134 pub const fn truncated(
135 bytes: &'a [u8],
136 total_len: Option<usize>,
137 ) -> Result<Self, BodyCaptureError> {
138 if let Some(total) = total_len
139 && total <= bytes.len()
140 {
141 return Err(BodyCaptureError::InvalidTotalLength {
142 captured: bytes.len(),
143 total,
144 });
145 }
146 Ok(Self {
147 bytes,
148 total_len,
149 source_truncated: true,
150 })
151 }
152
153 /// Returns the body bytes available before the redactor's hard budget.
154 ///
155 /// # Returns
156 ///
157 /// The borrowed captured byte slice.
158 #[inline(always)]
159 pub const fn bytes(self) -> &'a [u8] {
160 self.bytes
161 }
162
163 /// Returns the number of captured bytes.
164 ///
165 /// # Returns
166 ///
167 /// The borrowed slice length.
168 ///
169 /// ```compile_fail
170 /// #![deny(unused_must_use)]
171 /// use qubit_redact::http::BodyCapture;
172 ///
173 /// BodyCapture::complete(b"payload").captured_len();
174 /// ```
175 #[must_use]
176 #[inline(always)]
177 pub const fn captured_len(self) -> usize {
178 self.bytes.len()
179 }
180
181 /// Returns the complete source length when known.
182 ///
183 /// # Returns
184 ///
185 /// `Some(total)` for an exact length, or `None` for a truncated capture
186 /// whose omitted byte count is unknown.
187 #[inline(always)]
188 pub const fn total_len(self) -> Option<usize> {
189 self.total_len
190 }
191
192 /// Returns the number of source bytes omitted before capture.
193 ///
194 /// # Returns
195 ///
196 /// `Some(0)` for complete input, `Some(count)` for a known truncated
197 /// total, or `None` when the total length is unknown.
198 #[inline(always)]
199 pub const fn omitted_len(self) -> Option<usize> {
200 match self.total_len {
201 Some(total) => Some(total - self.bytes.len()),
202 None => None,
203 }
204 }
205
206 /// Reports whether source bytes were omitted before capture.
207 ///
208 /// # Returns
209 ///
210 /// `true` for captures created with [`Self::prefix`] when the source does
211 /// not fit, [`Self::truncated_unknown`], or [`Self::truncated`].
212 #[must_use]
213 #[inline(always)]
214 pub const fn is_source_truncated(self) -> bool {
215 self.source_truncated
216 }
217}