refactor(jsoncommentstrip): replace go-jsoncommentstrip with custom JSON comment stripping
This commit is contained in:
@@ -9,10 +9,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/RaveNoX/go-jsoncommentstrip"
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/model/criteria"
|
"github.com/navidrome/navidrome/model/criteria"
|
||||||
|
"github.com/navidrome/navidrome/utils/jsoncommentstrip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *playlists) newSyncedPlaylist(baseDir string, playlistFile string) (*model.Playlist, error) {
|
func (s *playlists) newSyncedPlaylist(baseDir string, playlistFile string) (*model.Playlist, error) {
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package jsoncommentstrip_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/utils/jsoncommentstrip"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJsonCommentStrip(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "JsonCommentStrip Suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("NewReader", func() {
|
||||||
|
read := func(input string) string {
|
||||||
|
r := jsoncommentstrip.NewReader(strings.NewReader(input))
|
||||||
|
out, err := io.ReadAll(r)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compact returns the compacted JSON form of s, for readable comparisons.
|
||||||
|
compact := func(s string) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
ExpectWithOffset(1, json.Compact(&buf, []byte(s))).To(Succeed())
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
It("passes through JSON without comments unchanged", func() {
|
||||||
|
input := `{"key": "value", "num": 42}`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips single-line comments", func() {
|
||||||
|
input := `{
|
||||||
|
// this is a comment
|
||||||
|
"key": "value"
|
||||||
|
}`
|
||||||
|
Expect(compact(read(input))).To(Equal(compact(`{
|
||||||
|
"key": "value"
|
||||||
|
}`)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips single-line comments at end of line", func() {
|
||||||
|
input := `{
|
||||||
|
"key": "value" // inline comment
|
||||||
|
}`
|
||||||
|
Expect(compact(read(input))).To(Equal(compact(`{
|
||||||
|
"key": "value"
|
||||||
|
}`)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips block comments", func() {
|
||||||
|
input := `{/* comment */"key": "value"}`
|
||||||
|
Expect(compact(read(input))).To(Equal(`{"key":"value"}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips multi-line block comments", func() {
|
||||||
|
input := `{
|
||||||
|
/* this is
|
||||||
|
a multi-line
|
||||||
|
comment */
|
||||||
|
"key": "value"
|
||||||
|
}`
|
||||||
|
Expect(compact(read(input))).To(Equal(compact(`{
|
||||||
|
"key": "value"
|
||||||
|
}`)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("preserves // inside JSON strings", func() {
|
||||||
|
input := `{"key": "value // not a comment"}`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("preserves /* inside JSON strings", func() {
|
||||||
|
input := `{"key": "value /* not a comment */"}`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles escaped quotes in strings", func() {
|
||||||
|
input := `{"key": "val\"ue // not a comment"}`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles / at end of input as literal", func() {
|
||||||
|
input := `{"key": "value"}/`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles * inside block comment not followed by /", func() {
|
||||||
|
input := `{/* a * b */"key": "value"}`
|
||||||
|
Expect(compact(read(input))).To(Equal(`{"key":"value"}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles empty input", func() {
|
||||||
|
Expect(read("")).To(Equal(""))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles mixed comments with real content", func() {
|
||||||
|
input := `{
|
||||||
|
// line comment
|
||||||
|
"name": "test", /* inline block */
|
||||||
|
/* multi
|
||||||
|
line */
|
||||||
|
"value": "hello // world",
|
||||||
|
"other": 123 // trailing
|
||||||
|
}`
|
||||||
|
Expect(compact(read(input))).To(Equal(compact(`{
|
||||||
|
"name": "test",
|
||||||
|
"value": "hello // world",
|
||||||
|
"other": 123
|
||||||
|
}`)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles consecutive slashes that are not comments", func() {
|
||||||
|
input := `{"path": "/a/b"}`
|
||||||
|
Expect(read(input)).To(Equal(input))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles block comment at end of input", func() {
|
||||||
|
input := `{"key": "value"}/* comment */`
|
||||||
|
Expect(compact(read(input))).To(Equal(`{"key":"value"}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips comment with windows-style line endings", func() {
|
||||||
|
input := "{\r\n// comment\r\n\"key\": \"value\"\r\n}"
|
||||||
|
Expect(compact(read(input))).To(Equal(compact(`{"key": "value"}`)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips line comments with mixed line endings", func() {
|
||||||
|
// From original library: // comments with both \n and \r\n, including multiple on same line
|
||||||
|
input := "{\n\"one\": 1, // test //\n\"two\": 2, //test //\r\n\"string\": \"value\"\n//test\n}"
|
||||||
|
expected := "{\n\"one\": 1, \n\"two\": 2, \r\n\"string\": \"value\"\n\n}"
|
||||||
|
Expect(read(input)).To(Equal(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips line comment at start of JSON", func() {
|
||||||
|
// From original library: // comment as first thing in JSON
|
||||||
|
input := "{// woot\n\"one\": 1, // test //\n\"two\": 2, //test //\r\n\"string\": \"value\"\n//test\n}"
|
||||||
|
expected := "{\n\"one\": 1, \n\"two\": 2, \r\n\"string\": \"value\"\n\n}"
|
||||||
|
Expect(read(input)).To(Equal(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("strips block comments with mixed line endings inside", func() {
|
||||||
|
// From original library: block comment containing \r\n
|
||||||
|
input := "{/* multi\nline\r\ncomment */\"one\":1}"
|
||||||
|
expected := "{\"one\":1}"
|
||||||
|
Expect(read(input)).To(Equal(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles complex mix of escaped quotes, comments, and strings", func() {
|
||||||
|
// From original library TestQuotationEscape: escaped quote inside string followed by
|
||||||
|
// comment-like chars, then real comments of both types
|
||||||
|
input := "{/* multi\nline\r\ncomment */\"one\": \"a value \\\" // /*woot\"/* m\nl *///woot\r\n}"
|
||||||
|
expected := "{\"one\": \"a value \\\" // /*woot\"\r\n}"
|
||||||
|
Expect(read(input)).To(Equal(expected))
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user