Skip to main content

rocketmq_error/
context.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt;
16
17use crate::field::BoolField;
18use crate::field::ContextVisibility;
19use crate::field::FieldKey;
20use crate::field::FieldSchema;
21use crate::field::I64Field;
22use crate::field::SecretPresenceField;
23use crate::field::TextField;
24use crate::field::U64Field;
25
26/// The redacted constant.
27pub const REDACTED: &str = "<redacted>";
28
29/// Wrapper for values that must never be formatted directly.
30#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct Sensitive<T> {
32    inner: T,
33}
34
35impl<T> Sensitive<T> {
36    #[inline]
37    /// Creates a new `Sensitive`.
38    pub const fn new(inner: T) -> Self {
39        Self { inner }
40    }
41
42    #[inline]
43    /// Returns the expose secret.
44    pub const fn expose_secret(&self) -> &T {
45        &self.inner
46    }
47
48    #[inline]
49    /// Converts this value into inner.
50    pub fn into_inner(self) -> T {
51        self.inner
52    }
53}
54
55impl<T> From<T> for Sensitive<T> {
56    #[inline]
57    fn from(value: T) -> Self {
58        Self::new(value)
59    }
60}
61
62impl<T> fmt::Display for Sensitive<T> {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.write_str(REDACTED)
65    }
66}
67
68impl<T> fmt::Debug for Sensitive<T> {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str("Sensitive(<redacted>)")
71    }
72}
73
74/// Borrowed value of a structured context field.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub enum FieldValueRef<'a> {
77    /// Bounded, normalized text.
78    Text(&'a str),
79    /// A signed integer.
80    I64(i64),
81    /// An unsigned integer.
82    U64(u64),
83    /// A Boolean value.
84    Bool(bool),
85    /// A value-free presence marker.
86    Presence,
87}
88
89#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90enum FieldValue {
91    Text(Box<str>),
92    I64(i64),
93    U64(u64),
94    Bool(bool),
95    Presence,
96}
97
98impl FieldValue {
99    fn as_ref(&self) -> FieldValueRef<'_> {
100        match self {
101            Self::Text(value) => FieldValueRef::Text(value),
102            Self::I64(value) => FieldValueRef::I64(*value),
103            Self::U64(value) => FieldValueRef::U64(*value),
104            Self::Bool(value) => FieldValueRef::Bool(*value),
105            Self::Presence => FieldValueRef::Presence,
106        }
107    }
108}
109
110/// One immutable structured error context field.
111#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub struct ErrorContextField {
113    schema: FieldSchema,
114    value: FieldValue,
115}
116
117impl ErrorContextField {
118    /// Returns the catalog-owned field schema.
119    #[inline]
120    pub const fn schema(&self) -> FieldSchema {
121        self.schema
122    }
123
124    /// Returns the external field name.
125    #[inline]
126    pub const fn name(&self) -> &'static str {
127        self.schema.name()
128    }
129
130    /// Returns the field visibility.
131    #[inline]
132    pub const fn visibility(&self) -> ContextVisibility {
133        self.schema.visibility()
134    }
135
136    /// Returns the field value.
137    ///
138    /// Public callers can obtain fields only through
139    /// [`ErrorContext::public_fields`], which excludes diagnostic and
140    /// secret-presence entries. Descriptor-validated views apply their own
141    /// descriptor declaration ordering before exposing context fields.
142    #[inline]
143    pub fn value(&self) -> FieldValueRef<'_> {
144        self.value.as_ref()
145    }
146}
147
148impl fmt::Debug for ErrorContextField {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter
151            .debug_struct("ErrorContextField")
152            .field("name", &self.name())
153            .field("visibility", &self.visibility())
154            .field("value", &RedactedValue(self))
155            .finish()
156    }
157}
158
159/// Redaction-aware structured context for errors.
160///
161/// Context retains at most 16 unique catalog fields. The first value for a
162/// name wins and insertion order is preserved. Duplicate and over-capacity
163/// inserts are ignored and recorded by [`Self::is_truncated`].
164#[derive(Clone, Default, PartialEq, Eq)]
165pub struct ErrorContext {
166    fields: Vec<ErrorContextField>,
167    truncated: bool,
168}
169
170impl ErrorContext {
171    const MAX_FIELDS: usize = 16;
172
173    #[inline]
174    /// Creates an empty context.
175    pub const fn new() -> Self {
176        Self {
177            fields: Vec::new(),
178            truncated: false,
179        }
180    }
181
182    /// Adds bounded text under a catalog-owned text key.
183    ///
184    /// Every Unicode control character is replaced with one space before the
185    /// value is truncated at a UTF-8 boundary to the schema's byte limit.
186    pub fn with_text(mut self, key: FieldKey<TextField>, value: impl AsRef<str>) -> Self {
187        let schema = key.schema();
188        if !self.reserve_name(schema) {
189            return self;
190        }
191        let limit = schema.text_byte_limit().unwrap_or(crate::field::MAX_TEXT_FIELD_BYTES);
192        let (value, truncated) = normalize_text(value.as_ref(), limit);
193        self.truncated |= truncated;
194        self.fields.push(ErrorContextField {
195            schema,
196            value: FieldValue::Text(value),
197        });
198        self
199    }
200
201    /// Adds a signed integer under a catalog-owned key.
202    pub fn with_i64(mut self, key: FieldKey<I64Field>, value: i64) -> Self {
203        self.insert_scalar(key.schema(), FieldValue::I64(value));
204        self
205    }
206
207    /// Adds an unsigned integer under a catalog-owned key.
208    pub fn with_u64(mut self, key: FieldKey<U64Field>, value: u64) -> Self {
209        self.insert_scalar(key.schema(), FieldValue::U64(value));
210        self
211    }
212
213    /// Adds a Boolean under a catalog-owned key.
214    pub fn with_bool(mut self, key: FieldKey<BoolField>, value: bool) -> Self {
215        self.insert_scalar(key.schema(), FieldValue::Bool(value));
216        self
217    }
218
219    /// Records the presence of secret-bearing input without accepting a value.
220    pub fn with_secret_presence(mut self, key: FieldKey<SecretPresenceField>) -> Self {
221        self.insert_scalar(key.schema(), FieldValue::Presence);
222        self
223    }
224
225    /// Iterates fields approved for public projections in insertion order.
226    pub fn public_fields(&self) -> impl Iterator<Item = &ErrorContextField> {
227        self.fields()
228            .iter()
229            .filter(|field| matches!(field.visibility(), ContextVisibility::Public))
230    }
231
232    /// Returns retained fields to crate-private descriptor validators.
233    pub(crate) fn fields(&self) -> &[ErrorContextField] {
234        &self.fields
235    }
236
237    #[inline]
238    /// Returns whether the context contains no fields.
239    pub fn is_empty(&self) -> bool {
240        self.fields.is_empty()
241    }
242
243    #[inline]
244    /// Returns the number of retained fields.
245    pub fn len(&self) -> usize {
246        self.fields.len()
247    }
248
249    /// Returns whether text, a duplicate field, or an over-capacity insert was truncated.
250    #[inline]
251    pub const fn is_truncated(&self) -> bool {
252        self.truncated
253    }
254
255    fn insert_scalar(&mut self, schema: FieldSchema, value: FieldValue) {
256        if self.reserve_name(schema) {
257            self.fields.push(ErrorContextField { schema, value });
258        }
259    }
260
261    fn reserve_name(&mut self, schema: FieldSchema) -> bool {
262        if self.fields.iter().any(|field| field.name() == schema.name()) || self.fields.len() >= Self::MAX_FIELDS {
263            self.truncated = true;
264            return false;
265        }
266        true
267    }
268}
269
270impl fmt::Display for ErrorContext {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        for (index, field) in self.fields().iter().enumerate() {
273            if index > 0 {
274                f.write_str(", ")?;
275            }
276            write!(f, "{}={}", field.name(), DisplayValue(field))?;
277        }
278        Ok(())
279    }
280}
281
282impl fmt::Debug for ErrorContext {
283    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284        formatter
285            .debug_struct("ErrorContext")
286            .field("fields", &self.fields)
287            .field("truncated", &self.truncated)
288            .finish()
289    }
290}
291
292struct DisplayValue<'a>(&'a ErrorContextField);
293
294impl fmt::Display for DisplayValue<'_> {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        if !matches!(self.0.visibility(), ContextVisibility::Public) {
297            return formatter.write_str(REDACTED);
298        }
299        match self.0.value() {
300            FieldValueRef::Text(value) => formatter.write_str(value),
301            FieldValueRef::I64(value) => value.fmt(formatter),
302            FieldValueRef::U64(value) => value.fmt(formatter),
303            FieldValueRef::Bool(value) => value.fmt(formatter),
304            FieldValueRef::Presence => formatter.write_str(REDACTED),
305        }
306    }
307}
308
309struct RedactedValue<'a>(&'a ErrorContextField);
310
311impl fmt::Debug for RedactedValue<'_> {
312    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313        write!(formatter, "{}", DisplayValue(self.0))
314    }
315}
316
317fn normalize_text(value: &str, limit: usize) -> (Box<str>, bool) {
318    let mut normalized = String::with_capacity(limit);
319    for character in value.chars() {
320        let character = if character.is_control() { ' ' } else { character };
321        if normalized.len() + character.len_utf8() > limit {
322            return (normalized.into_boxed_str(), true);
323        }
324        normalized.push(character);
325    }
326    (normalized.into_boxed_str(), false)
327}