fix(conf): validate PlaylistsPath the same way it is matched (#6276)

validatePlaylistsPath checked the raw pattern, while InPath matches the
converted one. On Windows, 'Playlists\{rock,jazz}' failed validation
and stopped the server, although InPath matches it, and
'Playlists\[Mix' passed validation but is invalid once converted.
Validation now uses conf.PlaylistsPathPatterns, like InPath.

Tests load real TOML files through conf.LoadFromFile.
This commit is contained in:
Deluan 2026-10-07 10:11:00 -04:00
commit c892862c12
2 changed files with 37 additions and 4 deletions

View file

@ -861,10 +861,9 @@ func isEscape(c byte, rest string) bool {
}
func validatePlaylistsPath() error {
for path := range strings.SplitSeq(Server.PlaylistsPath, string(filepath.ListSeparator)) {
_, err := doublestar.Match(path, "")
if err != nil {
return fmt.Errorf("invalid PlaylistsPath %q: %w", path, err)
for _, pattern := range PlaylistsPathPatterns() {
if _, err := doublestar.Match(pattern, ""); err != nil {
return fmt.Errorf("invalid PlaylistsPath %q: %w", pattern, err)
}
}
return nil

View file

@ -417,6 +417,40 @@ var _ = Describe("Configuration", func() {
)
})
Describe("PlaylistsPath validation", func() {
// A real TOML file with literal (single-quoted) strings, like Windows users write paths
loadConfig := func(pattern string) (err error) {
file := filepath.Join(GinkgoT().TempDir(), "navidrome.toml")
Expect(os.WriteFile(file, []byte("PlaylistsPath = '"+pattern+"'\n"), 0600)).To(Succeed())
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
}
}()
conf.LoadFromFile(file)
return nil
}
DescribeTable("accepts exactly the patterns InPath can use",
func(pattern string, accepted bool) {
err := loadConfig(pattern)
if accepted {
Expect(err).ToNot(HaveOccurred())
Expect(conf.Server.PlaylistsPath).To(Equal(pattern))
} else {
Expect(err).To(MatchError(ContainSubstring("invalid PlaylistsPath")))
}
},
Entry("backslash path (issue #6276)", `Playlists\navidrome`, true),
Entry("escaped brackets", `\[Mix\]`, true),
Entry("backslash before a bracket range", `Playlists\[ab]`, true),
Entry("unterminated character class", `[Mix`, false),
Entry("unterminated brace alternatives", `{rock,jazz`, false),
Entry("backslash before brace alternatives", `Playlists\{rock,jazz}`, runtime.GOOS == "windows"),
Entry("backslash before an unterminated class", `Playlists\[Mix`, runtime.GOOS != "windows"),
)
})
Describe("MaxImageSize floor", func() {
BeforeEach(func() {
viper.Reset()