Skip to main content

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}