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#[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}