package dalamud import ( "archive/zip" "crypto/md5" "encoding/hex" "encoding/json" "os" "path/filepath" "strings" "testing" ) func writeZip(t *testing.T, path string, files map[string]string) { t.Helper() f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() w := zip.NewWriter(f) for name, content := range files { fw, err := w.Create(name) if err != nil { t.Fatal(err) } if _, err := fw.Write([]byte(content)); err != nil { t.Fatal(err) } } if err := w.Close(); err != nil { t.Fatal(err) } } func TestExtractZipCreatesTree(t *testing.T) { dir := t.TempDir() zp := filepath.Join(dir, "in.zip") writeZip(t, zp, map[string]string{ "Dalamud.Injector.exe": "MZ", "UIRes/bannedplugin.json": `{"a":1}`, }) dest := filepath.Join(dir, "out") if err := extractZip(zp, dest); err != nil { t.Fatal(err) } for _, p := range []string{ filepath.Join(dest, "Dalamud.Injector.exe"), filepath.Join(dest, "UIRes", "bannedplugin.json"), } { if b, err := os.ReadFile(p); err != nil { t.Fatalf("missing %s: %v", p, err) } else if len(b) == 0 { t.Fatalf("%s is empty", p) } } } func TestExtractZipRejectsTraversal(t *testing.T) { dir := t.TempDir() zp := filepath.Join(dir, "in.zip") writeZip(t, zp, map[string]string{ "ok.txt": "fine", "../evil.txt": "escape", "a/../../evil2": "deeper escape", }) dest := filepath.Join(dir, "out") if err := extractZip(zp, dest); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dest, "ok.txt")); err != nil { t.Fatalf("legit file missing: %v", err) } for _, escaped := range []string{filepath.Join(dir, "evil.txt"), filepath.Join(dir, "..", "evil2")} { if _, err := os.Stat(escaped); err == nil { t.Errorf("zip slip: %s exists outside dest", escaped) } } } func TestVerifyIntegrity(t *testing.T) { dir := t.TempDir() a, b := "alpha\n", "beta" if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte(a), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "sub", "b.txt"), []byte(b), 0o644); err != nil { t.Fatal(err) } hashes := map[string]string{ "a.txt": strings.ToUpper(hex.EncodeToString(md5Sum(a))), "sub\\b.txt": strings.ToUpper(hex.EncodeToString(md5Sum(b))), // backslashes as shipped in zips } if !writeHashes(t, dir, hashes) { t.Fatal("setup") } if !verifyIntegrity(dir, "hashes.json") { t.Fatal("expected integrity to pass") } if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("tampered"), 0o644); err != nil { t.Fatal(err) } if verifyIntegrity(dir, "hashes.json") { t.Fatal("expected integrity to fail after tampering") } os.Remove(filepath.Join(dir, "hashes.json")) if verifyIntegrity(dir, "hashes.json") { t.Fatal("expected integrity to fail without hashes.json") } } func TestBuildInjectorArgs(t *testing.T) { p := &Paths{ HooksDir: "/cfg/dalamud/hooks/15.0.3.6", RuntimeDir: "/cfg/dalamud/runtime", AssetDir: "/cfg/dalamud/assets/444", } lc := LaunchConfig{ GameExe: "/game/game/ffxiv_dx11.exe", ConfigDir: "/cfg/dalamud/config", LogDir: "/cfg/dalamud/logs", Language: 1, GameArgs: `//**sqex0003abc*4**//`, } args := buildInjectorArgs(p, lc) wantPrefix := []string{"launch", "--mode=inject"} for i, w := range wantPrefix { if args[i] != w { t.Fatalf("args[%d] = %q, want %q", i, args[i], w) } } joined := strings.Join(args, " ") for _, sub := range []string{ "--game=/game/game/ffxiv_dx11.exe", "--dalamud-working-directory=/cfg/dalamud/hooks/15.0.3.6", "--dalamud-configuration-path=", "dalamudConfig.json", "installedPlugins", "--dalamud-asset-directory=/cfg/dalamud/assets/444", "--dalamud-client-language=1", "--dalamud-delay-initialize=0", "--dalamud-tspack-b64=", } { if !strings.Contains(joined, sub) { t.Errorf("args missing %q:\n%s", sub, joined) } } // The encrypted game args must be the final element after a bare "--". if args[len(args)-2] != "--" || args[len(args)-1] != lc.GameArgs { t.Fatalf("game args not last: %q / %q", args[len(args)-2], args[len(args)-1]) } for _, s := range args { if strings.Contains(s, `"`) { t.Errorf("argv element contains a quote character: %q", s) } } } func writeHashes(t *testing.T, dir string, hashes map[string]string) bool { t.Helper() data, err := json.Marshal(hashes) if err != nil { return false } return os.WriteFile(filepath.Join(dir, "hashes.json"), data, 0o644) == nil } func md5Sum(s string) []byte { h := md5.Sum([]byte(s)) return h[:] }