github.com/go-haru/field@v0.0.2/json.go (about)

     1  package field
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"unicode/utf8"
     8  	_ "unsafe"
     9  )
    10  
    11  //go:linkname htmlSafeSet encoding/json.htmlSafeSet
    12  var htmlSafeSet [utf8.RuneSelf]bool
    13  
    14  //go:linkname safeSet encoding/json.safeSet
    15  var safeSet [utf8.RuneSelf]bool
    16  
    17  var hex = "0123456789abcdef"
    18  
    19  func appendString[Bytes []byte | string](dst []byte, src Bytes, escapeHTML bool) []byte {
    20  	dst = append(dst, '"')
    21  	start := 0
    22  	for i := 0; i < len(src); {
    23  		if b := src[i]; b < utf8.RuneSelf {
    24  			if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
    25  				i++
    26  				continue
    27  			}
    28  			dst = append(dst, src[start:i]...)
    29  			switch b {
    30  			case '\\', '"':
    31  				dst = append(dst, '\\', b)
    32  			case '\n':
    33  				dst = append(dst, '\\', 'n')
    34  			case '\r':
    35  				dst = append(dst, '\\', 'r')
    36  			case '\t':
    37  				dst = append(dst, '\\', 't')
    38  			default:
    39  				// This encodes bytes < 0x20 except for \t, \n and \r.
    40  				// If escapeHTML is set, it also escapes <, >, and &
    41  				// because they can lead to security holes when
    42  				// user-controlled strings are rendered into JSON
    43  				// and served to some browsers.
    44  				dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
    45  			}
    46  			i++
    47  			start = i
    48  			continue
    49  		}
    50  		// TODO(https://go.dev/issue/56948): Use generic utf8 functionality.
    51  		// For now, cast only a small portion of byte slices to a string
    52  		// so that it can be stack allocated. This slows down []byte slightly
    53  		// due to the extra copy, but keeps string performance roughly the same.
    54  		n := len(src) - i
    55  		if n > utf8.UTFMax {
    56  			n = utf8.UTFMax
    57  		}
    58  		c, size := utf8.DecodeRuneInString(string(src[i : i+n]))
    59  		if c == utf8.RuneError && size == 1 {
    60  			dst = append(dst, src[start:i]...)
    61  			dst = append(dst, `\ufffd`...)
    62  			i += size
    63  			start = i
    64  			continue
    65  		}
    66  		// U+2028 is LINE SEPARATOR.
    67  		// U+2029 is PARAGRAPH SEPARATOR.
    68  		// They are both technically valid characters in JSON strings,
    69  		// but don't work in JSONP, which has to be evaluated as JavaScript,
    70  		// and can lead to security holes there. It is valid JSON to
    71  		// escape them, so we do so unconditionally.
    72  		// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
    73  		if c == '\u2028' || c == '\u2029' {
    74  			dst = append(dst, src[start:i]...)
    75  			dst = append(dst, '\\', 'u', '2', '0', '2', hex[c&0xF])
    76  			i += size
    77  			start = i
    78  			continue
    79  		}
    80  		i += size
    81  	}
    82  	dst = append(dst, src[start:]...)
    83  	dst = append(dst, '"')
    84  	return dst
    85  }
    86  
    87  func appendJsonStringBuf(w Buffer, s string) (err error) {
    88  	var _, wrote = w.Write(appendString[string](nil, s, false))
    89  	return wrote
    90  }
    91  
    92  type jsonErr interface {
    93  	error
    94  	json.Marshaler
    95  }
    96  
    97  func asJsonErrMarshaler(err error) (jsonErr jsonErr) {
    98  	if err == nil {
    99  		return nil
   100  	}
   101  	if errors.As(err, &jsonErr) {
   102  		return jsonErr
   103  	}
   104  	return jsonErrMarshaler{err}
   105  }
   106  
   107  type jsonErrMarshaler struct{ error }
   108  
   109  func (e jsonErrMarshaler) MarshalJSON() ([]byte, error) {
   110  	if e.error == nil {
   111  		return []byte("null"), nil
   112  	}
   113  	var buffer bytes.Buffer
   114  	if err := appendJsonStringBuf(&buffer, e.Error()); err != nil {
   115  		return nil, err
   116  	}
   117  	return buffer.Bytes(), nil
   118  }