Skip to main content

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}