Skip to content

Commit 6ca0de4

Browse files
thesprockeeclaude
andcommitted
Add test coverage for analyze and inventory modes
Includes pkg/naming dependency required by analyze.go and inventory.go (merged in PR #10) which was missing from main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4113850 commit 6ca0de4

4 files changed

Lines changed: 436 additions & 0 deletions

File tree

cmd/evrtools/analyze_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestDetectMagic_AllSignatures(t *testing.T) {
9+
// Track which magic byte patterns we have already seen. Entries that
10+
// share identical (offset, magic) with an earlier entry are shadowed
11+
// and can never be the first match -- skip them (the RIFF duplicate
12+
// bug is covered by TestDetectMagic_DuplicateRIFF).
13+
type magicKey struct {
14+
offset int
15+
magic string
16+
}
17+
seen := make(map[magicKey]string)
18+
19+
for _, sig := range magicSignatures {
20+
key := magicKey{sig.offset, string(sig.magic)}
21+
if first, ok := seen[key]; ok {
22+
t.Logf("skipping %q: shadowed by earlier entry %q (same magic)", sig.name, first)
23+
continue
24+
}
25+
seen[key] = sig.name
26+
27+
t.Run(sig.name, func(t *testing.T) {
28+
// Build a data slice with the magic bytes at the correct offset,
29+
// padded with zeros before and after.
30+
data := make([]byte, sig.offset+len(sig.magic)+4)
31+
copy(data[sig.offset:], sig.magic)
32+
33+
got := detectMagic(data)
34+
if got != sig.name {
35+
t.Errorf("detectMagic() = %q, want %q", got, sig.name)
36+
}
37+
})
38+
}
39+
}
40+
41+
// TestDetectMagic_DuplicateRIFF demonstrates a bug: any RIFF-based format
42+
// (e.g., AVI with bytes "RIFF....AVI ") is misclassified as "WAV audio"
43+
// because the magic table checks only the first 4 bytes ("RIFF") and the
44+
// "WAV audio" entry appears before the "RIFF generic" entry. The table
45+
// does not inspect the RIFF sub-format bytes at offset 8.
46+
func TestDetectMagic_DuplicateRIFF(t *testing.T) {
47+
// Construct a RIFF/AVI header: "RIFF" + 4-byte size + "AVI "
48+
avi := []byte{
49+
0x52, 0x49, 0x46, 0x46, // "RIFF"
50+
0x00, 0x00, 0x00, 0x00, // size placeholder
51+
0x41, 0x56, 0x49, 0x20, // "AVI " sub-format
52+
}
53+
54+
got := detectMagic(avi)
55+
56+
// The correct classification would be something like "RIFF generic" or
57+
// "AVI video", but because the WAV entry matches first on the shared
58+
// 4-byte prefix, we get "WAV audio" for non-WAV RIFF data.
59+
if got != "WAV audio" {
60+
t.Errorf("expected bug: detectMagic(AVI data) = %q, want %q (bug: RIFF sub-format not checked)", got, "WAV audio")
61+
}
62+
}
63+
64+
func TestDetectMagic_UnknownBytes(t *testing.T) {
65+
data := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE}
66+
got := detectMagic(data)
67+
if got != "unknown" {
68+
t.Errorf("detectMagic() = %q, want %q", got, "unknown")
69+
}
70+
}
71+
72+
func TestShannonEntropy_AllSame(t *testing.T) {
73+
data := make([]byte, 256)
74+
for i := range data {
75+
data[i] = 0x41
76+
}
77+
got := shannonEntropy(data)
78+
if got != 0 {
79+
t.Errorf("shannonEntropy(all same) = %f, want 0", got)
80+
}
81+
}
82+
83+
func TestShannonEntropy_Uniform(t *testing.T) {
84+
data := make([]byte, 256)
85+
for i := range data {
86+
data[i] = byte(i)
87+
}
88+
got := shannonEntropy(data)
89+
// Maximum entropy for 256 equally-likely symbols is exactly 8.0.
90+
if math.Abs(got-8.0) > 0.001 {
91+
t.Errorf("shannonEntropy(uniform) = %f, want ~8.0", got)
92+
}
93+
}
94+
95+
func TestShannonEntropy_Empty(t *testing.T) {
96+
got := shannonEntropy(nil)
97+
if got != 0 {
98+
t.Errorf("shannonEntropy(empty) = %f, want 0", got)
99+
}
100+
}
101+
102+
func TestTopFormat_Basic(t *testing.T) {
103+
formats := map[string]int{
104+
"DDS texture": 10,
105+
"PNG image": 3,
106+
"unknown": 1,
107+
}
108+
got := topFormat(formats)
109+
if got != "DDS texture" {
110+
t.Errorf("topFormat() = %q, want %q", got, "DDS texture")
111+
}
112+
}
113+
114+
func TestTopFormat_Empty(t *testing.T) {
115+
got := topFormat(map[string]int{})
116+
if got != "unknown" {
117+
t.Errorf("topFormat(empty) = %q, want %q", got, "unknown")
118+
}
119+
}

cmd/evrtools/inventory_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/EchoTools/evrFileTools/pkg/naming"
9+
)
10+
11+
func TestFormatBytes(t *testing.T) {
12+
tests := []struct {
13+
input int64
14+
want string
15+
}{
16+
{0, "0 B"},
17+
{512, "512 B"},
18+
{1024, "1.0 KB"},
19+
{1048576, "1.0 MB"},
20+
{1073741824, "1.0 GB"},
21+
}
22+
for _, tc := range tests {
23+
t.Run(tc.want, func(t *testing.T) {
24+
got := formatBytes(tc.input)
25+
if got != tc.want {
26+
t.Errorf("formatBytes(%d) = %q, want %q", tc.input, got, tc.want)
27+
}
28+
})
29+
}
30+
}
31+
32+
func TestCountFiles(t *testing.T) {
33+
dir := t.TempDir()
34+
35+
// Create regular files with known sizes.
36+
for _, name := range []string{"a.txt", "b.txt", "c.txt"} {
37+
path := filepath.Join(dir, name)
38+
if err := os.WriteFile(path, []byte("hello"), 0644); err != nil {
39+
t.Fatal(err)
40+
}
41+
}
42+
43+
// Create a subdirectory (should be excluded from count).
44+
if err := os.Mkdir(filepath.Join(dir, "subdir"), 0755); err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
count, totalBytes, err := countFiles(dir)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
if count != 3 {
53+
t.Errorf("countFiles count = %d, want 3", count)
54+
}
55+
// Each file contains "hello" = 5 bytes, total = 15.
56+
if totalBytes != 15 {
57+
t.Errorf("countFiles totalBytes = %d, want 15", totalBytes)
58+
}
59+
}
60+
61+
func TestCountKnownTypes(t *testing.T) {
62+
stats := []typeStats{
63+
{typeSymbol: naming.TypeDDSTexture, count: 10}, // known
64+
{typeSymbol: naming.TypeAudioReference, count: 5}, // known
65+
{typeSymbol: naming.TypeSymbol(0x1234), count: 3}, // unknown
66+
{typeSymbol: naming.TypeSymbol(0x5678), count: 1}, // unknown
67+
}
68+
69+
got := countKnownTypes(stats)
70+
if got != 2 {
71+
t.Errorf("countKnownTypes() = %d, want 2", got)
72+
}
73+
}

pkg/naming/asset_mapper.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Package naming provides asset name mappings for EVR assets.
2+
package naming
3+
4+
// AssetNameMapper maps file symbols to human-readable asset names.
5+
// This is populated with known assets and can be extended.
6+
type AssetNameMapper struct {
7+
symbolToName map[int64]string
8+
nameToSymbol map[string]int64
9+
}
10+
11+
// NewAssetNameMapper creates a new mapper with known assets.
12+
func NewAssetNameMapper() *AssetNameMapper {
13+
m := &AssetNameMapper{
14+
symbolToName: make(map[int64]string),
15+
nameToSymbol: make(map[string]int64),
16+
}
17+
m.initializeKnownAssets()
18+
return m
19+
}
20+
21+
// initializeKnownAssets populates the mapper with known asset names.
22+
// These are discovered from game analysis and documentation.
23+
func (m *AssetNameMapper) initializeKnownAssets() {
24+
// Level/Environment Assets (from game strings and analysis)
25+
// Store as uint64 then convert to int64 to handle the sign bit properly
26+
known := []struct {
27+
symbol uint64
28+
name string
29+
}{
30+
// Social/Lobby Level
31+
{0x43e2da7914642604, "social_2.0_arena"},
32+
{0x43e2da7a0c623a19, "social_2.0_private"},
33+
{0xd09afd15b1c75c04, "social_2.0_npe"},
34+
35+
// Common texture references
36+
{0xdf5ca7b7dfa383d4, "arena_environment"},
37+
{0xcb9977f7fc2b4526, "lobby_environment"},
38+
{0x576ed3f8428ebc4b, "courtyard_environment"},
39+
{0x3f9915d3001dc28e, "environment_lighting"},
40+
{0x3c8d74713ced8c3f, "environment_props"},
41+
{0xac360e41e4ede056, "environment_decals"},
42+
{0xe24c89df8235dd7a, "environment_particles"},
43+
{0x4d82118c7c91b6bb, "environment_effects"},
44+
45+
// Note: Additional mappings can be added as they are discovered
46+
// through game binary analysis and community research
47+
}
48+
49+
for _, item := range known {
50+
sym := int64(item.symbol)
51+
m.symbolToName[sym] = item.name
52+
m.nameToSymbol[item.name] = sym
53+
}
54+
}
55+
56+
// GetName returns the name for a file symbol, or a fallback if unknown.
57+
// If the symbol is unknown, returns a hex representation.
58+
func (m *AssetNameMapper) GetName(fileSymbol int64) string {
59+
if name, ok := m.symbolToName[fileSymbol]; ok {
60+
return name
61+
}
62+
// Fallback: return hex representation
63+
return formatHexSymbol(fileSymbol)
64+
}
65+
66+
// HasName returns true if a name is known for the given symbol.
67+
func (m *AssetNameMapper) HasName(fileSymbol int64) bool {
68+
_, ok := m.symbolToName[fileSymbol]
69+
return ok
70+
}
71+
72+
// GetSymbol returns the symbol for a known name, or 0 if not found.
73+
func (m *AssetNameMapper) GetSymbol(name string) int64 {
74+
if sym, ok := m.nameToSymbol[name]; ok {
75+
return sym
76+
}
77+
return 0
78+
}
79+
80+
// AddMapping adds or updates an asset name mapping.
81+
func (m *AssetNameMapper) AddMapping(fileSymbol int64, name string) {
82+
m.symbolToName[fileSymbol] = name
83+
m.nameToSymbol[name] = fileSymbol
84+
}
85+
86+
// AddMappings adds multiple asset name mappings at once.
87+
func (m *AssetNameMapper) AddMappings(mappings map[int64]string) {
88+
for sym, name := range mappings {
89+
m.AddMapping(sym, name)
90+
}
91+
}
92+
93+
// KnownSymbolCount returns the number of known symbol mappings.
94+
func (m *AssetNameMapper) KnownSymbolCount() int {
95+
return len(m.symbolToName)
96+
}
97+
98+
// formatHexSymbol returns a hex representation of a symbol.
99+
func formatHexSymbol(symbol int64) string {
100+
return formatHex(uint64(symbol))
101+
}
102+
103+
// formatHex returns a hex string representation of a uint64.
104+
// This is intentionally simple - returns the 16-digit hex value.
105+
func formatHex(value uint64) string {
106+
const hexChars = "0123456789abcdef"
107+
var result [16]byte
108+
109+
for i := 15; i >= 0; i-- {
110+
result[i] = hexChars[value&0xf]
111+
value >>= 4
112+
}
113+
114+
return string(result[:])
115+
}
116+
117+
// GlobalAssetMapper is the default global mapper instance.
118+
var globalAssetMapper = NewAssetNameMapper()
119+
120+
// GetAssetName returns the name for a file symbol using the global mapper.
121+
func GetAssetName(fileSymbol int64) string {
122+
return globalAssetMapper.GetName(fileSymbol)
123+
}
124+
125+
// HasAssetName returns true if a name is known using the global mapper.
126+
func HasAssetName(fileSymbol int64) bool {
127+
return globalAssetMapper.HasName(fileSymbol)
128+
}
129+
130+
// AddGlobalMapping adds a mapping to the global mapper.
131+
func AddGlobalMapping(fileSymbol int64, name string) {
132+
globalAssetMapper.AddMapping(fileSymbol, name)
133+
}
134+
135+
// AddGlobalMappings adds multiple mappings to the global mapper.
136+
func AddGlobalMappings(mappings map[int64]string) {
137+
globalAssetMapper.AddMappings(mappings)
138+
}

0 commit comments

Comments
 (0)