package plugins import ( "archive/zip" "errors" "fmt" "io" ) const ( // PackageExtension is the file extension for Navidrome plugin packages. PackageExtension = ".ndp" // manifestFileName is the name of the manifest file inside the package. manifestFileName = "manifest.json" // wasmFileName is the name of the WebAssembly module inside the package. wasmFileName = "plugin.wasm" ) // ndpPackage represents a loaded .ndp plugin package. // It contains the manifest and wasm bytes read from the archive. type ndpPackage struct { Manifest *Manifest WasmBytes []byte } // openPackage opens an .ndp file and extracts the manifest and wasm bytes. // The caller does not need to call Close() - all resources are read into memory. func openPackage(ndpPath string) (*ndpPackage, error) { // Open the zip archive zr, err := zip.OpenReader(ndpPath) if err != nil { return nil, fmt.Errorf("opening package: %w", err) } defer zr.Close() var manifestBytes []byte var wasmBytes []byte for _, f := range zr.File { switch f.Name { case manifestFileName: manifestBytes, err = readZipFile(f) if err != nil { return nil, fmt.Errorf("reading manifest: %w", err) } case wasmFileName: wasmBytes, err = readZipFile(f) if err != nil { return nil, fmt.Errorf("reading wasm: %w", err) } } } if manifestBytes == nil { return nil, errors.New("package missing manifest.json") } if wasmBytes == nil { return nil, errors.New("package missing plugin.wasm") } // Parse and validate manifest manifest, err := ParseManifest(manifestBytes) if err != nil { return nil, fmt.Errorf("parsing manifest: %w", err) } return &ndpPackage{ Manifest: manifest, WasmBytes: wasmBytes, }, nil } // ReadManifest reads and validates the manifest from a .ndp file without loading // the wasm bytes (it runs ParseManifest, so JSON-schema and cross-field // validation are applied). Useful for quick plugin discovery and validation. func ReadManifest(ndpPath string) (*Manifest, error) { // Open the zip archive zr, err := zip.OpenReader(ndpPath) if err != nil { return nil, fmt.Errorf("opening package: %w", err) } defer zr.Close() for _, f := range zr.File { if f.Name == manifestFileName { manifestBytes, err := readZipFile(f) if err != nil { return nil, fmt.Errorf("reading manifest: %w", err) } manifest, err := ParseManifest(manifestBytes) if err != nil { return nil, fmt.Errorf("parsing manifest: %w", err) } return manifest, nil } } return nil, errors.New("package missing manifest.json") } // readZipFile reads the contents of a file from a zip archive. func readZipFile(f *zip.File) ([]byte, error) { rc, err := f.Open() if err != nil { return nil, err } defer rc.Close() return io.ReadAll(rc) }