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