mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-27 09:22:17 +00:00
The plugin ID is derived from the package filename and used verbatim as a directory name under DataFolder/plugins by the kvstore and taskqueue host services. A package installed as '..ndp' yields the ID '.', whose data directory resolves to the parent of every other plugin's directory, so it overlaps their private data. On Windows, trailing dots and spaces are dropped during path normalization, so 'foo..ndp' and 'foo.ndp' yield distinct IDs that resolve to the same directory and would share the same SQLite files. Discovery and the file watcher now derive the ID through pluginIDFromPath, which rejects '.', '..', empty names, separators, trailing dots or spaces, and anything filepath.IsLocal refuses (Windows reserved names, drive-relative paths). The loader repeats the check, since a sync failure is non-fatal and could otherwise leave a stale row reaching the host services.
93 lines
3 KiB
Go
93 lines
3 KiB
Go
package plugins
|
|
|
|
import (
|
|
"github.com/navidrome/navidrome/model"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("buildExtismManifest", func() {
|
|
var pkg *ndpPackage
|
|
|
|
BeforeEach(func() {
|
|
pkg = &ndpPackage{
|
|
WasmBytes: []byte("wasm"),
|
|
Manifest: &Manifest{Permissions: &Permissions{
|
|
Library: &LibraryPermission{Reason: new("test"), Filesystem: true},
|
|
Http: &HTTPPermission{Reason: new("test"), RequiredHosts: []string{"example.com"}},
|
|
}},
|
|
}
|
|
})
|
|
|
|
It("never sets AllowedPaths, even with filesystem permission", func() {
|
|
Expect(buildExtismManifest(pkg, nil).AllowedPaths).To(BeEmpty())
|
|
})
|
|
|
|
It("carries the hosts the plugin is allowed to reach", func() {
|
|
Expect(buildExtismManifest(pkg, nil).AllowedHosts).To(Equal([]string{"example.com"}))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("loadPluginWithConfig", func() {
|
|
// Discovery already rejects these, but a row predating that check, or one
|
|
// left behind by a failed sync, must not reach the mount setup
|
|
It("refuses a plugin whose ID is not usable as a directory name", func() {
|
|
m := &Manager{plugins: make(map[string]*plugin)}
|
|
|
|
err := m.loadPluginWithConfig(&model.Plugin{ID: "..", Path: "/does/not/matter.ndp"})
|
|
|
|
Expect(err).To(MatchError(ContainSubstring("invalid plugin ID")))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("parsePluginConfig", func() {
|
|
It("returns nil for empty string", func() {
|
|
result, err := parsePluginConfig("")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).To(BeNil())
|
|
})
|
|
|
|
It("serializes object values as JSON strings", func() {
|
|
result, err := parsePluginConfig(`{"settings": {"enabled": true, "count": 5}}`)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).To(HaveLen(1))
|
|
Expect(result["settings"]).To(Equal(`{"count":5,"enabled":true}`))
|
|
})
|
|
|
|
It("handles mixed value types", func() {
|
|
result, err := parsePluginConfig(`{"api_key": "secret", "timeout": 30, "rate": 1.5, "enabled": true, "tags": ["a", "b"]}`)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).To(HaveLen(5))
|
|
Expect(result["api_key"]).To(Equal("secret"))
|
|
Expect(result["timeout"]).To(Equal("30"))
|
|
Expect(result["rate"]).To(Equal("1.5"))
|
|
Expect(result["enabled"]).To(Equal("true"))
|
|
Expect(result["tags"]).To(Equal(`["a","b"]`))
|
|
})
|
|
|
|
It("returns error for invalid JSON", func() {
|
|
_, err := parsePluginConfig(`{invalid json}`)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("parsing plugin config"))
|
|
})
|
|
|
|
It("returns error for non-object JSON", func() {
|
|
_, err := parsePluginConfig(`["array", "not", "object"]`)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("parsing plugin config"))
|
|
})
|
|
|
|
It("handles null values", func() {
|
|
result, err := parsePluginConfig(`{"key": null}`)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).To(HaveLen(1))
|
|
Expect(result["key"]).To(Equal("null"))
|
|
})
|
|
|
|
It("handles empty object", func() {
|
|
result, err := parsePluginConfig(`{}`)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(result).To(HaveLen(0))
|
|
Expect(result).ToNot(BeNil())
|
|
})
|
|
})
|