🏡 index : quine-global/quintodrome.git

package plugins

import (
	"archive/zip"
	"errors"
	"fmt"
	"io"
	"path/filepath"
	"strings"
)

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

// validPluginID reports whether id is usable. It names a directory under
// DataFolder/plugins, so a path-like one would point at another plugin's data.
func validPluginID(id string) bool {
	if id == "." || id == ".." || strings.ContainsAny(id, `/\`) || !filepath.IsLocal(id) {
		return false
	}
	// Windows drops trailing dots and spaces, so "foo." and "foo" would end up
	// sharing a directory
	return strings.TrimRight(id, ". ") == id
}

// pluginIDFromPath derives the plugin ID from a package path.
func pluginIDFromPath(path string) (string, bool) {
	id := strings.TrimSuffix(filepath.Base(path), PackageExtension)
	if !validPluginID(id) {
		return "", false
	}
	return id, true
}

// 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)
}