fix(server): prevent refinement crashes from missing private option messages

This commit is contained in:
2026-10-05 17:42:34 +08:00
parent 0e6a8ef97b
commit e5ce92a77b
2 changed files with 52 additions and 1 deletions
+6 -1
View File
@@ -351,9 +351,14 @@ func EquipmentWire(entry Equipment) []byte {
for _, option := range entry.SubOption {
base = wire.AppendBytes(base, 4, equipmentOptionWire(option))
}
// The native ResolvedPrivateOption getter dereferences PrivateOption before
// checking IsValid. A present empty message selects its GameData fallback;
// an omitted field leaves the protobuf object null and crashes the UI.
var private []byte
if entry.PrivateOption != nil {
base = wire.AppendBytes(base, 5, equipmentOptionWire(*entry.PrivateOption))
private = equipmentOptionWire(*entry.PrivateOption)
}
base = wire.AppendBytes(base, 5, private)
for _, rank := range entry.Rank {
base = wire.AppendVarint(base, 6, rank)
}
@@ -45,6 +45,7 @@ func TestEquipmentUpgradeAndSequenceUseGameDataCosts(t *testing.T) {
if err != nil || !handled || code != 37 {
t.Fatalf("single upgrade code=%d handled=%v err=%v", code, handled, err)
}
assertEquipmentPrivateMessage(t, response)
encoded, found, err := wire.Bytes(response, 1)
if err != nil || !found {
t.Fatalf("single upgrade equipment: %v", err)
@@ -60,6 +61,7 @@ func TestEquipmentUpgradeAndSequenceUseGameDataCosts(t *testing.T) {
if err != nil || !handled || code != 176 {
t.Fatalf("sequence upgrade code=%d handled=%v err=%v", code, handled, err)
}
assertEquipmentPrivateMessage(t, response)
if result, _, _ := wire.Varint(response, 3); result != equipUpgradeStopMaxLevel {
t.Fatalf("sequence result=%d", result)
}
@@ -125,6 +127,7 @@ func TestEquipmentSmeltingImprovesByTotalScoreAndReplaysWithoutSecondCharge(t *t
if err != nil || !handled || code != 105 {
t.Fatalf("smelting code=%d handled=%v err=%v", code, handled, err)
}
assertEquipmentPrivateMessage(t, response)
if result, found, _ := wire.Varint(response, 3); found || result != 0 {
t.Fatalf("successful smelting result=%d found=%v", result, found)
}
@@ -227,6 +230,7 @@ func TestEquipmentSequenceSmeltingRepeatsAndStopsAtTargetScore(t *testing.T) {
if err != nil || !handled || code != 177 {
t.Fatalf("sequence smelting code=%d handled=%v err=%v", code, handled, err)
}
assertEquipmentPrivateMessage(t, response)
if result, _, _ := wire.Varint(response, 3); result != equipUpgradeStopTargetLevel {
t.Fatalf("sequence smelting result=%d", result)
}
@@ -723,3 +727,45 @@ func TestEquipmentChangeReplacesOnlyMatchingGameDataSlot(t *testing.T) {
t.Fatalf("changed equipment=%+v", got)
}
}
func assertEquipmentPrivateMessage(t *testing.T, response []byte) {
t.Helper()
entry, ok, err := wire.Bytes(response, 1)
if err != nil || !ok {
t.Fatal("equipment entry absent", err)
}
base, ok, err := wire.Bytes(entry, 5)
if err != nil || !ok {
t.Fatal("equipment base absent", err)
}
private, ok, err := wire.Bytes(base, 5)
if err != nil || !ok {
t.Fatal("native PrivateOption reference would be null", err)
}
if len(private) != 0 {
t.Fatal("ordinary equipment has unexpected private option")
}
}
func TestEquipmentWirePrivateObjectPresenceAndExplicitOption(t *testing.T) {
for _, private := range []*EquipmentOption{nil, {GroupID: 301, ID: 2}} {
entry := Equipment{ID: 943035, InvenIndex: 1, Rank: []uint64{1, 2, 3}, PrivateOption: private}
base, _, _ := wire.Bytes(EquipmentWire(entry), 5)
option, present, err := wire.Bytes(base, 5)
if err != nil || !present {
t.Fatal("PrivateOption was omitted", err)
}
group, _, _ := wire.Varint(option, 1)
id, _, _ := wire.Varint(option, 2)
if private == nil {
if len(option) != 0 || group != 0 || id != 0 {
t.Fatal("empty fallback object malformed")
}
} else if group != private.GroupID || id != private.ID {
t.Fatal("explicit private option altered")
}
if entry.PrivateOption != private {
t.Fatal("wire generation mutated ownership")
}
}
}