Skip to content

Commit 2f540cd

Browse files
authored
Merge pull request minekube#667 from minekube/fix/297-bcc-status-decoder-tests
test: add integration tests for BCC status response (fixes minekube#297)
2 parents ca468b1 + 2a001f1 commit 2f540cd

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package codec
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"testing"
7+
8+
"github.com/go-logr/logr"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"go.minekube.com/gate/pkg/edition/java/proto/packet"
13+
"go.minekube.com/gate/pkg/edition/java/proto/state"
14+
"go.minekube.com/gate/pkg/edition/java/proto/util"
15+
"go.minekube.com/gate/pkg/edition/java/proto/version"
16+
"go.minekube.com/gate/pkg/gate/proto"
17+
)
18+
19+
// buildStatusResponsePacket constructs a raw Minecraft status response packet.
20+
// The packet format is: VarInt(frameLength) + VarInt(packetID=0x00) + String(status) + extraBytes
21+
func buildStatusResponsePacket(status string, extraBytes []byte) []byte {
22+
var payload bytes.Buffer
23+
_ = util.WriteVarInt(&payload, 0x00) // StatusResponse packet ID
24+
_ = util.WriteString(&payload, status)
25+
payload.Write(extraBytes)
26+
27+
var frame bytes.Buffer
28+
_ = util.WriteVarInt(&frame, payload.Len())
29+
frame.Write(payload.Bytes())
30+
return frame.Bytes()
31+
}
32+
33+
// buildBCCExtraData constructs the extra bytes that BetterCompatibilityChecker (BCC)
34+
// appends after the standard status response string within the same packet frame.
35+
//
36+
// BCC appends a second VarInt-prefixed JSON string containing mod compatibility metadata.
37+
// This was confirmed by probing a real Fabric 1.20.1 server with BCC v4.0.1 installed.
38+
//
39+
// See:
40+
// - https://github.com/minekube/gate/issues/297
41+
// - https://github.com/nanite/BetterCompatibilityChecker (source repository)
42+
// - Config defaults "CHANGE_ME": https://github.com/nanite/BetterCompatibilityChecker/blob/main/common/src/main/java/dev/wuffs/bcc/Config.java
43+
// - ServerStatus CODEC modification (Fabric): https://github.com/nanite/BetterCompatibilityChecker/blob/main/fabric/src/main/java/dev/wuffs/bcc/mixin/ServerStatusMixin.java
44+
func buildBCCExtraData(bccJSON string) []byte {
45+
var buf bytes.Buffer
46+
_ = util.WriteString(&buf, bccJSON)
47+
return buf.Bytes()
48+
}
49+
50+
func TestDecoder_StatusResponse_NormalPacket(t *testing.T) {
51+
status := `{"version":{"name":"1.20.1","protocol":763},"players":{"max":20,"online":5},"description":"A Minecraft Server"}`
52+
raw := buildStatusResponsePacket(status, nil)
53+
54+
dec := NewDecoder(bytes.NewReader(raw), proto.ClientBound, logr.Discard())
55+
dec.SetState(state.Status)
56+
dec.SetProtocol(version.Minecraft_1_20.Protocol)
57+
58+
ctx, err := dec.Decode()
59+
require.NoError(t, err)
60+
require.NotNil(t, ctx)
61+
62+
res, ok := ctx.Packet.(*packet.StatusResponse)
63+
require.True(t, ok, "expected *packet.StatusResponse, got %T", ctx.Packet)
64+
assert.Equal(t, status, res.Status)
65+
}
66+
67+
// TestDecoder_StatusResponse_BCC tests the scenario from issue #297 where
68+
// BetterCompatibilityChecker (BCC) appends extra mod metadata after the
69+
// standard status response JSON within the same packet frame.
70+
//
71+
// BCC modifies Minecraft's ServerStatus CODEC to append a second VarInt-prefixed
72+
// JSON string containing fields like releaseType, projectId, name, and version.
73+
// Gate's StatusResponse.Decode only reads the first string (the standard status JSON),
74+
// leaving BCC's extra string unread, which triggers ErrDecoderLeftBytes.
75+
//
76+
// The fix ensures that:
77+
// 1. The decoder returns the decoded PacketContext even when ErrDecoderLeftBytes occurs
78+
// (decoder.go readPacket)
79+
// 2. Callers like decodeStatusResponse() in lite/forward.go can ignore ErrDecoderLeftBytes
80+
// and use the valid status response
81+
//
82+
// See: https://github.com/minekube/gate/issues/297
83+
func TestDecoder_StatusResponse_BCC(t *testing.T) {
84+
// Standard Minecraft status response JSON
85+
status := `{"version":{"name":"1.20.1","protocol":763},"description":{"text":"A Minecraft Server"},"players":{"max":20,"online":0}}`
86+
87+
// BCC appends a second VarInt-prefixed JSON string with mod compatibility data.
88+
// These are the real defaults from BCC's Config.java ("CHANGE_ME" values):
89+
// https://github.com/nanite/BetterCompatibilityChecker/blob/main/common/src/main/java/dev/wuffs/bcc/Config.java
90+
bccJSON := `{"releaseType":"unknown","projectId":0,"name":"CHANGE_ME","version":"CHANGE_ME"}`
91+
extraBytes := buildBCCExtraData(bccJSON)
92+
93+
raw := buildStatusResponsePacket(status, extraBytes)
94+
95+
dec := NewDecoder(bytes.NewReader(raw), proto.ClientBound, logr.Discard())
96+
dec.SetState(state.Status)
97+
dec.SetProtocol(version.Minecraft_1_20.Protocol)
98+
99+
ctx, err := dec.Decode()
100+
101+
// The extra BCC bytes trigger ErrDecoderLeftBytes
102+
require.Error(t, err)
103+
assert.True(t, errors.Is(err, proto.ErrDecoderLeftBytes),
104+
"expected ErrDecoderLeftBytes, got: %v", err)
105+
106+
// Critical: ctx must NOT be nil — this was the core bug.
107+
// Before the fix, readPacket returned (nil, ErrDecoderLeftBytes),
108+
// causing a nil pointer dereference in decodeStatusResponse().
109+
require.NotNil(t, ctx, "PacketContext must not be nil when ErrDecoderLeftBytes is returned")
110+
require.NotNil(t, ctx.Packet, "Packet must not be nil")
111+
112+
// The status response should be correctly decoded despite the extra bytes
113+
res, ok := ctx.Packet.(*packet.StatusResponse)
114+
require.True(t, ok, "expected *packet.StatusResponse, got %T", ctx.Packet)
115+
assert.Equal(t, status, res.Status)
116+
}
117+
118+
// TestDecoder_StatusResponse_BCC_EndToEnd tests the full error-handling flow
119+
// as implemented in lite/forward.go decodeStatusResponse():
120+
// ignore ErrDecoderLeftBytes but propagate any other errors.
121+
//
122+
// See: https://github.com/minekube/gate/issues/297
123+
func TestDecoder_StatusResponse_BCC_EndToEnd(t *testing.T) {
124+
status := `{"version":{"name":"1.20.1","protocol":763},"description":{"text":"A Minecraft Server"},"players":{"max":20,"online":0}}`
125+
bccJSON := `{"releaseType":"unknown","projectId":0,"name":"CHANGE_ME","version":"CHANGE_ME"}`
126+
extraBytes := buildBCCExtraData(bccJSON)
127+
raw := buildStatusResponsePacket(status, extraBytes)
128+
129+
dec := NewDecoder(bytes.NewReader(raw), proto.ClientBound, logr.Discard())
130+
dec.SetState(state.Status)
131+
dec.SetProtocol(version.Minecraft_1_20.Protocol)
132+
133+
ctx, err := dec.Decode()
134+
135+
// Simulate what decodeStatusResponse() in lite/forward.go does:
136+
// ignore ErrDecoderLeftBytes but propagate other errors.
137+
if err != nil && !errors.Is(err, proto.ErrDecoderLeftBytes) {
138+
t.Fatalf("unexpected error: %v", err)
139+
}
140+
require.NotNil(t, ctx, "PacketContext must not be nil")
141+
142+
res, ok := ctx.Packet.(*packet.StatusResponse)
143+
require.True(t, ok)
144+
assert.Equal(t, status, res.Status)
145+
}
146+
147+
// TestDecoder_StatusResponse_WithArbitraryExtraBytes verifies the fix works for
148+
// any kind of extra bytes appended to a status response, not just BCC's format.
149+
func TestDecoder_StatusResponse_WithArbitraryExtraBytes(t *testing.T) {
150+
status := `{"version":{"name":"1.20.1","protocol":763},"description":"Test"}`
151+
extraBytes := make([]byte, 256)
152+
for i := range extraBytes {
153+
extraBytes[i] = byte(i)
154+
}
155+
raw := buildStatusResponsePacket(status, extraBytes)
156+
157+
dec := NewDecoder(bytes.NewReader(raw), proto.ClientBound, logr.Discard())
158+
dec.SetState(state.Status)
159+
dec.SetProtocol(version.Minecraft_1_20.Protocol)
160+
161+
ctx, err := dec.Decode()
162+
163+
if err != nil && !errors.Is(err, proto.ErrDecoderLeftBytes) {
164+
t.Fatalf("unexpected error: %v", err)
165+
}
166+
require.NotNil(t, ctx)
167+
168+
res, ok := ctx.Packet.(*packet.StatusResponse)
169+
require.True(t, ok)
170+
assert.Equal(t, status, res.Status)
171+
}

0 commit comments

Comments
 (0)