Skip to main content

zerolaunch_plugin_protocol/
codec.rs

1//! LSP Content-Length 帧的编码工具。
2//!
3//! 本模块提供纯同步函数,不依赖 tokio。异步 I/O 由各 crate 自行处理。
4
5use crate::jsonrpc::{Message, Notification, Request, Response};
6
7/// 单帧的最大字节数(16 MB)。
8/// 超过此大小的帧视为无效,防止内存溢出。
9pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
10
11/// LSP 帧头部的最大字节数限制(512 字节)。
12/// 防止恶意或损坏的发送方发送无限长的头部行。
13pub const MAX_HEADER_SIZE: usize = 512;
14
15/// 将 payload 编码为完整的 LSP Content-Length 帧格式。
16///
17/// 返回 `Content-Length: N\r\n\r\n{payload}` 格式的字节序列,
18/// 可直接写入任意 `AsyncWrite` 或 `Write`。
19///
20pub fn encode_frame(payload: &[u8]) -> Vec<u8> {
21    let header = format!("Content-Length: {}\r\n\r\n", payload.len());
22    let mut frame = Vec::with_capacity(header.len() + payload.len());
23    frame.extend_from_slice(header.as_bytes());
24    frame.extend_from_slice(payload);
25    frame
26}
27
28/// 判断字符串是否为“疑似二进制数据”(如 base64 编码的图标/资源字节)。
29///
30/// 启发式规则:
31/// - 长度超过阈值(64 字符);
32/// - base64 字母表(A-Za-z0-9+/=)字符占比超过 98%——
33///   普通文本/JSON 含空格、标点与非 ASCII 字符,占比会显著更低。
34fn looks_like_base64(s: &str) -> bool {
35    if s.len() <= 64 {
36        return false;
37    }
38    // base64 字母表(A-Za-z0-9+/=)覆盖绝大多数字符即判定为疑似二进制。
39    // 普通文本含空格、标点与非 ASCII 字符,占比会被显著拉低。
40    let total = s.chars().count() as f64;
41    let base64_chars = s
42        .chars()
43        .filter(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
44        .count() as f64;
45    base64_chars / total > 0.98
46}
47
48/// 将 JSON 值转换为适合 debug 日志展示的形式:
49/// 文本结构完整保留,疑似二进制的长 base64 字符串替换为
50/// `"<base64 len=... prefix=...>"` 摘要,避免日志被二进制数据刷屏。
51pub fn summarize_value(v: &serde_json::Value) -> serde_json::Value {
52    match v {
53        serde_json::Value::String(s) if looks_like_base64(s) => {
54            let prefix: String = s.chars().take(64).collect();
55            serde_json::Value::String(format!("<base64 len={} prefix={}>", s.len(), prefix))
56        }
57        serde_json::Value::Array(items) => {
58            serde_json::Value::Array(items.iter().map(summarize_value).collect())
59        }
60        serde_json::Value::Object(map) => serde_json::Value::Object(
61            map.iter()
62                .map(|(k, val)| (k.clone(), summarize_value(val)))
63                .collect(),
64        ),
65        other => other.clone(),
66    }
67}
68
69/// 将 JSON-RPC 消息转换为适合 debug 日志完整展示的格式
70/// (二进制数据字段被摘要,其余原样)。
71pub fn summarize_message(msg: &Message) -> Message {
72    match msg {
73        Message::Request(req) => Message::Request(Request {
74            jsonrpc: req.jsonrpc.clone(),
75            id: req.id,
76            method: req.method.clone(),
77            params: summarize_value(&req.params),
78        }),
79        Message::Response(resp) => Message::Response(Response {
80            jsonrpc: resp.jsonrpc.clone(),
81            id: resp.id,
82            result: resp.result.as_ref().map(summarize_value),
83            error: resp.error.clone(),
84        }),
85        Message::Notification(notif) => Message::Notification(Notification {
86            jsonrpc: notif.jsonrpc.clone(),
87            method: notif.method.clone(),
88            params: summarize_value(&notif.params),
89        }),
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use serde_json::json;
97
98    /// 普通文本结构必须完整保留(含嵌套数组/对象)。
99    #[test]
100    fn summarize_keeps_text_structure() {
101        let v = json!({
102            "name": "hello",
103            "list": [1, 2, {"deep": "world"}],
104        });
105        assert_eq!(summarize_value(&v), v);
106    }
107
108    /// 短 base64 字符串(< 64 字符)不算二进制,原样展示。
109    #[test]
110    fn summarize_keeps_short_base64() {
111        let s = "aGVsbG8=";
112        assert_eq!(summarize_value(&json!(s)), json!("aGVsbG8="));
113    }
114
115    /// 长 base64 字符串(如图标/资源字节)替换为摘要。
116    #[test]
117    fn summarize_masks_long_base64() {
118        // 手工构造的 base64 样例:"ZeroLaunch" 重复 20 次的 base64 编码
119        // (WmVyb0xhdW5jaA== 是 "ZeroLaunch" 的 base64)。
120        let unit = "WmVyb0xhdW5jaA==";
121        let b64 = unit.repeat(20);
122        let summarized = summarize_value(&json!(b64));
123        let s = summarized.as_str().expect("仍是字符串");
124        assert!(s.starts_with("<base64 len="), "应被摘要: {}", s);
125        assert!(s.contains(&b64[..16]), "摘要应包含前缀");
126    }
127
128    /// 普通长文本(如模型回复)即使很长也不应被误判为二进制。
129    #[test]
130    fn summarize_keeps_long_text() {
131        let text = "这是普通的模型回复文本,".repeat(50);
132        assert_eq!(summarize_value(&json!(text)), json!(text));
133    }
134
135    /// 纯小写字母长串(无空格)是合法 base64 形态,应被摘要。
136    #[test]
137    fn summarize_masks_pure_lowercase_long_string() {
138        let s = "a".repeat(80);
139        let summarized = summarize_value(&json!(s));
140        let out = summarized.as_str().expect("仍是字符串");
141        assert!(
142            out.starts_with("<base64 len="),
143            "纯小写长串应被摘要: {}",
144            out
145        );
146    }
147
148    /// 带空格的英文长文本不算 base64,原样保留。
149    #[test]
150    fn summarize_keeps_long_english_with_spaces() {
151        let text = "the quick brown fox jumps over the lazy dog ".repeat(10);
152        assert_eq!(summarize_value(&json!(text)), json!(text));
153    }
154}