Skip to content

Commit ba5af60

Browse files
Fix diff escaping issues (#2694)
* Fix diff escaping issues * Potential fix for code scanning alert no. 75: Double escaping or unescaping Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 4bf746d commit ba5af60

File tree

5 files changed

+76
-12
lines changed

5 files changed

+76
-12
lines changed

src/core/tools/applyDiffTool.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import path from "path"
1111
import fs from "fs/promises"
1212
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1313
import { telemetryService } from "../../services/telemetry/TelemetryService"
14-
14+
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1515
export async function applyDiffTool(
1616
cline: Cline,
1717
block: ToolUse,
@@ -21,7 +21,11 @@ export async function applyDiffTool(
2121
removeClosingTag: RemoveClosingTag,
2222
) {
2323
const relPath: string | undefined = block.params.path
24-
const diffContent: string | undefined = block.params.diff
24+
let diffContent: string | undefined = block.params.diff
25+
26+
if (diffContent && !cline.api.getModel().id.includes("claude")) {
27+
diffContent = unescapeHtmlEntities(diffContent)
28+
}
2529

2630
const sharedMessageProps: ClineSayTool = {
2731
tool: "appliedDiff",

src/core/tools/executeCommandTool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Cline } from "../Cline"
22
import { ToolUse } from "../assistant-message"
33
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
44
import { formatResponse } from "../prompts/responses"
5+
import { unescapeHtmlEntities } from "../../utils/text-normalization"
56

67
export async function executeCommandTool(
78
cline: Cline,
@@ -32,8 +33,8 @@ export async function executeCommandTool(
3233
return
3334
}
3435

35-
// unescape html entities (e.g. &lt; -> <)
36-
command = command.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&")
36+
// Unescape HTML entities
37+
command = unescapeHtmlEntities(command)
3738

3839
cline.consecutiveMistakeCount = 0
3940

src/core/tools/writeToFileTool.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils"
1414
import { everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
1515
import delay from "delay"
1616
import { detectCodeOmission } from "../../integrations/editor/detect-omission"
17+
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1718

1819
export async function writeToFileTool(
1920
cline: Cline,
@@ -60,13 +61,7 @@ export async function writeToFileTool(
6061
}
6162

6263
if (!cline.api.getModel().id.includes("claude")) {
63-
// it seems not just llama models are doing cline, but also gemini and potentially others
64-
if (newContent.includes("&gt;") || newContent.includes("&lt;") || newContent.includes("&quot;")) {
65-
newContent = newContent
66-
.replace(/&gt;/g, ">")
67-
.replace(/&lt;/g, "<")
68-
.replace(/&quot;/g, '"')
69-
}
64+
newContent = unescapeHtmlEntities(newContent)
7065
}
7166

7267
// Determine if the path is outside the workspace

src/utils/__tests__/text-normalization.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { normalizeString } from "../text-normalization"
1+
import { normalizeString, unescapeHtmlEntities } from "../text-normalization"
22

33
describe("Text normalization utilities", () => {
44
describe("normalizeString", () => {
@@ -30,4 +30,50 @@ describe("Text normalization utilities", () => {
3030
expect(normalizeString(input)).toBe('Let\'s test this-with some "fancy" punctuation... and spaces')
3131
})
3232
})
33+
34+
describe("unescapeHtmlEntities", () => {
35+
test("unescapes basic HTML entities", () => {
36+
expect(unescapeHtmlEntities("&lt;div&gt;Hello&lt;/div&gt;")).toBe("<div>Hello</div>")
37+
})
38+
39+
test("unescapes ampersand entity", () => {
40+
expect(unescapeHtmlEntities("This &amp; that")).toBe("This & that")
41+
})
42+
43+
test("unescapes quote entities", () => {
44+
expect(unescapeHtmlEntities("&quot;quoted&quot; and &#39;single-quoted&#39;")).toBe(
45+
"\"quoted\" and 'single-quoted'",
46+
)
47+
})
48+
49+
test("unescapes apostrophe entity", () => {
50+
expect(unescapeHtmlEntities("Don&apos;t worry")).toBe("Don't worry")
51+
})
52+
53+
test("handles mixed content with multiple entity types", () => {
54+
expect(
55+
unescapeHtmlEntities(
56+
"&lt;a href=&quot;https://example.com?param1=value&amp;param2=value&quot;&gt;Link&lt;/a&gt;",
57+
),
58+
).toBe('<a href="https://example.com?param1=value&param2=value">Link</a>')
59+
})
60+
61+
test("handles mixed content with apostrophe entities", () => {
62+
expect(
63+
unescapeHtmlEntities(
64+
"&lt;div&gt;Don&apos;t forget that Tom&amp;Jerry&apos;s show is at 3 o&apos;clock&lt;/div&gt;",
65+
),
66+
).toBe("<div>Don't forget that Tom&Jerry's show is at 3 o'clock</div>")
67+
})
68+
69+
test("returns original string when no entities are present", () => {
70+
const original = "Plain text without entities"
71+
expect(unescapeHtmlEntities(original)).toBe(original)
72+
})
73+
74+
test("handles empty or undefined input", () => {
75+
expect(unescapeHtmlEntities("")).toBe("")
76+
expect(unescapeHtmlEntities(undefined as unknown as string)).toBe(undefined)
77+
})
78+
})
3379
})

src/utils/text-normalization.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,21 @@ export function normalizeString(str: string, options: NormalizeOptions = DEFAULT
7575

7676
return normalized
7777
}
78+
79+
/**
80+
* Unescapes common HTML entities in a string
81+
*
82+
* @param text The string containing HTML entities to unescape
83+
* @returns The unescaped string with HTML entities converted to their literal characters
84+
*/
85+
export function unescapeHtmlEntities(text: string): string {
86+
if (!text) return text
87+
88+
return text
89+
.replace(/&lt;/g, "<")
90+
.replace(/&gt;/g, ">")
91+
.replace(/&quot;/g, '"')
92+
.replace(/&#39;/g, "'")
93+
.replace(/&apos;/g, "'")
94+
.replace(/&amp;/g, "&")
95+
}

0 commit comments

Comments
 (0)