navidrome/utils/jsoncommentstrip/jsoncommentstrip.go

128 lines
2.5 KiB
Go

// Package jsoncommentstrip provides an io.Reader that strips JavaScript-style
// comments (// line and /* block */) from JSON input while preserving
// comment-like sequences inside JSON string values.
package jsoncommentstrip
import (
"bufio"
"io"
)
type state int
const (
stateNormal state = iota
stateInString
stateInStringEscape
stateMaybeComment // saw '/'
stateLineComment // inside // ...
stateBlockComment // inside /* ... */
stateMaybeBlockEnd // saw '*' inside block comment
)
type reader struct {
r *bufio.Reader
state state
}
// NewReader returns an io.Reader that strips JSON comments from the
// underlying reader. It removes single-line comments (// to end of line)
// and block comments (/* ... */), while preserving comment-like sequences
// that appear inside JSON string values.
func NewReader(r io.Reader) io.Reader {
return &reader{
r: bufio.NewReader(r),
state: stateNormal,
}
}
func (cr *reader) Read(p []byte) (int, error) {
n := 0
for n < len(p) {
b, err := cr.r.ReadByte()
if err != nil {
if cr.state == stateMaybeComment {
// Emit the pending '/' before returning EOF
p[n] = '/'
n++
cr.state = stateNormal
}
return n, err
}
switch cr.state {
case stateNormal:
switch b {
case '"':
p[n] = b
n++
cr.state = stateInString
case '/':
cr.state = stateMaybeComment
default:
p[n] = b
n++
}
case stateInString:
p[n] = b
n++
switch b {
case '\\':
cr.state = stateInStringEscape
case '"':
cr.state = stateNormal
}
case stateInStringEscape:
p[n] = b
n++
cr.state = stateInString
case stateMaybeComment:
switch b {
case '/':
cr.state = stateLineComment
case '*':
cr.state = stateBlockComment
default:
// The '/' was not a comment start; emit it and the current byte
p[n] = '/'
n++
if n < len(p) {
p[n] = b
n++
} else {
// We need to "unread" the current byte since buffer is full
_ = cr.r.UnreadByte()
}
cr.state = stateNormal
}
case stateLineComment:
if b == '\n' || b == '\r' {
p[n] = b
n++
cr.state = stateNormal
}
// Otherwise, consume and discard
case stateBlockComment:
if b == '*' {
cr.state = stateMaybeBlockEnd
}
// Otherwise, consume and discard
case stateMaybeBlockEnd:
if b == '/' {
cr.state = stateNormal
} else if b == '*' {
// Stay in stateMaybeBlockEnd (consecutive *'s)
cr.state = stateMaybeBlockEnd
} else {
cr.state = stateBlockComment
}
}
}
return n, nil
}