Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 160 additions & 110 deletions nandfixpro.py
Original file line number Diff line number Diff line change
Expand Up @@ -1581,16 +1581,68 @@ def _save_config(self):
config.write(configfile)
self._log(f"INFO: Configuration saved to {self.config_file}")

def _is_path_valid(self, key):
"""Helper to check if a path from the dict is non-empty and exists."""
path_str = self.paths[key].get()
if not path_str:
return False
return Path(path_str).exists()

def _check_disk_space(self, required_gb=60):
try:
import shutil
def _is_path_valid(self, key):
"""Helper to check if a path from the dict is non-empty and exists."""
path_str = self.paths[key].get()
if not path_str:
return False
return Path(path_str).exists()

def _get_app_base_dir(self):
"""Return the actual app directory (stable for both script and packaged exe)."""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
try:
return Path(__file__).resolve().parent
except NameError:
return Path.cwd().resolve()

def _get_partitions_folder(self):
"""Resolve donor NAND partitions folder from settings with a safe fallback."""
configured_path = self.paths['partitions_folder'].get().strip()
if configured_path:
partitions_folder = Path(configured_path)
if partitions_folder.is_dir():
return partitions_folder
self._log(f"ERROR: Partitions folder path is invalid: {partitions_folder}")
return None

fallback_folder = self._get_app_base_dir() / "lib" / "NAND"
if fallback_folder.is_dir():
self._log(f"WARNING: Partitions folder not set, using fallback: {fallback_folder}")
return fallback_folder

self._log("ERROR: Partitions folder is not configured and fallback lib/NAND was not found.")
return None

def _estimate_nand_size_gb(self, nand_source, source_type):
"""Estimate NAND source size in GB from file or physical device path."""
try:
if source_type == 'file':
source_path = Path(nand_source)
if source_path.is_file():
return source_path.stat().st_size / (1024**3)
elif source_type == 'device':
size_bytes = self._get_disk_size_bytes(nand_source)
if size_bytes and size_bytes > 0:
return size_bytes / (1024**3)
except Exception as e:
self._log(f"WARNING: Could not estimate NAND size for {nand_source}: {e}")
return None

def _required_temp_space_gb(self, nand_size_gb=None):
"""
Required free temp space:
- 32GB NAND class -> 64GB free
- 64GB NAND class (OLED) -> 128GB free
"""
if nand_size_gb is not None and nand_size_gb > 40:
return 128
return 64

def _check_disk_space(self, required_gb=60):
try:
import shutil
# Use custom temp directory if set, otherwise use system default
if self.paths['temp_directory'].get():
temp_dir = self.paths['temp_directory'].get()
Expand Down Expand Up @@ -1755,18 +1807,12 @@ def _show_about_window(self):
"Developed and maintained by: sthetix")
CustomDialog(self, title="About NAND Fix Pro", message=about_message)

def _show_usage_guide_window(self):
"""Creates a new window and displays the contents of usage.txt."""
try:
# Determine the path to the usage guide
try:
# Path when running as a script
base_path = Path(__file__).parent
except NameError:
# Path when running as a frozen executable (PyInstaller)
base_path = Path(sys.executable).parent

guide_path = base_path / "lib" / "docs" / "usage.txt"
def _show_usage_guide_window(self):
"""Creates a new window and displays the contents of usage.txt."""
try:
# Determine the path to the usage guide from the app folder
base_path = self._get_app_base_dir()
guide_path = base_path / "lib" / "docs" / "usage.txt"

if guide_path.is_file():
with open(guide_path, 'r', encoding='utf-8') as f:
Expand Down Expand Up @@ -1950,9 +1996,8 @@ def _reset_application_state(self):
self._log("--- Application has been reset. Ready for a new operation. ---")
self._validate_paths_and_update_buttons()

def _auto_detect_paths(self):
try: script_dir = Path(__file__).parent
except NameError: script_dir = Path.cwd()
def _auto_detect_paths(self):
script_dir = self._get_app_base_dir()

osfmount_path = Path("C:/Program Files/OSFMount/OSFMount.com")
if osfmount_path.is_file(): self.paths["osfmount"].set(str(osfmount_path.resolve()))
Expand Down Expand Up @@ -2284,15 +2329,14 @@ def _run_user_fix_process(self):
nand_target = drive_path
self._log(f"--- SUCCESS: User confirmed eMMC at {drive_path}")

# STEP 2: Extract the correct USER partition
self._log("\n[STEP 2/4] Preparing donor USER partition...")
try:
script_dir = Path(__file__).parent
except NameError:
script_dir = Path.cwd()
partitions_folder = script_dir / "lib" / "NAND"

user_archive = "USER-64.7z" if target_size_gb > 40 else "USER-32.7z"
# STEP 2: Extract the correct USER partition
self._log("\n[STEP 2/4] Preparing donor USER partition...")
partitions_folder = self._get_partitions_folder()
if not partitions_folder:
self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.")
return

user_archive = "USER-64.7z" if target_size_gb > 40 else "USER-32.7z"

cmd = [self.paths['7z'].get(), 'x', str(partitions_folder / user_archive), f'-o{temp_dir}', '-bsp1', '-y']
if self._run_command_with_progress(cmd, "Extracting USER partition")[0] != 0:
Expand Down Expand Up @@ -2674,17 +2718,14 @@ def _selective_copy_system_contents(self, source_system_path, drive_letter):
self._log(traceback.format_exc())
return False

def _get_donor_nand_path(self, target_size_gb, temp_dir):
"""
Automatically detect and extract the appropriate donor NAND based on eMMC size.
Returns the path to the extracted donor NAND image.
"""
try:
script_dir = Path(__file__).parent
except NameError:
script_dir = Path.cwd()

nand_lib_dir = script_dir / "lib" / "NAND"
def _get_donor_nand_path(self, target_size_gb, temp_dir):
"""
Automatically detect and extract the appropriate donor NAND based on eMMC size.
Returns the path to the extracted donor NAND image.
"""
nand_lib_dir = self._get_partitions_folder()
if not nand_lib_dir:
return None

if target_size_gb > 40:
donor_archive, donor_bin_name, size = (nand_lib_dir / "donor64.7z", "rawnand64.bin", "64GB")
Expand Down Expand Up @@ -2895,13 +2936,10 @@ def _run_level3_process(self, temp_dir):
if self.offline_mode.get():
self._log("\n--- OFFLINE MODE ---")
self._log("Level 3 will create a completely reconstructed NAND file.")
else:
self._log("\n--- WARNING ---")
self._log("Level 3 will completely overwrite your Switch's eMMC with a reconstructed NAND.")
self._log("This is irreversible. Ensure you have backups and a stable connection.")

if not self._check_disk_space(60):
return
else:
self._log("\n--- WARNING ---")
self._log("Level 3 will completely overwrite your Switch's eMMC with a reconstructed NAND.")
self._log("This is irreversible. Ensure you have backups and a stable connection.")

# Determine target size for offline mode or get from physical eMMC
if self.offline_mode.get():
Expand Down Expand Up @@ -2958,13 +2996,18 @@ def _run_level3_process(self, temp_dir):
"WARNING: ALL DATA ON THIS DRIVE WILL BE PERMANENTLY ERASED.\n\n"
"This will perform a complete Level 3 recovery. Continue?")

if not self._dialog("Confirm Level 3 Recovery", msg, buttons="yesno"):
self._log("--- User cancelled Level 3 recovery.")
return

self._log(f"SUCCESS: User confirmed target eMMC at {target_path} ({target_drive['size']})")

self._log(f"\n[STEP 2/8] Preparing donor NAND skeleton...")
if not self._dialog("Confirm Level 3 Recovery", msg, buttons="yesno"):
self._log("--- User cancelled Level 3 recovery.")
return

self._log(f"SUCCESS: User confirmed target eMMC at {target_path} ({target_drive['size']})")

required_space = self._required_temp_space_gb(target_size_gb)
self._log(f"--- Target NAND size: {target_size_gb:.1f}GB. Required temp free space: {required_space}GB.")
if not self._check_disk_space(required_space):
return

self._log(f"\n[STEP 2/8] Preparing donor NAND skeleton...")

# Automatically detect and extract donor NAND skeleton based on target eMMC size
donor_nand_path = self._get_donor_nand_path(target_size_gb, temp_dir)
Expand Down Expand Up @@ -3026,9 +3069,12 @@ def _run_level3_process(self, temp_dir):
self._log("ERROR: No EmmcHaccGen output folder found.")
return

self._log(f"\n[STEP 5/8] Preparing all partition data from donor archives...")
nx_exe = self.paths['nxnandmanager'].get()
partitions_folder = Path(self.paths['partitions_folder'].get())
self._log(f"\n[STEP 5/8] Preparing all partition data from donor archives...")
nx_exe = self.paths['nxnandmanager'].get()
partitions_folder = self._get_partitions_folder()
if not partitions_folder:
self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.")
return

# --- MODIFIED FOR V1.0.2: Progress Bar for all 7z extractions ---
for part_info in [("SYSTEM", "SYSTEM.7z"), ("PRODINFOF", "PRODINFOF.7z"), ("SAFE", "SAFE.7z")]:
Expand Down Expand Up @@ -3806,28 +3852,28 @@ def _get_nand_source(self):
self._log(f"SUCCESS: User confirmed eMMC at {drive_path}")
return drive_path, 'device'

def _run_level1_process(self, temp_dir):
if self.offline_mode.get():
self._log("\n--- OFFLINE MODE ---")
self._log("The Level 1 process will create a fixed NAND file.")
else:
self._log("\n--- WARNING ---")
self._log("The Level 1 process will write directly to your Switch's eMMC.")
if not self._check_disk_space(60):
return
# Get NAND source (file or device)
if not self.offline_mode.get():
self._log("\n[STEP 1/8] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).")
else:
self._log("\n[STEP 1/8] Using NAND file(s) from settings...")
def _run_level1_process(self, temp_dir):
if self.offline_mode.get():
self._log("\n--- OFFLINE MODE ---")
self._log("The Level 1 process will create a fixed NAND file.")
else:
self._log("\n--- WARNING ---")
self._log("The Level 1 process will write directly to your Switch's eMMC.")

if not self._check_disk_space(60):
return

# Get NAND source (file or device)
if not self.offline_mode.get():
self._log("\n[STEP 1/8] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).")
else:
self._log("\n[STEP 1/8] Using NAND file(s) from settings...")

nand_source, source_type = self._get_nand_source()
if not nand_source:
return
nx_exe = self.paths['nxnandmanager'].get()
nand_source, source_type = self._get_nand_source()
if not nand_source:
return

nx_exe = self.paths['nxnandmanager'].get()

self._log(f"\n[STEP 2/8] Dumping and decrypting PRODINFO from {source_type}...")
keyset_path = self.paths['keys'].get()
Expand Down Expand Up @@ -4022,35 +4068,39 @@ def _run_and_interrupt_flash(self, command, partition_name, target_mb):
except Exception as e:
self._log(f"FATAL ERROR during interruptible flash: {e}"); return -1

def _run_level2_process(self, temp_dir):
if self.offline_mode.get():
self._log("\n--- OFFLINE MODE ---")
self._log("The Level 2 process will create a fixed NAND file.")
else:
self._log("\n--- WARNING ---")
self._log("The Level 2 process will write directly to your Switch's eMMC.")

if not self._check_disk_space(60):
return

# Get NAND source (file or device)
if not self.offline_mode.get():
self._log("\n[STEP 1/7] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).")
def _run_level2_process(self, temp_dir):
if self.offline_mode.get():
self._log("\n--- OFFLINE MODE ---")
self._log("The Level 2 process will create a fixed NAND file.")
else:
self._log("\n--- WARNING ---")
self._log("The Level 2 process will write directly to your Switch's eMMC.")

# Get NAND source (file or device)
if not self.offline_mode.get():
self._log("\n[STEP 1/7] Please connect your Switch in Hekate's eMMC RAW GPP mode (Read-Only OFF).")
else:
self._log("\n[STEP 1/7] Using NAND file(s) from settings...")

nand_source, source_type = self._get_nand_source()
if not nand_source:
return

nx_exe = self.paths['nxnandmanager'].get()

try:
script_dir = Path(__file__).parent
except NameError:
script_dir = Path.cwd()
partitions_folder = script_dir / "lib" / "NAND"
keyset_path = self.paths['keys'].get()
nand_source, source_type = self._get_nand_source()
if not nand_source:
return

nand_size_gb = self._estimate_nand_size_gb(nand_source, source_type)
required_space = self._required_temp_space_gb(nand_size_gb)
if nand_size_gb is not None:
self._log(f"--- Detected NAND size: {nand_size_gb:.1f}GB. Required temp free space: {required_space}GB.")
else:
self._log(f"--- Could not determine NAND size. Using default required temp free space: {required_space}GB.")
if not self._check_disk_space(required_space):
return

nx_exe = self.paths['nxnandmanager'].get()
partitions_folder = self._get_partitions_folder()
if not partitions_folder:
self._dialog("Error", "NAND archive folder not found. Please set 'Partitions Folder (NAND)...' in Settings.")
return
keyset_path = self.paths['keys'].get()

self._log(f"\n[STEP 2/7] Acquiring PRODINFO from {source_type}...")
prodinfo_path = Path(temp_dir) / "PRODINFO"
Expand Down Expand Up @@ -4262,4 +4312,4 @@ def _run_level2_process(self, temp_dir):
# Note: Admin privileges and dependencies are handled by NandFixProLauncher.exe
# This allows the main script to remain clean and avoid antivirus false positives
app = SwitchGuiApp()
app.mainloop()
app.mainloop()