Skip to content

Commit d2cdc46

Browse files
authored
Merge pull request #229 from runlevel5/fix-html-char-escaping
Fix HTML character escaping in JSON output
2 parents 9e42bc7 + 10d4e15 commit d2cdc46

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
lines changed

json/walker.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,33 @@ func (ew *Walker) runAction(data []byte) ([]byte, error) {
176176
return append(quoted, data[len(trimmed):]...), nil
177177
}
178178

179-
// probably a better way to do this, but...
179+
// quoteBytes takes a byte slice and returns it as a properly quoted JSON string.
180+
// Unlike json.Marshal, this does not escape HTML characters (<, >, &) to their
181+
// unicode equivalents, preserving the original content.
180182
func quoteBytes(in []byte) ([]byte, error) {
181-
data := []string{string(in)}
182-
out, err := json.Marshal(data)
183-
if err != nil {
184-
return nil, err
183+
var buf bytes.Buffer
184+
buf.WriteByte('"')
185+
for _, b := range in {
186+
switch b {
187+
case '"':
188+
buf.WriteString(`\"`)
189+
case '\\':
190+
buf.WriteString(`\\`)
191+
case '\n':
192+
buf.WriteString(`\n`)
193+
case '\r':
194+
buf.WriteString(`\r`)
195+
case '\t':
196+
buf.WriteString(`\t`)
197+
default:
198+
if b < 0x20 {
199+
// Control characters must be escaped
200+
buf.WriteString(fmt.Sprintf(`\u%04x`, b))
201+
} else {
202+
buf.WriteByte(b)
203+
}
204+
}
185205
}
186-
return out[1 : len(out)-1], nil
206+
buf.WriteByte('"')
207+
return buf.Bytes(), nil
187208
}

json/walker_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ var walkTestCases = []testCase{
5050
{`{"_a": {"b": "c"}}`, `{"_a": {"b": "E"}}`}, // comments don't inherit
5151
}
5252

53+
func TestQuoteBytes(t *testing.T) {
54+
Convey("quoteBytes preserves special characters without HTML escaping", t, func() {
55+
tests := []struct {
56+
in, expected string
57+
}{
58+
{"hello", `"hello"`},
59+
{"<>&", `"<>&"`}, // HTML chars not escaped
60+
{"aaa<bbb>ccc^ddd~eee&fff", `"aaa<bbb>ccc^ddd~eee&fff"`},
61+
{`with"quote`, `"with\"quote"`}, // quotes escaped
62+
{"with\\backslash", `"with\\backslash"`}, // backslash escaped
63+
{"with\nnewline", `"with\nnewline"`}, // newline escaped
64+
{"with\ttab", `"with\ttab"`}, // tab escaped
65+
{"with\rcarriage", `"with\rcarriage"`}, // carriage return escaped
66+
}
67+
for _, tc := range tests {
68+
result, err := quoteBytes([]byte(tc.in))
69+
So(err, ShouldBeNil)
70+
So(string(result), ShouldEqual, tc.expected)
71+
}
72+
})
73+
}
74+
5375
var collapseTestCases = []testCase{
5476
{
5577
"{\"a\": \"b\r\nc\nd\"\r\n}",

0 commit comments

Comments
 (0)