package patching import ( "bytes" "epochsilicon/pkg/log" "errors" "fmt" "git.burkey.co/eburk/epochcli/pkg/epoch" "io" "os" "os/exec" "path/filepath" "regexp" "strings" "epochsilicon/pkg/paths" // Corrected import path "epochsilicon/pkg/utils" // Corrected import path "fyne.io/fyne/v2" "fyne.io/fyne/v2/dialog" ) func PatchEpoch(myWindow fyne.Window, updateAllStatuses func()) { log.Debug("Patch Epoch clicked") if paths.EpochPath == "" { dialog.ShowError(fmt.Errorf("Epoch path not set. Please set it first."), myWindow) return } targetWinerosettaDll := filepath.Join(paths.EpochPath, "winerosetta.dll") targetD3d9Dll := filepath.Join(paths.EpochPath, "d3d9.dll") targetRosettaX87Dir := filepath.Join(paths.EpochPath, "rosettax87") dllsTextFile := filepath.Join(paths.EpochPath, "dlls.txt") filesToCopy := map[string]string{ "winerosetta/winerosetta.dll": targetWinerosettaDll, "winerosetta/d3d9.dll": targetD3d9Dll, } for resourceName, destPath := range filesToCopy { log.Debugf("Processing resource: %s to %s", resourceName, destPath) // Check if file already exists and has correct size if utils.PathExists(destPath) && utils.CompareFileWithBundledResource(destPath, resourceName) { log.Debugf("File %s already exists with correct size, skipping copy", destPath) continue } if utils.PathExists(destPath) { log.Debugf("File %s exists but has incorrect size, updating...", destPath) } else { log.Debugf("File %s does not exist, creating...", destPath) } resource, err := fyne.LoadResourceFromPath(resourceName) if err != nil { errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } destinationFile, err := os.Create(destPath) if err != nil { errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } defer destinationFile.Close() _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) if err != nil { errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } log.Debugf("Successfully copied %s to %s", resourceName, destPath) } log.Debugf("Preparing rosettax87 directory at: %s", targetRosettaX87Dir) if err := os.RemoveAll(targetRosettaX87Dir); err != nil { log.Debugf("Warning: could not remove existing rosettax87 folder '%s': %v", targetRosettaX87Dir, err) } if err := os.MkdirAll(targetRosettaX87Dir, 0755); err != nil { errMsg := fmt.Sprintf("failed to create directory %s: %v", targetRosettaX87Dir, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } rosettaFilesToCopy := map[string]string{ "rosettax87/rosettax87": filepath.Join(targetRosettaX87Dir, "rosettax87"), "rosettax87/libRuntimeRosettax87": filepath.Join(targetRosettaX87Dir, "libRuntimeRosettax87"), } for resourceName, destPath := range rosettaFilesToCopy { log.Debugf("Processing rosetta resource: %s to %s", resourceName, destPath) resource, err := fyne.LoadResourceFromPath(resourceName) if err != nil { errMsg := fmt.Sprintf("failed to open bundled resource %s: %v", resourceName, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } destinationFile, err := os.Create(destPath) if err != nil { errMsg := fmt.Sprintf("failed to create destination file %s: %v", destPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } _, err = io.Copy(destinationFile, bytes.NewReader(resource.Content())) if err != nil { destinationFile.Close() errMsg := fmt.Sprintf("failed to copy bundled resource %s to %s: %v", resourceName, destPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } destinationFile.Close() if filepath.Base(destPath) == "rosettax87" { log.Debugf("Setting execute permission for %s", destPath) if err := os.Chmod(destPath, 0755); err != nil { errMsg := fmt.Sprintf("failed to set execute permission for %s: %v", destPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedEpoch = false updateAllStatuses() return } } log.Debugf("Successfully copied %s to %s", resourceName, destPath) } log.Debugf("Checking dlls.txt file at: %s", dllsTextFile) winerosettaEntry := "winerosetta.dll" needsWinerosettaUpdate := true if fileContentBytes, err := os.ReadFile(dllsTextFile); err == nil { fileContent := string(fileContentBytes) if strings.Contains(fileContent, winerosettaEntry) { log.Debugf("dlls.txt already contains %s", winerosettaEntry) needsWinerosettaUpdate = false } } else { log.Debugf("dlls.txt not found, will create a new one") } if needsWinerosettaUpdate { var fileContentBytes []byte var err error if utils.PathExists(dllsTextFile) { fileContentBytes, err = os.ReadFile(dllsTextFile) if err != nil { errMsg := fmt.Sprintf("failed to read dlls.txt for update: %v", err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } } currentContent := string(fileContentBytes) updatedContent := currentContent if len(updatedContent) > 0 && !strings.HasSuffix(updatedContent, "\n") { updatedContent += "\n" } if needsWinerosettaUpdate { if !strings.Contains(updatedContent, winerosettaEntry+"\n") { updatedContent += winerosettaEntry + "\n" log.Debugf("Adding %s to dlls.txt", winerosettaEntry) } } if err := os.WriteFile(dllsTextFile, []byte(updatedContent), 0644); err != nil { errMsg := fmt.Sprintf("failed to update dlls.txt: %v", err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } else { log.Debugf("Successfully updated dlls.txt") } } log.Debug("Downloading updates from Project Epoch servers.") // TODO: Change from dialog to pulsing animation dialog.ShowInformation("Downloading patches", "Downloading patches for Project Epoch, this will take some time. Please wait until the status changes to \"Patched\"", myWindow) paths.DownloadingPatches = true go func() { log.Debug("Attempting to download patches...") stats := epoch.Update(paths.EpochPath, true, true, false) if stats.Error != nil { errMsg := fmt.Sprintf("failed to update Epoch files: %v", stats.Error) fyne.Do(func() { dialog.ShowError(errors.New(errMsg), myWindow) }) log.Error(errMsg) } else { for _, msg := range stats.LogMessages { log.Debug(msg) } log.Infof("Successfully updated %d Epoch files", stats.Updated) log.Debug("Epoch patching with bundled resources completed successfully.") fyne.Do(func() { dialog.ShowInformation("Success", "Epoch patching process completed.", myWindow) }) } fyne.DoAndWait(func() { paths.DownloadingPatches = false updateAllStatuses() }) }() updateAllStatuses() } func PatchCrossOver(myWindow fyne.Window, updateAllStatuses func()) { log.Debug("Patch CrossOver clicked") if paths.CrossoverPath == "" { dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it first."), myWindow) return } wineloaderBasePath := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application") wineloaderOrig := filepath.Join(wineloaderBasePath, "wineloader") wineloaderCopy := filepath.Join(wineloaderBasePath, "wineloader2") if !utils.PathExists(wineloaderOrig) { dialog.ShowError(fmt.Errorf("original wineloader not found at %s", wineloaderOrig), myWindow) paths.PatchesAppliedCrossOver = false updateAllStatuses() return } log.Debugf("Copying %s to %s", wineloaderOrig, wineloaderCopy) if err := utils.CopyFile(wineloaderOrig, wineloaderCopy); err != nil { errMsg := fmt.Sprintf("failed to copy wineloader: %v", err) if strings.Contains(err.Error(), "operation not permitted") { errMsg += "\n\nSolution: Open System Settings, go to Privacy & Security > App Management, and enable EpochSilicon." } dialog.ShowError(fmt.Errorf(errMsg), myWindow) paths.PatchesAppliedCrossOver = false updateAllStatuses() return } log.Debugf("Executing: codesign --remove-signature %s", wineloaderCopy) cmd := exec.Command("codesign", "--remove-signature", wineloaderCopy) combinedOutput, err := cmd.CombinedOutput() if err != nil { derrMsg := fmt.Sprintf("failed to remove signature from %s: %v\nOutput: %s", wineloaderCopy, err, string(combinedOutput)) dialog.ShowError(errors.New(derrMsg), myWindow) log.Debug(derrMsg) paths.PatchesAppliedCrossOver = false if err := os.Remove(wineloaderCopy); err != nil { log.Debugf("Warning: failed to cleanup wineloader2 after codesign failure: %v", err) } updateAllStatuses() return } log.Debugf("codesign output: %s", string(combinedOutput)) log.Debugf("Setting execute permissions for %s", wineloaderCopy) if err := os.Chmod(wineloaderCopy, 0755); err != nil { errMsg := fmt.Sprintf("failed to set executable permissions for %s: %v", wineloaderCopy, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) paths.PatchesAppliedCrossOver = false updateAllStatuses() return } log.Debug("CrossOver patching completed successfully.") paths.PatchesAppliedCrossOver = true dialog.ShowInformation("Success", "CrossOver patching process completed.", myWindow) updateAllStatuses() } func UnpatchEpoch(myWindow fyne.Window, updateAllStatuses func()) { log.Debug("Unpatch Epoch clicked") if paths.EpochPath == "" { dialog.ShowError(fmt.Errorf("Epoch path not set. Please set it first."), myWindow) return } // Files to remove winerosettaDllPath := filepath.Join(paths.EpochPath, "winerosetta.dll") d3d9DllPath := filepath.Join(paths.EpochPath, "d3d9.dll") rosettaX87DirPath := filepath.Join(paths.EpochPath, "rosettax87") dllsTextFile := filepath.Join(paths.EpochPath, "dlls.txt") // Remove the rosettaX87 directory if utils.DirExists(rosettaX87DirPath) { log.Debugf("Removing directory: %s", rosettaX87DirPath) if err := os.RemoveAll(rosettaX87DirPath); err != nil { errMsg := fmt.Sprintf("failed to remove directory %s: %v", rosettaX87DirPath, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } else { log.Debugf("Successfully removed directory: %s", rosettaX87DirPath) } } // Remove DLL files filesToRemove := []string{winerosettaDllPath, d3d9DllPath} for _, file := range filesToRemove { if utils.PathExists(file) { log.Debugf("Removing file: %s", file) if err := os.Remove(file); err != nil { errMsg := fmt.Sprintf("failed to remove file %s: %v", file, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } else { log.Debugf("Successfully removed file: %s", file) } } } // Update dlls.txt file - remove winerosetta.dll if utils.PathExists(dllsTextFile) { log.Debugf("Updating dlls.txt file: %s", dllsTextFile) content, err := os.ReadFile(dllsTextFile) if err != nil { errMsg := fmt.Sprintf("failed to read dlls.txt file: %v", err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } else { lines := strings.Split(string(content), "\n") filteredLines := make([]string, 0, len(lines)) for _, line := range lines { trimmedLine := strings.TrimSpace(line) if trimmedLine != "winerosetta.dll" { filteredLines = append(filteredLines, line) } } updatedContent := strings.Join(filteredLines, "\n") if err := os.WriteFile(dllsTextFile, []byte(updatedContent), 0644); err != nil { errMsg := fmt.Sprintf("failed to update dlls.txt file: %v", err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) } else { log.Debugf("Successfully updated dlls.txt file") } } } log.Debug("Epoch unpatching completed successfully.") paths.PatchesAppliedEpoch = false dialog.ShowInformation("Success", "Epoch unpatching process completed.", myWindow) updateAllStatuses() } func UnpatchCrossOver(myWindow fyne.Window, updateAllStatuses func()) { log.Debug("Unpatch CrossOver clicked") if paths.CrossoverPath == "" { dialog.ShowError(fmt.Errorf("CrossOver path not set. Please set it first."), myWindow) return } wineloaderCopy := filepath.Join(paths.CrossoverPath, "Contents", "SharedSupport", "CrossOver", "CrossOver-Hosted Application", "wineloader2") if utils.PathExists(wineloaderCopy) { log.Debugf("Removing file: %s", wineloaderCopy) if err := os.Remove(wineloaderCopy); err != nil { errMsg := fmt.Sprintf("failed to remove file %s: %v", wineloaderCopy, err) dialog.ShowError(errors.New(errMsg), myWindow) log.Debug(errMsg) updateAllStatuses() return } else { log.Debugf("Successfully removed file: %s", wineloaderCopy) } } else { log.Debugf("File not found to remove: %s", wineloaderCopy) } log.Debug("CrossOver unpatching completed successfully.") paths.PatchesAppliedCrossOver = false dialog.ShowInformation("Success", "CrossOver unpatching process completed.", myWindow) updateAllStatuses() } // updateOrAddConfigSetting updates an existing setting or adds a new one if it doesn't exist func updateOrAddConfigSetting(configText, setting, value string) string { // Create regex pattern to match the setting pattern := fmt.Sprintf(`SET\s+%s\s+"[^"]*"`, regexp.QuoteMeta(setting)) re := regexp.MustCompile(pattern) newSetting := fmt.Sprintf(`SET %s "%s"`, setting, value) if re.MatchString(configText) { // Replace existing setting configText = re.ReplaceAllString(configText, newSetting) log.Debugf("Updated setting %s to %s", setting, value) } else { // Add new setting if configText != "" && !strings.HasSuffix(configText, "\n") { configText += "\n" } configText += newSetting + "\n" log.Debugf("Added new setting %s with value %s", setting, value) } return configText } // removeConfigSetting removes a setting from the config text func removeConfigSetting(configText, setting string) string { // Create regex pattern to match the setting pattern := fmt.Sprintf(`SET\s+%s\s+"[^"]*"[\r\n]*`, regexp.QuoteMeta(setting)) re := regexp.MustCompile(pattern) if re.MatchString(configText) { configText = re.ReplaceAllString(configText, "") log.Debugf("Removed setting %s from config", setting) } return configText } // isConfigSettingCorrect checks if a specific setting has the correct value in the config text func isConfigSettingCorrect(configText, setting, expectedValue string) bool { // Create regex pattern to match the setting pattern := fmt.Sprintf(`SET\s+%s\s+"([^"]*)"`, regexp.QuoteMeta(setting)) re := regexp.MustCompile(pattern) matches := re.FindStringSubmatch(configText) if len(matches) < 2 { return false } currentValue := matches[1] return currentValue == expectedValue }