Closes #64. Five entries in `internal/erf/erf.go`'s restype table disagreed with upstream neverwinter.nim (`neverwinter/restype.nim`). `HAKResourceTypeForExtension` packs arbitrary source files, so a `.jpg` packed into a HAK today is written as restype 2076, which the game reads as `tml`. | ext | was | now | |-----|-----|-----| | gff | 2039 (upstream `bte`) | 2037 | | ltr | 2067 (upstream `bak`) | 2036 | | jpg | 2076 (upstream `tml`) | 2081 | | dfa | 2045, name typo | `dft`, 2045 (number was already right) | | mdb | 2070 (upstream `xbc`) | removed | Upstream registers no restype for `mdb`, so there is no correct number to give it, and leaving it squatting on `xbc` silently mislabels the resource. It is dropped from `AssetExtensions` too, which turns a `.mdb` source into a loud "unsupported extension" build error instead of a corrupt HAK entry. **This is the one judgment call the issue left open** — if `mdb` should instead stay as a deliberate local addition like `lyt`/`vis`/`gr2`, say so and I will move it to a free number in the 0x0BB8+ local range. No asset in the current corpus uses any of these paths, so nothing in shipped content changes. The six local additions upstream does not have (`lyt` 3000, `vis` 3001, `mdx` 3008, `wlk` 3020, `xml` 3021, `gr2` 4003) are left alone and now carry a comment saying they are deliberate. ## Root cause The `init()` consistency guard only panicked when a number was missing from the reverse map. Its `if canonicalExt == ext { continue }` branch did nothing when the two maps disagreed, so a mismatch was never caught. The guard now panics on disagreement and also checks the reverse direction. ## Tests - `TestRestypeTableMatchesUpstream` pins every number shared with upstream, and asserts the four numbers upstream reserves for other extensions (2039 `bte`, 2067 `bak`, 2070 `xbc`, 2076 `tml`) stay unclaimed. - `TestRestypeTablesAreMutualInverses` is the check `init()` is meant to enforce. `make check` passes.Reviewed-on: #69 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
258 lines
7.8 KiB
Go
258 lines
7.8 KiB
Go
package erf
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeTempPayload(t *testing.T, payload []byte) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "payload.bin")
|
|
if err := os.WriteFile(path, payload, 0o644); err != nil {
|
|
t.Fatalf("write payload: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestWriteRejectsSourcePathSHA256Mismatch(t *testing.T) {
|
|
payload := []byte("hello world")
|
|
path := writeTempPayload(t, payload)
|
|
|
|
archive := New("HAK ", []Resource{{
|
|
Name: "asset",
|
|
Type: 0x0003,
|
|
SourcePath: path,
|
|
Size: int64(len(payload)),
|
|
ExpectedSHA256: strings.Repeat("a", 64),
|
|
}})
|
|
|
|
var buf bytes.Buffer
|
|
err := Write(&buf, archive)
|
|
if err == nil {
|
|
t.Fatal("expected sha256 mismatch error")
|
|
}
|
|
if !strings.Contains(err.Error(), "sha256 mismatch") {
|
|
t.Fatalf("error = %v, want sha256 mismatch", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteAcceptsMatchingSourcePathSHA256(t *testing.T) {
|
|
payload := []byte("hello world")
|
|
path := writeTempPayload(t, payload)
|
|
sum := sha256.Sum256(payload)
|
|
|
|
archive := New("HAK ", []Resource{{
|
|
Name: "asset",
|
|
Type: 0x0003,
|
|
SourcePath: path,
|
|
Size: int64(len(payload)),
|
|
ExpectedSHA256: hex.EncodeToString(sum[:]),
|
|
}})
|
|
|
|
var buf bytes.Buffer
|
|
if err := Write(&buf, archive); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
decoded, err := Read(bytes.NewReader(buf.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if len(decoded.Resources) != 1 || string(decoded.Resources[0].Data) != string(payload) {
|
|
t.Fatalf("unexpected payload: %#v", decoded.Resources)
|
|
}
|
|
}
|
|
|
|
func TestArchiveRoundTrip(t *testing.T) {
|
|
archive := New("MOD ", []Resource{
|
|
{Name: "module", Type: 0x07DE, Data: []byte("ifo")},
|
|
{Name: "start", Type: 0x07DC, Data: []byte("are")},
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
if err := Write(&buf, archive); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
decoded, err := Read(bytes.NewReader(buf.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
|
|
if len(decoded.Resources) != 2 {
|
|
t.Fatalf("expected 2 resources, got %d", len(decoded.Resources))
|
|
}
|
|
if decoded.Resources[0].Name != "module" || string(decoded.Resources[0].Data) != "ifo" {
|
|
t.Fatalf("unexpected first resource: %#v", decoded.Resources[0])
|
|
}
|
|
}
|
|
|
|
func TestExtensionMappingsKeep2DAAndTLKDistinct(t *testing.T) {
|
|
resourceType, ok := ResourceTypeForExtension("2da")
|
|
if !ok {
|
|
t.Fatal("expected 2da resource type")
|
|
}
|
|
if resourceType != 0x07E1 {
|
|
t.Fatalf("expected 2da type 0x07E1, got 0x%04X", resourceType)
|
|
}
|
|
extension, ok := ExtensionForResourceType(resourceType)
|
|
if !ok {
|
|
t.Fatal("expected canonical extension for 2da type")
|
|
}
|
|
if extension != "2da" {
|
|
t.Fatalf("expected 2da extension, got %q", extension)
|
|
}
|
|
|
|
tlkType, ok := ResourceTypeForExtension("tlk")
|
|
if !ok {
|
|
t.Fatal("expected tlk resource type")
|
|
}
|
|
if tlkType != 0x07E2 {
|
|
t.Fatalf("expected tlk type 0x07E2, got 0x%04X", tlkType)
|
|
}
|
|
if tlkType == resourceType {
|
|
t.Fatal("expected 2da and tlk resource types to stay distinct")
|
|
}
|
|
}
|
|
|
|
func TestHAKResourceTypeForExtensionSupportsPNG(t *testing.T) {
|
|
if resourceType, ok := HAKResourceTypeForExtension("png"); !ok || resourceType != 0x0820 {
|
|
t.Fatalf("expected png restype 0x0820 (2080) for HAK generation, got ok=%v type=0x%04X", ok, resourceType)
|
|
}
|
|
if resourceType, ok := HAKResourceTypeForExtension("2da"); !ok || resourceType != 0x07E1 {
|
|
t.Fatalf("expected 2da to stay valid for HAK generation, got ok=%v type=0x%04X", ok, resourceType)
|
|
}
|
|
}
|
|
|
|
func TestExtensionMappingsSupportModernAssetTypes(t *testing.T) {
|
|
cases := map[string]uint16{
|
|
"mtr": 0x0818,
|
|
"shd": 0x0815,
|
|
"txi": 0x07E6,
|
|
"jpg": 0x0821,
|
|
"lyt": 0x0BB8,
|
|
"vis": 0x0BB9,
|
|
"mdx": 0x0BC0,
|
|
}
|
|
for ext, wantType := range cases {
|
|
gotType, ok := ResourceTypeForExtension(ext)
|
|
if !ok {
|
|
t.Fatalf("expected %s to resolve to a resource type", ext)
|
|
}
|
|
if gotType != wantType {
|
|
t.Fatalf("expected %s type 0x%04X, got 0x%04X", ext, wantType, gotType)
|
|
}
|
|
gotExt, ok := ExtensionForResourceType(wantType)
|
|
if !ok {
|
|
t.Fatalf("expected type 0x%04X to resolve back to an extension", wantType)
|
|
}
|
|
if gotExt != ext {
|
|
t.Fatalf("expected type 0x%04X to resolve to %s, got %s", wantType, ext, gotExt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtensionMappingsSupportAllUTBlueprintTypes(t *testing.T) {
|
|
cases := map[string]uint16{
|
|
"uti": 0x07E9,
|
|
"utc": 0x07EB,
|
|
"utt": 0x07F0,
|
|
"uts": 0x07F3,
|
|
"ute": 0x07F8,
|
|
"utd": 0x07FA,
|
|
"utp": 0x07FC,
|
|
"utm": 0x0803,
|
|
"utg": 0x0807,
|
|
"utw": 0x080A,
|
|
}
|
|
for ext, wantType := range cases {
|
|
gotType, ok := ResourceTypeForExtension(ext)
|
|
if !ok {
|
|
t.Fatalf("expected %s to resolve to a resource type", ext)
|
|
}
|
|
if gotType != wantType {
|
|
t.Fatalf("expected %s type 0x%04X, got 0x%04X", ext, wantType, gotType)
|
|
}
|
|
gotExt, ok := ExtensionForResourceType(wantType)
|
|
if !ok {
|
|
t.Fatalf("expected type 0x%04X to resolve back to an extension", wantType)
|
|
}
|
|
if gotExt != ext {
|
|
t.Fatalf("expected type 0x%04X to resolve to %s, got %s", wantType, ext, gotExt)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRestypeTableMatchesUpstream pins the restype numbers Crucible shares with
|
|
// neverwinter.nim's restype.nim. The values below are upstream's; a mismatch
|
|
// means a HAK we write is mislabelled for the game.
|
|
func TestRestypeTableMatchesUpstream(t *testing.T) {
|
|
upstream := map[string]uint16{
|
|
"res": 0, "bmp": 1, "mve": 2, "tga": 3, "wav": 4, "plt": 6, "ini": 7,
|
|
"bmu": 8, "txt": 10, "mdl": 2002, "nss": 2009, "ncs": 2010, "are": 2012,
|
|
"set": 2013, "ifo": 2014, "bic": 2015, "wok": 2016, "2da": 2017,
|
|
"tlk": 2018, "txi": 2022, "git": 2023, "uti": 2025, "utc": 2027,
|
|
"dlg": 2029, "itp": 2030, "utt": 2032, "dds": 2033, "uts": 2035,
|
|
"ltr": 2036, "gff": 2037, "fac": 2038, "ute": 2040, "utd": 2042,
|
|
"utp": 2044, "dft": 2045, "gic": 2046, "gui": 2047, "utm": 2051,
|
|
"dwk": 2052, "pwk": 2053, "utg": 2055, "jrl": 2056, "utw": 2058,
|
|
"ssf": 2060, "hak": 2061, "nwm": 2062, "bik": 2063, "ndb": 2064,
|
|
"ptm": 2065, "ptt": 2066, "shd": 2069, "mtr": 2072, "lod": 2078,
|
|
"gif": 2079, "png": 2080, "jpg": 2081,
|
|
}
|
|
for ext, want := range upstream {
|
|
got, ok := ResourceTypeForExtension(ext)
|
|
if !ok {
|
|
t.Errorf("%s: not registered, upstream has %d", ext, want)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("%s: registered as %d, upstream has %d", ext, got, want)
|
|
}
|
|
}
|
|
// Extensions upstream registers that Crucible must not reuse for anything else.
|
|
reserved := map[uint16]string{2039: "bte", 2067: "bak", 2070: "xbc", 2076: "tml"}
|
|
for number, upstreamExt := range reserved {
|
|
if ext, ok := ExtensionForResourceType(number); ok {
|
|
t.Errorf("%d: registered as %s, upstream reserves it for %s", number, ext, upstreamExt)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNWN2FormatsAreNotRegistered keeps NWN2 file formats out of an NWN:EE
|
|
// table. mdb and gr2 have Aurora numbers that mean something else (or nothing)
|
|
// in NWN:EE; wlk and xml have no archive number in any Aurora game, so the
|
|
// values Crucible used for them were invented. Packing any of these into a HAK
|
|
// writes a resource the game misreads.
|
|
func TestNWN2FormatsAreNotRegistered(t *testing.T) {
|
|
for _, ext := range []string{"mdb", "gr2", "wlk", "xml"} {
|
|
if number, ok := ResourceTypeForExtension(ext); ok {
|
|
t.Errorf("%s is an NWN2 format but is registered as 0x%04X", ext, number)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRestypeTablesAreMutualInverses is the check init() is meant to enforce.
|
|
func TestRestypeTablesAreMutualInverses(t *testing.T) {
|
|
for ext, number := range extensionTypes {
|
|
canonical, ok := typeExtensions[number]
|
|
if !ok {
|
|
t.Errorf("%s: number 0x%04X missing from typeExtensions", ext, number)
|
|
continue
|
|
}
|
|
if canonical != ext {
|
|
t.Errorf("%s: maps to 0x%04X, which maps back to %s", ext, number, canonical)
|
|
}
|
|
}
|
|
for number, ext := range typeExtensions {
|
|
if got, ok := extensionTypes[ext]; !ok || got != number {
|
|
t.Errorf("0x%04X: maps to %s, which maps back to 0x%04X (ok=%v)", number, ext, got, ok)
|
|
}
|
|
}
|
|
}
|