package pipeline import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" ) const directProjectConfig = `{ "module": { "name": "Test Module", "resref": "testmod", "hak_order": ["core_01"] }, "paths": { "source": "src", "assets": "assets", "build": "build" } } ` func writeBlobAt(t *testing.T, root, sha, content string) { t.Helper() dir := filepath.Join(root, "sha256", sha[0:2], sha[2:4]) mustMkdir(t, dir) mustWriteFile(t, filepath.Join(dir, sha), content) } func writeBlob(t *testing.T, root string, payload []byte) string { t.Helper() sum := sha256.Sum256(payload) sha := hex.EncodeToString(sum[:]) writeBlobAt(t, root, sha, string(payload)) return sha } func writeDirectManifest(t *testing.T, root string, sources map[string]SourceAsset, assets []string) string { t.Helper() manifest := SourceBuildManifest{ Schema: 1, BuilderID: buildinfo.String(), ModuleHAKs: []string{"core_01"}, HAKs: []SourceManifestHAK{{ Name: "core_01", Group: "core", Priority: 1, MaxBytes: 1 << 20, Assets: assets, }}, AssetSources: sources, } raw, err := json.Marshal(manifest) if err != nil { t.Fatalf("marshal manifest: %v", err) } path := filepath.Join(root, "build-source-manifest.json") mustWriteFile(t, path, string(raw)) return path } func loadDirectProject(t *testing.T, root string) *project.Project { t.Helper() mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), directProjectConfig) mustMkdir(t, filepath.Join(root, "build")) p, err := project.Load(root) if err != nil { t.Fatalf("load project: %v", err) } return p } func TestBuildHAKsFromContentAddressedManifestWithoutAssetsTree(t *testing.T) { root := t.TempDir() p := loadDirectProject(t, root) blobs := filepath.Join(root, "blobs") payload := []byte("tga-payload-bytes") sha := writeBlob(t, blobs, payload) manifestPath := writeDirectManifest(t, root, map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: int64(len(payload))}}, []string{"core/a.tga"}) result, err := BuildHAKsWithOptions(p, BuildHAKOptions{ SourceManifestPath: manifestPath, ContentAddressedRoot: blobs, }) if err != nil { t.Fatalf("build direct haks: %v", err) } if result.HAKAssets != 1 { t.Fatalf("hak assets = %d, want 1", result.HAKAssets) } if _, err := os.Stat(filepath.Join(root, "assets")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("direct build must not materialize an assets tree (stat err = %v)", err) } hakPath := filepath.Join(root, "build", "core_01.hak") raw, err := os.ReadFile(hakPath) if err != nil { t.Fatalf("read hak: %v", err) } archive, err := erf.Read(bytes.NewReader(raw)) if err != nil { t.Fatalf("decode hak: %v", err) } if len(archive.Resources) != 1 { t.Fatalf("hak resources = %d, want 1", len(archive.Resources)) } if got := string(archive.Resources[0].Data); got != string(payload) { t.Fatalf("resource payload = %q, want %q", got, payload) } if archive.Resources[0].Name != "a" { t.Fatalf("resref = %q, want a", archive.Resources[0].Name) } } func TestBuildHAKsFromContentAddressedManifestRejectsMissingRoot(t *testing.T) { root := t.TempDir() p := loadDirectProject(t, root) blobs := filepath.Join(root, "blobs") payload := []byte("tga-payload-bytes") sha := writeBlob(t, blobs, payload) manifestPath := writeDirectManifest(t, root, map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: int64(len(payload))}}, []string{"core/a.tga"}) _, err := BuildHAKsWithOptions(p, BuildHAKOptions{SourceManifestPath: manifestPath}) if err == nil { t.Fatal("expected failure when content-addressed root is missing") } } func TestBuildHAKsFromContentAddressedManifestRejectsMissingBlob(t *testing.T) { root := t.TempDir() p := loadDirectProject(t, root) blobs := filepath.Join(root, "blobs") sha := strings.Repeat("b", 64) manifestPath := writeDirectManifest(t, root, map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: 4}}, []string{"core/a.tga"}) _, err := BuildHAKsWithOptions(p, BuildHAKOptions{ SourceManifestPath: manifestPath, ContentAddressedRoot: blobs, }) if err == nil { t.Fatal("expected failure for missing source blob") } } func TestBuildHAKsFromContentAddressedManifestRejectsCorruptBlob(t *testing.T) { root := t.TempDir() p := loadDirectProject(t, root) blobs := filepath.Join(root, "blobs") declared := sha256.Sum256([]byte("abcd")) sha := hex.EncodeToString(declared[:]) // Same length, different bytes: size check passes, content hash mismatches. writeBlobAt(t, blobs, sha, "wxyz") manifestPath := writeDirectManifest(t, root, map[string]SourceAsset{"core/a.tga": {SHA256: sha, SizeBytes: 4}}, []string{"core/a.tga"}) _, err := BuildHAKsWithOptions(p, BuildHAKOptions{ SourceManifestPath: manifestPath, ContentAddressedRoot: blobs, }) if err == nil { t.Fatal("expected sha256 mismatch failure for corrupt blob") } if !strings.Contains(err.Error(), "sha256 mismatch") { t.Fatalf("error = %v, want sha256 mismatch", err) } if _, statErr := os.Stat(filepath.Join(root, "build", "core_01.hak")); !errors.Is(statErr, os.ErrNotExist) { t.Fatalf("corrupt build must not leave a completed hak (stat err = %v)", statErr) } } func TestLegacySourceManifestStillBuildsFromAssetsTree(t *testing.T) { root := t.TempDir() mustMkdir(t, filepath.Join(root, "src", "module")) mustMkdir(t, filepath.Join(root, "assets", "core")) mustMkdir(t, filepath.Join(root, "build")) mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ "module": {"name": "Test Module", "resref": "testmod", "hak_order": ["group:core"]}, "paths": {"source": "src", "assets": "assets", "build": "build"}, "haks": [{"name": "core", "priority": 1, "include": ["core/**"]}] } `) mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{ "file_type": "IFO ", "file_version": "V3.2", "root": {"struct_type": 0, "fields": [ {"label": "Mod_Name", "type": "CExoString", "value": "Test Module"}, {"label": "Mod_HakList", "type": "List", "value": []} ]} } `) mustWriteFile(t, filepath.Join(root, "assets", "core", "a.tga"), strings.Repeat("a", 16)) mustWriteFile(t, filepath.Join(root, "assets", "core", "b.tga"), strings.Repeat("b", 16)) manifestPath := filepath.Join(root, "legacy-source-manifest.json") mustWriteFile(t, manifestPath, `{ "module_haks": ["core"], "haks": [{"name": "core", "group": "core", "priority": 1, "assets": ["core/a.tga", "core/b.tga"]}] } `) p, err := project.Load(root) if err != nil { t.Fatalf("load project: %v", err) } if err := p.Scan(); err != nil { t.Fatalf("scan: %v", err) } result, err := BuildHAKsWithOptions(p, BuildHAKOptions{SourceManifestPath: manifestPath}) if err != nil { t.Fatalf("legacy build: %v", err) } if len(result.HAKPaths) != 1 { t.Fatalf("hak paths = %d, want 1", len(result.HAKPaths)) } raw, err := os.ReadFile(filepath.Join(root, "build", "core.hak")) if err != nil { t.Fatalf("read legacy hak: %v", err) } archive, err := erf.Read(bytes.NewReader(raw)) if err != nil { t.Fatalf("decode legacy hak: %v", err) } if len(archive.Resources) != 2 { t.Fatalf("legacy hak resources = %d, want 2", len(archive.Resources)) } }