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, "xml": 0x0BCD, "wlk": 0x0BCC, "gr2": 0x0FA3, } 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) } } } // 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) } } }