mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-09 17:18:45 +00:00
Some checks are pending
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
* feat(plugins): export ReadPackageManifest and ValidatePackage for off-disk inspection * test(plugins): cover ValidatePackage cross-field validation branch * feat(cmd): add 'plugin list' command * refactor(cmd): drop premature pluginManager interface until first use * feat(cmd): add 'plugin enable' and 'plugin disable' commands * feat(cmd): add 'plugin edit' for config and permission updates * test(cmd): cover plugin edit aborting on config validation failure * feat(cmd): add 'plugin info' and 'plugin validate' (installed id or .ndp file) * test(cmd): unit-test formatManifestInfo nil-guard and json branches * feat(cmd): add 'plugin rescan' command * feat(cmd): enrich plugin info text output and re-validate config on validate * fix(cmd): omit zero-value timestamps in plugin info text output * fix(cmd): give plugin list and info separate format flag vars A shared package-global bound to both commands' --format flag caused the later init() registration (info, default "text") to clobber the earlier one (list, default "table"), so 'plugin list' with no -f failed with 'invalid output format "text"'. Split into pluginListFormat / pluginInfoFormat and add a regression test on the registered defaults. * fix(plugins): address code-review findings on plugin CLI - edit: read-modify-write merge so flipping one permission flag no longer wipes unspecified fields (matches the native API); reject non-JSON --users/--libraries values. - info: return an error on unknown --format (was silently text); include sha256 in off-disk JSON output; align the text columns. - move declaredPermissions onto plugins.Permissions.DeclaredNames() as the single source of truth; drop ValidatePackage's redundant Validate call. - readConfigFile: pass ctx to log.Fatal; copy flag globals before taking their address; trim comments that restated the code. * refactor(plugins): export ReadManifest/ComputeFileSHA256, drop thin CLI wrappers Remove the ReadPackageManifest/ComputeFileSHA256/ValidatePackage wrappers in favor of exporting the underlying readManifest->ReadManifest and computeFileSHA256->ComputeFileSHA256 directly. ValidatePackage was identical to ReadPackageManifest (readManifest already validates via ParseManifest), so the CLI's validate path now calls ReadManifest too. * test(plugins): consolidate ReadManifest specs and move ComputeFileSHA256 tests Merge the two ReadManifest Describe blocks into one, and move the ComputeFileSHA256 tests out of package_test.go into a new manager_sync_test.go (where the function now lives), deduping the overlapping hash specs. * test(plugins): use createTestPackage everywhere, drop writeNDP helper The schema-invalid and cross-field ReadManifest specs now build packages with createTestPackage (typed Manifest) instead of the raw-JSON writeNDP helper, so there is a single package-building helper. Also trims the gratuitous 1MB wasm buffer in the read-only spec to a few bytes. * test(plugins): drop duplicate missing-manifest spec from ReadManifest The openPackage block already covers the missing-manifest.json error path (shared zip-walk code); ReadManifest's identical copy added no distinct coverage. * test(plugins): fix misleading ReadManifest spec name and drop unused wasm bytes The spec was named 'should read only the manifest without loading wasm' but ReadManifest returns only (*Manifest, error) — it cannot assert whether wasm was loaded. Rename to what it actually verifies (manifest parses from a package that also contains a wasm entry) and pass nil wasm bytes, since the contents are never observed. * fix(cmd): address PR review feedback on plugin CLI - edit --users/--libraries now accept both comma-separated and JSON-array input (CSV is converted to the JSON the manager stores); help text documents both. - Permissions.DeclaredNames() derives names by reflecting over the generated struct's json tags instead of a hand-maintained list, so new permission types are picked up automatically. - isPackagePath checks only the .ndp suffix, so a mistyped package path yields a precise 'no such file' error instead of falling back to a misleading 'Plugin not found'. - Restore a ReadManifest test for the missing-manifest.json error path (it has its own branch, separate from openPackage). * docs(cmd): trim verbose comments to the why * fix(cmd): setting an explicit users/libraries list clears the all-* flag Per Codex review: with allUsers/allLibraries true, 'plugin edit --users <list>' preserved the all-* flag so the allow-list was silently ignored, and the mutually-exclusive flags meant it couldn't be corrected in one command. An explicit list now implies all-*=false.
122 lines
4 KiB
Go
122 lines
4 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
)
|
|
|
|
// DeclaredNames returns the sorted names of the non-nil permission fields. It
|
|
// reflects over the generated json tags so new permission types are picked up
|
|
// automatically rather than via a hand-maintained list.
|
|
func (p *Permissions) DeclaredNames() []string {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
var names []string
|
|
v := reflect.ValueOf(*p)
|
|
t := v.Type()
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := v.Field(i)
|
|
if f.Kind() != reflect.Pointer || f.IsNil() {
|
|
continue
|
|
}
|
|
tag := t.Field(i).Tag.Get("json")
|
|
if name, _, _ := strings.Cut(tag, ","); name != "" && name != "-" {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
//go:generate go tool go-jsonschema -p plugins --struct-name-from-title -o manifest_gen.go manifest-schema.json
|
|
|
|
// ParseManifest unmarshals manifest JSON and performs cross-field validation.
|
|
// This is the single entry point for manifest parsing after reading from a file.
|
|
func ParseManifest(data []byte) (*Manifest, error) {
|
|
var m Manifest
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return nil, fmt.Errorf("parsing manifest JSON: %w", err)
|
|
}
|
|
if err := m.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validating manifest: %w", err)
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
// Validate performs cross-field validation that cannot be expressed in JSON Schema.
|
|
// This validates rules like "SubsonicAPI permission requires users permission".
|
|
func (m *Manifest) Validate() error {
|
|
// SubsonicAPI permission requires users permission
|
|
if m.Permissions != nil && m.Permissions.Subsonicapi != nil {
|
|
if m.Permissions.Users == nil {
|
|
return fmt.Errorf("'subsonicapi' permission requires 'users' permission to be declared")
|
|
}
|
|
}
|
|
|
|
// Validate config schema if present
|
|
if m.Config != nil && m.Config.Schema != nil {
|
|
if err := validateConfigSchema(m.Config.Schema); err != nil {
|
|
return fmt.Errorf("invalid config schema: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConfigSchema validates that the schema is a valid JSON Schema that can be compiled.
|
|
func validateConfigSchema(schema map[string]any) error {
|
|
compiler := jsonschema.NewCompiler()
|
|
if err := compiler.AddResource("schema.json", schema); err != nil {
|
|
return fmt.Errorf("invalid schema structure: %w", err)
|
|
}
|
|
if _, err := compiler.Compile("schema.json"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateWithCapabilities validates the manifest against detected capabilities.
|
|
// This must be called after WASM capability detection since Scrobbler capability
|
|
// is detected from exported functions, not manifest declarations.
|
|
func ValidateWithCapabilities(m *Manifest, capabilities []Capability) error {
|
|
// Scrobbler capability requires users permission
|
|
if hasCapability(capabilities, CapabilityScrobbler) {
|
|
if m.Permissions == nil || m.Permissions.Users == nil {
|
|
return fmt.Errorf("scrobbler capability requires 'users' permission to be declared in manifest")
|
|
}
|
|
}
|
|
|
|
// Scheduler permission requires SchedulerCallback capability
|
|
if m.Permissions != nil && m.Permissions.Scheduler != nil {
|
|
if !hasCapability(capabilities, CapabilityScheduler) {
|
|
return fmt.Errorf("'scheduler' permission requires plugin to export '%s' function", FuncSchedulerCallback)
|
|
}
|
|
}
|
|
|
|
// Task (taskqueue) permission requires TaskWorker capability
|
|
if m.Permissions != nil && m.Permissions.Taskqueue != nil {
|
|
if !hasCapability(capabilities, CapabilityTaskWorker) {
|
|
return fmt.Errorf("'taskqueue' permission requires plugin to export '%s' function", FuncTaskWorkerCallback)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasExperimentalThreads returns true if the manifest requests experimental threads support.
|
|
func (m *Manifest) HasExperimentalThreads() bool {
|
|
return m.Experimental != nil && m.Experimental.Threads != nil
|
|
}
|
|
|
|
// HasLibraryFilesystemPermission checks if the manifest grants filesystem permission for libraries.
|
|
func (m *Manifest) HasLibraryFilesystemPermission() bool {
|
|
return m.Permissions != nil &&
|
|
m.Permissions.Library != nil &&
|
|
m.Permissions.Library.Filesystem
|
|
}
|