fix(jellycompat): stop binding NUL advisory lock keys

#529 stripped NUL from durable playback-session JSON, but
replaceUnstartedNegotiation still joined token/device/item with U+0000
and bound that string as PostgreSQL text. Postgres rejects NUL in text
(SQLSTATE 22021), so every negotiated compat session with a DeviceId
failed to persist and stayed cache-only.

Join the lock key with chr(31) inside PostgreSQL, matching the marker
contribution lock, so the bind parameters stay valid text.

Co-authored-by: Quick <Quick104@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-12 18:09:30 +00:00
co-authored by Quick
parent e36612e619
commit e485b3ab9d
2 changed files with 25 additions and 2 deletions
@@ -42,6 +42,13 @@ var (
var jsonNULCodePoint = []byte(`\u0000`)
// compatNegotiationLockSQL takes token, device ID, and route item ID as
// separate text parameters and joins them with chr(31) inside PostgreSQL.
const compatNegotiationLockSQL = `
SELECT pg_advisory_xact_lock(
hashtextextended($1 || chr(31) || $2 || chr(31) || $3, 0)
)`
// marshalPlaybackSession removes NUL code points from every nested string in
// the JSON document. PostgreSQL JSONB rejects U+0000 even when Go's encoder
// represents it as \u0000. Escaped literal text (\\u0000) remains unchanged.
@@ -218,8 +225,14 @@ func (d *DurableCompatPlaybackStore) replaceUnstartedNegotiation(
var removed []string
if session.CompatToken != "" && session.ClientDeviceID != "" && session.RouteItemID != "" {
scope := session.CompatToken + "\x00" + session.ClientDeviceID + "\x00" + session.RouteItemID
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, scope); err != nil {
// Join the lock key inside PostgreSQL. A Go-side NUL delimiter cannot
// be bound as text: PostgreSQL rejects U+0000 (SQLSTATE 22021), the
// transaction aborts, and the negotiated session stays cache-only.
if _, err := tx.Exec(ctx, compatNegotiationLockSQL,
stripCompatNUL(session.CompatToken),
stripCompatNUL(session.ClientDeviceID),
stripCompatNUL(session.RouteItemID),
); err != nil {
return nil, err
}
rows, err := tx.Query(ctx, `
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
@@ -13,6 +14,15 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
func TestCompatNegotiationLockSQLDoesNotBindNULDelimiter(t *testing.T) {
if strings.Contains(compatNegotiationLockSQL, "\x00") {
t.Fatal("advisory lock SQL must not embed a NUL delimiter")
}
if !strings.Contains(compatNegotiationLockSQL, "chr(31)") {
t.Fatal("advisory lock SQL should join key parts with chr(31) inside PostgreSQL")
}
}
func TestMarshalPlaybackSessionStripsNestedNUL(t *testing.T) {
wantLiteral := `literal\u0000text`
data, err := marshalPlaybackSession(PlaybackSession{