powerio_core/
validation.rs1pub const MAX_IDENTIFIER_BYTES: usize = 65_536;
2pub const MAX_DIAGNOSTIC_MESSAGE_BYTES: usize = 16_384;
3pub const MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES: usize = 4 * MAX_DIAGNOSTIC_MESSAGE_BYTES;
8pub(crate) const MAX_ARTIFACT_PATH_BYTES: usize = 4_096;
9pub(crate) const MAX_ARTIFACT_SEGMENT_BYTES: usize = 255;
10pub(crate) const MAX_FORMAT_ID_BYTES: usize = 127;
11pub const MAX_DIAGNOSTIC_CODE_BYTES: usize = 255;
12pub const MAX_DIAGNOSTIC_TARGET_BYTES: usize = 8_192;
13pub const MAX_DIAGNOSTIC_SPANS: usize = 256;
14pub const MAX_DIAGNOSTIC_RELATED: usize = 256;
15pub const MAX_DIAGNOSTIC_DETAIL_KEYS: usize = 256;
16pub const MAX_SOURCE_MAP_SPANS: usize = 256;
17pub const MAX_HISTORY_PARAMETERS: usize = 256;
18pub const MAX_HISTORY_NOTES: usize = 256;
19pub const MAX_MODULE_SOURCES: usize = 262_144;
23pub const MAX_MODULE_SOURCE_MAP_ENTRIES: usize = 262_144;
24pub const MAX_MODULE_DIAGNOSTICS: usize = 262_144;
25pub const MAX_MODULE_HISTORY_ENTRIES: usize = 65_536;
26pub const MAX_MODULE_EXTENSION_KEYS: usize = 4_096;
27
28pub(crate) fn valid_diagnostic_target(target: &str) -> bool {
31 !target.is_empty() && target.len() <= MAX_DIAGNOSTIC_TARGET_BYTES && !target.contains('\0')
32}
33
34pub(crate) fn valid_nonempty_text(value: &str) -> bool {
35 !value.is_empty() && value.len() <= MAX_IDENTIFIER_BYTES && !value.contains('\0')
36}
37
38pub(crate) fn sanitize_message(message: impl Into<String>) -> String {
39 let message = message.into();
40 let single_line = message
41 .split(['\n', '\r'])
42 .map(str::trim)
43 .filter(|part| !part.is_empty())
44 .collect::<Vec<_>>()
45 .join(" ");
46 truncate_utf8(single_line, MAX_DIAGNOSTIC_MESSAGE_BYTES)
47}
48
49fn truncate_utf8(mut value: String, limit: usize) -> String {
50 if value.len() <= limit {
51 return value;
52 }
53 let suffix = "…";
54 let mut end = limit.saturating_sub(suffix.len());
55 while !value.is_char_boundary(end) {
56 end -= 1;
57 }
58 value.truncate(end);
59 value.push_str(suffix);
60 value
61}
62
63pub(crate) fn valid_rfc6901_pointer(pointer: &str) -> bool {
64 if !pointer.is_empty() && !pointer.starts_with('/') {
65 return false;
66 }
67 let bytes = pointer.as_bytes();
68 let mut index = 0;
69 while index < bytes.len() {
70 if bytes[index] == b'~'
71 && (index + 1 == bytes.len() || !matches!(bytes[index + 1], b'0' | b'1'))
72 {
73 return false;
74 }
75 index += 1;
76 }
77 true
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn message_sanitization_is_one_line_and_utf8_safe() {
86 let message = format!("first\n{}", "é".repeat(MAX_DIAGNOSTIC_MESSAGE_BYTES));
87 let message = sanitize_message(message);
88 assert!(message.len() <= MAX_DIAGNOSTIC_MESSAGE_BYTES);
89 assert!(!message.contains(['\n', '\r']));
90 assert!(message.ends_with('…'));
91 }
92
93 #[test]
94 fn pointer_validation_checks_escape_sequences() {
95 assert!(valid_rfc6901_pointer(""));
96 assert!(valid_rfc6901_pointer("/a~1b/~0value"));
97 assert!(!valid_rfc6901_pointer("a"));
98 assert!(!valid_rfc6901_pointer("/~2"));
99 assert!(!valid_rfc6901_pointer("/~"));
100 }
101}