summaryrefslogtreecommitdiff
path: root/compat/json/compat.go
diff options
context:
space:
mode:
Diffstat (limited to 'compat/json/compat.go')
-rw-r--r--compat/json/compat.go150
1 files changed, 135 insertions, 15 deletions
diff --git a/compat/json/compat.go b/compat/json/compat.go
index c96470d..695c1a8 100644
--- a/compat/json/compat.go
+++ b/compat/json/compat.go
@@ -11,10 +11,13 @@ import (
"bytes"
"encoding/json"
"errors"
+ "fmt"
"io"
"strconv"
+ "unicode/utf8"
"git.lukeshu.com/go/lowmemjson"
+ "git.lukeshu.com/go/lowmemjson/internal/jsonstring"
)
//nolint:stylecheck // ST1021 False positive; these aren't comments on individual types.
@@ -144,7 +147,23 @@ func convertReEncodeError(err error) error {
}
func HTMLEscape(dst *bytes.Buffer, src []byte) {
- _, _ = lowmemjson.NewReEncoder(dst, lowmemjson.ReEncoderConfig{}).Write(src)
+ for n := 0; n < len(src); {
+ c, size := utf8.DecodeRune(src[n:])
+ if c == utf8.RuneError && size == 1 {
+ dst.WriteByte(src[n])
+ } else {
+ mode := lowmemjson.EscapeHTMLSafe(c, lowmemjson.BackslashEscapeNone)
+ switch mode {
+ case lowmemjson.BackslashEscapeNone:
+ dst.WriteRune(c)
+ case lowmemjson.BackslashEscapeUnicode:
+ _ = jsonstring.WriteStringUnicodeEscape(dst, c, mode)
+ default:
+ panic(fmt.Errorf("lowmemjson.EscapeHTMLSafe returned an unexpected escape mode=%d", mode))
+ }
+ }
+ n += size
+ }
}
func reencode(dst io.Writer, src []byte, cfg lowmemjson.ReEncoderConfig) error {
@@ -157,38 +176,75 @@ func reencode(dst io.Writer, src []byte, cfg lowmemjson.ReEncoderConfig) error {
}
func Compact(dst *bytes.Buffer, src []byte) error {
- return reencode(dst, src, lowmemjson.ReEncoderConfig{
+ start := dst.Len()
+ err := reencode(dst, src, lowmemjson.ReEncoderConfig{
Compact: true,
+ InvalidUTF8: lowmemjson.InvalidUTF8Preserve,
BackslashEscape: lowmemjson.EscapePreserve,
})
+ if err != nil {
+ dst.Truncate(start)
+ }
+ return err
+}
+
+func isSpace(c byte) bool {
+ switch c {
+ case 0x0020, 0x000A, 0x000D, 0x0009:
+ return true
+ default:
+ return false
+ }
}
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
- return reencode(dst, src, lowmemjson.ReEncoderConfig{
+ start := dst.Len()
+ err := reencode(dst, src, lowmemjson.ReEncoderConfig{
Indent: indent,
Prefix: prefix,
+ InvalidUTF8: lowmemjson.InvalidUTF8Preserve,
BackslashEscape: lowmemjson.EscapePreserve,
})
+ if err != nil {
+ dst.Truncate(start)
+ return err
+ }
+
+ // Preserve trailing whitespace.
+ lastNonWS := len(src) - 1
+ for ; lastNonWS >= 0 && isSpace(src[lastNonWS]); lastNonWS-- {
+ }
+ if _, err := dst.Write(src[lastNonWS+1:]); err != nil {
+ return err
+ }
+
+ return nil
}
func Valid(data []byte) bool {
formatter := lowmemjson.NewReEncoder(io.Discard, lowmemjson.ReEncoderConfig{
- Compact: true,
+ Compact: true,
+ InvalidUTF8: lowmemjson.InvalidUTF8Error,
})
- _, err := formatter.Write(data)
- return err == nil
+ if _, err := formatter.Write(data); err != nil {
+ return false
+ }
+ if err := formatter.Close(); err != nil {
+ return false
+ }
+ return true
}
// Decode wrappers ///////////////////////////////////////////////////
-func convertDecodeError(err error) error {
+func convertDecodeError(err error, isUnmarshal bool) error {
if derr, ok := err.(*lowmemjson.DecodeError); ok {
switch terr := derr.Err.(type) {
case *lowmemjson.DecodeSyntaxError:
switch {
case errors.Is(terr.Err, io.EOF):
err = io.EOF
- case errors.Is(terr.Err, io.ErrUnexpectedEOF):
+ case errors.Is(terr.Err, io.ErrUnexpectedEOF) && isUnmarshal:
err = &SyntaxError{
msg: "unexpected end of JSON input",
Offset: terr.Offset,
@@ -228,13 +284,66 @@ func convertDecodeError(err error) error {
return err
}
+type decodeValidator struct{}
+
+func (*decodeValidator) DecodeJSON(r io.RuneScanner) error {
+ for {
+ if _, _, err := r.ReadRune(); err != nil {
+
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+ }
+}
+
+var _ lowmemjson.Decodable = (*decodeValidator)(nil)
+
func Unmarshal(data []byte, ptr any) error {
- return convertDecodeError(lowmemjson.NewDecoder(bytes.NewReader(data)).DecodeThenEOF(ptr))
+ if err := convertDecodeError(lowmemjson.NewDecoder(bytes.NewReader(data)).DecodeThenEOF(&decodeValidator{}), true); err != nil {
+ return err
+ }
+ if err := convertDecodeError(lowmemjson.NewDecoder(bytes.NewReader(data)).DecodeThenEOF(ptr), true); err != nil {
+ return err
+ }
+ return nil
+}
+
+type teeRuneScanner struct {
+ src io.RuneScanner
+ dst *bytes.Buffer
+ lastSize int
+}
+
+func (tee *teeRuneScanner) ReadRune() (r rune, size int, err error) {
+ r, size, err = tee.src.ReadRune()
+ if err == nil {
+ if _, err := tee.dst.WriteRune(r); err != nil {
+ return 0, 0, err
+ }
+ }
+
+ tee.lastSize = size
+ return
+}
+
+func (tee *teeRuneScanner) UnreadRune() error {
+ if tee.lastSize == 0 {
+ return lowmemjson.ErrInvalidUnreadRune
+ }
+ _ = tee.src.UnreadRune()
+ tee.dst.Truncate(tee.dst.Len() - tee.lastSize)
+ tee.lastSize = 0
+ return nil
}
type Decoder struct {
+ validatorBuf *bufio.Reader
+ validator *lowmemjson.Decoder
+
+ decoderBuf bytes.Buffer
*lowmemjson.Decoder
- buf *bufio.Reader
}
func NewDecoder(r io.Reader) *Decoder {
@@ -242,18 +351,29 @@ func NewDecoder(r io.Reader) *Decoder {
if !ok {
br = bufio.NewReader(r)
}
- return &Decoder{
- Decoder: lowmemjson.NewDecoder(br),
- buf: br,
+ ret := &Decoder{
+ validatorBuf: br,
}
+ ret.validator = lowmemjson.NewDecoder(&teeRuneScanner{
+ src: ret.validatorBuf,
+ dst: &ret.decoderBuf,
+ })
+ ret.Decoder = lowmemjson.NewDecoder(&ret.decoderBuf)
+ return ret
}
func (dec *Decoder) Decode(ptr any) error {
- return convertDecodeError(dec.Decoder.Decode(ptr))
+ if err := convertDecodeError(dec.validator.Decode(&decodeValidator{}), false); err != nil {
+ return err
+ }
+ if err := convertDecodeError(dec.Decoder.Decode(ptr), false); err != nil {
+ return err
+ }
+ return nil
}
func (dec *Decoder) Buffered() io.Reader {
- dat, _ := dec.buf.Peek(dec.buf.Buffered())
+ dat, _ := dec.validatorBuf.Peek(dec.validatorBuf.Buffered())
return bytes.NewReader(dat)
}