From e4f1c6a13def6c72c65f6e930cda065bd28e773c Mon Sep 17 00:00:00 2001 From: marrow16 Date: Thu, 27 Oct 2022 21:22:32 +0100 Subject: [PATCH] Escaping * Fixed/improved quote escaping * Specific errors * Improved `PostElementFix` (more info) --- enclosures.go | 10 +- errors.go | 38 ++++++ splitter.go | 303 ++++++++++++++++++++++++++++++++++++----------- splitter_test.go | 243 ++++++++++++++++++++++++++++++------- 4 files changed, 481 insertions(+), 113 deletions(-) create mode 100644 errors.go diff --git a/enclosures.go b/enclosures.go index dd9cec9..bccf19c 100644 --- a/enclosures.go +++ b/enclosures.go @@ -30,6 +30,14 @@ func (e *Enclosure) clone() Enclosure { } } +func (e *Enclosure) isDoubleEscaping() bool { + return e.IsQuote && e.Escapable && e.End == e.Escape +} + +func (e *Enclosure) isEscapable() bool { + return e.IsQuote && e.Escapable +} + var ( DoubleQuotes = _DoubleQuotes DoubleQuotesBackSlashEscaped = _DoubleQuotesBackSlashEscaped @@ -48,7 +56,7 @@ var ( LeftRightDoublePrimeQuotes = _LeftRightDoublePrimeQuotes SingleLowHigh9Quotes = _SingleLowHigh9Quotes DoubleLowHigh9Quotes = _DoubleLowHigh9Quotes - Brackets = _Parenthesis + Parenthesis = _Parenthesis CurlyBrackets = _CurlyBrackets SquareBrackets = _SquareBrackets LtGtAngleBrackets = _LtGtAngleBrackets diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..c0f8fa9 --- /dev/null +++ b/errors.go @@ -0,0 +1,38 @@ +package splitter + +import "fmt" + +type SplittingErrorType int + +const ( + Unopened SplittingErrorType = iota + Unclosed +) + +type SplittingError struct { + Type SplittingErrorType + Position int + Rune rune + Enclosure *Enclosure +} + +func newSplittingError(t SplittingErrorType, pos int, r rune, enc *Enclosure) error { + return &SplittingError{ + Type: t, + Position: pos, + Rune: r, + Enclosure: enc, + } +} + +const ( + unopenedFmt = "unopened '%s' at position %d" + unclosedFmt = "unclosed '%s' at position %d" +) + +func (e *SplittingError) Error() string { + if e.Type == Unopened { + return fmt.Sprintf(unopenedFmt, string(e.Rune), e.Position) + } + return fmt.Sprintf(unclosedFmt, string(e.Rune), e.Position) +} diff --git a/splitter.go b/splitter.go index d2b8d7b..0f52159 100644 --- a/splitter.go +++ b/splitter.go @@ -2,17 +2,75 @@ package splitter import ( "fmt" + "strings" ) // Splitter is the actual splitter interface type Splitter interface { // Split performs a split on the supplied string - returns the split parts and any error encountered - Split(str string) ([]string, error) - // PostElementFix supplies a function that can fix-up elements prior to being added (e.g. trimming) - PostElementFix(f PostElementFix) Splitter + Split(s string) ([]string, error) + // SetPostElementFixer supplies a function that can fix-up elements prior to being added (e.g. trimming) + // see PostElementFix for more information + SetPostElementFixer(f PostElementFix) Splitter } -type PostElementFix func(s string) (string, error) +// PostElementFix is the func that can be set using Splitter.SetPostElementFixer() to enable +// the captured split part to be altered prior to adding to the result +// +// It can also return false, to indicate that the part is not to be added to the result (e.g. stripping empty items) +// or an error to indicate the part is unacceptable (and cease splitting with that error) +// +// s - is the entire found string part found +// +// pos - is the start position (relative to the original string) +// +// captured - is the number of parts already captured +// +// subParts - is the sub-parts of the found string part (such as quote or bracket enclosures) +// +// The subParts can be used to determine if the overall part was made of, for example, contiguous quotes +// (which may need to be rejected - e.g. contiguous quotes in a CSV) +type PostElementFix func(s string, pos int, captured int, subParts ...SubPart) (string, bool, error) + +type SubPartType int + +const ( + Fixed SubPartType = iota + Quotes + Brackets +) + +// SubPart is the interfaces passed PostElementCheck - to enable contiguous enclosures to be spotted +type SubPart interface { + // StartPos returns the start position (relative to the original string) of the part + StartPos() int + // EndPos returns the end position (relative to the original string) of the part + EndPos() int + // IsQuote returns whether the part is quotes enclosure + IsQuote() bool + // IsBrackets returns whether the part is a brackets enclosure + IsBrackets() bool + // IsFixed returns whether the part was fixed text (i.e not quotes or brackets) + IsFixed() bool + // Type returns the part type (fixed, quotes, brackets) + Type() SubPartType + // Escapable returns whether the original enclosure was escapable + Escapable() bool + // StartRune returns the start rune of the enclosure + StartRune() rune + // EndRune returns the end rune of the enclosure + EndRune() rune + // EscapeRune returns the original escape rune for the enclosure + EscapeRune() rune + // UnEscaped returns the unescaped string + // + // If the part was a quote enclosure, the enclosing quote marks are stripped and, if escapable, any escaped quotes are transposed. + // If the quote enclosure was not escapable, just the enclosing quote marks are removed + // + // If the part was not a quote enclosure, the original string part is returned + UnEscaped() string + IsWhitespaceOnly(cutset ...string) bool +} // NewSplitter creates a new splitter // @@ -26,19 +84,19 @@ func NewSplitter(separator rune, encs ...*Enclosure) (Splitter, error) { separator: separator, enclosures: make([]Enclosure, 0, len(encs)), openers: map[rune]Enclosure{}, - closers: map[rune]bool{}, + closers: map[rune]Enclosure{}, } - for i, enc := range encs { if enc != nil { - if _, ok := result.openers[enc.Start]; ok { + if _, exists := result.openers[enc.Start]; exists { return nil, fmt.Errorf("existing start encloser ('%s' in Enclosure[%d])", string(enc.Start), i+1) } - if result.closers[enc.End] { + if _, exists := result.closers[enc.End]; exists { return nil, fmt.Errorf("existing end encloser ('%s' in Enclosure[%d])", string(enc.End), i+1) } - result.openers[enc.Start] = enc.clone() - result.closers[enc.End] = true + cEnc := enc.clone() + result.openers[enc.Start] = cEnc + result.closers[enc.End] = cEnc result.enclosures = append(result.enclosures, *enc) } } @@ -58,7 +116,7 @@ type splitter struct { separator rune enclosures []Enclosure openers map[rune]Enclosure - closers map[rune]bool + closers map[rune]Enclosure postFix PostElementFix } @@ -66,7 +124,7 @@ func (s *splitter) Split(str string) ([]string, error) { return newSplitterContext(str, s).split() } -func (s *splitter) PostElementFix(f PostElementFix) Splitter { +func (s *splitter) SetPostElementFixer(f PostElementFix) Splitter { s.postFix = f return s } @@ -76,79 +134,80 @@ type splitterContext struct { runes []rune len int lastAt int + current *delimitedEnclosure + stack []*delimitedEnclosure + delims []SubPart captured []string - current *delimiter - stack []*delimiter } func newSplitterContext(str string, splitter *splitter) *splitterContext { runes := []rune(str) + cp := 1 + for _, r := range runes { + if r == splitter.separator { + cp++ + } + } return &splitterContext{ splitter: splitter, runes: runes, lastAt: 0, len: len(runes), current: nil, - stack: []*delimiter{}, - captured: make([]string, 0), + stack: make([]*delimitedEnclosure, 0), + delims: make([]SubPart, 0), + captured: make([]string, 0, cp), } } -func (c *splitterContext) split() ([]string, error) { - for i, r := range c.runes { - if r == c.splitter.separator { - if !c.inAny() { - if err := c.purge(i); err != nil { +func (ctx *splitterContext) split() ([]string, error) { + for i := 0; i < ctx.len; i++ { + r := ctx.runes[i] + if r == ctx.splitter.separator { + if !ctx.inAny() { + if err := ctx.purge(i); err != nil { return nil, err } } - } else if isEnd, inQuote := c.isQuoteEnd(r, i); isEnd { - c.pop() - } else if !inQuote { - if c.isClose(r) { - c.pop() - } else if enc, isOpener := c.isOpener(r); isOpener { - c.push(enc, i) - } else if c.splitter.closers[r] { - return nil, fmt.Errorf("unopened '%s' at position %d", string(r), i) + } else if isEnd, inQuote, inc := ctx.isQuoteEnd(r, i); isEnd { + ctx.pop(i) + } else { + i += inc + if !inQuote { + if ctx.isClose(r) { + ctx.pop(i) + } else if enc, isOpener := ctx.isOpener(r); isOpener { + ctx.push(enc, i) + } else if cEnc, ok := ctx.splitter.closers[r]; ok { + return nil, newSplittingError(Unopened, i, r, &cEnc) + } } } } - if c.inAny() { - return nil, fmt.Errorf("unclosed '%s' at position %d", string(c.current.enc.Start), c.current.openPos) + if ctx.inAny() { + return nil, newSplittingError(Unclosed, ctx.current.openPos, ctx.current.enc.Start, &ctx.current.enc) } - if err := c.purge(c.len); err != nil { + if err := ctx.purge(ctx.len); err != nil { return nil, err } - return c.captured, nil + return ctx.captured, nil } -func (c *splitterContext) purge(i int) (err error) { - if i >= c.lastAt { - capture := string(c.runes[c.lastAt:i]) - if c.splitter.postFix != nil { - capture, err = c.splitter.postFix(capture) - } - c.captured = append(c.captured, capture) - c.lastAt = i + 1 - } - return -} - -func (c *splitterContext) inAny() bool { - return c.current != nil -} - -func (c *splitterContext) isQuoteEnd(r rune, pos int) (isEnd bool, inQuote bool) { - if c.current != nil && c.current.enc.IsQuote { +func (ctx *splitterContext) isQuoteEnd(r rune, pos int) (isEnd bool, inQuote bool, skip int) { + if ctx.current != nil && ctx.current.enc.IsQuote { inQuote = true - if c.current.enc.End == r { + if ctx.current.enc.End == r { isEnd = true - if c.current.enc.Escapable { + if ctx.current.enc.isDoubleEscaping() { + if pos < ctx.len-1 && ctx.runes[pos+1] == r { + isEnd = false + skip = 1 + } + } else if ctx.current.enc.isEscapable() { escaped := false - minPos := c.current.openPos + minPos := ctx.current.openPos for i := pos - 1; i > minPos; i-- { - if c.runes[i] == c.current.enc.Escape { + if ctx.runes[i] == ctx.current.enc.Escape { escaped = !escaped } else { break @@ -161,35 +220,135 @@ func (c *splitterContext) isQuoteEnd(r rune, pos int) (isEnd bool, inQuote bool) return } -func (c *splitterContext) isClose(r rune) bool { - return c.current != nil && c.current.enc.End == r +func (ctx *splitterContext) purge(i int) (err error) { + if i >= ctx.lastAt { + ctx.purgeFixed(i) + capture := string(ctx.runes[ctx.lastAt:i]) + addIt := true + if ctx.splitter.postFix != nil { + capture, addIt, err = ctx.splitter.postFix(capture, ctx.lastAt, len(ctx.captured), ctx.delims...) + } + if addIt { + ctx.captured = append(ctx.captured, capture) + } + ctx.lastAt = i + 1 + ctx.delims = make([]SubPart, 0) + } + return } -func (c *splitterContext) isOpener(r rune) (Enclosure, bool) { - enc, ok := c.splitter.openers[r] +func (ctx *splitterContext) inAny() bool { + return ctx.current != nil +} + +func (ctx *splitterContext) isClose(r rune) bool { + return ctx.current != nil && ctx.current.enc.End == r +} + +func (ctx *splitterContext) isOpener(r rune) (Enclosure, bool) { + enc, ok := ctx.splitter.openers[r] return enc, ok } -func (c *splitterContext) push(enc Enclosure, pos int) { - if c.current != nil { - c.stack = append(c.stack, c.current) +func (ctx *splitterContext) push(enc Enclosure, pos int) { + if ctx.current != nil { + ctx.stack = append(ctx.stack, ctx.current) } - c.current = &delimiter{ + ctx.current = &delimitedEnclosure{ openPos: pos, enc: enc, + ctx: ctx, + } + if len(ctx.stack) == 0 { + ctx.purgeFixed(pos) + ctx.delims = append(ctx.delims, ctx.current) } } -func (c *splitterContext) pop() { - if l := len(c.stack); l > 0 { - c.current = c.stack[l-1] - c.stack = c.stack[0 : l-1] +func (ctx *splitterContext) purgeFixed(pos int) { + last := ctx.lastAt + if len(ctx.delims) > 0 { + last = ctx.delims[len(ctx.delims)-1].EndPos() + 1 + } + if last < pos { + ctx.delims = append(ctx.delims, &delimitedEnclosure{ + enc: Enclosure{}, + openPos: last, + closePos: pos - 1, + ctx: ctx, + fixed: true, + }) + } +} + +func (ctx *splitterContext) pop(pos int) { + ctx.current.closePos = pos + if l := len(ctx.stack); l > 0 { + ctx.current = ctx.stack[l-1] + ctx.stack = ctx.stack[0 : l-1] } else { - c.current = nil + ctx.current = nil } } -type delimiter struct { - enc Enclosure - openPos int +type delimitedEnclosure struct { + enc Enclosure + openPos int + closePos int + ctx *splitterContext + fixed bool +} + +func (d *delimitedEnclosure) StartPos() int { + return d.openPos +} +func (d *delimitedEnclosure) EndPos() int { + return d.closePos +} +func (d *delimitedEnclosure) IsQuote() bool { + return d.enc.IsQuote +} +func (d *delimitedEnclosure) IsBrackets() bool { + return !d.fixed && !d.enc.IsQuote +} +func (d *delimitedEnclosure) IsFixed() bool { + return d.fixed +} +func (d *delimitedEnclosure) Type() SubPartType { + if d.fixed { + return Fixed + } else if !d.enc.IsQuote { + return Brackets + } + return Quotes +} +func (d *delimitedEnclosure) Escapable() bool { + return d.enc.isEscapable() +} +func (d *delimitedEnclosure) StartRune() rune { + return d.enc.Start +} +func (d *delimitedEnclosure) EndRune() rune { + return d.enc.End +} +func (d *delimitedEnclosure) EscapeRune() rune { + return d.enc.Escape +} +func (d *delimitedEnclosure) UnEscaped() string { + if d.fixed || !d.enc.IsQuote { + return string(d.ctx.runes[d.openPos : d.closePos+1]) + } else if !d.enc.isEscapable() { + return string(d.ctx.runes[d.openPos+1 : d.closePos]) + } + return strings.ReplaceAll(string(d.ctx.runes[d.openPos+1:d.closePos]), string([]rune{d.enc.Escape, d.enc.End}), string(d.enc.End)) +} +func (d *delimitedEnclosure) IsWhitespaceOnly(cutset ...string) bool { + if !d.fixed { + return false + } + cuts := " \t\n" + if len(cutset) > 0 { + cuts = strings.Join(cutset, "") + } + return strings.Trim(string(d.ctx.runes[d.openPos:d.closePos+1]), cuts) == "" } diff --git a/splitter_test.go b/splitter_test.go index ec38842..2f5be3f 100644 --- a/splitter_test.go +++ b/splitter_test.go @@ -117,47 +117,20 @@ func TestSplitter_Split(t *testing.T) { } } -func TestSplitter_SplitCsv(t *testing.T) { - enc := &Enclosure{ - Start: '"', - End: '"', - IsQuote: true, - Escapable: true, - Escape: '"', - } - s, err := NewSplitter(',', enc) - require.NoError(t, err) - parts, err := s.Split(`"aaa","bbb","cc""cc"`) - require.NoError(t, err) - require.Equal(t, 3, len(parts)) - require.Equal(t, `"aaa"`, parts[0]) - require.Equal(t, `"bbb"`, parts[1]) - require.Equal(t, `"cc""cc"`, parts[2]) - - parts, err = s.Split(`"aaa","cc""""cc"`) +func TestSplitter_Split_DoubleEscapes(t *testing.T) { + s, err := NewSplitter(',', DoubleQuotesDoubleEscaped) require.NoError(t, err) - require.Equal(t, 2, len(parts)) - require.Equal(t, `"aaa"`, parts[0]) - require.Equal(t, `"cc""""cc"`, parts[1]) - parts, err = s.Split(`"aaa",""ccc""`) + parts, err := s.Split(`"aa"","",,,,,,""""""""""bbb"`) require.NoError(t, err) - require.Equal(t, 2, len(parts)) - require.Equal(t, `"aaa"`, parts[0]) - require.Equal(t, `""ccc""`, parts[1]) - - _, err = s.Split(`"aaa","cc"""cc"`) - // 012345678901234 - // o c o __c o - require.Error(t, err) - require.Equal(t, `unclosed '"' at position 14`, err.Error()) + require.Equal(t, 1, len(parts)) } func TestSplitter_PostElementFix(t *testing.T) { s, err := NewSplitter(',') require.NoError(t, err) - s.PostElementFix(func(s string) (string, error) { - return strings.Trim(s, " "), nil + s.SetPostElementFixer(func(s string, pos int, captured int, subParts ...SubPart) (string, bool, error) { + return strings.Trim(s, " "), true, nil }) parts, err := s.Split(`a, b, c `) require.NoError(t, err) @@ -170,11 +143,11 @@ func TestSplitter_PostElementFix(t *testing.T) { func TestSplitter_PostElementFix_Errors(t *testing.T) { s, err := NewSplitter(',') require.NoError(t, err) - s.PostElementFix(func(s string) (string, error) { + s.SetPostElementFixer(func(s string, pos int, captured int, subParts ...SubPart) (string, bool, error) { if s == "" { - return "", errors.New("whoops") + return "", false, errors.New("whoops") } - return s, nil + return s, true, nil }) _, err = s.Split(`a,b,c`) require.NoError(t, err) @@ -188,6 +161,31 @@ func TestSplitter_PostElementFix_Errors(t *testing.T) { require.Equal(t, `whoops`, err.Error()) } +func TestSplitter_PostElementCheck_Errors(t *testing.T) { + s, err := NewSplitter(',') + require.NoError(t, err) + s.SetPostElementFixer(func(s string, pos int, captured int, subParts ...SubPart) (string, bool, error) { + if s == "" && captured == 0 { + return "", false, errors.New("first cannot be empty") + } else if s == "" { + return "", false, nil + } + return s, true, nil + }) + + parts, err := s.Split(`aaa,bbb,ccc`) + require.NoError(t, err) + require.Equal(t, 3, len(parts)) + + parts, err = s.Split(`,bbb,ccc`) + require.Error(t, err) + require.Equal(t, `first cannot be empty`, err.Error()) + + parts, err = s.Split(`aaa,,ccc`) + require.NoError(t, err) + require.Equal(t, 2, len(parts)) +} + func TestSplitter_Split_Errors(t *testing.T) { encs := []*Enclosure{ { @@ -199,7 +197,7 @@ func TestSplitter_Split_Errors(t *testing.T) { End: '\'', IsQuote: true, Escapable: true, - Escape: '\\', + Escape: '\'', }, { Start: '"', @@ -215,17 +213,65 @@ func TestSplitter_Split_Errors(t *testing.T) { str string expectErr string }{ + { + `}`, + fmt.Sprintf(unopenedFmt, "}", 0), + }, + { + `{},{}}`, + fmt.Sprintf(unopenedFmt, "}", 5), + }, { `{/}}`, - `unopened '}' at position 3`, + fmt.Sprintf(unopenedFmt, "}", 3), }, { `{{{/}}`, - `unclosed '{' at position 0`, + fmt.Sprintf(unclosedFmt, "{", 0), }, { `{{{/}`, - `unclosed '{' at position 1`, + fmt.Sprintf(unclosedFmt, "{", 1), + }, + { + `"`, + fmt.Sprintf(unclosedFmt, `"`, 0), + }, + { + `"\"`, + fmt.Sprintf(unclosedFmt, `"`, 0), + }, + { + `"\"""`, + fmt.Sprintf(unclosedFmt, `"`, 4), + }, + { + `'`, + fmt.Sprintf(unclosedFmt, `'`, 0), + }, + { + `'''`, + fmt.Sprintf(unclosedFmt, `'`, 0), + }, + { + `'''''`, + fmt.Sprintf(unclosedFmt, `'`, 0), + }, + { + `'''''''`, + fmt.Sprintf(unclosedFmt, `'`, 0), + }, + { + `{'`, + fmt.Sprintf(unclosedFmt, `'`, 1), + }, + { + `{\'`, + fmt.Sprintf(unclosedFmt, `'`, 2), + }, + { + `{''`, + fmt.Sprintf(unclosedFmt, `{`, 0), }, } for i, tc := range testCases { @@ -236,3 +282,120 @@ func TestSplitter_Split_Errors(t *testing.T) { }) } } + +func TestSplitter_Split_Contiguous(t *testing.T) { + s, err := NewSplitter(',', DoubleQuotesDoubleEscaped, SingleQuotes, CurlyBrackets) + require.NoError(t, err) + subs := 0 + startPositions := make([]int, 0) + endPositions := make([]int, 0) + isQuotes := make([]bool, 0) + isBrackets := make([]bool, 0) + isFixeds := make([]bool, 0) + escapables := make([]bool, 0) + startRunes := make([]rune, 0) + endRunes := make([]rune, 0) + escRunes := make([]rune, 0) + unescaped := make([]string, 0) + whitespacing := make([]bool, 0) + types := make([]SubPartType, 0) + s.SetPostElementFixer(func(s string, pos int, captured int, subParts ...SubPart) (string, bool, error) { + subs = len(subParts) + for _, sub := range subParts { + startPositions = append(startPositions, sub.StartPos()) + endPositions = append(endPositions, sub.EndPos()) + isQuotes = append(isQuotes, sub.IsQuote()) + isBrackets = append(isBrackets, sub.IsBrackets()) + isFixeds = append(isFixeds, sub.IsFixed()) + escapables = append(escapables, sub.Escapable()) + startRunes = append(startRunes, sub.StartRune()) + endRunes = append(endRunes, sub.EndRune()) + escRunes = append(escRunes, sub.EscapeRune()) + unescaped = append(unescaped, sub.UnEscaped()) + whitespacing = append(whitespacing, sub.IsWhitespaceOnly(" ")) + types = append(types, sub.Type()) + } + return s, true, nil + }) + parts, err := s.Split(` "bbb""" '222'{'a','b','c'} `) + // 0123456789012345678901234567 + require.NoError(t, err) + require.Equal(t, 1, len(parts)) + require.Equal(t, 6, subs) + require.Equal(t, 6, len(startPositions)) + require.Equal(t, 6, len(endPositions)) + require.Equal(t, 6, len(isQuotes)) + require.Equal(t, 6, len(isBrackets)) + require.Equal(t, 6, len(isFixeds)) + require.Equal(t, 6, len(escapables)) + require.Equal(t, 6, len(startRunes)) + require.Equal(t, 6, len(endRunes)) + require.Equal(t, 6, len(escRunes)) + require.Equal(t, 6, len(unescaped)) + require.Equal(t, 6, len(whitespacing)) + require.Equal(t, 6, len(types)) + + require.Equal(t, 0, startPositions[0]) + require.Equal(t, 0, endPositions[0]) + require.Equal(t, 1, startPositions[1]) + require.Equal(t, 7, endPositions[1]) + require.Equal(t, 8, startPositions[2]) + require.Equal(t, 8, endPositions[2]) + require.Equal(t, 9, startPositions[3]) + require.Equal(t, 13, endPositions[3]) + require.Equal(t, 14, startPositions[4]) + require.Equal(t, 26, endPositions[4]) + require.Equal(t, 27, startPositions[5]) + require.Equal(t, 27, endPositions[5]) + + require.True(t, isFixeds[0]) + require.True(t, isQuotes[1]) + require.True(t, isFixeds[2]) + require.True(t, isQuotes[3]) + require.True(t, isBrackets[4]) + require.True(t, isFixeds[5]) + + require.Equal(t, Fixed, types[0]) + require.Equal(t, Quotes, types[1]) + require.Equal(t, Fixed, types[2]) + require.Equal(t, Quotes, types[3]) + require.Equal(t, Brackets, types[4]) + require.Equal(t, Fixed, types[5]) + + require.False(t, escapables[0]) + require.True(t, escapables[1]) + require.False(t, escapables[2]) + + require.True(t, whitespacing[0]) + require.False(t, whitespacing[1]) + require.True(t, whitespacing[2]) + require.False(t, whitespacing[3]) + require.False(t, whitespacing[4]) + require.True(t, whitespacing[5]) + + require.Equal(t, int32(0), startRunes[0]) + require.Equal(t, int32(0), endRunes[0]) + require.Equal(t, '"', startRunes[1]) + require.Equal(t, '"', endRunes[1]) + require.Equal(t, int32(0), startRunes[2]) + require.Equal(t, int32(0), endRunes[2]) + require.Equal(t, '\'', startRunes[3]) + require.Equal(t, '\'', endRunes[3]) + require.Equal(t, '{', startRunes[4]) + require.Equal(t, '}', endRunes[4]) + require.Equal(t, int32(0), startRunes[5]) + require.Equal(t, int32(0), endRunes[5]) + + require.Equal(t, int32(0), escRunes[0]) + require.Equal(t, '"', escRunes[1]) + require.Equal(t, int32(0), escRunes[2]) + require.Equal(t, int32(0), escRunes[3]) + require.Equal(t, int32(0), escRunes[4]) + require.Equal(t, int32(0), escRunes[5]) + require.Equal(t, ` `, unescaped[0]) + require.Equal(t, `bbb"`, unescaped[1]) + require.Equal(t, ` `, unescaped[2]) + require.Equal(t, `222`, unescaped[3]) + require.Equal(t, `{'a','b','c'}`, unescaped[4]) + require.Equal(t, ` `, unescaped[5]) +}