use serde_json::{Value, json};
const VALID_ESCAPES: [char; 8] = ['"', '\\', '/', 'b', 'f', 'n', 'r', 't'];
fn escape_control(c: char) -> String {
match c {
'\u{08}' => "\\b".to_string(),
'\u{0c}' => "\\f".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
other => format!("\\u{:04x}", other as u32),
}
}
pub(crate) fn repair_json(json: &str) -> String {
let chars: Vec<char> = json.chars().collect();
let mut out = String::with_capacity(json.len());
let mut in_string = false;
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if !in_string {
out.push(c);
if c == '"' {
in_string = true;
}
i += 1;
continue;
}
if c == '"' {
out.push(c);
in_string = false;
i += 1;
continue;
}
if c == '\\' {
let Some(&next) = chars.get(i + 1) else {
out.push_str("\\\\");
i += 1;
continue;
};
if next == 'u' {
if chars.len() - (i + 2) < 4 {
break;
}
let digits: String = chars[i + 2..i + 6].iter().collect();
if digits.chars().all(|d| d.is_ascii_hexdigit()) {
out.push_str("\\u");
out.push_str(&digits);
i += 6;
continue;
}
}
if VALID_ESCAPES.contains(&next) {
out.push('\\');
out.push(next);
i += 2;
continue;
}
out.push_str("\\\\");
i += 1;
continue;
}
if (c as u32) <= 0x1f {
out.push_str(&escape_control(c));
} else {
out.push(c);
}
i += 1;
}
out
}
fn parse_json_with_repair(json: &str) -> Result<Value, String> {
match serde_json::from_str::<Value>(json) {
Ok(v) => Ok(v),
Err(err) => {
let repaired = repair_json(json);
if repaired != json
&& let Ok(v) = serde_json::from_str::<Value>(&repaired)
{
return Ok(v);
}
Err(err.to_string())
}
}
}
fn partial_parse(s: &str) -> Option<Value> {
let chars: Vec<char> = s.chars().collect();
for cut in (1..=chars.len()).rev() {
let candidate: String = chars[..cut].iter().collect();
if let Ok(v) = serde_json::from_str::<Value>(&candidate) {
return Some(v);
}
}
None
}
fn close_incomplete_json(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
let mut closers: Vec<char> = Vec::new();
let mut in_string = false;
let mut escaped = false;
for c in s.chars() {
out.push(c);
if in_string {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
continue;
}
match c {
'"' => in_string = true,
'{' => closers.push('}'),
'[' => closers.push(']'),
'}' | ']' => {
closers.pop();
}
_ => {}
}
}
if in_string {
if escaped {
out.pop();
}
out.push('"');
}
while let Some(closer) = closers.pop() {
out.push(closer);
}
out
}
pub(crate) fn parse_streaming_json(s: &str) -> Value {
if s.trim().is_empty() {
return json!({});
}
if let Ok(v) = parse_json_with_repair(s) {
return v;
}
if let Some(v) = partial_parse(s) {
return v;
}
let repaired = repair_json(s);
if let Some(v) = partial_parse(&repaired) {
return v;
}
if let Ok(v) = serde_json::from_str::<Value>(&close_incomplete_json(&repaired)) {
return v;
}
json!({})
}
pub(crate) fn parse_tool_call_arguments(json: Option<&str>) -> (Value, Option<String>) {
let Some(json) = json else {
return (json!({}), None);
};
if json.trim().is_empty() {
return (json!({}), None);
}
match parse_json_with_repair(json) {
Ok(v @ Value::Object(_)) => (v, None),
Ok(_) => (
json!({}),
Some(format!(
"Tool arguments must be a JSON object\n\nReceived arguments:\n{json}"
)),
),
Err(err) => (
json!({}),
Some(format!("{err}\n\nReceived arguments:\n{json}")),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repairs_invalid_unicode_escapes_without_dropping_fields() {
assert_eq!(
parse_streaming_json(r#"{"text":"\url{x}"}"#),
json!({ "text": "\\url{x}" })
);
assert_eq!(
parse_streaming_json(r#"{"path":"a.tex","content":"\usepackage{x}","mode":"w"}"#),
json!({ "path": "a.tex", "content": "\\usepackage{x}", "mode": "w" })
);
assert_eq!(
parse_streaming_json(r#"{"text":"caf\u00e9 \url"}"#),
json!({ "text": "café \\url" })
);
}
#[test]
fn truncated_unicode_escape_keeps_earlier_fields() {
assert_eq!(
parse_streaming_json(r#"{"text":"ab\u00"#),
json!({ "text": "ab" })
);
assert_eq!(
parse_streaming_json(r#"{"text":"ok","tail":"ab\u00"#),
json!({ "text": "ok", "tail": "ab" })
);
}
#[test]
fn escapes_raw_control_characters() {
assert_eq!(
parse_streaming_json("{\"text\":\"a\nb\"}"),
json!({ "text": "a\nb" })
);
}
#[test]
fn finalized_arguments_never_keep_a_partial_prefix() {
let (args, err) = parse_tool_call_arguments(Some(r#"{"path":"a.rs","limit":}"#));
assert_eq!(args, json!({}));
let err = err.expect("malformed arguments must report an error");
assert!(err.contains("Received arguments:"), "{err}");
assert!(err.contains(r#""path":"a.rs""#), "{err}");
}
#[test]
fn finalized_arguments_require_an_object() {
for raw in ["[1,2]", "null", "42", r#""text""#] {
let (args, err) = parse_tool_call_arguments(Some(raw));
assert_eq!(args, json!({}), "{raw}");
assert!(err.is_some(), "{raw}");
}
let (args, err) = parse_tool_call_arguments(Some(r#"{"content":"\usepackage{x}"}"#));
assert_eq!(err, None);
assert_eq!(args, json!({ "content": "\\usepackage{x}" }));
assert_eq!(parse_tool_call_arguments(None), (json!({}), None));
assert_eq!(parse_tool_call_arguments(Some(" ")), (json!({}), None));
}
}