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}