Skip to main content

gix_error/concrete/
metadata.rs

1use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
2
3use crate::{Class, Error, ErrorExt, ResourceExhaustionKind};
4
5/// An ordered dictionary of named diagnostic values belonging to a single error context.
6///
7/// Functions returning metadata document the keys and their meaning. [`crate::Exn::metadata()`] and
8/// [`crate::Error::metadata()`] yield non-empty dictionaries separately; dictionaries from independent causes
9/// are never merged. Keys are iterated and formatted in lexicographic order.
10/// Debug formatting and [`Message`]'s display omit key quotes for non-empty names containing only ASCII
11/// letters, digits, `_`, `-`, or `.`. Other keys use quoted, escaped string formatting.
12///
13/// # Common schemas
14///
15/// These schemas define conventional keys, not required fields. Store only information already available at the
16/// failure site; do not collect additional information just to populate a schema. Omitted keys mean the information
17/// was not recorded, not that it was empty or absent. Functions returning metadata document which keys they provide.
18/// Metadata alone does not establish a semantic classification or whether recovery is safe.
19///
20/// ## Input validation
21///
22/// | Key | Value | Meaning |
23/// | --- | --- | --- |
24/// | `input` | Any [`MetadataValue`] | The offending input, preserving its original representation where possible. |
25///
26/// Use [`Message::with_input()`] to record already-known input. This convention also applies to malformed data
27/// classified as [`Class::Corruption`]; attaching `input` does not imply [`Class::Validation`]. Do not record secrets
28/// or other sensitive input that should not appear in diagnostics.
29///
30/// ## External program runtime failure
31///
32/// | Key | Value | Meaning |
33/// | --- | --- | --- |
34/// | `program` | [`MetadataValue::Path`] | The invoked program name or path, without resolving it. |
35/// | `exit_status` | [`MetadataValue::String`] | The display representation of [`std::process::ExitStatus`], including termination without an exit code. |
36/// | `exit_code` | [`MetadataValue::I64`] | The exit code, when [`std::process::ExitStatus::code()`] returns one. |
37/// | `stdout` | [`MetadataValue::Bytes`] | Captured standard output, without text decoding. |
38/// | `stderr` | [`MetadataValue::Bytes`] | Captured standard error, without text decoding. |
39///
40/// Use [`Message::with_command_status()`] to record `program`, `exit_status`, and an available `exit_code` together,
41/// or [`Message::with_command_output()`] to include already-captured output. [`Message::with_program()`] and
42/// [`Message::with_exit_status()`] record the fields available at spawn failures or after program identity was lost.
43///
44/// A failed exit status does not by itself establish a recovery class. Preserve native errors from spawning or
45/// communicating with a program as causes; their contexts can also use `program`, without an exit status or output.
46#[derive(Clone, Default, PartialEq)]
47pub struct Metadata(BTreeMap<Cow<'static, str>, MetadataValue>);
48
49impl Metadata {
50    /// Create an empty dictionary.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Return the number of values in this dictionary.
56    pub fn len(&self) -> usize {
57        self.0.len()
58    }
59
60    /// Return whether this dictionary has no values.
61    pub fn is_empty(&self) -> bool {
62        self.0.is_empty()
63    }
64
65    /// Return whether a value is recorded under `key`.
66    pub fn contains_key(&self, key: &str) -> bool {
67        self.0.contains_key(key)
68    }
69
70    /// Return the value recorded under `key`, if present.
71    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
72        self.0.get(key)
73    }
74
75    /// Return a mutable reference to the value recorded under `key`, if present.
76    pub fn get_mut(&mut self, key: &str) -> Option<&mut MetadataValue> {
77        self.0.get_mut(key)
78    }
79
80    /// Record `value` under `key`, returning any previous value.
81    pub fn insert(
82        &mut self,
83        key: impl Into<Cow<'static, str>>,
84        value: impl Into<MetadataValue>,
85    ) -> Option<MetadataValue> {
86        self.0.insert(key.into(), value.into())
87    }
88
89    /// Remove and return the value recorded under `key`, if present.
90    pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
91        self.0.remove(key)
92    }
93
94    /// Remove all recorded values.
95    pub fn clear(&mut self) {
96        self.0.clear();
97    }
98
99    /// Iterate over keys and values in lexicographic key order.
100    pub fn iter(
101        &self,
102    ) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &MetadataValue)> + ExactSizeIterator + '_ {
103        self.0.iter()
104    }
105
106    /// Iterate over keys and mutable values in lexicographic key order.
107    pub fn iter_mut(
108        &mut self,
109    ) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &mut MetadataValue)> + ExactSizeIterator + '_ {
110        self.0.iter_mut()
111    }
112}
113
114impl std::ops::Index<&str> for Metadata {
115    type Output = MetadataValue;
116
117    fn index(&self, key: &str) -> &Self::Output {
118        &self.0[key]
119    }
120}
121
122impl FromIterator<(Cow<'static, str>, MetadataValue)> for Metadata {
123    fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(iter: T) -> Self {
124        Self(iter.into_iter().collect())
125    }
126}
127
128impl<const N: usize> From<[(Cow<'static, str>, MetadataValue); N]> for Metadata {
129    fn from(values: [(Cow<'static, str>, MetadataValue); N]) -> Self {
130        values.into_iter().collect()
131    }
132}
133
134impl Extend<(Cow<'static, str>, MetadataValue)> for Metadata {
135    fn extend<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(&mut self, iter: T) {
136        self.0.extend(iter);
137    }
138}
139
140impl fmt::Debug for Metadata {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        let mut debug = f.debug_map();
143        for (key, value) in self.iter() {
144            debug.entry(&DebugKey(key), value);
145        }
146        debug.finish()
147    }
148}
149
150struct DebugKey<'a>(&'a str);
151
152impl fmt::Debug for DebugKey<'_> {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        if !self.0.is_empty()
155            && self
156                .0
157                .bytes()
158                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
159        {
160            f.write_str(self.0)
161        } else {
162            fmt::Debug::fmt(self.0, f)
163        }
164    }
165}
166
167/// A diagnostic message with an optional semantic class and named diagnostic values.
168///
169/// Use this instead of chaining message, classification, and scalar-context errors when they describe a single
170/// failure. [`Self::new()`] starts without a class or values; [`Self::with_class()`] and [`Self::with()`] add them.
171/// Class builders such as [`Self::corrupted()`], [`Self::validation()`], [`Self::not_found()`], [`Self::retryable()`],
172/// and [`Self::resource_exhaustion()`] are useful with formatted [`crate::message!`]s.
173/// [`Self::allocation_limit()`] and [`Self::allocation_failure()`] select common resource exhaustion kinds.
174/// Builders ending in `_error`, such as [`Self::corrupted_error()`] and [`Self::validation_error()`],
175/// also raise the classified message as an [`Error`].
176/// Class-based constructors such as [`crate::not_found()`] combine the message and class in one step.
177///
178/// Unlike [`ClassificationMarker`](crate::ClassificationMarker), this is a visible diagnostic: it participates in
179/// error iteration, downcasting, reports, and cause selection. A marker only adds a classification to an existing
180/// error without a diagnostic of its own, preserving that error's concrete type. Both are inspected by [`crate::classify()`].
181/// The class itself isn't displayed, and [`crate::types::Classification::error()`] refers to this error, not a synthetic source.
182///
183/// Preserve real callee errors with [`ResultExt::or_raise()`](crate::ResultExt::or_raise) or
184/// [`Exn::raise()`](crate::Exn::raise). Keep concrete error types when recovery requires a specific condition or payload;
185/// use classification predicates to recognize categories, and document diagnostic keys on the function returning them.
186/// [`Exn::metadata()`](crate::Exn::metadata) and [`crate::Error::metadata()`] yield each message's non-empty value dictionary.
187/// Use [`Exn::metadata_merged()`](crate::Exn::metadata_merged) or [`crate::Error::metadata_merged()`] to combine them,
188/// letting more specific causes override their enclosing contexts. To identify a specific failure, downcast to its
189/// operation's error enum and match a variant; see [matching a specific failure](crate#matching-a-specific-failure).
190///
191/// Debug formatting omits absent classes and empty values. Present classes omit their `Some` wrapper,
192/// and the class and values stay on single lines, even in pretty output.
193pub struct Message {
194    /// The operation or situation described by these values.
195    pub message: Cow<'static, str>,
196    /// The semantic class of this diagnostic, if known.
197    pub class: Option<Class>,
198    /// Diagnostic values, ordered by key. Functions returning metadata document their keys.
199    pub values: Metadata,
200}
201
202/// Lifecycle
203impl Message {
204    /// Create a diagnostic with `message`, no classification, and no values.
205    pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
206        Self {
207            message: message.into(),
208            class: None,
209            values: Metadata::new(),
210        }
211    }
212
213    /// Set `class`, replacing any previous classification without adding a cause.
214    pub fn with_class(mut self, class: Class) -> Self {
215        self.class = Some(class);
216        self
217    }
218}
219
220/// Builders
221impl Message {
222    /// Classify malformed or internally inconsistent stored or streamed data as [`Class::Corruption`].
223    ///
224    /// Like [`Self::with_class()`], this replaces any previous class without changing the message or values or adding a cause.
225    pub fn corrupted(self) -> Self {
226        self.with_class(Class::Corruption)
227    }
228
229    /// Classify malformed or internally inconsistent stored or streamed data and raise it as an [`Error`].
230    ///
231    /// Like [`Self::corrupted()`], this preserves the message and values and replaces any previous class without adding a cause.
232    /// The error records the caller's location, just like [`ErrorExt::raise()`].
233    #[track_caller]
234    pub fn corrupted_error(self) -> Error {
235        self.corrupted().raise()
236    }
237
238    /// Classify invalid function or method input as [`Class::Validation`].
239    ///
240    /// Like [`Self::with_class()`], this replaces any previous class without changing the message or values or adding a cause.
241    pub fn validation(self) -> Self {
242        self.with_class(Class::Validation)
243    }
244
245    /// Classify invalid function or method input and raise it as an [`Error`].
246    ///
247    /// Like [`Self::validation()`], this preserves the message and values and replaces any previous class without adding a cause.
248    /// The error records the caller's location, just like [`ErrorExt::raise()`].
249    #[track_caller]
250    pub fn validation_error(self) -> Error {
251        self.validation().raise()
252    }
253
254    /// Classify a missing resource as [`Class::NotFound`].
255    ///
256    /// Like [`Self::with_class()`], this replaces any previous class without changing the message or values or adding a cause.
257    pub fn not_found(self) -> Self {
258        self.with_class(Class::NotFound)
259    }
260
261    /// Classify a missing resource and raise it as an [`Error`].
262    ///
263    /// Like [`Self::not_found()`], this preserves the message and values and replaces any previous class without adding a cause.
264    /// The error records the caller's location, just like [`ErrorExt::raise()`].
265    #[track_caller]
266    pub fn not_found_error(self) -> Error {
267        self.not_found().raise()
268    }
269
270    /// Classify an operation that may succeed when retried as [`Class::Retryable`].
271    ///
272    /// Like [`Self::with_class()`], this replaces any previous class without changing the message or values or adding a cause.
273    pub fn retryable(self) -> Self {
274        self.with_class(Class::Retryable)
275    }
276
277    /// Classify an operation that may succeed when retried and raise it as an [`Error`].
278    ///
279    /// Like [`Self::retryable()`], this preserves the message and values and replaces any previous class without adding a cause.
280    /// The error records the caller's location, just like [`ErrorExt::raise()`].
281    #[track_caller]
282    pub fn retryable_error(self) -> Error {
283        self.retryable().raise()
284    }
285
286    /// The caller requested cancellation; stop rather than retry.
287    ///
288    /// Replaces any previous class without changing the message or values or adding a cause.
289    pub fn cancelled(self) -> Self {
290        self.with_class(Class::Cancelled)
291    }
292
293    /// Apply [`Self::cancelled()`] and raise the message, recording the caller location.
294    #[track_caller]
295    pub fn cancelled_error(self) -> Error {
296        self.cancelled().raise()
297    }
298
299    /// Authorization or permissions are insufficient; obtain authorization or change permissions.
300    ///
301    /// Replaces any previous class without changing the message or values or adding a cause.
302    pub fn permission_denied(self) -> Self {
303        self.with_class(Class::PermissionDenied)
304    }
305
306    /// Apply [`Self::permission_denied()`] and raise the message, recording the caller location.
307    #[track_caller]
308    pub fn permission_denied_error(self) -> Error {
309        self.permission_denied().raise()
310    }
311
312    /// Credentials are missing or rejected; obtain or refresh credentials.
313    ///
314    /// Replaces any previous class without changing the message or values or adding a cause.
315    pub fn unauthenticated(self) -> Self {
316        self.with_class(Class::Unauthenticated)
317    }
318
319    /// Apply [`Self::unauthenticated()`] and raise the message, recording the caller location.
320    #[track_caller]
321    pub fn unauthenticated_error(self) -> Error {
322        self.unauthenticated().raise()
323    }
324
325    /// Current state conflicts with the operation; refresh or reconcile state before retrying.
326    ///
327    /// Replaces any previous class without changing the message or values or adding a cause.
328    pub fn conflict(self) -> Self {
329        self.with_class(Class::Conflict)
330    }
331
332    /// Apply [`Self::conflict()`] and raise the message, recording the caller location.
333    #[track_caller]
334    pub fn conflict_error(self) -> Error {
335        self.conflict().raise()
336    }
337
338    /// A required capability is unsupported; switch implementation, format, protocol, or strategy.
339    ///
340    /// Replaces any previous class without changing the message or values or adding a cause.
341    pub fn unsupported(self) -> Self {
342        self.with_class(Class::Unsupported)
343    }
344
345    /// Apply [`Self::unsupported()`] and raise the message, recording the caller location.
346    #[track_caller]
347    pub fn unsupported_error(self) -> Error {
348        self.unsupported().raise()
349    }
350
351    /// Classify an exhausted resource as [`Class::ResourceExhaustion`] of `kind`.
352    ///
353    /// Like [`Self::with_class()`], this replaces any previous class without changing the message or values or adding a cause.
354    pub fn resource_exhaustion(self, kind: ResourceExhaustionKind) -> Self {
355        self.with_class(Class::ResourceExhaustion(kind))
356    }
357
358    /// Classify an exhausted resource of `kind` and raise it as an [`Error`].
359    ///
360    /// Like [`Self::resource_exhaustion()`], this preserves the message and values and replaces any previous class without adding a cause.
361    /// The error records the caller's location, just like [`ErrorExt::raise()`].
362    #[track_caller]
363    pub fn resource_exhaustion_error(self, kind: ResourceExhaustionKind) -> Error {
364        self.resource_exhaustion(kind).raise()
365    }
366
367    /// Classify an exceeded application-configured allocation limit.
368    ///
369    /// Like [`Self::resource_exhaustion()`], this preserves the message and values and replaces any previous class without adding a cause.
370    pub fn allocation_limit(self) -> Self {
371        self.resource_exhaustion(ResourceExhaustionKind::AllocationLimit)
372    }
373
374    /// Classify an exceeded application-configured allocation limit and raise it as an [`Error`].
375    ///
376    /// Like [`Self::allocation_limit()`], this preserves the message and values and replaces any previous class without adding a cause.
377    /// The error records the caller's location, just like [`ErrorExt::raise()`].
378    #[track_caller]
379    pub fn allocation_limit_error(self) -> Error {
380        self.allocation_limit().raise()
381    }
382
383    /// Classify an unrepresentable allocation size or memory that could not be reserved.
384    ///
385    /// Like [`Self::resource_exhaustion()`], this preserves the message and values and replaces any previous class without adding a cause.
386    pub fn allocation_failure(self) -> Self {
387        self.resource_exhaustion(ResourceExhaustionKind::AllocationFailure)
388    }
389
390    /// Classify an unrepresentable allocation size or memory that could not be reserved and raise it as an [`Error`].
391    ///
392    /// Like [`Self::allocation_failure()`], this preserves the message and values and replaces any previous class without adding a cause.
393    /// The error records the caller's location, just like [`ErrorExt::raise()`].
394    #[track_caller]
395    pub fn allocation_failure_error(self) -> Error {
396        self.allocation_failure().raise()
397    }
398}
399
400/// Metadata builders
401impl Message {
402    /// Record the offending input using the [input validation schema](Metadata#input-validation).
403    ///
404    /// Replaces `input` in this context, preserving its representation through [`MetadataValue`]. Other values,
405    /// the message, and the classification are unchanged; input does not imply [`Class::Validation`].
406    /// Only record already-known input that is appropriate for diagnostics, never secrets or other sensitive data.
407    pub fn with_input(self, input: impl Into<MetadataValue>) -> Self {
408        self.with("input", input)
409    }
410
411    /// Record a command's program and exit status using the [external program runtime failure schema](Metadata#external-program-runtime-failure).
412    ///
413    /// Replaces `program` with the native program name or path from [`std::process::Command::get_program()`], without
414    /// resolving it, and `exit_status` with the status's display representation. Sets `exit_code` when available,
415    /// otherwise removes any previous `exit_code`. Other values, the message, and the classification are unchanged.
416    /// This does not require a failed status or imply a recovery class, and does not capture output.
417    pub fn with_command_status(self, command: &std::process::Command, status: std::process::ExitStatus) -> Self {
418        self.with_program(command.get_program()).with_exit_status(status)
419    }
420
421    /// Record an already-known program name or path as `program`, without resolving it or changing the classification.
422    ///
423    /// Use [`std::process::Command::get_program()`] when a prepared command is available.
424    pub fn with_program(self, program: impl AsRef<std::ffi::OsStr>) -> Self {
425        self.with("program", std::path::Path::new(program.as_ref()))
426    }
427
428    /// Record `exit_status` and an available `exit_code` using the
429    /// [external program runtime failure schema](Metadata#external-program-runtime-failure).
430    ///
431    /// Replaces any previous status and removes a previous `exit_code` if this status has none.
432    /// Does not require a failed status, change the classification, or collect program identity or output.
433    pub fn with_exit_status(mut self, status: std::process::ExitStatus) -> Self {
434        self = self.with("exit_status", status.to_string());
435        if let Some(code) = status.code() {
436            self = self.with("exit_code", code);
437        } else {
438            self.values.remove("exit_code");
439        }
440        self
441    }
442
443    /// Record a command's program, exit status, and already-captured output using the
444    /// [external program runtime failure schema](Metadata#external-program-runtime-failure).
445    ///
446    /// Like [`Self::with_command_status()`], replaces the program and status fields, then replaces `stdout` and
447    /// `stderr` with the captured bytes, including empty buffers. Does not execute a command or capture more output.
448    /// Only use this when both streams are appropriate for diagnostics; in particular, do not record credential output.
449    pub fn with_command_output(self, command: &std::process::Command, output: std::process::Output) -> Self {
450        self.with_command_status(command, output.status)
451            .with("stdout", output.stdout)
452            .with("stderr", output.stderr)
453    }
454
455    /// Add `value` under `key`, replacing any previous value in this context.
456    /// Inspect values through [`crate::Error::metadata()`], or [`crate::Exn::metadata()`] on typed exceptions.
457    pub fn with(mut self, key: impl Into<Cow<'static, str>>, value: impl Into<MetadataValue>) -> Self {
458        self.values.insert(key, value);
459        self
460    }
461}
462
463impl fmt::Debug for Message {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        let mut debug = f.debug_struct("Message");
466        debug.field("message", &self.message);
467        if let Some(class) = self.class {
468            debug.field("class", &format_args!("{class:?}"));
469        }
470        if !self.values.is_empty() {
471            debug.field("values", &format_args!("{:?}", self.values));
472        }
473        debug.finish()
474    }
475}
476
477impl fmt::Display for Message {
478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479        f.write_str(&self.message)?;
480        for (key, value) in self.values.iter() {
481            write!(f, ", {:?}={value}", DebugKey(key))?;
482        }
483        Ok(())
484    }
485}
486
487impl std::error::Error for Message {}
488
489impl From<Cow<'static, str>> for Message {
490    fn from(message: Cow<'static, str>) -> Self {
491        Self::new(message)
492    }
493}
494
495impl From<String> for Message {
496    fn from(message: String) -> Self {
497        Self::new(message)
498    }
499}
500
501impl From<&'static str> for Message {
502    fn from(message: &'static str) -> Self {
503        Self::new(message)
504    }
505}
506
507/// Create a diagnostic for invalid function or method input, classified as [`Class::Validation`].
508///
509/// If the offending input is recorded, use the `input` key from the [input validation schema](Metadata#input-validation).
510pub fn validation(message: impl Into<Cow<'static, str>>) -> Message {
511    Message::new(message).validation()
512}
513
514/// Create a diagnostic for malformed or internally inconsistent data, classified as [`Class::Corruption`].
515pub fn corruption(message: impl Into<Cow<'static, str>>) -> Message {
516    Message::new(message).corrupted()
517}
518
519/// Create a diagnostic for a missing resource, classified as [`Class::NotFound`].
520pub fn not_found(message: impl Into<Cow<'static, str>>) -> Message {
521    Message::new(message).not_found()
522}
523
524/// Create a diagnostic for an operation that may succeed when retried, classified as [`Class::Retryable`].
525pub fn retryable(message: impl Into<Cow<'static, str>>) -> Message {
526    Message::new(message).retryable()
527}
528
529/// The caller requested cancellation; stop rather than retry.
530pub fn cancelled(message: impl Into<Cow<'static, str>>) -> Message {
531    Message::new(message).cancelled()
532}
533
534/// Authorization or permissions are insufficient; obtain authorization or change permissions.
535pub fn permission_denied(message: impl Into<Cow<'static, str>>) -> Message {
536    Message::new(message).permission_denied()
537}
538
539/// Credentials are missing or rejected; obtain or refresh credentials.
540pub fn unauthenticated(message: impl Into<Cow<'static, str>>) -> Message {
541    Message::new(message).unauthenticated()
542}
543
544/// Current state conflicts with the operation; refresh or reconcile state before retrying.
545pub fn conflict(message: impl Into<Cow<'static, str>>) -> Message {
546    Message::new(message).conflict()
547}
548
549/// A required capability is unsupported; switch implementation, format, protocol, or strategy.
550pub fn unsupported(message: impl Into<Cow<'static, str>>) -> Message {
551    Message::new(message).unsupported()
552}
553
554/// Create a diagnostic for an exhausted resource, classified as [`Class::ResourceExhaustion`] of `kind`.
555pub fn resource_exhaustion(kind: ResourceExhaustionKind, message: impl Into<Cow<'static, str>>) -> Message {
556    Message::new(message).resource_exhaustion(kind)
557}
558
559/// Create a diagnostic for an exceeded application-configured allocation limit.
560pub fn allocation_limit(message: impl Into<Cow<'static, str>>) -> Message {
561    Message::new(message).allocation_limit()
562}
563
564/// Create a diagnostic for an unrepresentable allocation size or memory that could not be reserved.
565pub fn allocation_failure(message: impl Into<Cow<'static, str>>) -> Message {
566    Message::new(message).allocation_failure()
567}
568
569/// An owned scalar value in a [`Metadata`] dictionary. Bytes and native paths retain their original representation.
570///
571/// Debug formatting keeps the variant and its value on a single line, even in pretty output.
572/// Byte values always use `Vec<u8>`; the optional `bstr` feature adds conversions from `BString` and `&BStr`.
573#[derive(Clone, PartialEq)]
574#[non_exhaustive]
575pub enum MetadataValue {
576    /// A boolean.
577    Bool(bool),
578    /// A signed integer.
579    I64(i64),
580    /// An unsigned integer.
581    U64(u64),
582    /// A floating-point number.
583    F64(f64),
584    /// UTF-8 text.
585    String(String),
586    /// An arbitrary byte string, pretty-printed on debug or display.
587    Bytes(Vec<u8>),
588    /// A native filesystem path.
589    Path(PathBuf),
590}
591
592impl fmt::Debug for MetadataValue {
593    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594        match self {
595            MetadataValue::Bool(value) => write!(f, "Bool({value:?})"),
596            MetadataValue::I64(value) => write!(f, "I64({value:?})"),
597            MetadataValue::U64(value) => write!(f, "U64({value:?})"),
598            MetadataValue::F64(value) => write!(f, "F64({value:?})"),
599            MetadataValue::String(value) => write!(f, "String({value:?})"),
600            MetadataValue::Bytes(value) => {
601                f.write_str("Bytes(")?;
602                fmt::Debug::fmt(&DebugBytes(value), f)?;
603                f.write_str(")")
604            }
605            MetadataValue::Path(value) => write!(f, "Path({value:?})"),
606        }
607    }
608}
609
610impl fmt::Display for MetadataValue {
611    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612        match self {
613            MetadataValue::Bool(value) => fmt::Display::fmt(value, f),
614            MetadataValue::I64(value) => fmt::Display::fmt(value, f),
615            MetadataValue::U64(value) => fmt::Display::fmt(value, f),
616            MetadataValue::F64(value) => fmt::Display::fmt(value, f),
617            MetadataValue::String(value) => fmt::Debug::fmt(value, f),
618            MetadataValue::Bytes(value) => fmt::Debug::fmt(&DebugBytes(value), f),
619            MetadataValue::Path(value) => fmt::Debug::fmt(value, f),
620        }
621    }
622}
623
624macro_rules! from {
625    ($variant:ident: $($ty:ty),+ $(,)?) => {
626        $(impl From<$ty> for MetadataValue {
627            fn from(value: $ty) -> Self {
628                Self::$variant(value.into())
629            }
630        })+
631    };
632}
633
634from!(Bool: bool);
635from!(I64: i8, i16, i32, i64);
636from!(U64: u8, u16, u32, u64);
637from!(F64: f32, f64);
638from!(String: String, &str);
639from!(Bytes: Vec<u8>, &[u8]);
640#[cfg(feature = "bstr")]
641from!(Bytes: bstr::BString);
642from!(Path: PathBuf, &std::path::Path);
643
644#[cfg(feature = "bstr")]
645impl From<&bstr::BStr> for MetadataValue {
646    fn from(value: &bstr::BStr) -> Self {
647        Self::Bytes(value.to_vec())
648    }
649}
650
651impl From<usize> for MetadataValue {
652    fn from(value: usize) -> Self {
653        Self::U64(value as u64)
654    }
655}
656
657impl From<isize> for MetadataValue {
658    fn from(value: isize) -> Self {
659        Self::I64(value as i64)
660    }
661}
662
663struct DebugBytes<'a>(&'a [u8]);
664
665impl fmt::Debug for DebugBytes<'_> {
666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667        f.write_str("\"")?;
668        let mut bytes = self.0;
669        while !bytes.is_empty() {
670            let (text, invalid) = match std::str::from_utf8(bytes) {
671                Ok(text) => (text, &[][..]),
672                Err(err) => {
673                    let (valid, rest) = bytes.split_at(err.valid_up_to());
674                    let text = std::str::from_utf8(valid).map_err(|_| fmt::Error)?;
675                    (text, &rest[..err.error_len().unwrap_or(rest.len())])
676                }
677            };
678            for ch in text.chars() {
679                match ch {
680                    '\0' => f.write_str("\\0")?,
681                    '\x01'..='\x7f' => write!(f, "{}", (ch as u8).escape_ascii())?,
682                    _ => write!(f, "{}", ch.escape_debug())?,
683                }
684            }
685            for byte in invalid {
686                write!(f, "\\x{byte:02x}")?;
687            }
688            bytes = &bytes[text.len() + invalid.len()..];
689        }
690        f.write_str("\"")
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    #[cfg(feature = "bstr")]
697    #[test]
698    fn bstr_inputs_convert_to_byte_metadata() {
699        let input = b"ref\xff";
700        let owned = bstr::BString::from(input.as_slice());
701        let allocation = owned.as_ptr();
702        let value = super::Message::new("invalid input")
703            .with_input(owned)
704            .values
705            .remove("input");
706        let Some(super::MetadataValue::Bytes(bytes)) = value else {
707            panic!("owned byte strings must become byte metadata");
708        };
709        assert_eq!(bytes, input, "owned input retains every byte");
710        assert_eq!(bytes.as_ptr(), allocation, "owned conversion reuses the allocation");
711        assert_eq!(
712            super::Message::new("invalid input")
713                .with_input(bstr::BStr::new(input))
714                .values["input"],
715            super::MetadataValue::Bytes(bytes),
716            "borrowed byte strings convert directly without losing invalid UTF-8"
717        );
718    }
719
720    #[test]
721    fn byte_metadata_preserves_and_formats_input() {
722        let input = b"hello\0\n\"'\\\xff\xf0\x9f";
723        let value = super::MetadataValue::from(input.as_slice());
724        let super::MetadataValue::Bytes(bytes) = &value else {
725            panic!("byte input must remain byte metadata");
726        };
727        assert_eq!(bytes.as_slice(), input, "metadata retains the exact input bytes");
728        assert_eq!(
729            format!("{value}"),
730            r#""hello\0\n\"\'\\\xff\xf0\x9f""#,
731            "display escapes control characters and truncated UTF-8 without data loss"
732        );
733    }
734
735    #[cfg(feature = "bstr")]
736    #[test]
737    fn dependency_free_byte_formatting_matches_bstr() {
738        for bytes in [
739            Vec::new(),
740            (0..=u8::MAX).collect(),
741            "你好\u{fffd}\u{200d}\n\0\"'\\".as_bytes().to_vec(),
742            b"valid\xf0\x9f\x92\xa9\xff\xe2\x82text\xc0\xaf\xed\xa0\x80\xf0\x9f".to_vec(),
743        ] {
744            assert_eq!(
745                format!("{:?}", super::DebugBytes(&bytes)),
746                format!("{:?}", bstr::BStr::new(&bytes)),
747                "dependency-free formatting preserves UTF-8 and escapes invalid bytes like bstr"
748            );
749        }
750    }
751}