use serde_json::Value;
pub fn parse_or_repair(raw: &str) -> Value {
if raw.trim().is_empty() {
return serde_json::json!({});
}
if let Ok(v) = serde_json::from_str::<Value>(raw) {
return v;
}
if let Some(repaired) = try_repair(raw)
&& let Ok(v) = serde_json::from_str::<Value>(&repaired)
{
tracing::warn!(
"[JSON_REPAIR] recovered partial args ({} bytes → {} bytes): {:?}",
raw.len(),
repaired.len(),
raw.chars()
.rev()
.take(80)
.collect::<String>()
.chars()
.rev()
.collect::<String>()
);
return v;
}
tracing::warn!(
"[JSON_REPAIR] FAILED to recover partial args ({} bytes): {:?}",
raw.len(),
raw.chars().take(200).collect::<String>()
);
serde_json::json!({
"_partial": raw,
"_repair_failed": true,
})
}
pub fn try_repair(raw: &str) -> Option<String> {
let mut chars: Vec<char> = raw.chars().collect();
while chars.last().is_some_and(|c| c.is_whitespace()) {
chars.pop();
}
if chars.is_empty() {
return None;
}
let mut in_string = false;
let mut escape = false;
let mut stack: Vec<char> = Vec::new();
let mut last_complete_value_end: Option<usize> = None;
let mut after_colon = false;
for (i, &c) in chars.iter().enumerate() {
if escape {
escape = false;
continue;
}
if c == '\\' && in_string {
escape = true;
continue;
}
if c == '"' {
in_string = !in_string;
if !in_string {
last_complete_value_end = Some(i);
after_colon = false; }
continue;
}
if in_string {
continue;
}
match c {
'{' | '[' => {
stack.push(c);
after_colon = false; }
'}' => {
if stack.last() == Some(&'{') {
stack.pop();
last_complete_value_end = Some(i);
after_colon = false;
} else {
return None; }
}
']' => {
if stack.last() == Some(&'[') {
stack.pop();
last_complete_value_end = Some(i);
after_colon = false;
} else {
return None;
}
}
':' => after_colon = true,
',' => after_colon = false,
c if !c.is_whitespace() => {
last_complete_value_end = Some(i);
after_colon = false;
}
_ => {}
}
}
let mut out: String = chars.iter().collect();
if in_string {
out.push('"');
}
if after_colon
&& !in_string
&& let Some(end) = last_complete_value_end
{
let bytes = out.as_bytes();
let mut cut = None;
for (i, &b) in bytes.iter().enumerate().take(end + 1).rev() {
if b == b',' {
cut = Some(i);
break;
}
if b == b'{' {
cut = Some(i + 1);
break;
}
}
if let Some(c) = cut {
out.truncate(c);
if out.ends_with(',') {
out.pop();
}
}
}
let trimmed = out.trim_end();
if let Some(stripped) = trimmed.strip_suffix(',') {
out = stripped.to_string();
}
while let Some(open) = stack.pop() {
match open {
'{' => out.push('}'),
'[' => out.push(']'),
_ => {}
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passes_through_valid_json() {
let v = parse_or_repair(r#"{"a":1,"b":"x"}"#);
assert_eq!(v["a"], 1);
assert_eq!(v["b"], "x");
}
#[test]
fn empty_returns_object() {
let v = parse_or_repair("");
assert!(v.is_object());
}
#[test]
fn closes_open_string() {
let v = parse_or_repair(r#"{"command":"git status"#);
assert_eq!(v["command"], "git status");
}
#[test]
fn closes_missing_brace() {
let v = parse_or_repair(r#"{"a":1,"b":2"#);
assert_eq!(v["a"], 1);
assert_eq!(v["b"], 2);
}
#[test]
fn drops_trailing_key_without_value() {
let v = parse_or_repair(r#"{"a":1,"b":"#);
assert_eq!(v["a"], 1);
assert!(v.get("b").is_none());
}
#[test]
fn closes_nested_array() {
let v = parse_or_repair(r#"{"items":[1,2,3"#);
assert_eq!(v["items"][0], 1);
assert_eq!(v["items"][2], 3);
}
#[test]
fn closes_string_inside_array() {
let v = parse_or_repair(r#"{"items":["a","b"#);
assert_eq!(v["items"][0], "a");
assert_eq!(v["items"][1], "b");
}
#[test]
fn unrecoverable_returns_partial_envelope() {
let v = parse_or_repair(r#"this is not json"#);
assert!(v["_repair_failed"].as_bool().unwrap_or(false));
assert_eq!(v["_partial"], "this is not json");
}
#[test]
fn handles_escaped_quote_in_string() {
let v = parse_or_repair(r#"{"msg":"he said \"hi"#);
assert_eq!(v["msg"], "he said \"hi");
}
#[test]
fn strips_trailing_comma() {
let v = parse_or_repair(r#"{"a":1,"#);
assert_eq!(v["a"], 1);
}
}