🏡 index : ironmagma/quintodrome.git

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 only the manifest from an .ndp file without loading the wasm bytes.
// This is useful for quick plugin discovery.
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)
}