1use serde_json::Value;
2
3pub fn parse_jsonc(input: &str) -> Result<Value, serde_json::Error> {
6 let stripped = strip_json_comments(input);
7 serde_json::from_str(&stripped)
8}
9
10fn strip_json_comments(input: &str) -> String {
11 let bytes = input.as_bytes();
12 let len = bytes.len();
13 let mut out = String::with_capacity(len);
14 let mut i = 0;
15
16 while i < len {
17 let b = bytes[i];
18
19 if b == b'"' {
20 out.push('"');
21 i += 1;
22 while i < len {
23 let c = bytes[i];
24 out.push(c as char);
25 i += 1;
26 if c == b'\\' && i < len {
27 out.push(bytes[i] as char);
28 i += 1;
29 } else if c == b'"' {
30 break;
31 }
32 }
33 continue;
34 }
35
36 if b == b'/' && i + 1 < len {
37 if bytes[i + 1] == b'/' {
38 i += 2;
39 while i < len && bytes[i] != b'\n' {
40 i += 1;
41 }
42 continue;
43 }
44 if bytes[i + 1] == b'*' {
45 i += 2;
46 while i + 1 < len {
47 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
48 i += 2;
49 break;
50 }
51 i += 1;
52 }
53 continue;
54 }
55 }
56
57 out.push(b as char);
58 i += 1;
59 }
60
61 out
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn strips_line_comments() {
70 let input = r#"{
71 // this is a comment
72 "key": "value"
73}"#;
74 let v = parse_jsonc(input).unwrap();
75 assert_eq!(v["key"], "value");
76 }
77
78 #[test]
79 fn strips_block_comments() {
80 let input = r#"{
81 /* block
82 comment */
83 "key": "value"
84}"#;
85 let v = parse_jsonc(input).unwrap();
86 assert_eq!(v["key"], "value");
87 }
88
89 #[test]
90 fn preserves_slashes_in_strings() {
91 let input = r#"{"url": "https://example.com/path"}"#;
92 let v = parse_jsonc(input).unwrap();
93 assert_eq!(v["url"], "https://example.com/path");
94 }
95
96 #[test]
97 fn preserves_comment_like_content_in_strings() {
98 let input = r#"{"note": "see // inline", "code": "/* not a comment */"}"#;
99 let v = parse_jsonc(input).unwrap();
100 assert_eq!(v["note"], "see // inline");
101 assert_eq!(v["code"], "/* not a comment */");
102 }
103
104 #[test]
105 fn handles_escaped_quotes_in_strings() {
106 let input = r#"{"msg": "say \"hello\" // world"}"#;
107 let v = parse_jsonc(input).unwrap();
108 assert_eq!(v["msg"], r#"say "hello" // world"#);
109 }
110
111 #[test]
112 fn handles_trailing_comma_free_json() {
113 let input = r#"{
114 "a": 1,
115 // comment between entries
116 "b": 2
117}"#;
118 let v = parse_jsonc(input).unwrap();
119 assert_eq!(v["a"], 1);
120 assert_eq!(v["b"], 2);
121 }
122
123 #[test]
124 fn empty_input() {
125 assert!(parse_jsonc("").is_err());
126 }
127
128 #[test]
129 fn pure_json_passthrough() {
130 let input = r#"{"key": "value", "num": 42}"#;
131 let v = parse_jsonc(input).unwrap();
132 assert_eq!(v["key"], "value");
133 assert_eq!(v["num"], 42);
134 }
135
136 #[test]
137 fn real_opencode_config_with_comments() {
138 let input = r#"{
139 // OpenCode configuration
140 "$schema": "https://opencode.ai/config.json",
141 "mcp": {
142 /* existing tool */
143 "my-tool": {
144 "type": "local",
145 "command": ["my-tool"],
146 "enabled": true
147 }
148 }
149}"#;
150 let v = parse_jsonc(input).unwrap();
151 assert_eq!(v["$schema"], "https://opencode.ai/config.json");
152 assert!(v["mcp"]["my-tool"]["enabled"].as_bool().unwrap());
153 }
154}