diff --git a/json/walker.go b/json/walker.go index d67bec4..d07204c 100644 --- a/json/walker.go +++ b/json/walker.go @@ -176,12 +176,33 @@ func (ew *Walker) runAction(data []byte) ([]byte, error) { return append(quoted, data[len(trimmed):]...), nil } -// probably a better way to do this, but... +// quoteBytes takes a byte slice and returns it as a properly quoted JSON string. +// Unlike json.Marshal, this does not escape HTML characters (<, >, &) to their +// unicode equivalents, preserving the original content. func quoteBytes(in []byte) ([]byte, error) { - data := []string{string(in)} - out, err := json.Marshal(data) - if err != nil { - return nil, err + var buf bytes.Buffer + buf.WriteByte('"') + for _, b := range in { + switch b { + case '"': + buf.WriteString(`\"`) + case '\\': + buf.WriteString(`\\`) + case '\n': + buf.WriteString(`\n`) + case '\r': + buf.WriteString(`\r`) + case '\t': + buf.WriteString(`\t`) + default: + if b < 0x20 { + // Control characters must be escaped + buf.WriteString(fmt.Sprintf(`\u%04x`, b)) + } else { + buf.WriteByte(b) + } + } } - return out[1 : len(out)-1], nil + buf.WriteByte('"') + return buf.Bytes(), nil } diff --git a/json/walker_test.go b/json/walker_test.go index e7348d9..186b986 100644 --- a/json/walker_test.go +++ b/json/walker_test.go @@ -50,6 +50,28 @@ var walkTestCases = []testCase{ {`{"_a": {"b": "c"}}`, `{"_a": {"b": "E"}}`}, // comments don't inherit } +func TestQuoteBytes(t *testing.T) { + Convey("quoteBytes preserves special characters without HTML escaping", t, func() { + tests := []struct { + in, expected string + }{ + {"hello", `"hello"`}, + {"<>&", `"<>&"`}, // HTML chars not escaped + {"aaaccc^ddd~eee&fff", `"aaaccc^ddd~eee&fff"`}, + {`with"quote`, `"with\"quote"`}, // quotes escaped + {"with\\backslash", `"with\\backslash"`}, // backslash escaped + {"with\nnewline", `"with\nnewline"`}, // newline escaped + {"with\ttab", `"with\ttab"`}, // tab escaped + {"with\rcarriage", `"with\rcarriage"`}, // carriage return escaped + } + for _, tc := range tests { + result, err := quoteBytes([]byte(tc.in)) + So(err, ShouldBeNil) + So(string(result), ShouldEqual, tc.expected) + } + }) +} + var collapseTestCases = []testCase{ { "{\"a\": \"b\r\nc\nd\"\r\n}",