git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/compat/json/equiv_test.go (about)

     1  // Copyright (C) 2023  Luke Shumaker <lukeshu@lukeshu.com>
     2  //
     3  // SPDX-License-Identifier: GPL-2.0-or-later
     4  
     5  package json_test
     6  
     7  import (
     8  	"bytes"
     9  	std "encoding/json"
    10  	"errors"
    11  	"io"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"unicode/utf8"
    16  
    17  	"github.com/stretchr/testify/assert"
    18  
    19  	low "git.lukeshu.com/go/lowmemjson/compat/json"
    20  )
    21  
    22  func assertEquivErr(t *testing.T, stdErr, lowErr error) {
    23  	if (stdErr == nil) || (lowErr == nil) {
    24  		// Nil-equal.
    25  		assert.Equal(t, stdErr, lowErr)
    26  		return
    27  	}
    28  	switch stdErr.(type) {
    29  	case *std.SyntaxError:
    30  		if lowErr != nil {
    31  			stdMsg := stdErr.Error()
    32  			lowMsg := lowErr.Error()
    33  
    34  			// https://github.com/golang/go/issues/58680
    35  			if strings.HasPrefix(stdMsg, `invalid character ' ' `) &&
    36  				(errors.Is(lowErr, io.ErrUnexpectedEOF) || lowMsg == "unexpected end of JSON input") {
    37  				return
    38  			}
    39  
    40  			// https://github.com/golang/go/issues/58713
    41  			prefix := `invalid character '`
    42  			if stdMsg != lowMsg && strings.HasPrefix(stdMsg, prefix) && strings.HasPrefix(lowMsg, prefix) {
    43  				stdRune, stdRuneSize := utf8.DecodeRuneInString(stdMsg[len(prefix):])
    44  				lowByte := lowMsg[len(prefix)]
    45  				if lowByte == '\\' {
    46  					switch lowMsg[len(prefix)+1] {
    47  					case 'a':
    48  						lowByte = '\a'
    49  					case 'b':
    50  						lowByte = '\b'
    51  					case 'f':
    52  						lowByte = '\f'
    53  					case 'n':
    54  						lowByte = '\n'
    55  					case 'r':
    56  						lowByte = '\r'
    57  					case 't':
    58  						lowByte = '\t'
    59  					case 'v':
    60  						lowByte = '\v'
    61  					case '\\', '\'':
    62  						lowByte = lowMsg[len(prefix)+1]
    63  					case 'x':
    64  						lowByte64, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:2], 16, 8)
    65  						lowByte = byte(lowByte64)
    66  					case 'u':
    67  						lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:4], 16, 16)
    68  						var buf [4]byte
    69  						utf8.EncodeRune(buf[:], rune(lowRune))
    70  						lowByte = buf[0]
    71  					case 'U':
    72  						lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:8], 16, 32)
    73  						var buf [4]byte
    74  						utf8.EncodeRune(buf[:], rune(lowRune))
    75  						lowByte = buf[0]
    76  					}
    77  				}
    78  				if stdRune == rune(lowByte) {
    79  					lowRuneStr := lowMsg[len(prefix):]
    80  					lowRuneStr = lowRuneStr[:strings.IndexByte(lowRuneStr, '\'')]
    81  					stdMsg = prefix + lowRuneStr + stdMsg[len(prefix)+stdRuneSize:]
    82  					stdErr = errors.New(stdMsg)
    83  				}
    84  			}
    85  
    86  			// I'd file a ticket for this, but @dsnet (one of the encoding/json maintainers) says that he's
    87  			// working on a parser-rewrite that would fix a bunch of this type of issue.
    88  			// https://github.com/golang/go/issues/58680#issuecomment-1444224084
    89  			if strings.HasPrefix(stdMsg, `invalid character '\u00`) && strings.HasPrefix(lowMsg, `invalid character '\x`) {
    90  				stdMsg = `invalid character '\x` + strings.TrimPrefix(stdMsg, `invalid character '\u00`)
    91  				stdErr = errors.New(stdMsg)
    92  			}
    93  		}
    94  		// Text-equal.
    95  		assert.Equal(t, stdErr.Error(), lowErr.Error())
    96  		// TODO: Assert that they are deep-equal (but be permissive of these not being type aliases).
    97  	case *std.MarshalerError:
    98  		// Text-equal.
    99  		assert.Equal(t, stdErr.Error(), lowErr.Error())
   100  		// TODO: Assert that they are deep-equal (but be permissive of these not being type aliases).
   101  	default:
   102  		// Text-equal.
   103  		assert.Equal(t, stdErr.Error(), lowErr.Error())
   104  		// TODO: Assert that they are deep-equal.
   105  	}
   106  }
   107  
   108  func FuzzEquiv(f *testing.F) {
   109  	f.Fuzz(func(t *testing.T, str []byte) {
   110  		t.Logf("str=%q", str)
   111  		t.Run("HTMLEscape", func(t *testing.T) {
   112  			var stdOut bytes.Buffer
   113  			std.HTMLEscape(&stdOut, str)
   114  
   115  			var lowOut bytes.Buffer
   116  			low.HTMLEscape(&lowOut, str)
   117  
   118  			assert.Equal(t, stdOut.String(), lowOut.String())
   119  		})
   120  		t.Run("Compact", func(t *testing.T) {
   121  			var stdOut bytes.Buffer
   122  			stdErr := std.Compact(&stdOut, str)
   123  
   124  			var lowOut bytes.Buffer
   125  			lowErr := low.Compact(&lowOut, str)
   126  
   127  			assert.Equal(t, stdOut.String(), lowOut.String())
   128  			assertEquivErr(t, stdErr, lowErr)
   129  		})
   130  		t.Run("Indent", func(t *testing.T) {
   131  			var stdOut bytes.Buffer
   132  			stdErr := std.Indent(&stdOut, str, "»", "\t")
   133  
   134  			var lowOut bytes.Buffer
   135  			lowErr := low.Indent(&lowOut, str, "»", "\t")
   136  
   137  			assert.Equal(t, stdOut.String(), lowOut.String())
   138  			assertEquivErr(t, stdErr, lowErr)
   139  		})
   140  		t.Run("Valid", func(t *testing.T) {
   141  			stdValid := std.Valid(str) && utf8.Valid(str) // https://github.com/golang/go/issues/58517
   142  			lowValid := low.Valid(str)
   143  			assert.Equal(t, stdValid, lowValid)
   144  		})
   145  		t.Run("Decode-Encode", func(t *testing.T) {
   146  			var stdObj any
   147  			stdErr := std.NewDecoder(bytes.NewReader(str)).Decode(&stdObj)
   148  
   149  			var lowObj any
   150  			lowErr := low.NewDecoder(bytes.NewReader(str)).Decode(&lowObj)
   151  
   152  			assert.Equal(t, stdObj, lowObj)
   153  			assertEquivErr(t, stdErr, lowErr)
   154  			if t.Failed() {
   155  				return
   156  			}
   157  
   158  			var stdOut bytes.Buffer
   159  			stdErr = std.NewEncoder(&stdOut).Encode(stdObj)
   160  
   161  			var lowOut bytes.Buffer
   162  			lowErr = low.NewEncoder(&lowOut).Encode(lowObj)
   163  
   164  			assert.Equal(t, stdOut.String(), lowOut.String())
   165  			assertEquivErr(t, stdErr, lowErr)
   166  		})
   167  		t.Run("Unmarshal-Marshal", func(t *testing.T) {
   168  			var stdObj any
   169  			stdErr := std.Unmarshal(str, &stdObj)
   170  
   171  			var lowObj any
   172  			lowErr := low.Unmarshal(str, &lowObj)
   173  
   174  			assert.Equal(t, stdObj, lowObj)
   175  			assertEquivErr(t, stdErr, lowErr)
   176  			if t.Failed() {
   177  				return
   178  			}
   179  
   180  			stdOut, stdErr := std.Marshal(stdObj)
   181  			lowOut, lowErr := low.Marshal(lowObj)
   182  
   183  			assert.Equal(t, string(stdOut), string(lowOut))
   184  			assertEquivErr(t, stdErr, lowErr)
   185  		})
   186  	})
   187  }