Seregon/ShadPKG

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

C++/47.3 KB/No license
gui/views/ExtractorView.cpp
ShadPKG / gui / views / ExtractorView.cpp
1// SPDX-FileCopyrightText: Copyright 2025 shadPKG
2// SPDX-License-Identifier: GPL-2.0-or-later
3 
4#include "../include/views/ExtractorView.h"
5#include "../include/GuiLogSink.h"
6#include "../include/StyleManager.h"
7#include "common/assert.h"
8#include "core/file_format/pkg.h"
9#include "core/file_format/psf.h"
10#include "core/file_format/rif_generator.h"
11#include <assert.h>
12#ifndef IM_ASSERT
13#define IM_ASSERT(_EXPR) ASSERT(_EXPR)
14#endif
15#include "imgui.h"
16#include <array>
17#include <cstdio>
18#include <cstring>
19#include <filesystem>
20 
21#ifdef _WIN32
22#include <commdlg.h>
23#include <shlobj.h>
24#include <windows.h>
25#else
26// macOS/Linux: Will use portable file dialog later
27#endif
28 
29namespace ShadPKG::GUI {
30 
31// ╔═══════════════════════════════════════════════════════════════════════════╗
32// ║ ExtractorView::Draw - Main render function ║
33// ╚═══════════════════════════════════════════════════════════════════════════╝
34void ExtractorView::Draw(GUIContext &ctx) {
35 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 10));
36 
37 DrawSourceSection(ctx);
38 ImGui::Spacing();
39 ImGui::Separator();
40 ImGui::Spacing();
41 
42 DrawDecryptionSection();
43 ImGui::Spacing();
44 ImGui::Separator();
45 ImGui::Spacing();
46 
47 DrawOutputSection();
48 ImGui::Spacing();
49 ImGui::Separator();
50 ImGui::Spacing();
51 
52 DrawExtractButton(ctx);
53 
54 if (ctx.IsExtracting() || ctx.IsCompleted()) {
55 ImGui::Spacing();
56 DrawProgressSection(ctx);
57 }
58 
59 ImGui::PopStyleVar();
60}
61 
62// ┌─────────────────────────────────────────────────────────────────────────┐
63// │ SOURCE FILE Section │
64// └─────────────────────────────────────────────────────────────────────────┘
65void ExtractorView::DrawSourceSection(GUIContext &ctx) {
66 ImGui::TextColored(Colors::Primary, ICON_FA_FILE_ZIPPER " SOURCE FILE");
67 ImGui::Spacing();
68 
69 // PKG path input
70 ImGui::SetNextItemWidth(-100);
71 
72 // Color border based on validation
73 bool pathValid = ValidatePath(pkgPathBuf_);
74 if (strlen(pkgPathBuf_) > 0) {
75 ImGui::PushStyleColor(ImGuiCol_Border,
76 pathValid ? Colors::Success : Colors::Error);
77 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
78 }
79 
80 if (ImGui::InputTextWithHint("##pkgpath",
81 "Drag & drop or browse for PKG file...",
82 pkgPathBuf_, sizeof(pkgPathBuf_))) {
83 ValidatePkgFile(&ctx);
84 }
85 
86 if (strlen(pkgPathBuf_) > 0) {
87 ImGui::PopStyleVar();
88 ImGui::PopStyleColor();
89 }
90 
91 ImGui::SameLine();
92 if (ImGui::Button("Browse##pkg", ImVec2(90, 0))) {
93 if (ShowOpenFileDialog("Select PKG File",
94 "PKG Files\0*.pkg\0All Files\0*.*\0", pkgPathBuf_,
95 sizeof(pkgPathBuf_))) {
96 ValidatePkgFile(&ctx);
97 AutoDetectRif();
98 }
99 }
100 
101 // Show PKG info if loaded
102 if (pkgLoaded_ && pkgInfo_.valid) {
103 ImGui::Indent();
104 ImGui::TextColored(Colors::TextDim, "Title ID: ");
105 ImGui::SameLine();
106 ImGui::Text("%s", pkgInfo_.titleId.c_str());
107 
108 ImGui::TextColored(Colors::TextDim, "Content ID: ");
109 ImGui::SameLine();
110 ImGui::Text("%s", pkgInfo_.contentId.c_str());
111 
112 ImGui::TextColored(Colors::TextDim, "Size: ");
113 ImGui::SameLine();
114 
115 // Format size nicely
116 double sizeGB =
117 static_cast<double>(pkgInfo_.pkgSize) / (1024.0 * 1024.0 * 1024.0);
118 if (sizeGB > 1.0) {
119 ImGui::Text("%.2f GB", sizeGB);
120 } else {
121 double sizeMB = static_cast<double>(pkgInfo_.pkgSize) / (1024.0 * 1024.0);
122 ImGui::Text("%.2f MB", sizeMB);
123 }
124 
125 ImGui::Unindent();
126 }
127 
128 // Drag & drop hint
129 ImGui::TextColored(Colors::TextDim,
130 ICON_FA_DOWNLOAD " Drag & drop PKG file here");
131}
132 
133// ┌─────────────────────────────────────────────────────────────────────────┐
134// │ DECRYPTION SETTINGS Section │
135// └─────────────────────────────────────────────────────────────────────────┘
136void ExtractorView::DrawDecryptionSection() {
137 ImGui::TextColored(Colors::Primary, ICON_FA_KEY " DECRYPTION SETTINGS");
138 ImGui::Spacing();
139 
140 if (ImGui::Checkbox("Enable RIF Decryption", &useRif_)) {
141 if (useRif_ && strlen(rifPathBuf_) > 0) {
142 ValidateRifFile();
143 }
144 }
145 
146 if (useRif_) {
147 ImGui::Indent();
148 
149 ImGui::SetNextItemWidth(-100);
150 
151 // Color border based on validation
152 if (strlen(rifPathBuf_) > 0) {
153 ImGui::PushStyleColor(ImGuiCol_Border,
154 rifValid_ ? Colors::Success : Colors::Error);
155 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
156 }
157 
158 if (ImGui::InputTextWithHint("##rifpath", "Path to RIF file...",
159 rifPathBuf_, sizeof(rifPathBuf_))) {
160 ValidateRifFile();
161 }
162 
163 if (strlen(rifPathBuf_) > 0) {
164 ImGui::PopStyleVar();
165 ImGui::PopStyleColor();
166 }
167 
168 ImGui::SameLine();
169 if (ImGui::Button("Browse##rif", ImVec2(90, 0))) {
170 if (ShowOpenFileDialog("Select RIF File",
171 "RIF Files\0*.rif\0All Files\0*.*\0", rifPathBuf_,
172 sizeof(rifPathBuf_))) {
173 ValidateRifFile();
174 }
175 }
176 
177 // RIF Status
178 if (strlen(rifPathBuf_) > 0) {
179 ImGui::Spacing();
180 if (rifValid_) {
181 ImGui::TextColored(Colors::Success, ICON_FA_CHECK " VALID");
182 if (!rifContentId_.empty()) {
183 ImGui::SameLine();
184 ImGui::TextColored(Colors::TextDim, " | Content-ID: %s",
185 rifContentId_.c_str());
186 }
187 } else {
188 ImGui::TextColored(Colors::Error, ICON_FA_XMARK " INVALID");
189 }
190 }
191 
192 ImGui::Unindent();
193 }
194}
195 
196// ┌─────────────────────────────────────────────────────────────────────────┐
197// │ OUTPUT DESTINATION Section │
198// └─────────────────────────────────────────────────────────────────────────┘
199void ExtractorView::DrawOutputSection() {
200 ImGui::TextColored(Colors::Primary,
201 ICON_FA_FOLDER_OPEN " OUTPUT DESTINATION");
202 ImGui::Spacing();
203 
204 ImGui::SetNextItemWidth(-100);
205 
206 bool pathValid = ValidatePath(outPathBuf_) || strlen(outPathBuf_) == 0;
207 if (strlen(outPathBuf_) > 0) {
208 ImGui::PushStyleColor(ImGuiCol_Border,
209 pathValid ? Colors::Success : Colors::Warning);
210 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
211 }
212 
213 ImGui::InputTextWithHint("##outpath", "Output directory (optional)...",
214 outPathBuf_, sizeof(outPathBuf_));
215 
216 if (strlen(outPathBuf_) > 0) {
217 ImGui::PopStyleVar();
218 ImGui::PopStyleColor();
219 }
220 
221 ImGui::SameLine();
222 if (ImGui::Button("Browse##out", ImVec2(90, 0))) {
223 ShowSelectFolderDialog("Select Output Directory", outPathBuf_,
224 sizeof(outPathBuf_));
225 }
226 
227 ImGui::Checkbox("Create subfolder with TitleID", &createSubfolder_);
228}
229 
230// ┌─────────────────────────────────────────────────────────────────────────┐
231// │ EXTRACT BUTTON │
232// └─────────────────────────────────────────────────────────────────────────┘
233void ExtractorView::DrawExtractButton(GUIContext &ctx) {
234 bool canExtract = pkgLoaded_ && pkgInfo_.valid && !ctx.IsExtracting();
235 
236 // Center the button
237 float buttonWidth = 200.0f;
238 float buttonHeight = 50.0f;
239 float windowWidth = ImGui::GetContentRegionAvail().x;
240 ImGui::SetCursorPosX((windowWidth - buttonWidth) / 2.0f);
241 
242 // Style the main extract button
243 ImGui::PushStyleColor(ImGuiCol_Button,
244 canExtract ? Colors::Primary : Colors::FrameBg);
245 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
246 canExtract ? Colors::PrimaryHover : Colors::FrameBg);
247 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
248 canExtract ? Colors::PrimaryActive : Colors::FrameBg);
249 
250 if (!canExtract) {
251 ImGui::PushStyleColor(ImGuiCol_Text, Colors::TextDim);
252 }
253 
254 if (ImGui::Button(ICON_FA_DOWNLOAD " EXTRACT PKG",
255 ImVec2(buttonWidth, buttonHeight))) {
256 if (canExtract) {
257 ExtractionJob job;
258 job.pkgPath = pkgPathBuf_;
259 job.outPath = strlen(outPathBuf_) > 0 ? outPathBuf_ : ".";
260 job.rifPath = rifPathBuf_;
261 job.useRif = useRif_ && rifValid_;
262 job.createSubfolder = createSubfolder_;
263 
264 GuiLogSink::Instance().Info("Starting extraction: " + job.pkgPath);
265 ctx.StartExtraction(job);
266 }
267 }
268 
269 if (!canExtract) {
270 ImGui::PopStyleColor();
271 }
272 ImGui::PopStyleColor(3);
273 
274 // Show hint if can't extract
275 if (!canExtract && !ctx.IsExtracting()) {
276 ImGui::SetCursorPosX((windowWidth - 200.0f) / 2.0f);
277 ImGui::TextColored(Colors::TextDim, "Select a valid PKG file to extract");
278 }
279}
280 
281// ┌─────────────────────────────────────────────────────────────────────────┐
282// │ PROGRESS Section │
283// └─────────────────────────────────────────────────────────────────────────┘
284void ExtractorView::DrawProgressSection(GUIContext &ctx) {
285 // Status text
286 std::string status = ctx.GetCurrentOperation();
287 ImGui::Text("Status: %s", status.c_str());
288 
289 // Progress bar
290 float progress = ctx.GetProgress();
291 
292 ImGui::PushStyleColor(ImGuiCol_PlotHistogram, Colors::Primary);
293 char progressText[32];
294 snprintf(progressText, sizeof(progressText), "%.0f%%", progress * 100.0f);
295 ImGui::ProgressBar(progress, ImVec2(-1, 25), progressText);
296 ImGui::PopStyleColor();
297 
298 // Cancel button during extraction
299 if (ctx.IsExtracting()) {
300 ImGui::Spacing();
301 float cancelWidth = 100.0f;
302 float windowWidth = ImGui::GetContentRegionAvail().x;
303 ImGui::SetCursorPosX((windowWidth - cancelWidth) / 2.0f);
304 
305 ImGui::PushStyleColor(ImGuiCol_Button, Colors::Error);
306 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
307 ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
308 if (ImGui::Button(ICON_FA_XMARK " Cancel", ImVec2(cancelWidth, 0))) {
309 ctx.CancelExtraction();
310 GuiLogSink::Instance().Warn("Extraction cancelled by user");
311 }
312 ImGui::PopStyleColor(2);
313 }
314 
315 // Completion message
316 if (ctx.IsCompleted() && !ctx.IsExtracting()) {
317 ImGui::Spacing();
318 if (ctx.WasSuccessful()) {
319 ImGui::TextColored(Colors::Success,
320 ICON_FA_CHECK " Extraction completed successfully!");
321 } else {
322 ImGui::TextColored(Colors::Error, ICON_FA_XMARK " Extraction failed: %s",
323 ctx.GetLastError().c_str());
324 }
325 }
326}
327 
328// ┌─────────────────────────────────────────────────────────────────────────┐
329// │ Drag & Drop Handler │
330// └─────────────────────────────────────────────────────────────────────────┘
331void ExtractorView::OnFileDrop(GUIContext &ctx, const std::string &path) {
332 // Check if it's a PKG file
333 std::filesystem::path p(path);
334 std::string ext = p.extension().string();
335 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
336 
337 if (ext == ".pkg") {
338 strncpy(pkgPathBuf_, path.c_str(), sizeof(pkgPathBuf_) - 1);
339 pkgPathBuf_[sizeof(pkgPathBuf_) - 1] = '\0';
340 ValidatePkgFile(&ctx);
341 AutoDetectRif();
342 GuiLogSink::Instance().Info("PKG file dropped: " + path);
343 } else if (ext == ".rif") {
344 strncpy(rifPathBuf_, path.c_str(), sizeof(rifPathBuf_) - 1);
345 rifPathBuf_[sizeof(rifPathBuf_) - 1] = '\0';
346 useRif_ = true;
347 ValidateRifFile();
348 GuiLogSink::Instance().Info("RIF file dropped: " + path);
349 }
350}
351 
352// ┌─────────────────────────────────────────────────────────────────────────┐
353// │ Validation Functions │
354// └─────────────────────────────────────────────────────────────────────────┘
355void ExtractorView::ValidatePkgFile(GUIContext *ctx) {
356 pkgLoaded_ = false;
357 pkgInfo_ = PkgInfo{};
358 
359 if (strlen(pkgPathBuf_) == 0) {
360 return;
361 }
362 
363 std::filesystem::path pkgPath(pkgPathBuf_);
364 if (!std::filesystem::exists(pkgPath)) {
365 return;
366 }
367 
368 // Try to open and parse PKG
369 auto pkg = std::make_shared<PKG>();
370 std::string failreason;
371 if (!pkg->Open(pkgPath, failreason)) {
372 GuiLogSink::Instance().Error("Failed to open PKG: " + failreason);
373 return;
374 }
375 
376 pkgInfo_.titleId = std::string(pkg->GetTitleID());
377 pkgInfo_.pkgSize = pkg->GetPkgSize();
378 pkgInfo_.valid = true;
379 
380 // Get content ID from SFO
381 if (!pkg->sfo.empty()) {
382 PSF psf;
383 if (psf.Open(pkg->sfo)) {
384 if (auto cid = psf.GetString("CONTENT_ID"); cid.has_value()) {
385 pkgInfo_.contentId = std::string(*cid);
386 }
387 if (auto ver = psf.GetString("APP_VER"); ver.has_value()) {
388 pkgInfo_.appVersion = std::string(*ver);
389 }
390 }
391 }
392 
393 pkgLoaded_ = true;
394 GuiLogSink::Instance().Info("PKG loaded: " + pkgInfo_.titleId + " (" +
395 pkgInfo_.contentId + ")");
396 
397 // Sync to shared context if provided
398 if (ctx) {
399 ctx->loadedPkgPath = pkgPath.string();
400 ctx->pkgLoaded = true;
401 ctx->currentPkg = pkg; // Propagate to Decompiler/Inspector
402 }
403}
404 
405void ExtractorView::ValidateRifFile() {
406 rifValid_ = false;
407 rifContentId_.clear();
408 
409 if (strlen(rifPathBuf_) == 0) {
410 return;
411 }
412 
413 std::filesystem::path rifPath(rifPathBuf_);
414 rifValid_ = RIFGenerator::ValidateRIF(rifPath);
415 
416 // TODO: Extract content ID from RIF for display
417}
418 
419bool ExtractorView::ValidatePath(const char *path) {
420 if (strlen(path) == 0) {
421 return false;
422 }
423 return std::filesystem::exists(path);
424}
425 
426void ExtractorView::AutoDetectRif() {
427 if (strlen(pkgPathBuf_) == 0 || !pkgInfo_.valid) {
428 return;
429 }
430 
431 // Look for RIF file in same directory as PKG
432 std::filesystem::path pkgPath(pkgPathBuf_);
433 std::filesystem::path pkgDir = pkgPath.parent_path();
434 
435 // Try common RIF naming patterns
436 std::vector<std::string> patterns = {pkgInfo_.contentId + ".rif",
437 pkgInfo_.titleId + ".rif",
438 pkgPath.stem().string() + ".rif"};
439 
440 for (const auto &pattern : patterns) {
441 std::filesystem::path rifPath = pkgDir / pattern;
442 if (std::filesystem::exists(rifPath)) {
443 strncpy(rifPathBuf_, rifPath.string().c_str(), sizeof(rifPathBuf_) - 1);
444 rifPathBuf_[sizeof(rifPathBuf_) - 1] = '\0';
445 useRif_ = true;
446 ValidateRifFile();
447 GuiLogSink::Instance().Info("Auto-detected RIF file: " +
448 rifPath.string());
449 return;
450 }
451 }
452}
453 
454// ┌─────────────────────────────────────────────────────────────────────────┐
455// │ File Dialog Functions (Platform-specific) │
456// └─────────────────────────────────────────────────────────────────────────┘
457#ifdef _WIN32
458bool ExtractorView::ShowOpenFileDialog(const char *title, const char *filter,
459 char *outPath, size_t pathSize) {
460 OPENFILENAMEA ofn = {};
461 ofn.lStructSize = sizeof(ofn);
462 ofn.lpstrFilter = filter;
463 ofn.lpstrFile = outPath;
464 ofn.nMaxFile = static_cast<DWORD>(pathSize);
465 ofn.lpstrTitle = title;
466 ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
467 return GetOpenFileNameA(&ofn) != 0;
468}
469 
470bool ExtractorView::ShowSelectFolderDialog(const char *title, char *outPath,
471 size_t pathSize) {
472 BROWSEINFOA bi = {};
473 bi.lpszTitle = title;
474 bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
475 
476 LPITEMIDLIST pidl = SHBrowseForFolderA(&bi);
477 if (pidl) {
478 SHGetPathFromIDListA(pidl, outPath);
479 CoTaskMemFree(pidl);
480 return true;
481 }
482 return false;
483}
484#else
485// Helper to execute command and get output
486static std::string ExecCmd(const char *cmd) {
487 std::array<char, 128> buffer;
488 std::string result;
489 std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
490 if (!pipe)
491 return "";
492 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
493 result += buffer.data();
494 }
495 // Remove trailing newline
496 if (!result.empty() && result.back() == '\n') {
497 result.pop_back();
498 }
499 return result;
500}
501 
502bool ExtractorView::ShowOpenFileDialog(const char *title, const char *filter,
503 char *outPath, size_t pathSize) {
504 // Use osascript to open native macOS file picker
505 // Note: Filtering by extension in AppleScript is possible but simplified here
506 
507 std::string cmd = "osascript -e 'POSIX path of (choose file with prompt \"" +
508 std::string(title) + "\"";
509 
510 // Basic extension filtering hint
511 if (strstr(filter, "*.pkg")) {
512 cmd += " of type {\"pkg\"}";
513 } else if (strstr(filter, "*.rif")) {
514 cmd += " of type {\"rif\"}";
515 }
516 
517 cmd += ")'";
518 
519 std::string result = ExecCmd(cmd.c_str());
520 if (!result.empty() && result.find("User canceled") == std::string::npos) {
521 strncpy(outPath, result.c_str(), pathSize - 1);
522 outPath[pathSize - 1] = '\0';
523 return true;
524 }
525 return false;
526}
527 
528bool ExtractorView::ShowSelectFolderDialog(const char *title, char *outPath,
529 size_t pathSize) {
530 // Use osascript to open native macOS folder picker
531 std::string cmd =
532 "osascript -e 'POSIX path of (choose folder with prompt \"" +
533 std::string(title) + "\")'";
534 
535 std::string result = ExecCmd(cmd.c_str());
536 if (!result.empty() && result.find("User canceled") == std::string::npos) {
537 strncpy(outPath, result.c_str(), pathSize - 1);
538 outPath[pathSize - 1] = '\0';
539 return true;
540 }
541 return false;
542}
543#endif
544 
545} // namespace ShadPKG::GUI
546