qubit_redact/formats/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#[derive(Clone, Copy, PartialEq, Eq)]
20pub struct BodyCapture<'a> {
21 /// Source bytes available to the redactor before its hard input budget.
22 bytes: &'a [u8],
23 /// Exact total source length, or `None` when omitted length is unknown.
24 total_len: Option<usize>,
25 /// Whether the source already omitted bytes before reaching the redactor.
26 source_truncated: bool,
27}
28
29impl fmt::Debug for BodyCapture<'_> {
30 /// Formats safe capture metadata without exposing body bytes.
31 ///
32 /// # Parameters
33 ///
34 /// * `formatter` - Destination formatting context.
35 ///
36 /// # Returns
37 ///
38 /// The formatter result from writing the safe metadata.
39 ///
40 /// # Errors
41 ///
42 /// Returns [`fmt::Error`] when the destination rejects a write.
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter
45 .debug_struct("BodyCapture")
46 .field("bytes", &"<redacted>")
47 .field("captured_len", &self.bytes.len())
48 .field("total_len", &self.total_len)
49 .field("omitted_len", &self.omitted_len())
50 .field("source_truncated", &self.source_truncated)
51 .finish()
52 }
53}
54
55impl<'a> BodyCapture<'a> {
56 /// Creates a capture containing the complete source body.
57 ///
58 /// # Parameters
59 ///
60 /// * `bytes` - Complete source body bytes.
61 ///
62 /// # Returns
63 ///
64 /// A capture whose total length equals the borrowed slice length.
65 #[must_use]
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 #[must_use]
88 pub fn prefix(bytes: &'a [u8], max_bytes: usize) -> Self {
89 let captured_len = bytes.len().min(max_bytes);
90 if captured_len == bytes.len() {
91 Self::complete(bytes)
92 } else {
93 Self {
94 bytes: &bytes[..captured_len],
95 total_len: Some(bytes.len()),
96 source_truncated: true,
97 }
98 }
99 }
100
101 /// Creates a capture known to omit an unknown number of source bytes.
102 ///
103 /// # Parameters
104 ///
105 /// * `bytes` - Captured prefix of the source body.
106 ///
107 /// # Returns
108 ///
109 /// An infallible truncated capture whose complete source length is
110 /// unknown.
111 #[must_use]
112 pub const fn truncated_unknown(bytes: &'a [u8]) -> Self {
113 Self {
114 bytes,
115 total_len: None,
116 source_truncated: true,
117 }
118 }
119
120 /// Creates a capture known to omit source bytes.
121 ///
122 /// # Parameters
123 ///
124 /// * `bytes` - Captured prefix of the source body.
125 /// * `total_len` - Exact complete source length, which must exceed the
126 /// captured prefix length.
127 ///
128 /// # Returns
129 ///
130 /// A checked truncated capture.
131 ///
132 /// # Errors
133 ///
134 /// Returns [`BodyCaptureError::InvalidTotalLength`] when `total_len` is
135 /// less than or equal to the captured slice length. Use
136 /// [`Self::truncated_unknown`] when the complete source length is unknown.
137 #[inline]
138 pub const fn truncated(bytes: &'a [u8], total_len: usize) -> Result<Self, BodyCaptureError> {
139 if total_len <= bytes.len() {
140 return Err(BodyCaptureError::InvalidTotalLength {
141 captured: bytes.len(),
142 total: total_len,
143 });
144 }
145 Ok(Self {
146 bytes,
147 total_len: Some(total_len),
148 source_truncated: true,
149 })
150 }
151
152 /// Returns the body bytes available before the redactor's hard budget.
153 ///
154 /// # Returns
155 ///
156 /// The borrowed captured byte slice.
157 #[inline(always)]
158 #[must_use]
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::formats::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 #[must_use]
188 #[inline(always)]
189 pub const fn total_len(self) -> Option<usize> {
190 self.total_len
191 }
192
193 /// Returns the number of source bytes omitted before capture.
194 ///
195 /// # Returns
196 ///
197 /// `Some(0)` for complete input, `Some(count)` for a known truncated
198 /// total, or `None` when the total length is unknown.
199 #[must_use]
200 #[inline(always)]
201 pub const fn omitted_len(self) -> Option<usize> {
202 match self.total_len {
203 Some(total) => Some(total - self.bytes.len()),
204 None => None,
205 }
206 }
207
208 /// Reports whether source bytes were omitted before capture.
209 ///
210 /// # Returns
211 ///
212 /// `true` for captures created with [`Self::prefix`] when the source does
213 /// not fit, [`Self::truncated_unknown`], or [`Self::truncated`].
214 #[must_use]
215 #[inline(always)]
216 pub const fn is_source_truncated(self) -> bool {
217 self.source_truncated
218 }
219}