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()