Skip to main content

dialcache/
preview.rs

1//! Bounded previews and caps for mismatch warnings.
2
3use std::io::{self, Write};
4
5/// Maximum UTF-8 bytes of a logged cache key.
6pub const SHADOW_LOG_KEY_MAX_BYTES: usize = 2 * 1024;
7/// Maximum UTF-8 bytes of a logged value preview.
8pub const SHADOW_LOG_VALUE_MAX_BYTES: usize = 8 * 1024;
9/// Maximum UTF-8 bytes of a logged `diff_json`.
10pub const SHADOW_LOG_DIFF_MAX_BYTES: usize = 8 * 1024;
11/// Most difference entries a warning lists; `mismatch_path_count` keeps the
12/// uncut total.
13pub const SHADOW_MISMATCH_MAX_ENTRIES: usize = 64;
14/// Maximum UTF-8 bytes of one logged JSON Pointer.
15pub const SHADOW_MISMATCH_PATH_MAX_BYTES: usize = 512;
16/// Maximum UTF-8 bytes of one entry side's JSON in `diff_json`; a longer side
17/// is logged as its clamped JSON text.
18pub const SHADOW_MISMATCH_SIDE_MAX_BYTES: usize = 1024;
19/// Reference tokens in the deepest pointer the built-in walk reports.
20pub const SHADOW_MISMATCH_MAX_DEPTH: usize = 32;
21/// Marker appended to a clamped preview.
22pub const SHADOW_LOG_TRUNCATION_MARKER: &str = "...[truncated]";
23
24/// Retain only the logged prefix, but consume the complete serialization so
25/// errors beyond the cap still suppress the preview.
26pub(crate) struct JsonPrefix {
27    bytes: Vec<u8>,
28    truncated: bool,
29}
30
31impl JsonPrefix {
32    pub(crate) fn new() -> Self {
33        Self {
34            bytes: Vec::with_capacity(SHADOW_LOG_VALUE_MAX_BYTES),
35            truncated: false,
36        }
37    }
38
39    pub(crate) fn finish(mut self) -> String {
40        if self.truncated {
41            self.bytes
42                .truncate(SHADOW_LOG_VALUE_MAX_BYTES - SHADOW_LOG_TRUNCATION_MARKER.len());
43        }
44        // The prefix can end inside a multibyte character. JSON serialization
45        // produces valid UTF-8, so only that trailing character can be partial.
46        if let Err(error) = std::str::from_utf8(&self.bytes) {
47            self.bytes.truncate(error.valid_up_to());
48        }
49        let mut text = String::from_utf8(self.bytes).expect("valid JSON prefix");
50        if self.truncated {
51            text.push_str(SHADOW_LOG_TRUNCATION_MARKER);
52        }
53        text
54    }
55}
56
57impl Write for JsonPrefix {
58    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
59        let retained = bytes
60            .len()
61            .min(SHADOW_LOG_VALUE_MAX_BYTES - self.bytes.len());
62        self.bytes.extend_from_slice(&bytes[..retained]);
63        self.truncated |= retained < bytes.len();
64        Ok(bytes.len())
65    }
66
67    fn flush(&mut self) -> io::Result<()> {
68        Ok(())
69    }
70}
71
72/// The streamed value-field prefix of `value`, as warnings render it: the
73/// default preview of an operation built with JSON.
74pub(crate) fn json_preview<T: serde::Serialize>(value: &T) -> Option<String> {
75    let mut prefix = JsonPrefix::new();
76    serde_json::to_writer(&mut prefix, value).ok()?;
77    Some(prefix.finish())
78}
79
80/// Clamp text to `max_bytes` of UTF-8, ending a clamped result with the
81/// truncation marker on a character boundary.
82pub fn clamp_utf8(value: &str, max_bytes: usize) -> String {
83    if value.len() <= max_bytes {
84        return value.to_string();
85    }
86    let mut limit = max_bytes.saturating_sub(SHADOW_LOG_TRUNCATION_MARKER.len());
87    while limit > 0 && !value.is_char_boundary(limit) {
88        limit -= 1;
89    }
90    let mut result = String::with_capacity(limit + SHADOW_LOG_TRUNCATION_MARKER.len());
91    result.push_str(&value[..limit]);
92    result.push_str(SHADOW_LOG_TRUNCATION_MARKER);
93    result
94}
95
96/// Clamp a logical key to [`SHADOW_LOG_KEY_MAX_BYTES`].
97pub fn preview_key(key: &str) -> String {
98    clamp_utf8(key, SHADOW_LOG_KEY_MAX_BYTES)
99}
100
101/// Clamp a value's JSON preview to [`SHADOW_LOG_VALUE_MAX_BYTES`].
102pub fn preview_value(json: &str) -> String {
103    clamp_utf8(json, SHADOW_LOG_VALUE_MAX_BYTES)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use serde::ser::{Error, SerializeSeq};
110    use serde::Serialize;
111
112    #[test]
113    fn clamps_on_character_boundaries() {
114        let text = "é".repeat(2000);
115        let clamped = clamp_utf8(&text, 100);
116        assert!(clamped.len() <= 100);
117        assert!(clamped.ends_with(SHADOW_LOG_TRUNCATION_MARKER));
118        assert_eq!(clamp_utf8("short", 100), "short");
119    }
120
121    #[test]
122    fn streamed_json_preserves_the_existing_preview_prefix() {
123        for value in [
124            "short".to_owned(),
125            "x".repeat(SHADOW_LOG_VALUE_MAX_BYTES - 2),
126            "x".repeat(SHADOW_LOG_VALUE_MAX_BYTES - 1),
127            "é🙂".repeat(SHADOW_LOG_VALUE_MAX_BYTES),
128            "\n\t\"\\".repeat(SHADOW_LOG_VALUE_MAX_BYTES),
129        ] {
130            assert_eq!(
131                json_preview(&value).unwrap(),
132                preview_value(&serde_json::to_string(&value).unwrap())
133            );
134        }
135    }
136
137    struct LargeSequence {
138        fail_at_end: bool,
139    }
140
141    impl Serialize for LargeSequence {
142        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
143            let mut sequence = serializer.serialize_seq(Some(10_000))?;
144            for _ in 0..10_000 {
145                sequence.serialize_element(&"large element".repeat(100))?;
146            }
147            if self.fail_at_end {
148                return Err(S::Error::custom("failure after the logged prefix"));
149            }
150            sequence.end()
151        }
152    }
153
154    #[test]
155    fn large_json_construction_keeps_a_capped_buffer_and_detects_late_errors() {
156        let mut prefix = JsonPrefix::new();
157        serde_json::to_writer(&mut prefix, &LargeSequence { fail_at_end: false }).unwrap();
158        assert_eq!(prefix.bytes.len(), SHADOW_LOG_VALUE_MAX_BYTES);
159        assert_eq!(prefix.bytes.capacity(), SHADOW_LOG_VALUE_MAX_BYTES);
160        assert!(prefix.finish().ends_with(SHADOW_LOG_TRUNCATION_MARKER));
161        assert!(json_preview(&LargeSequence { fail_at_end: true }).is_none());
162    }
163}