mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 23:54:33 +01:00
Port ECD encryption/decryption from ReFrontier (C#) and FrontierTextHandler (Python) into common/decryption. The cipher uses a 32-bit LCG key stream with an 8-round Feistel-like nibble transformation and CFB chaining; all six key sets are supported, key 4 being the default for all MHF files. On startup, loadRengokuBinary now decrypts (ECD) and decompresses (JKR) the binary to validate pointer bounds and entry counts, then logs a structured summary (floor counts, spawn table counts, unique monster IDs). Failures are non-fatal — the encrypted blob is still cached and served to clients unchanged, preserving existing behaviour. Closes #173.
183 lines
5.0 KiB
Go
183 lines
5.0 KiB
Go
package channelserver
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// buildRengokuData constructs a minimal but structurally valid rengoku binary
|
||
// for testing. It contains one floor and one spawn table per road mode.
|
||
//
|
||
// Layout:
|
||
//
|
||
// 0x00–0x13 header (magic + version + padding)
|
||
// 0x14–0x2B multiDef RoadMode
|
||
// 0x2C–0x43 soloDef RoadMode
|
||
// 0x44–0x5B multiDef FloorStats (24 bytes)
|
||
// 0x5C–0x63 multiDef spawnTablePtrs (1×u32 = 4 bytes)
|
||
// 0x64–0x67 multiDef spawnCountPtrs (1×u32 = 4 bytes)
|
||
// 0x68–0x87 multiDef SpawnTable (32 bytes)
|
||
// 0x88–0x9F soloDef FloorStats (24 bytes)
|
||
// 0xA0–0xA3 soloDef spawnTablePtrs (1×u32)
|
||
// 0xA4–0xA7 soloDef spawnCountPtrs (1×u32)
|
||
// 0xA8–0xC7 soloDef SpawnTable (32 bytes)
|
||
func buildRengokuData(multiMonster1, multiMonster2, soloMonster1, soloMonster2 uint32) []byte {
|
||
buf := make([]byte, 0xC8)
|
||
|
||
// Header
|
||
buf[0] = 'r'
|
||
buf[1] = 'e'
|
||
buf[2] = 'f'
|
||
buf[3] = 0x1A
|
||
buf[4] = 1 // version
|
||
|
||
le := binary.LittleEndian
|
||
|
||
// multiDef RoadMode at 0x14
|
||
le.PutUint32(buf[0x14:], 1) // floorStatsCount
|
||
le.PutUint32(buf[0x18:], 1) // spawnCountCount
|
||
le.PutUint32(buf[0x1C:], 1) // spawnTablePtrCount
|
||
le.PutUint32(buf[0x20:], 0x44) // floorStatsPtr
|
||
le.PutUint32(buf[0x24:], 0x5C) // spawnTablePtrsPtr
|
||
le.PutUint32(buf[0x28:], 0x64) // spawnCountPtrsPtr
|
||
|
||
// soloDef RoadMode at 0x2C
|
||
le.PutUint32(buf[0x2C:], 1) // floorStatsCount
|
||
le.PutUint32(buf[0x30:], 1) // spawnCountCount
|
||
le.PutUint32(buf[0x34:], 1) // spawnTablePtrCount
|
||
le.PutUint32(buf[0x38:], 0x88) // floorStatsPtr
|
||
le.PutUint32(buf[0x3C:], 0xA0) // spawnTablePtrsPtr
|
||
le.PutUint32(buf[0x40:], 0xA4) // spawnCountPtrsPtr
|
||
|
||
// multiDef FloorStats at 0x44 (24 bytes)
|
||
le.PutUint32(buf[0x44:], 1) // floorNumber
|
||
|
||
// multiDef spawnTablePtrs at 0x5C: points to SpawnTable at 0x68
|
||
le.PutUint32(buf[0x5C:], 0x68)
|
||
|
||
// multiDef SpawnTable at 0x68 (32 bytes)
|
||
le.PutUint32(buf[0x68:], multiMonster1)
|
||
le.PutUint32(buf[0x70:], multiMonster2)
|
||
|
||
// soloDef FloorStats at 0x88 (24 bytes)
|
||
le.PutUint32(buf[0x88:], 1) // floorNumber
|
||
|
||
// soloDef spawnTablePtrs at 0xA0: points to SpawnTable at 0xA8
|
||
le.PutUint32(buf[0xA0:], 0xA8)
|
||
|
||
// soloDef SpawnTable at 0xA8 (32 bytes)
|
||
le.PutUint32(buf[0xA8:], soloMonster1)
|
||
le.PutUint32(buf[0xB0:], soloMonster2)
|
||
|
||
return buf
|
||
}
|
||
|
||
func TestParseRengokuBinary_ValidMinimal(t *testing.T) {
|
||
data := buildRengokuData(101, 102, 103, 101) // monster 101 appears in both roads
|
||
|
||
info, err := parseRengokuBinary(data)
|
||
if err != nil {
|
||
t.Fatalf("parseRengokuBinary: %v", err)
|
||
}
|
||
if info.MultiFloors != 1 {
|
||
t.Errorf("MultiFloors = %d, want 1", info.MultiFloors)
|
||
}
|
||
if info.MultiSpawnTables != 1 {
|
||
t.Errorf("MultiSpawnTables = %d, want 1", info.MultiSpawnTables)
|
||
}
|
||
if info.SoloFloors != 1 {
|
||
t.Errorf("SoloFloors = %d, want 1", info.SoloFloors)
|
||
}
|
||
if info.SoloSpawnTables != 1 {
|
||
t.Errorf("SoloSpawnTables = %d, want 1", info.SoloSpawnTables)
|
||
}
|
||
// IDs present: 101, 102, 103 → 3 unique (101 shared between roads)
|
||
if info.UniqueMonsters != 3 {
|
||
t.Errorf("UniqueMonsters = %d, want 3", info.UniqueMonsters)
|
||
}
|
||
}
|
||
|
||
func TestParseRengokuBinary_ZeroMonsterIDsExcluded(t *testing.T) {
|
||
data := buildRengokuData(0, 55, 0, 0) // only monster 55 is non-zero
|
||
|
||
info, err := parseRengokuBinary(data)
|
||
if err != nil {
|
||
t.Fatalf("parseRengokuBinary: %v", err)
|
||
}
|
||
if info.UniqueMonsters != 1 {
|
||
t.Errorf("UniqueMonsters = %d, want 1 (zeros excluded)", info.UniqueMonsters)
|
||
}
|
||
}
|
||
|
||
func TestParseRengokuBinary_Errors(t *testing.T) {
|
||
validData := buildRengokuData(1, 2, 3, 4)
|
||
|
||
cases := []struct {
|
||
name string
|
||
data []byte
|
||
wantErr string
|
||
}{
|
||
{
|
||
name: "too_small",
|
||
data: make([]byte, 10),
|
||
wantErr: "too small",
|
||
},
|
||
{
|
||
name: "bad_magic",
|
||
data: func() []byte {
|
||
d := make([]byte, len(validData))
|
||
copy(d, validData)
|
||
d[0] = 0xFF
|
||
return d
|
||
}(),
|
||
wantErr: "invalid magic",
|
||
},
|
||
{
|
||
name: "wrong_version",
|
||
data: func() []byte {
|
||
d := make([]byte, len(validData))
|
||
copy(d, validData)
|
||
d[4] = 2
|
||
return d
|
||
}(),
|
||
wantErr: "unexpected version",
|
||
},
|
||
{
|
||
name: "floorStats_ptr_out_of_bounds",
|
||
data: func() []byte {
|
||
d := make([]byte, len(validData))
|
||
copy(d, validData)
|
||
// Set multiDef floorStatsPtr to beyond file end
|
||
binary.LittleEndian.PutUint32(d[0x20:], uint32(len(d)+1))
|
||
return d
|
||
}(),
|
||
wantErr: "out of bounds",
|
||
},
|
||
{
|
||
name: "spawnTable_ptr_target_out_of_bounds",
|
||
data: func() []byte {
|
||
d := make([]byte, len(validData))
|
||
copy(d, validData)
|
||
// Point the spawn table pointer to just before the end so SpawnTable
|
||
// (32 bytes) would extend beyond the file.
|
||
binary.LittleEndian.PutUint32(d[0x5C:], uint32(len(d)-4))
|
||
return d
|
||
}(),
|
||
wantErr: "out of bounds",
|
||
},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
_, err := parseRengokuBinary(tc.data)
|
||
if err == nil {
|
||
t.Fatal("expected error, got nil")
|
||
}
|
||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
|
||
}
|
||
})
|
||
}
|
||
}
|