monoloop_contracts/
safe.rs1use crate::limits::TransactionLimits;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct DiagnosticCode(String);
10
11impl DiagnosticCode {
12 pub const MAX_BYTES: usize = 64;
14
15 pub fn try_new(value: impl Into<String>) -> Result<Self, SafeDiagnosticError> {
17 let s = value.into();
18 if s.is_empty() {
19 return Err(SafeDiagnosticError::EmptyCode);
20 }
21 if s.len() > Self::MAX_BYTES {
22 return Err(SafeDiagnosticError::CodeTooLong);
23 }
24 if s.chars().any(|c| c.is_control()) {
25 return Err(SafeDiagnosticError::ControlCharacter);
26 }
27 Ok(Self(s))
28 }
29
30 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct SafeDiagnostic {
39 pub code: DiagnosticCode,
41 pub message: Option<String>,
43}
44
45impl SafeDiagnostic {
46 pub fn try_new(
48 code: impl Into<String>,
49 message: Option<impl Into<String>>,
50 max_message_bytes: usize,
51 ) -> Result<Self, SafeDiagnosticError> {
52 let code = DiagnosticCode::try_new(code)?;
53 let message = match message {
54 None => None,
55 Some(m) => {
56 let mut s = m.into();
57 if s.chars().any(|c| c.is_control()) {
58 return Err(SafeDiagnosticError::ControlCharacter);
59 }
60 if s.len() > max_message_bytes {
61 s.truncate(max_message_bytes);
62 while !s.is_char_boundary(s.len()) {
64 s.pop();
65 }
66 }
67 if s.is_empty() {
68 None
69 } else {
70 Some(s)
71 }
72 }
73 };
74 Ok(Self { code, message })
75 }
76
77 pub fn try_new_default(
79 code: impl Into<String>,
80 message: Option<impl Into<String>>,
81 ) -> Result<Self, SafeDiagnosticError> {
82 let limits = TransactionLimits::default();
83 Self::try_new(code, message, limits.max_diagnostic_bytes)
84 }
85}
86
87#[derive(Clone, Debug, Error, PartialEq, Eq)]
89pub enum SafeDiagnosticError {
90 #[error("diagnostic code must be non-empty")]
92 EmptyCode,
93 #[error("diagnostic code exceeds maximum length")]
95 CodeTooLong,
96 #[error("diagnostic must not contain control characters")]
98 ControlCharacter,
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn redacts_by_truncation_not_secret_content() {
107 let long = "x".repeat(2000);
108 let d = SafeDiagnostic::try_new("ok", Some(long), 32).unwrap();
109 assert!(d.message.as_ref().unwrap().len() <= 32);
110 }
111}