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}