Skip to main content

qubit_http/sanitize/
body_preview.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10
11use super::BodyLogContext;
12
13/// Bounded body bytes plus enough context to render a log-safe preview.
14#[derive(Debug, Clone, Copy)]
15pub struct BodyPreview<'a> {
16    /// Full body bytes, or an already bounded prefix when `truncated` is set.
17    pub(crate) bytes: &'a [u8],
18    /// Maximum bytes allowed in the rendered preview.
19    pub(crate) limit: usize,
20    /// Total source body length when known.
21    pub(crate) source_len: usize,
22    /// Whether `bytes` is already a truncated prefix.
23    pub(crate) truncated: bool,
24    /// Optional content type used to choose structured redaction rules.
25    pub(crate) content_type: Option<&'a str>,
26    /// Logging context that controls truncation marker text.
27    pub(crate) context: BodyLogContext,
28}
29
30impl<'a> BodyPreview<'a> {
31    /// Creates a body preview from source bytes and a byte limit.
32    ///
33    /// # Parameters
34    /// - `bytes`: Source body bytes.
35    /// - `limit`: Maximum preview bytes; values below 1 are clamped to 1.
36    /// - `context`: Logging call site for truncation marker selection.
37    ///
38    /// # Returns
39    /// Preview descriptor borrowing `bytes`.
40    pub fn new(bytes: &'a [u8], limit: usize, context: BodyLogContext) -> Self {
41        Self {
42            bytes,
43            limit: limit.max(1),
44            source_len: bytes.len(),
45            truncated: false,
46            content_type: None,
47            context,
48        }
49    }
50
51    /// Creates a preview from bytes that have already been limited by the caller.
52    ///
53    /// # Parameters
54    /// - `bytes`: Preview prefix bytes.
55    /// - `source_len`: Total source body length when known.
56    /// - `truncated`: Whether the source had more bytes after `bytes`.
57    /// - `context`: Logging call site for truncation marker selection.
58    ///
59    /// # Returns
60    /// Preview descriptor borrowing `bytes`.
61    pub(crate) fn from_limited_bytes(
62        bytes: &'a [u8],
63        source_len: usize,
64        truncated: bool,
65        context: BodyLogContext,
66    ) -> Self {
67        Self {
68            bytes,
69            limit: bytes.len().max(1),
70            source_len,
71            truncated,
72            content_type: None,
73            context,
74        }
75    }
76
77    /// Adds content type metadata used by structured body sanitizers.
78    ///
79    /// # Parameters
80    /// - `content_type`: Content-Type header value.
81    ///
82    /// # Returns
83    /// Updated preview descriptor.
84    pub fn with_content_type(mut self, content_type: &'a str) -> Self {
85        self.content_type = Some(content_type);
86        self
87    }
88
89    /// Returns the byte prefix that should be rendered.
90    ///
91    /// # Returns
92    /// Bounded byte slice.
93    pub(crate) fn prefix(&self) -> &'a [u8] {
94        if self.truncated {
95            self.bytes
96        } else {
97            let end = self.bytes.len().min(self.limit);
98            &self.bytes[..end]
99        }
100    }
101
102    /// Returns whether rendered output is truncated.
103    ///
104    /// # Returns
105    /// `true` when either the caller marked the prefix as truncated or `limit`
106    /// cuts the provided source bytes.
107    pub(crate) fn is_truncated(&self) -> bool {
108        self.truncated || self.bytes.len() > self.limit
109    }
110
111    /// Returns the total source body length used for binary previews.
112    ///
113    /// # Returns
114    /// Source body length in bytes.
115    pub(crate) fn source_len(&self) -> usize {
116        self.source_len.max(self.bytes.len())
117    }
118
119    /// Returns the truncation suffix for this preview.
120    ///
121    /// # Returns
122    /// Empty string when not truncated.
123    pub(crate) fn truncation_suffix(&self) -> String {
124        if !self.is_truncated() {
125            return String::new();
126        }
127        match self.context {
128            BodyLogContext::ErrorResponse => "...<truncated>".to_string(),
129            BodyLogContext::Request | BodyLogContext::Response => {
130                let truncated = self.source_len().saturating_sub(self.prefix().len());
131                format!("...<truncated {truncated} bytes>")
132            }
133        }
134    }
135}