// Copyright (C) 2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package json_test import ( "bytes" std "encoding/json" "errors" "io" "strconv" "strings" "testing" "unicode/utf8" "github.com/stretchr/testify/assert" low "git.lukeshu.com/go/lowmemjson/compat/json" ) func assertEquivErr(t *testing.T, stdErr, lowErr error) { if (stdErr == nil) || (lowErr == nil) { // Nil-equal. assert.Equal(t, stdErr, lowErr) return } switch stdErr.(type) { case *std.SyntaxError: if lowErr != nil { stdMsg := stdErr.Error() lowMsg := lowErr.Error() // https://github.com/golang/go/issues/58680 if strings.HasPrefix(stdMsg, `invalid character ' ' `) && (errors.Is(lowErr, io.ErrUnexpectedEOF) || lowMsg == "unexpected end of JSON input") { return } // https://github.com/golang/go/issues/58713 prefix := `invalid character '` if stdMsg != lowMsg && strings.HasPrefix(stdMsg, prefix) && strings.HasPrefix(lowMsg, prefix) { stdRune, stdRuneSize := utf8.DecodeRuneInString(stdMsg[len(prefix):]) lowByte := lowMsg[len(prefix)] if lowByte == '\\' { switch lowMsg[len(prefix)+1] { case 'a': lowByte = '\a' case 'b': lowByte = '\b' case 'f': lowByte = '\f' case 'n': lowByte = '\n' case 'r': lowByte = '\r' case 't': lowByte = '\t' case 'v': lowByte = '\v' case '\\', '\'': lowByte = lowMsg[len(prefix)+1] case 'x': lowByte64, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:2], 16, 8) lowByte = byte(lowByte64) case 'u': lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:4], 16, 16) var buf [4]byte utf8.EncodeRune(buf[:], rune(lowRune)) lowByte = buf[0] case 'U': lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:8], 16, 32) var buf [4]byte utf8.EncodeRune(buf[:], rune(lowRune)) lowByte = buf[0] } } if stdRune == rune(lowByte) { lowRuneStr := lowMsg[len(prefix):] lowRuneStr = lowRuneStr[:strings.IndexByte(lowRuneStr, '\'')] stdMsg = prefix + lowRuneStr + stdMsg[len(prefix)+stdRuneSize:] stdErr = errors.New(stdMsg) } } // I'd file a ticket for this, but @dsnet (one of the encoding/json maintainers) says that he's // working on a parser-rewrite that would fix a bunch of this type of issue. // https://github.com/golang/go/issues/58680#issuecomment-1444224084 if strings.HasPrefix(stdMsg, `invalid character '\u00`) && strings.HasPrefix(lowMsg, `invalid character '\x`) { stdMsg = `invalid character '\x` + strings.TrimPrefix(stdMsg, `invalid character '\u00`) stdErr = errors.New(stdMsg) } } // Text-equal. assert.Equal(t, stdErr.Error(), lowErr.Error()) // TODO: Assert that they are deep-equal (but be permissive of these not being type aliases). case *std.MarshalerError: // Text-equal. assert.Equal(t, stdErr.Error(), lowErr.Error()) // TODO: Assert that they are deep-equal (but be permissive of these not being type aliases). default: // Text-equal. assert.Equal(t, stdErr.Error(), lowErr.Error()) // TODO: Assert that they are deep-equal. } } func FuzzEquiv(f *testing.F) { f.Fuzz(func(t *testing.T, str []byte) { t.Logf("str=%q", str) t.Run("HTMLEscape", func(t *testing.T) { var stdOut bytes.Buffer std.HTMLEscape(&stdOut, str) var lowOut bytes.Buffer low.HTMLEscape(&lowOut, str) assert.Equal(t, stdOut.String(), lowOut.String()) }) t.Run("Compact", func(t *testing.T) { var stdOut bytes.Buffer stdErr := std.Compact(&stdOut, str) var lowOut bytes.Buffer lowErr := low.Compact(&lowOut, str) assert.Equal(t, stdOut.String(), lowOut.String()) assertEquivErr(t, stdErr, lowErr) }) t.Run("Indent", func(t *testing.T) { var stdOut bytes.Buffer stdErr := std.Indent(&stdOut, str, "»", "\t") var lowOut bytes.Buffer lowErr := low.Indent(&lowOut, str, "»", "\t") assert.Equal(t, stdOut.String(), lowOut.String()) assertEquivErr(t, stdErr, lowErr) }) t.Run("Valid", func(t *testing.T) { stdValid := std.Valid(str) && utf8.Valid(str) // https://github.com/golang/go/issues/58517 lowValid := low.Valid(str) assert.Equal(t, stdValid, lowValid) }) t.Run("Decode-Encode", func(t *testing.T) { var stdObj any stdErr := std.NewDecoder(bytes.NewReader(str)).Decode(&stdObj) var lowObj any lowErr := low.NewDecoder(bytes.NewReader(str)).Decode(&lowObj) assert.Equal(t, stdObj, lowObj) assertEquivErr(t, stdErr, lowErr) if t.Failed() { return } var stdOut bytes.Buffer stdErr = std.NewEncoder(&stdOut).Encode(stdObj) var lowOut bytes.Buffer lowErr = low.NewEncoder(&lowOut).Encode(lowObj) assert.Equal(t, stdOut.String(), lowOut.String()) assertEquivErr(t, stdErr, lowErr) }) t.Run("Unmarshal-Marshal", func(t *testing.T) { var stdObj any stdErr := std.Unmarshal(str, &stdObj) var lowObj any lowErr := low.Unmarshal(str, &lowObj) assert.Equal(t, stdObj, lowObj) assertEquivErr(t, stdErr, lowErr) if t.Failed() { return } stdOut, stdErr := std.Marshal(stdObj) lowOut, lowErr := low.Marshal(lowObj) assert.Equal(t, string(stdOut), string(lowOut)) assertEquivErr(t, stdErr, lowErr) }) }) }