From a12b6e5ec46c70ed800d803fa8280702062e2173 Mon Sep 17 00:00:00 2001 From: johnnyfish Date: Tue, 26 May 2026 23:58:36 +0300 Subject: [PATCH] fix: handle JSONC settings files when injecting proxy config --- cmd/onecli/run.go | 72 ++++++++++++++++++++++++++++++++++-- cmd/onecli/run_test.go | 84 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/cmd/onecli/run.go b/cmd/onecli/run.go index cf12a21..c1ef411 100644 --- a/cmd/onecli/run.go +++ b/cmd/onecli/run.go @@ -392,7 +392,7 @@ func maybeInstallGatewayHook(out *output.Writer, agentName, baseDir string) { settings := make(map[string]any) data, readErr := os.ReadFile(settingsPath) if readErr == nil && len(data) > 0 { - if err := json.Unmarshal(data, &settings); err != nil { + if err := json.Unmarshal(stripJSONC(data), &settings); err != nil { return } } @@ -525,8 +525,8 @@ func mergeVSCodeProxySettings(path, proxyURL, authHeader string, terminalEnv map settings := make(map[string]any) data, readErr := os.ReadFile(path) if readErr == nil && len(data) > 0 { - if err := json.Unmarshal(data, &settings); err != nil { - return fmt.Errorf("settings contains comments or invalid JSON; cannot merge proxy config") + if err := json.Unmarshal(stripJSONC(data), &settings); err != nil { + return fmt.Errorf("settings contains invalid JSON; cannot merge proxy config: %w", err) } } settings["http.proxy"] = proxyURL @@ -560,6 +560,72 @@ func mergeVSCodeProxySettings(path, proxyURL, authHeader string, terminalEnv map return os.WriteFile(path, append(out, '\n'), 0o600) } +// stripJSONC converts JSONC (JSON with Comments) to strict JSON by removing +// single-line comments (//), block comments (/* */), and trailing commas +// before ] or }. String contents are preserved. +func stripJSONC(data []byte) []byte { + out := make([]byte, 0, len(data)) + i := 0 + for i < len(data) { + // String literal — copy verbatim, including escaped quotes. + if data[i] == '"' { + out = append(out, '"') + i++ + for i < len(data) { + if data[i] == '\\' && i+1 < len(data) { + out = append(out, data[i], data[i+1]) + i += 2 + continue + } + out = append(out, data[i]) + if data[i] == '"' { + i++ + break + } + i++ + } + continue + } + // Single-line comment. + if i+1 < len(data) && data[i] == '/' && data[i+1] == '/' { + for i < len(data) && data[i] != '\n' { + i++ + } + continue + } + // Block comment. + if i+1 < len(data) && data[i] == '/' && data[i+1] == '*' { + i += 2 + for i < len(data) { + if data[i] == '*' && i+1 < len(data) && data[i+1] == '/' { + i += 2 + break + } + i++ + } + continue + } + out = append(out, data[i]) + i++ + } + + // Remove trailing commas: `,` followed by optional whitespace then `]` or `}`. + result := make([]byte, 0, len(out)) + for j := 0; j < len(out); j++ { + if out[j] == ',' { + k := j + 1 + for k < len(out) && (out[k] == ' ' || out[k] == '\t' || out[k] == '\n' || out[k] == '\r') { + k++ + } + if k < len(out) && (out[k] == ']' || out[k] == '}') { + continue + } + } + result = append(result, out[j]) + } + return result +} + func stripProxyCredentials(env []string) []string { proxyKeys := map[string]bool{ "HTTPS_PROXY": true, "HTTP_PROXY": true, diff --git a/cmd/onecli/run_test.go b/cmd/onecli/run_test.go index 3bbfbc9..d7e522a 100644 --- a/cmd/onecli/run_test.go +++ b/cmd/onecli/run_test.go @@ -252,21 +252,93 @@ func TestMergeVSCodeProxySettings_TerminalEnv(t *testing.T) { } } -func TestMergeVSCodeProxySettings_RejectsJSONC(t *testing.T) { +func TestMergeVSCodeProxySettings_HandlesJSONC(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.json") writeJSON(t, path, `{ // this is a comment - "editor.fontSize": 14 + "editor.fontSize": 14, } `) err := mergeVSCodeProxySettings(path, "https://proxy:8080", "Basic dTpw", nil) - if err == nil { - t.Fatal("expected error for JSONC input") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + got := readSettingsMap(t, path) + if got["editor.fontSize"] != float64(14) { + t.Errorf("editor.fontSize = %v, want 14", got["editor.fontSize"]) + } + if got["http.proxy"] != "https://proxy:8080" { + t.Errorf("http.proxy = %v", got["http.proxy"]) } - if !strings.Contains(err.Error(), "comments or invalid JSON") { - t.Errorf("error = %q, want mention of comments", err.Error()) +} + +func TestStripJSONC(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + "strict JSON unchanged", + `{"key": "value", "num": 42}`, + `{"key": "value", "num": 42}`, + }, + { + "single-line comments", + "{// comment\n\"key\": \"value\"\n}", + "{\n\"key\": \"value\"\n}", + }, + { + "block comments", + `{"key": /* inline */ "value"}`, + `{"key": "value"}`, + }, + { + "trailing comma before }", + `{"a": 1, "b": 2,}`, + `{"a": 1, "b": 2}`, + }, + { + "trailing comma before ]", + `[1, 2, 3,]`, + `[1, 2, 3]`, + }, + { + "trailing comma with whitespace", + "{\"a\": 1,\n }", + "{\"a\": 1\n }", + }, + { + "slashes inside strings preserved", + `{"url": "https://example.com"}`, + `{"url": "https://example.com"}`, + }, + { + "escaped quotes in strings", + `{"key": "val\"ue // not a comment"}`, + `{"key": "val\"ue // not a comment"}`, + }, + { + "unterminated block comment", + `{"a": 1} /* oops`, + `{"a": 1} `, + }, + { + "combined comments and trailing commas", + "{\n // editor settings\n \"fontSize\": 14,\n \"theme\": \"dark\", // inline\n}", + "{\n \n \"fontSize\": 14,\n \"theme\": \"dark\" \n}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := string(stripJSONC([]byte(tt.input))) + if got != tt.want { + t.Errorf("got %q\nwant %q", got, tt.want) + } + }) } }