Seregon/ShadPKG

A tool for deriving PKG packet encryption keys for ps4 written in c++

C++/47.3 KB/No license
gui/GUIContext.cpp
ShadPKG / gui / GUIContext.cpp
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 
11namespace ShadPKG::GUI {
12 
13// ╔═══════════════════════════════════════════════════════════════════════════╗
14// ║ GUIContext Implementation ║
15// ╚═══════════════════════════════════════════════════════════════════════════╝
16 
17GUIContext::~GUIContext() {
18 CancelExtraction();
19 if (workerThread_.joinable()) {
20 workerThread_.join();
21 }
22}
23 
24void 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 
43void GUIContext::CancelExtraction() { workerState_.stopRequested = true; }
44 
45// ┌─────────────────────────────────────────────────────────────────────────┐
46// │ Worker Thread: Runs PKG extraction in background │
47// └─────────────────────────────────────────────────────────────────────────┘
48void 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// └─────────────────────────────────────────────────────────────────────────┘
162void 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// └─────────────────────────────────────────────────────────────────────────┘
214void 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