Seregon/PkgToolBox

Toolbox for analyzing and editing pkg application files for psp,ps3, ps4 and ps5, includes the most useful functions you might need.

Python/57.3 KB/No license
packages/package_ps3.py
PkgToolBox / packages / package_ps3.py
1import os
2import struct
3import binascii
4from Crypto.Cipher import AES
5import logging
6from PIL import Image
7import io
8from .package_base import PackageBase
9import shutil
10import subprocess
11import time
12import datetime
13import sys
14 
15class PackagePS3(PackageBase):
16 MAGIC_PS3 = 0x7f504b47 # ?PKG per PS3
17
18 def __init__(self, pkg_path):
19 try:
20 super().__init__(pkg_path)
21 self.original_file = pkg_path
22 self.files = {}
23 self.content_id = None
24 self.pkg_type = None
25 self.pkg_info = {}
26 self.extracted_files = {}
27 self.is_ready = False
28 self.extraction_complete = False
29 self._cleanup_lock = False
30 self._closed = False
31
32 # Crea directory temporanea in AppData/Local con timestamp
33 appdata_local = os.getenv('LOCALAPPDATA')
34 base_temp_dir = os.path.join(appdata_local, "PkgToolBox", "temp_output")
35 os.makedirs(base_temp_dir, exist_ok=True)
36
37 # Crea una nuova directory con timestamp
38 timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S_%f')
39 self.temp_dir = os.path.join(base_temp_dir, f"pkg_{timestamp}")
40
41 # Pulisci le vecchie directory temporanee
42 self._cleanup_old_temp_dirs(base_temp_dir)
43
44 # Crea la nuova directory temporanea
45 os.makedirs(self.temp_dir, exist_ok=True)
46 logging.info(f"Created new temporary directory: {self.temp_dir}")
47
48 # Carica le informazioni di base
49 self.load_pkg_info()
50
51 # Avvia l'estrazione e attendi il completamento
52 self.extract_and_wait()
53
54 except Exception as e:
55 logging.error(f"Error initializing PackagePS3: {str(e)}")
56 raise
57 
58 def __del__(self):
59 """Cleanup solo quando l'oggetto viene effettivamente distrutto"""
60 self.close()
61 
62 def close(self):
63 """Explicit cleanup method"""
64 if not self._closed:
65 try:
66 if (hasattr(self, 'extraction_complete') and self.extraction_complete and
67 not self._cleanup_lock and hasattr(self, 'temp_dir')):
68 if os.path.exists(self.temp_dir):
69 try:
70 self._cleanup_lock = True
71 # Salva i contenuti importanti prima della pulizia
72 self._cache_important_files()
73 # Non eliminiamo la directory qui, verrà eliminata quando creiamo una nuova
74 logging.info("Package closed")
75 finally:
76 self._cleanup_lock = False
77 self._closed = True
78 except Exception as e:
79 logging.error(f"Error in close: {str(e)}")
80 
81 def _cache_important_files(self):
82 """Cache important files before cleanup"""
83 try:
84 for file_info in self.files.values():
85 if not file_info.get('content'): # Se il contenuto non è già in cache
86 if file_info['size'] < 1024*1024 or file_info['name'].lower().endswith(('.png', '.jpg', '.jpeg')):
87 try:
88 with open(file_info['path'], 'rb') as f:
89 file_info['content'] = f.read()
90 except Exception as e:
91 logging.error(f"Error caching file {file_info['name']}: {str(e)}")
92 except Exception as e:
93 logging.error(f"Error in _cache_important_files: {str(e)}")
94 
95 def decrypt_and_extract(self, progress=None):
96 """Decripta il PKG ed estrae i file usando Ps3DebugLib.exe"""
97 try:
98 if getattr(sys, 'frozen', False):
99 # Se l'app è "frozen" (compilata con PyInstaller)
100 base_path = sys._MEIPASS
101 else:
102 # Se l'app è in esecuzione da script
103 base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
104
105 ps3lib_path = os.path.join(base_path, "packages", "ps3lib", "Ps3DebugLib.exe")
106
107 if not os.path.exists(ps3lib_path):
108 logging.error(f"Ps3DebugLib.exe not found at: {ps3lib_path}")
109 raise FileNotFoundError(f"Ps3DebugLib.exe not found at: {ps3lib_path}")
110 
111 # Crea directory di output nel temp_dir
112 self.output_dir = os.path.join(self.temp_dir, "pkg_files")
113 os.makedirs(self.output_dir, exist_ok=True)
114 
115 # Costruisci il comando
116 cmd = [ps3lib_path, "-o", self.output_dir, self.original_file]
117 
118 try:
119 process = subprocess.Popen(
120 cmd,
121 stdout=subprocess.PIPE,
122 stderr=subprocess.PIPE,
123 universal_newlines=True,
124 creationflags=subprocess.CREATE_NO_WINDOW
125 )
126
127 # Monitora l'output
128 for line in process.stdout:
129 line = line.strip()
130 if "%" in line and progress:
131 try:
132 progress_str = line.split('%')[0].strip().split()[-1]
133 current_progress = int(progress_str)
134 progress.setValue(current_progress)
135 except:
136 pass
137 
138 process.wait()
139
140 if process.returncode != 0:
141 error = process.stderr.read()
142 raise RuntimeError(f"Ps3DebugLib.exe failed with error: {error}")
143 
144 # Aggiorna la lista dei file
145 self.files = {}
146 for root, _, files in os.walk(self.output_dir):
147 for file in sorted(files):
148 file_path = os.path.join(root, file)
149 relative_path = os.path.relpath(file_path, self.output_dir)
150 file_size = os.path.getsize(file_path)
151
152 # Leggi subito il contenuto per file piccoli o immagini
153 if file_size < 1024*1024 or file.lower().endswith(('.png', '.jpg', '.jpeg')):
154 with open(file_path, 'rb') as f:
155 content = f.read()
156 else:
157 content = None
158
159 self.files[relative_path] = {
160 'id': len(self.files),
161 'name': relative_path,
162 'size': file_size,
163 'path': file_path,
164 'content': content
165 }
166 
167 if progress:
168 progress.setValue(100)
169 
170 except subprocess.SubprocessError as e:
171 logging.error(f"Error executing Ps3DebugLib.exe: {str(e)}")
172 raise RuntimeError(f"Error executing Ps3DebugLib.exe: {str(e)}")
173 
174 except Exception as e:
175 logging.error(f"Error in decrypt_and_extract: {str(e)}")
176 raise
177 
178 def _decrypt_retail_pkg(self, pkg_file_name):
179 """Mantiene la logica esistente per PKG retail"""
180
181 
182 def get_info(self):
183 """Restituisce le informazioni del pacchetto in un formato leggibile"""
184 info = {}
185
186
187 is_retail = self.pkg_type == 0x01
188
189 if not is_retail:
190 info.update({
191 "Package Type": "Debug PKG",
192 "Content ID": self.content_id,
193 "Title ID": getattr(self, 'title_id', 'Unknown'),
194 "Total Size": f"{self.total_size:,} bytes ({self.total_size / (1024*1024*1024):.2f} GB)",
195 "File Count": len(self.files),
196 "Package Version": getattr(self, 'package_version', 'Unknown'),
197 "System Version": getattr(self, 'system_version', 'Unknown'),
198 "App Version": getattr(self, 'app_version', 'Unknown'),
199 "NPDRM Type": getattr(self, 'drm_type', 'Unknown'),
200 "Content Type": getattr(self, 'content_type', 'Unknown'),
201 "Package Flag": getattr(self, 'package_flag', 'Unknown'),
202 "Package Size": f"{self.package_size:,} bytes ({self.package_size / (1024*1024*1024):.2f} GB)",
203 "Data Offset": f"0x{getattr(self, 'data_offset', 0):X}",
204 "Data Size": f"{getattr(self, 'data_size', 0):,} bytes",
205 "Metadata Offset": f"0x{getattr(self, 'pkg_metadata_offset', 0):X}",
206 "Metadata Count": getattr(self, 'pkg_metadata_count', 0),
207 "Metadata Size": getattr(self, 'pkg_metadata_size', 0),
208 "Header Digest": getattr(self, 'pkg_header_digest', 'Unknown'),
209 "Data RIV": getattr(self, 'pkg_data_riv', 'Unknown'),
210 "Install Directory": getattr(self, 'install_directory', 'Unknown'),
211 "Is Debug": "Yes"
212 })
213 else:
214 info.update({
215 "Package Type": f"Retail PKG (0x{self.pkg_type:04X})",
216 "Package Revision": f"0x{self.pkg_revision:04X}",
217 "Content ID": self.content_id,
218 "Title ID": getattr(self, 'title_id', 'Unknown'),
219 "Total Size": f"{self.total_size:,} bytes ({self.total_size / (1024*1024*1024):.2f} GB)",
220 "File Count": len(self.files),
221 "Package Version": f"0x{getattr(self, 'package_version', 0):04X}",
222 "System Version": f"0x{getattr(self, 'system_version', 0):08X}",
223 "App Version": getattr(self, 'app_version', 'Unknown'),
224 "NPDRM Type": f"0x{getattr(self, 'drm_type', 0):08X}",
225 "Content Type": f"0x{getattr(self, 'content_type', 0):08X}",
226 "Package Flag": f"0x{getattr(self, 'package_flag', 0):04X}",
227 "Package Size": f"{self.package_size:,} bytes ({self.package_size / (1024*1024*1024):.2f} GB)",
228 "Data Offset": f"0x{getattr(self, 'data_offset', 0):X}",
229 "Data Size": f"{getattr(self, 'data_size', 0):,} bytes",
230 "Metadata Offset": f"0x{getattr(self, 'pkg_metadata_offset', 0):X}",
231 "Metadata Count": getattr(self, 'pkg_metadata_count', 0),
232 "Metadata Size": getattr(self, 'pkg_metadata_size', 0),
233 "Header Digest": getattr(self, 'pkg_header_digest', 'Unknown'),
234 "Data RIV": getattr(self, 'pkg_data_riv', 'Unknown'),
235 "Install Directory": getattr(self, 'install_directory', 'Unknown'),
236 "Is Debug": "No"
237 })
238
239 return info
240 
241 def load_pkg_info(self):
242 try:
243 with open(self.original_file, "rb") as pkg:
244 # Verifica magic number
245 magic = pkg.read(4)
246 if magic != b'\x7F\x50\x4B\x47':
247 logging.error(f"Invalid magic number: {magic.hex()}")
248 raise ValueError("Invalid PKG file format")
249 
250 # Leggi header PKG
251 pkg.seek(0x04)
252 self.pkg_revision = struct.unpack('>H', pkg.read(2))[0]
253 pkg.seek(0x07)
254 self.pkg_type = pkg.read(1)[0]
255
256 # Leggi metadata
257 pkg.seek(0x08)
258 self.pkg_metadata_offset = struct.unpack('>I', pkg.read(4))[0]
259 self.pkg_metadata_count = struct.unpack('>I', pkg.read(4))[0]
260 self.pkg_metadata_size = struct.unpack('>I', pkg.read(4))[0]
261 self.item_count = struct.unpack('>I', pkg.read(4))[0]
262
263 # Leggi informazioni sui file e dati
264 self.total_size = struct.unpack('>Q', pkg.read(8))[0]
265 self.data_offset = struct.unpack('>Q', pkg.read(8))[0]
266 self.data_size = struct.unpack('>Q', pkg.read(8))[0]
267
268 # Leggi content ID (0x30)
269 pkg.seek(0x30)
270 content_id_bytes = pkg.read(0x30)
271 try:
272 self.content_id = content_id_bytes.decode('utf-8').rstrip('\0')
273 if not self.content_id:
274 raise ValueError("Empty content ID")
275 except (UnicodeDecodeError, ValueError):
276 try:
277 self.content_id = content_id_bytes.decode('ascii', errors='ignore').rstrip('\0')
278 except:
279 self.content_id = content_id_bytes.hex()[:32]
280
281 # Leggi digest e altre informazioni di sicurezza (0x60)
282 pkg.seek(0x60)
283 self.digest = pkg.read(0x10).hex()
284 self.pkg_data_riv = pkg.read(0x10).hex()
285 self.pkg_header_digest = pkg.read(0x40).hex()
286
287 # Leggi informazioni DRM e contenuto (0xB0)
288 pkg.seek(0xB0)
289 self.drm_type = struct.unpack('>I', pkg.read(4))[0]
290 self.content_type = struct.unpack('>I', pkg.read(4))[0]
291 self.package_type = struct.unpack('>H', pkg.read(2))[0]
292 self.package_flag = struct.unpack('>H', pkg.read(2))[0]
293
294 # Informazioni aggiuntive
295 self.package_size = os.path.getsize(self.original_file)
296 pkg.seek(0xBC)
297 self.make_package_npdrm_revision = struct.unpack('>H', pkg.read(2))[0]
298 self.package_version = struct.unpack('>H', pkg.read(2))[0]
299
300 # Leggi title ID e altre informazioni (0xC4)
301 pkg.seek(0xC4)
302 title_id_bytes = pkg.read(0x9)
303 try:
304 self.title_id = title_id_bytes.decode('utf-8').rstrip('\0')
305 except:
306 self.title_id = title_id_bytes.hex()[:16]
307
308 self.qa_digest = pkg.read(0x10).hex()
309
310 # System version e app version (0xE4)
311 pkg.seek(0xE4)
312 self.system_version = struct.unpack('>I', pkg.read(4))[0]
313 self.app_version = struct.unpack('>I', pkg.read(4))[0]
314
315 # Install directory (0xF0)
316 pkg.seek(0xF0)
317 install_dir_bytes = pkg.read(0x20)
318 try:
319 self.install_directory = install_dir_bytes.decode('utf-8').rstrip('\0')
320 except:
321 self.install_directory = install_dir_bytes.hex()[:32]
322
323 # Stato crittografia
324 self.is_encrypted = True # PS3 PKG sono sempre criptati
325 
326 logging.info("PKG info loaded successfully")
327 logging.info(f"Content ID: {self.content_id}")
328 logging.info(f"Title ID: {self.title_id}")
329 
330 except Exception as e:
331 logging.error(f"Error loading PKG info: {str(e)}")
332 raise
333 
334 def load_file_entries(self, pkg):
335 try:
336 if not self.pkg_metadata_offset or not self.pkg_metadata_count:
337 raise ValueError("Invalid metadata information")
338
339 pkg.seek(self.pkg_metadata_offset)
340
341 for i in range(self.pkg_metadata_count):
342 try:
343 entry_data = pkg.read(0x20)
344 if len(entry_data) < 0x20:
345 logging.error(f"Incomplete entry data at index {i}")
346 break
347
348 name_offset, name_size, data_offset, data_size = struct.unpack('>IIII', entry_data[:16])
349
350 # Verifica valori validi
351 file_size = os.path.getsize(self.original_file)
352 if (name_offset > file_size or
353 data_offset > file_size or
354 name_size > 1024 or # Nome file ragionevolmente lungo
355 data_size > file_size):
356 logging.warning(f"Invalid entry values at index {i}")
357 continue
358
359 # Leggi il nome del file
360 current_pos = pkg.tell()
361 pkg.seek(name_offset)
362 name_bytes = pkg.read(name_size)
363 pkg.seek(current_pos)
364
365 # Gestione nome file
366 try:
367 name = name_bytes.decode('utf-8').rstrip('\0')
368 except UnicodeDecodeError:
369 try:
370 name = name_bytes.decode('latin-1').rstrip('\0')
371 except:
372 name = f"file_{i:04d}_{binascii.hexlify(name_bytes[:4]).decode()}"
373 logging.warning(f"Could not decode filename at index {i}")
374 
375 self.files[name] = {
376 'id': i,
377 'name': name,
378 'offset': data_offset,
379 'size': data_size
380 }
381
382 except Exception as e:
383 logging.error(f"Error processing entry {i}: {str(e)}")
384 continue
385 
386 logging.info(f"Loaded {len(self.files)} file entries")
387 
388 except Exception as e:
389 logging.error(f"Error loading file entries: {str(e)}")
390 raise
391 
392 def extract_file(self, file_id, output_stream):
393 try:
394 file_info = next((f for f in self.files.values() if f['id'] == file_id), None)
395 if not file_info:
396 raise ValueError(f"File ID {file_id} not found")
397 
398 with open(self.original_file, 'rb') as pkg:
399 pkg.seek(file_info['offset'])
400 data = pkg.read(file_info['size'])
401
402 # Decripta se necessario
403 if self.pkg_type == 0x01: # PS3
404 cipher = AES.new(self.ps3_aes_key, AES.MODE_ECB)
405 data = self.decrypt_data(data, cipher)
406 elif self.pkg_type == 0x02: # PSP
407 cipher = AES.new(self.psp_aes_key, AES.MODE_ECB)
408 data = self.decrypt_data(data, cipher)
409 
410 output_stream.write(data)
411 
412 except Exception as e:
413 logging.error(f"Error extracting file: {str(e)}")
414 raise
415 
416 def read_file(self, file_id):
417 """Legge un file dal PKG"""
418 try:
419 if not self.extraction_complete:
420 raise RuntimeError("Package extraction not completed yet")
421 
422 if self._cleanup_lock:
423 raise RuntimeError("Package is being extracted")
424 
425 file_info = next((f for f in self.files.values() if f['id'] == file_id), None)
426 if not file_info:
427 raise ValueError(f"File ID {file_id} not found")
428 
429 # Se il contenuto è già in cache, restituiscilo
430 if 'content' in file_info and file_info['content'] is not None:
431 return file_info['content']
432
433 # Altrimenti leggi il file da disco
434 if 'path' in file_info and os.path.exists(file_info['path']):
435 with open(file_info['path'], 'rb') as f:
436 content = f.read()
437 # Cache il contenuto per file piccoli o immagini
438 if file_info['size'] < 1024*1024 or file_info['name'].lower().endswith(('.png', '.jpg', '.jpeg')):
439 file_info['content'] = content
440 return content
441 
442 raise FileNotFoundError(f"File content not found for ID {file_id}")
443 
444 except Exception as e:
445 logging.error(f"Error reading file: {str(e)}")
446 raise
447 
448 def get_file_data(self, file_info):
449 """Ottiene i dati di un file dal PKG o dalla directory temporanea"""
450 try:
451 # Se il file è già stato estratto, leggi direttamente dal filesystem
452 if 'path' in file_info and os.path.exists(file_info['path']):
453 with open(file_info['path'], 'rb') as f:
454 return f.read()
455 
456 # Altrimenti leggi dal PKG
457 return self.read_file(file_info['id'])
458 
459 except Exception as e:
460 logging.error(f"Error getting file data: {str(e)}")
461 raise
462 
463 def decrypt_data(self, data_size, data_relative_offset, pkg_encrypted_file_start_offset, aes_key, encr_pkg_read_stream):
464 """Decripta i dati del PKG PS3"""
465 # Calcola la dimensione corretta
466 size = data_size
467 if size % 16 > 0:
468 size = ((data_size // 16) + 1) * 16
469 
470 encrypted_data = bytearray(size)
471 decrypted_data = bytearray(size)
472 pkg_file_key_consec = bytearray(size)
473 inc_pkg_file_key = bytearray(self.pkg_file_key)
474 
475 # Posizionamento corretto
476 encr_pkg_read_stream.seek(data_relative_offset + pkg_encrypted_file_start_offset)
477 encrypted_data = encr_pkg_read_stream.read(size)
478 
479 # Incrementa la chiave per la posizione relativa
480 for _ in range(data_relative_offset // 16):
481 self.increment_array(inc_pkg_file_key, 15)
482 
483 # Genera la chiave consecutiva
484 for pos in range(0, size, 16):
485 pkg_file_key_consec[pos:pos + 16] = inc_pkg_file_key
486 self.increment_array(inc_pkg_file_key, 15)
487 
488 # Cripta la chiave consecutiva
489 cipher = AES.new(aes_key, AES.MODE_ECB)
490 pkg_xor_key_consec = cipher.encrypt(bytes(pkg_file_key_consec))
491 
492 # XOR dei dati
493 for pos in range(size):
494 decrypted_data[pos] = encrypted_data[pos] ^ pkg_xor_key_consec[pos]
495 
496 return decrypted_data[:data_size] # Ritorna solo i dati effettivi, senza padding
497 
498 def decrypt_pkg_data(self, data, cipher):
499 """Decripta dati generici con AES"""
500 if len(data) % 16 != 0:
501 padding = 16 - (len(data) % 16)
502 data += b'\0' * padding
503 return cipher.decrypt(data)
504 
505 def increment_array(self, source_array, position):
506 """Incrementa l'array per la generazione della chiave"""
507 if source_array[position] == 0xFF:
508 if position != 0:
509 if self.increment_array(source_array, position - 1):
510 source_array[position] = 0x00
511 return True
512 else:
513 return False
514 else:
515 return False
516 else:
517 source_array[position] += 0x01
518 return True
519 
520 def extract_files(self, decrypted_pkg_file_name, output_dir):
521 """Estrae i file dal PKG decriptato"""
522 try:
523 twenty_mb = 1024 * 1024 * 20
524
525 with open(decrypted_pkg_file_name, "rb") as decr_pkg_read_stream:
526 # Leggi la tabella dei file
527 file_table = decr_pkg_read_stream.read(320000)
528 first_name_offset = struct.unpack(">I", file_table[:4])[0]
529 ui_file_nr = first_name_offset // 32
530 uifirst_file_offset = struct.unpack(">I", file_table[12:16])[0]
531 
532 # Leggi la tabella dei file completa
533 decr_pkg_read_stream.seek(0)
534 file_table = decr_pkg_read_stream.read(uifirst_file_offset)
535 
536 if ui_file_nr < 0:
537 raise ValueError("Decryption error detected during file extraction")
538 
539 # Reset del dizionario files
540 self.files.clear()
541 
542 for ii in range(ui_file_nr):
543 position_idx = ii * 32
544 extracted_file_offset = struct.unpack(">I", file_table[position_idx + 12:position_idx + 16])[0]
545 extracted_file_size = struct.unpack(">I", file_table[position_idx + 20:position_idx + 24])[0]
546 extracted_file_name_offset = struct.unpack(">I", file_table[position_idx:position_idx + 4])[0]
547 extracted_file_name_size = struct.unpack(">I", file_table[position_idx + 4:position_idx + 8])[0]
548 content_type = file_table[position_idx + 24]
549 file_type = file_table[position_idx + 27]
550 
551 name = file_table[extracted_file_name_offset:extracted_file_name_offset + extracted_file_name_size]
552
553 # Gestione corretta dei nomi dei file
554 if content_type == 0x90:
555 # File/directory PSP
556 extracted_file_name = self.byte_array_to_ascii(name, True)
557 else:
558 # File/directory PS3 - necessita decriptazione
559 decrypted_name = self.decrypt_data(extracted_file_name_size,
560 extracted_file_name_offset,
561 self.ui_encrypted_file_start_offset,
562 self.ps3_aes_key,
563 open(self.original_file, "rb"))
564 extracted_file_name = self.byte_array_to_ascii(decrypted_name, True)
565 
566 # Determina se è un file o una directory
567 is_file = not (file_type == 0x04 and extracted_file_size == 0x00)
568 
569 try:
570 file_path = os.path.join(output_dir, extracted_file_name)
571 if not is_file:
572 os.makedirs(file_path, exist_ok=True)
573 # Aggiungi la directory al dizionario files
574 self.files[extracted_file_name] = {
575 'id': ii,
576 'name': extracted_file_name,
577 'offset': extracted_file_offset,
578 'size': 0,
579 'is_directory': True,
580 'content_type': content_type
581 }
582 continue
583 
584 os.makedirs(os.path.dirname(file_path), exist_ok=True)
585
586 # Aggiungi il file al dizionario files
587 self.files[extracted_file_name] = {
588 'id': ii,
589 'name': extracted_file_name,
590 'offset': extracted_file_offset,
591 'size': extracted_file_size,
592 'is_directory': False,
593 'content_type': content_type,
594 'path': file_path # Aggiungi il percorso completo
595 }
596
597 with open(file_path, "wb") as extracted_file_write_stream:
598 if content_type == 0x90:
599 # Copia diretta per file PSP
600 decr_pkg_read_stream.seek(extracted_file_offset)
601 remaining = extracted_file_size
602 while remaining > 0:
603 chunk_size = min(twenty_mb, remaining)
604 chunk = decr_pkg_read_stream.read(chunk_size)
605 extracted_file_write_stream.write(chunk)
606 remaining -= chunk_size
607 else:
608 # Decripta per file PS3
609 remaining = extracted_file_size
610 offset = 0
611 with open(self.original_file, "rb") as encr_pkg_read_stream:
612 while remaining > 0:
613 chunk_size = min(twenty_mb, remaining)
614 decrypted_chunk = self.decrypt_data(chunk_size,
615 extracted_file_offset + offset,
616 self.ui_encrypted_file_start_offset,
617 self.ps3_aes_key,
618 encr_pkg_read_stream)
619 extracted_file_write_stream.write(decrypted_chunk[:chunk_size])
620 remaining -= chunk_size
621 offset += chunk_size
622 
623 except Exception as ex:
624 logging.error(f"Error processing {extracted_file_name}: {str(ex)}")
625 continue
626 
627 # Carica ICON0.PNG se presente
628 icon_path = os.path.join(output_dir, 'ICON0.PNG')
629 if os.path.exists(icon_path):
630 self.icon_path = icon_path
631 logging.info(f"Found ICON0.PNG at {icon_path}")
632 
633 # Carica EBOOT.BIN se presente
634 eboot_path = os.path.join(output_dir, 'USRDIR', 'EBOOT.BIN')
635 if os.path.exists(eboot_path):
636 self.eboot_path = eboot_path
637 logging.info(f"Found EBOOT.BIN at {eboot_path}")
638 
639 logging.info(f"Files extracted successfully. Total files: {len(self.files)}")
640 return True
641 
642 except Exception as ex:
643 logging.error(f"Error during file extraction: {str(ex)}")
644 return False
645 
646 def byte_array_to_ascii(self, byte_array, clean_end_of_string):
647 """Converte un array di bytes in una stringa ASCII"""
648 try:
649 hex_string = ''.join([f'{b:02X}' for b in byte_array])
650 ascii_string = ''
651 i = 0
652 while i < len(hex_string):
653 try:
654 char_code = int(hex_string[i:i+2], 16)
655 if clean_end_of_string and char_code == 0:
656 break
657 ascii_string += chr(char_code)
658 except:
659 pass
660 i += 2
661 return ascii_string.rstrip('\0')
662 except:
663 return f"unnamed_file_{binascii.hexlify(byte_array[:4]).decode()}"
664 
665 def dump(self, output_dir):
666 """Extract all files from the PKG to the specified directory"""
667 try:
668 os.makedirs(output_dir, exist_ok=True)
669
670 # Modifica il modo in cui otteniamo il percorso di Ps3DebugLib.exe
671 if getattr(sys, 'frozen', False):
672 # Se l'app è "frozen" (compilata con PyInstaller)
673 base_path = sys._MEIPASS
674 else:
675 # Se l'app è in esecuzione da script
676 base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
677
678 ps3lib_path = os.path.join(base_path, "packages", "ps3lib", "Ps3DebugLib.exe")
679
680 if not os.path.exists(ps3lib_path):
681 logging.error(f"Ps3DebugLib.exe not found at: {ps3lib_path}")
682 raise FileNotFoundError(f"Ps3DebugLib.exe not found at: {ps3lib_path}")
683 
684 # Costruisci il comando
685 cmd = [ps3lib_path, "-o", output_dir, self.original_file]
686 
687 # Crea una progress bar usando QProgressDialog
688 from PyQt5.QtWidgets import QProgressDialog
689 from PyQt5.QtCore import Qt
690 progress = QProgressDialog("Extracting PKG...", None, 0, 100)
691 progress.setWindowModality(Qt.WindowModal)
692 progress.setWindowTitle("Extracting")
693 progress.setMinimumDuration(0)
694 progress.setValue(0)
695 progress.show()
696 
697 try:
698 process = subprocess.Popen(
699 cmd,
700 stdout=subprocess.PIPE,
701 stderr=subprocess.PIPE,
702 universal_newlines=True,
703 creationflags=subprocess.CREATE_NO_WINDOW
704 )
705
706 # Monitora l'output
707 for line in process.stdout:
708 line = line.strip()
709
710 if "%" in line:
711 try:
712 progress_str = line.split('%')[0].strip().split()[-1]
713 current_progress = int(progress_str)
714 progress.setValue(current_progress)
715 except:
716 pass
717
718 # Attendi il completamento
719 process.wait()
720
721 if process.returncode != 0:
722 error = process.stderr.read()
723 raise RuntimeError(f"Ps3DebugLib.exe failed with error: {error}")
724 
725 # Aggiorna la lista dei file
726 self.files = {}
727 for root, _, files in os.walk(output_dir):
728 for file in sorted(files):
729 file_path = os.path.join(root, file)
730 relative_path = os.path.relpath(file_path, output_dir)
731
732 file_size = os.path.getsize(file_path)
733 self.files[relative_path] = {
734 'id': len(self.files),
735 'name': relative_path,
736 'size': file_size,
737 'path': file_path
738 }
739 
740 progress.setValue(100)
741 return f"Package extracted successfully to: {output_dir}"
742 
743 except subprocess.SubprocessError as e:
744 logging.error(f"Error executing Ps3DebugLib.exe: {str(e)}")
745 raise RuntimeError(f"Error executing Ps3DebugLib.exe: {str(e)}")
746 finally:
747 progress.close()
748 
749 except Exception as e:
750 logging.error(f"Error during package dump: {str(e)}")
751 raise ValueError(f"Error during package dump: {str(e)}")
752 
753 def extract_and_wait(self):
754 """Estrae i file e attendi il completamento"""
755 try:
756 # Crea una progress bar usando QProgressDialog
757 from PyQt5.QtWidgets import QProgressDialog
758 from PyQt5.QtCore import Qt
759 progress = QProgressDialog("Extracting PKG...", None, 0, 100)
760 progress.setWindowModality(Qt.WindowModal)
761 progress.setWindowTitle("Extracting")
762 progress.setMinimumDuration(0)
763 progress.setValue(0)
764 progress.show()
765 
766 try:
767 # Blocca il cleanup durante l'estrazione
768 self._cleanup_lock = True
769
770 # Esegui l'estrazione
771 self.decrypt_and_extract(progress)
772
773 # Attendi un momento per assicurarsi che tutti i file siano scritti
774 time.sleep(0.5)
775
776 # Imposta i flag quando l'estrazione è completata
777 self.is_ready = True
778 self.extraction_complete = True
779 logging.info("PS3 PKG file loaded.")
780
781 except Exception as e:
782 logging.error(f"Error during extraction: {str(e)}")
783 raise
784 finally:
785 progress.close()
786 self._cleanup_lock = False # Sblocca il cleanup
787 
788 except Exception as e:
789 logging.error(f"Error in extract_and_wait: {str(e)}")
790 raise
791 
792 def _cleanup_old_temp_dirs(self, base_dir):
793 """Pulisce le vecchie directory temporanee"""
794 try:
795 # Mantieni solo le ultime 2 directory
796 dirs = [os.path.join(base_dir, d) for d in os.listdir(base_dir)
797 if os.path.isdir(os.path.join(base_dir, d))]
798 dirs.sort(key=lambda x: os.path.getctime(x), reverse=True)
799
800 # Rimuovi tutte le directory eccetto le ultime 2
801 for old_dir in dirs[2:]:
802 try:
803 shutil.rmtree(old_dir)
804 logging.info(f"Cleaned old temporary directory: {old_dir}")
805 except Exception as e:
806 logging.error(f"Error cleaning old directory {old_dir}: {str(e)}")
807 except Exception as e:
808 logging.error(f"Error during cleanup of old directories: {str(e)}")