A tool for deriving PKG packet encryption keys for ps4 written in c++
| 1 | // SPDX-FileCopyrightText: Copyright 2025 shadPKG |
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | |
| 4 | #include "include/GUIContext.h" |
| 5 | #include "common/logging/log.h" |
| 6 | #include "common/version.h" |
| 7 | #include "core/file_format/pkg.h" |
| 8 | #include "core/file_format/psf.h" |
| 9 | #include <filesystem> |
| 10 | |
| 11 | namespace ShadPKG::GUI { |
| 12 | |
| 13 | // ╔═══════════════════════════════════════════════════════════════════════════╗ |
| 14 | // ║ GUIContext Implementation ║ |
| 15 | // ╚═══════════════════════════════════════════════════════════════════════════╝ |
| 16 | |
| 17 | GUIContext::~GUIContext() { |
| 18 | CancelExtraction(); |
| 19 | if (workerThread_.joinable()) { |
| 20 | workerThread_.join(); |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | void GUIContext::StartExtraction(const ExtractionJob &job) { |
| 25 | if (workerState_.isBusy) { |
| 26 | LOG_WARNING(Common, "Extraction already in progress"); |
| 27 | return; |
| 28 | } |
| 29 | |
| 30 | // Reset state |
| 31 | workerState_.Reset(); |
| 32 | workerState_.isBusy = true; |
| 33 | |
| 34 | // Detach old thread if any |
| 35 | if (workerThread_.joinable()) { |
| 36 | workerThread_.join(); |
| 37 | } |
| 38 | |
| 39 | // Launch worker thread |
| 40 | workerThread_ = std::thread(&GUIContext::WorkerFunction, this, job); |
| 41 | } |
| 42 | |
| 43 | void GUIContext::CancelExtraction() { workerState_.stopRequested = true; } |
| 44 | |
| 45 | // ┌─────────────────────────────────────────────────────────────────────────┐ |
| 46 | // │ Worker Thread: Runs PKG extraction in background │ |
| 47 | // └─────────────────────────────────────────────────────────────────────────┘ |
| 48 | void GUIContext::WorkerFunction(ExtractionJob job) { |
| 49 | LOG_INFO(Common, "[GUI] Starting extraction: {} -> {}", job.pkgPath, |
| 50 | job.outPath); |
| 51 | |
| 52 | try { |
| 53 | std::filesystem::path pkgPath(job.pkgPath); |
| 54 | std::filesystem::path outPath(job.outPath); |
| 55 | |
| 56 | // Validate paths |
| 57 | if (!std::filesystem::exists(pkgPath)) { |
| 58 | workerState_.SetError("PKG file does not exist: " + job.pkgPath); |
| 59 | workerState_.success = false; |
| 60 | workerState_.completed = true; |
| 61 | workerState_.isBusy = false; |
| 62 | return; |
| 63 | } |
| 64 | |
| 65 | // Create output directory |
| 66 | if (!std::filesystem::exists(outPath)) { |
| 67 | std::filesystem::create_directories(outPath); |
| 68 | LOG_INFO(Common, "[GUI] Created output directory: {}", outPath.string()); |
| 69 | } |
| 70 | |
| 71 | workerState_.SetOperation("Opening PKG..."); |
| 72 | workerState_.progress = 0.05f; |
| 73 | |
| 74 | // Open PKG |
| 75 | PKG pkg; |
| 76 | std::string failreason; |
| 77 | if (!pkg.Open(pkgPath, failreason)) { |
| 78 | workerState_.SetError("Failed to open PKG: " + failreason); |
| 79 | workerState_.success = false; |
| 80 | workerState_.completed = true; |
| 81 | workerState_.isBusy = false; |
| 82 | return; |
| 83 | } |
| 84 | |
| 85 | // Check for cancellation |
| 86 | if (workerState_.stopRequested) { |
| 87 | workerState_.SetOperation("Cancelled"); |
| 88 | workerState_.success = false; |
| 89 | workerState_.completed = true; |
| 90 | workerState_.isBusy = false; |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | // Smart output path: append CONTENT_ID |
| 95 | if (job.createSubfolder && !pkg.sfo.empty()) { |
| 96 | PSF psf; |
| 97 | if (psf.Open(pkg.sfo)) { |
| 98 | if (auto cid = psf.GetString("CONTENT_ID"); cid.has_value()) { |
| 99 | outPath /= std::string(*cid); |
| 100 | if (!std::filesystem::exists(outPath)) { |
| 101 | std::filesystem::create_directories(outPath); |
| 102 | } |
| 103 | LOG_INFO(Common, "[GUI] Using CONTENT_ID subfolder: {}", |
| 104 | outPath.string()); |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | workerState_.SetOperation("Preparing extraction..."); |
| 110 | workerState_.progress = 0.1f; |
| 111 | |
| 112 | // Extract PKG structure |
| 113 | if (!pkg.Extract(pkgPath, outPath, failreason)) { |
| 114 | workerState_.SetError("Extraction failed: " + failreason); |
| 115 | workerState_.success = false; |
| 116 | workerState_.completed = true; |
| 117 | workerState_.isBusy = false; |
| 118 | return; |
| 119 | } |
| 120 | |
| 121 | if (workerState_.stopRequested) { |
| 122 | workerState_.SetOperation("Cancelled"); |
| 123 | workerState_.success = false; |
| 124 | workerState_.completed = true; |
| 125 | workerState_.isBusy = false; |
| 126 | return; |
| 127 | } |
| 128 | |
| 129 | workerState_.SetOperation("Extracting files..."); |
| 130 | workerState_.progress = 0.15f; |
| 131 | |
| 132 | // Extract files with progress (uses internal progress for now) |
| 133 | // TODO: Use callback version when pkg.h is updated |
| 134 | pkg.ExtractAllFilesWithProgress(); |
| 135 | |
| 136 | workerState_.progress = 1.0f; |
| 137 | workerState_.SetOperation("Complete!"); |
| 138 | workerState_.success = true; |
| 139 | workerState_.completed = true; |
| 140 | workerState_.isBusy = false; |
| 141 | |
| 142 | { |
| 143 | std::lock_guard<std::mutex> lock(workerState_.stateMutex); |
| 144 | workerState_.extractedPath = outPath.string(); |
| 145 | } |
| 146 | |
| 147 | LOG_INFO(Common, "[GUI] Extraction completed successfully: {}", |
| 148 | outPath.string()); |
| 149 | |
| 150 | } catch (const std::exception &e) { |
| 151 | workerState_.SetError(std::string("Exception: ") + e.what()); |
| 152 | workerState_.success = false; |
| 153 | workerState_.completed = true; |
| 154 | workerState_.isBusy = false; |
| 155 | LOG_ERROR(Common, "[GUI] Extraction exception: {}", e.what()); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | // ┌─────────────────────────────────────────────────────────────────────────┐ |
| 160 | // │ Update Checker Implementation │ |
| 161 | // └─────────────────────────────────────────────────────────────────────────┘ |
| 162 | void GUIContext::CheckForUpdates() { |
| 163 | if (updateStatus_.checked) |
| 164 | return; |
| 165 | |
| 166 | // Run in detached thread to avoid blocking GUI |
| 167 | std::thread([this]() { |
| 168 | updateStatus_.checked = true; |
| 169 | const std::string api_url = |
| 170 | "https://api.github.com/repos/seregonwar/ShadPKG/releases/latest"; |
| 171 | std::string cmd = "curl -s -H \"User-Agent: ShadPKG\" " + api_url; |
| 172 | |
| 173 | FILE *pipe = popen(cmd.c_str(), "r"); |
| 174 | if (!pipe) |
| 175 | return; |
| 176 | |
| 177 | char buffer[128]; |
| 178 | std::string result = ""; |
| 179 | while (!feof(pipe)) { |
| 180 | if (fgets(buffer, 128, pipe) != NULL) |
| 181 | result += buffer; |
| 182 | } |
| 183 | pclose(pipe); |
| 184 | |
| 185 | // Simple JSON parsing to find "tag_name" |
| 186 | // Format: "tag_name": "v1.2.3", |
| 187 | size_t tagPos = result.find("\"tag_name\":"); |
| 188 | if (tagPos != std::string::npos) { |
| 189 | size_t start = |
| 190 | result.find("\"", tagPos + 11) + 1; // +11 for length of "tag_name": |
| 191 | size_t end = result.find("\"", start); |
| 192 | std::string version = result.substr(start, end - start); |
| 193 | |
| 194 | updateStatus_.latestVersion = version; |
| 195 | updateStatus_.releaseUrl = |
| 196 | "https://github.com/seregonwar/ShadPKG/releases/latest"; |
| 197 | |
| 198 | // Compare versions (very basic string comparison for now) |
| 199 | if (version != Common::VERSION && |
| 200 | version != ("v" + std::string(Common::VERSION))) { |
| 201 | updateStatus_.hasUpdate = true; |
| 202 | LOG_INFO(Common, "Update available: {} (Current: {})", version, |
| 203 | Common::VERSION); |
| 204 | } else { |
| 205 | LOG_INFO(Common, "ShadPKG is up to date."); |
| 206 | } |
| 207 | } |
| 208 | }).detach(); |
| 209 | } |
| 210 | |
| 211 | // ┌─────────────────────────────────────────────────────────────────────────┐ |
| 212 | // │ Contributors Fetcher Implementation │ |
| 213 | // └─────────────────────────────────────────────────────────────────────────┘ |
| 214 | void GUIContext::FetchContributors() { |
| 215 | if (!contributors.empty()) |
| 216 | return; |
| 217 | |
| 218 | // Run in detached thread |
| 219 | std::thread([this]() { |
| 220 | const std::string api_url = |
| 221 | "https://api.github.com/repos/seregonwar/ShadPKG/contributors"; |
| 222 | std::string cmd = "curl -s -H \"User-Agent: ShadPKG\" " + api_url; |
| 223 | |
| 224 | FILE *pipe = popen(cmd.c_str(), "r"); |
| 225 | if (!pipe) |
| 226 | return; |
| 227 | |
| 228 | char buffer[128]; |
| 229 | std::string result = ""; |
| 230 | while (!feof(pipe)) { |
| 231 | if (fgets(buffer, 128, pipe) != NULL) |
| 232 | result += buffer; |
| 233 | } |
| 234 | pclose(pipe); |
| 235 | |
| 236 | // DEBUG: Log the raw result (truncated) |
| 237 | if (!result.empty()) { |
| 238 | LOG_INFO(Common, "GitHub API Response Size: {}", result.size()); |
| 239 | LOG_INFO(Common, "GitHub API Response (First 300 chars): {}", |
| 240 | result.substr(0, 300)); |
| 241 | } else { |
| 242 | LOG_ERROR(Common, "GitHub API returned empty result"); |
| 243 | return; |
| 244 | } |
| 245 | |
| 246 | // Simple JSON parsing for array of objects |
| 247 | std::vector<Contributor> tempContributors; |
| 248 | size_t pos = 0; |
| 249 | while ((pos = result.find("\"login\":", pos)) != std::string::npos) { |
| 250 | // Find opening quote of value |
| 251 | size_t quoteStart = result.find("\"", pos + 8); |
| 252 | if (quoteStart == std::string::npos) |
| 253 | break; |
| 254 | |
| 255 | size_t quoteEnd = result.find("\"", quoteStart + 1); |
| 256 | if (quoteEnd == std::string::npos) |
| 257 | break; |
| 258 | |
| 259 | std::string name = |
| 260 | result.substr(quoteStart + 1, quoteEnd - quoteStart - 1); |
| 261 | |
| 262 | size_t urlKeyPos = result.find("\"html_url\":", quoteEnd); |
| 263 | if (urlKeyPos != std::string::npos) { |
| 264 | size_t urlQuoteStart = result.find("\"", urlKeyPos + 11); |
| 265 | if (urlQuoteStart == std::string::npos) |
| 266 | break; |
| 267 | |
| 268 | size_t urlQuoteEnd = result.find("\"", urlQuoteStart + 1); |
| 269 | if (urlQuoteEnd == std::string::npos) |
| 270 | break; |
| 271 | |
| 272 | std::string url = |
| 273 | result.substr(urlQuoteStart + 1, urlQuoteEnd - urlQuoteStart - 1); |
| 274 | |
| 275 | if (name.find("[bot]") == std::string::npos) { |
| 276 | tempContributors.push_back({name, url}); |
| 277 | LOG_INFO(Common, "Parsed Contributor: Name='{}'", name); |
| 278 | } |
| 279 | pos = urlQuoteEnd; |
| 280 | } else { |
| 281 | pos = quoteEnd; |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | if (!tempContributors.empty()) { |
| 286 | contributors = tempContributors; |
| 287 | LOG_INFO(Common, "Fetched {} contributors", contributors.size()); |
| 288 | } else { |
| 289 | LOG_WARNING(Common, "No contributors parsed."); |
| 290 | } |
| 291 | }).detach(); |
| 292 | } |
| 293 | |
| 294 | } // namespace ShadPKG::GUI |
| 295 |