From c892862c12cc4afefc3cf787b3821ff52ae23e0d Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 7 Oct 2026 10:11:00 -0400 Subject: [PATCH] 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. --- conf/configuration.go | 7 +++---- conf/configuration_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index cd5ea8d3a..3032cabd6 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -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 diff --git a/conf/configuration_test.go b/conf/configuration_test.go index e51d3084c..e62ca3d1b 100644 --- a/conf/configuration_test.go +++ b/conf/configuration_test.go @@ -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()