Skip to main content

powerio_core/
validation.rs

1pub const MAX_IDENTIFIER_BYTES: usize = 65_536;
2pub const MAX_DIAGNOSTIC_MESSAGE_BYTES: usize = 16_384;
3/// Raw bytes retained for one message while decoding, before sanitization.
4/// Every writer sanitizes at construction, so a stored message near this bound
5/// was not produced by PowerIO; past it the raw text is truncated, and the
6/// result is still sanitized to one bounded line.
7pub 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;
19/// Module level record counts. Each stored module list is refused at its
20/// count while it is decoded, so a small hostile document cannot declare its
21/// way into an unbounded record allocation.
22pub 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
28/// A locator identifies an element, so it is bounded but never shortened: a
29/// truncated RFC 6901 pointer names a different element, or none.
30pub(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}