fix(plugins): add base64 handling for []byte and remove raw=true (#5121)

* fix(plugins): add base64 handling for []byte and remove raw=true

Go's json.Marshal automatically base64-encodes []byte fields, but Rust's
serde_json serializes Vec<u8> as a JSON array and Python's json.dumps
raises TypeError on bytes. This fixes both directions of plugin
communication by adding proper base64 encoding/decoding in generated
client code.

For Rust templates (client and capability): adds a base64_bytes serde
helper module with #[serde(with = "base64_bytes")] on all Vec<u8> fields,
and adds base64 as a dependency. For Python templates: wraps bytes params
with base64.b64encode() and responses with base64.b64decode().

Also removes the raw=true binary framing protocol from all templates,
the parser, and the Method type. The raw mechanism added complexity that
is no longer needed once []byte works properly over JSON.

* fix(plugins): update production code and tests for base64 migration

Remove raw=true annotation from SubsonicAPI.CallRaw, delete all raw
test fixtures, remove raw-related test cases from parser, generator, and
integration tests, and add new test cases validating base64 handling
for Rust and Python templates.

* fix(plugins): update golden files and regenerate production code

Update golden test fixtures for codec and comprehensive services to
include base64 handling for []byte fields. Regenerate all production
PDK code (Go, Rust, Python) and host wrappers to use standard JSON
with base64-encoded byte fields instead of binary framing protocol.

* refactor: remove base64 helper duplication from rust template

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): add base64 dependency to capabilities' Cargo.toml

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-02-27 19:00:19 -05:00
committed by GitHub
parent 582d1b3cd9
commit bd8032b327
36 changed files with 460 additions and 854 deletions
+3 -3
View File
@@ -17,8 +17,8 @@ type SubsonicAPIService interface {
Call(ctx context.Context, uri string) (responseJSON string, err error)
// CallRaw executes a Subsonic API request and returns the raw binary response.
// Optimized for binary endpoints like getCoverArt and stream that return
// non-JSON data. The response is returned as raw bytes without JSON encoding overhead.
//nd:hostfunc raw=true
// Designed for binary endpoints like getCoverArt and stream that return
// non-JSON data. The data is base64-encoded over JSON on the wire.
//nd:hostfunc
CallRaw(ctx context.Context, uri string) (contentType string, data []byte, err error)
}
+15 -29
View File
@@ -4,7 +4,6 @@ package host
import (
"context"
"encoding/binary"
"encoding/json"
extism "github.com/extism/go-sdk"
@@ -26,6 +25,13 @@ type SubsonicAPICallRawRequest struct {
Uri string `json:"uri"`
}
// SubsonicAPICallRawResponse is the response type for SubsonicAPI.CallRaw.
type SubsonicAPICallRawResponse struct {
ContentType string `json:"contentType,omitempty"`
Data []byte `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// RegisterSubsonicAPIHostFunctions registers SubsonicAPI service host functions.
// The returned host functions should be added to the plugin's configuration.
func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService) []extism.HostFunction {
@@ -76,37 +82,28 @@ func newSubsonicAPICallRawHostFunction(service SubsonicAPIService) extism.HostFu
// Read JSON request from plugin memory
reqBytes, err := p.ReadBytes(stack[0])
if err != nil {
subsonicapiWriteRawError(p, stack, err)
subsonicapiWriteError(p, stack, err)
return
}
var req SubsonicAPICallRawRequest
if err := json.Unmarshal(reqBytes, &req); err != nil {
subsonicapiWriteRawError(p, stack, err)
subsonicapiWriteError(p, stack, err)
return
}
// Call the service method
contenttype, data, svcErr := service.CallRaw(ctx, req.Uri)
if svcErr != nil {
subsonicapiWriteRawError(p, stack, svcErr)
subsonicapiWriteError(p, stack, svcErr)
return
}
// Write binary-framed response to plugin memory:
// [0x00][4-byte content-type length (big-endian)][content-type string][raw data]
ctBytes := []byte(contenttype)
frame := make([]byte, 1+4+len(ctBytes)+len(data))
frame[0] = 0x00 // success
binary.BigEndian.PutUint32(frame[1:5], uint32(len(ctBytes)))
copy(frame[5:5+len(ctBytes)], ctBytes)
copy(frame[5+len(ctBytes):], data)
respPtr, err := p.WriteBytes(frame)
if err != nil {
stack[0] = 0
return
// Write JSON response to plugin memory
resp := SubsonicAPICallRawResponse{
ContentType: contenttype,
Data: data,
}
stack[0] = respPtr
subsonicapiWriteResponse(p, stack, resp)
},
[]extism.ValueType{extism.ValueTypePTR},
[]extism.ValueType{extism.ValueTypePTR},
@@ -137,14 +134,3 @@ func subsonicapiWriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
respPtr, _ := p.WriteBytes(respBytes)
stack[0] = respPtr
}
// subsonicapiWriteRawError writes a binary-framed error response to plugin memory.
// Format: [0x01][UTF-8 error message]
func subsonicapiWriteRawError(p *extism.CurrentPlugin, stack []uint64, err error) {
errMsg := []byte(err.Error())
frame := make([]byte, 1+len(errMsg))
frame[0] = 0x01 // error
copy(frame[1:], errMsg)
respPtr, _ := p.WriteBytes(frame)
stack[0] = respPtr
}