Skip to content

Commit f3ccdd3

Browse files
Merge pull request #5 from akshaybharambe14/ab_Fix_SlowDecode
Fix: Slow decode by reducing comparisons + Refactor
2 parents c998855 + 37810bb commit f3ccdd3

File tree

1 file changed

+91
-86
lines changed

1 file changed

+91
-86
lines changed

jsonc.go

Lines changed: 91 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
// package jsonc provides APIs to read commented (.jsonc) files.
2+
// It implements io.Reader which makes it compatible with standard library.
3+
4+
// EX.
5+
// ...
6+
// ba, _ := ioutil.Readall(jsonc.NewDecoder(r)) // r is io.Reader
7+
// ...
8+
19
package jsonc
210

311
import (
@@ -8,19 +16,27 @@ import (
816
)
917

1018
type (
11-
state int
19+
// Decoder represents a .jsonc decoder. It implements io.Reader.
20+
Decoder struct {
21+
r io.Reader
22+
c comment
23+
}
1224

25+
// comment represents status of Decoder while reading json document.
1326
comment struct {
1427
state state
15-
multiLn bool
16-
isJSON bool
28+
multiLn bool // if true, a multiline comment
29+
isJSON bool // if true, decoder is processing either a "key" or "value"
1730
}
1831

19-
// Decoder implements io.Reader. It wraps provided source.
20-
Decoder struct {
21-
r io.Reader
22-
c comment
23-
}
32+
state int
33+
)
34+
35+
const (
36+
stopped state = iota // 0, default state, started with 0 to make `zero value` of `comment` useful.
37+
canStart // 1
38+
started // 2
39+
canStop // 3
2440
)
2541

2642
const (
@@ -35,118 +51,107 @@ const (
3551
charN = 110 // (n)
3652
)
3753

38-
const (
39-
stopped state = iota // 0, default state
40-
canStart // 1
41-
started // 2
42-
canStop // 3
43-
)
44-
4554
var (
4655
ErrUnexpectedEndOfComment = errors.New("unexpected end of comment")
4756
)
4857

49-
// NewDecoder returns a new Decoder wrapping the provided io.Reader. The returned decoder implements io.Reader.
50-
func NewDecoder(r io.Reader) *Decoder {
51-
return &Decoder{
52-
c: comment{},
53-
r: r,
54-
}
58+
func (c *comment) reset() {
59+
c.state = stopped
60+
c.multiLn = false
5561
}
5662

57-
// Read reads from underlying reader and processes the stream to omit comments.
58-
// A single read doesn't guaranttee a valid JSON. Depends on length of passed slice.
59-
//
60-
// Produces ErrUnexpectedEndOfComment for incomplete comments.
61-
func (d *Decoder) Read(p []byte) (int, error) {
62-
63-
n, err := d.r.Read(p)
64-
if err != nil {
65-
return 0, err
66-
}
67-
68-
shortRead := n <= len(p)
69-
n = decode(p[:n], &d.c)
70-
71-
if shortRead && !d.c.complete() {
72-
return 0, ErrUnexpectedEndOfComment
73-
}
74-
75-
return n, nil
63+
func (c *comment) complete() bool {
64+
return c.state == stopped
7665
}
7766

7867
func decode(p []byte, c *comment) int {
7968
i := 0
8069
for _, s := range p {
81-
if c.handle(s) {
82-
p[i] = s
83-
i++
84-
}
85-
}
8670

87-
return i
88-
}
71+
switch c.state {
8972

90-
// handle the current byte, if returned true, add the byte to result.
91-
func (c *comment) handle(s byte) bool {
92-
switch c.state {
73+
case stopped:
74+
if s == quote { // all characters between "" are valid, can be added to result
75+
c.isJSON = !c.isJSON
76+
}
9377

94-
case stopped:
95-
if s == quote { // all characters between "" are valid, can be added to result
96-
c.isJSON = !c.isJSON
97-
}
78+
if c.isJSON {
79+
p[i] = s
80+
i++
81+
continue
82+
}
9883

99-
if c.isJSON {
100-
return true
101-
}
84+
// TODO: formatting can be preserved by allowing space and tab in JSON
85+
if s == space || s == tab || s == newLine {
86+
continue
87+
}
10288

103-
if s == space || s == tab || s == newLine {
104-
return false
105-
}
89+
if s == fwdSlash {
90+
c.state = canStart
91+
continue
92+
}
10693

107-
if s == fwdSlash {
108-
c.state = canStart
109-
return false
110-
}
94+
p[i] = s
95+
i++
11196

112-
return true
97+
case canStart:
11398

114-
case canStart:
99+
if s == star || s == fwdSlash {
100+
c.state = started
101+
}
115102

116-
if s == star || s == fwdSlash {
117-
c.state = started
118-
}
103+
c.multiLn = (s == star)
119104

120-
c.multiLn = (s == star)
105+
case started:
121106

122-
case started:
107+
if s == star || s == bkdSlash {
108+
c.state = canStop
109+
}
123110

124-
if s == star || s == bkdSlash {
125-
c.state = canStop
126-
}
111+
if s == newLine && !c.multiLn {
112+
c.reset()
113+
}
127114

128-
if s == newLine && !c.multiLn {
129-
c.reset()
130-
}
115+
case canStop:
131116

132-
case canStop:
117+
if s == fwdSlash || s == charN {
118+
c.reset()
119+
}
133120

134-
if s == fwdSlash || s == charN {
135-
c.reset()
136121
}
137122

138123
}
139124

140-
return false
125+
return i
141126
}
142127

143-
func (c *comment) reset() {
144-
c.state = stopped
145-
c.multiLn = false
128+
// NewDecoder returns a new Decoder wrapping the provided io.Reader.
129+
func NewDecoder(r io.Reader) *Decoder {
130+
return &Decoder{
131+
c: comment{},
132+
r: r,
133+
}
146134
}
147135

148-
func (c *comment) complete() bool {
149-
return c.state == stopped
136+
// Read reads from underlying reader and processes the stream to omit comments.
137+
// A single read doesn't guaranttee a valid JSON. Depends on length of passed slice.
138+
//
139+
// Produces ErrUnexpectedEndOfComment for incomplete comments else if not nil, then it is an error from underlying reader.
140+
func (d *Decoder) Read(p []byte) (int, error) {
141+
142+
n, err := d.r.Read(p)
143+
if err != nil {
144+
return 0, err
145+
}
146+
147+
shortRead := n <= len(p) // data to be processed is less than length of `p`
148+
n = decode(p[:n], &d.c)
149+
150+
if shortRead && !d.c.complete() {
151+
return 0, ErrUnexpectedEndOfComment
152+
}
153+
154+
return n, nil
150155
}
151156

152157
// DecodeBytes decodes passed commented json byte slice to normal json.

0 commit comments

Comments
 (0)