mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
255 lines
9.5 KiB
Python
255 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
VDFHandler module for safely handling VDF files.
|
|
This module provides wrappers around the VDF library with additional safety checks.
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import vdf
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
|
|
# Initialize logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# List of protected VDF files that should never be modified
|
|
PROTECTED_VDF_FILES = [
|
|
"libraryfolders.vdf",
|
|
"config.vdf",
|
|
"loginusers.vdf",
|
|
"registry.vdf",
|
|
"localconfig.vdf",
|
|
"remotecache.vdf",
|
|
"sharedconfig.vdf",
|
|
"appinfo.vdf",
|
|
"packageinfo.vdf",
|
|
"appmanifest_*.acf"
|
|
]
|
|
|
|
# Critical Steam directories we should never modify
|
|
CRITICAL_STEAM_DIRS = [
|
|
"appcache",
|
|
"controller_base",
|
|
"config",
|
|
"logs",
|
|
"package",
|
|
"public",
|
|
"resource",
|
|
"steam",
|
|
"steamapps",
|
|
"tenfoot"
|
|
]
|
|
|
|
class VDFHandler:
|
|
"""
|
|
Safe handler for VDF operations with protection against modifying critical Steam files.
|
|
"""
|
|
|
|
@staticmethod
|
|
def is_protected_file(file_path: str) -> bool:
|
|
"""
|
|
Check if a file is protected from modification.
|
|
|
|
Args:
|
|
file_path: Path to the VDF file
|
|
|
|
Returns:
|
|
bool: True if the file is protected, False otherwise
|
|
"""
|
|
file_name = os.path.basename(file_path)
|
|
|
|
# Special exception for shortcuts.vdf - we always want to be able to modify this
|
|
if file_name == "shortcuts.vdf":
|
|
return False
|
|
|
|
# Check exact filename match
|
|
if file_name in PROTECTED_VDF_FILES:
|
|
return True
|
|
|
|
# Check pattern match (for appmanifest_*.acf)
|
|
for pattern in PROTECTED_VDF_FILES:
|
|
if '*' in pattern and pattern.replace('*', '') in file_name:
|
|
return True
|
|
|
|
# Check if file is in critical Steam directories
|
|
for dir_name in CRITICAL_STEAM_DIRS:
|
|
if f"/{dir_name}/" in file_path or f"\\{dir_name}\\" in file_path:
|
|
return True
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def load(file_path: str, binary: bool = True) -> Dict[str, Any]:
|
|
"""
|
|
Safely load a VDF file.
|
|
|
|
Args:
|
|
file_path: Path to the VDF file
|
|
binary: Whether the file is binary VDF format
|
|
|
|
Returns:
|
|
Dict: Parsed VDF data
|
|
|
|
Raises:
|
|
ValueError: If the file is protected and being loaded for writing
|
|
"""
|
|
# Always create a backup before reading critical files
|
|
if VDFHandler.is_protected_file(file_path):
|
|
backup_path = f"{file_path}.bak"
|
|
if not os.path.exists(backup_path):
|
|
try:
|
|
import shutil
|
|
shutil.copy2(file_path, backup_path)
|
|
logger.debug(f"Created backup of {os.path.basename(file_path)} at {backup_path}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to create backup of {file_path}: {e}")
|
|
|
|
# Load the VDF file
|
|
try:
|
|
if binary:
|
|
# Use ValvePython/vdf library for binary files
|
|
logger.debug(f"Attempting to load binary VDF with ValvePython/vdf: {file_path}")
|
|
if not os.path.exists(file_path):
|
|
logger.error(f"Binary VDF file not found: {file_path}")
|
|
return None
|
|
with open(file_path, 'rb') as f_vdf:
|
|
return vdf.binary_loads(f_vdf.read())
|
|
else:
|
|
# Handle text VDF files (e.g., config.vdf)
|
|
logger.debug(f"Attempting to load text VDF with ValvePython/vdf: {file_path}")
|
|
if not os.path.exists(file_path):
|
|
logger.error(f"Text VDF file not found: {file_path}")
|
|
return None
|
|
with open(file_path, 'r', encoding='utf-8') as f_text:
|
|
return vdf.load(f_text)
|
|
|
|
except FileNotFoundError:
|
|
# This might be redundant due to os.path.exists checks, but keep for safety
|
|
logger.error(f"VDF file not found during load operation: {file_path}")
|
|
return None
|
|
except PermissionError:
|
|
logger.error(f"Permission denied when trying to read VDF file: {file_path}")
|
|
return None
|
|
except Exception as e:
|
|
# Catch any other unexpected errors (including parsing errors from vdf.binary_loads)
|
|
logger.error(f"Unexpected error loading VDF file {file_path}: {e}", exc_info=True)
|
|
return None # Return None instead of {}
|
|
|
|
@staticmethod
|
|
def save(file_path: str, data: Dict[str, Any], binary: bool = True) -> bool:
|
|
"""
|
|
Safely save a VDF file with protection for critical files.
|
|
|
|
Args:
|
|
file_path: Path to the VDF file
|
|
data: VDF data to save
|
|
binary: Whether to save in binary VDF format
|
|
|
|
Returns:
|
|
bool: True if save was successful, False otherwise
|
|
|
|
Raises:
|
|
ValueError: If attempting to modify a protected file
|
|
"""
|
|
# Normalize path for consistent checks
|
|
file_path = os.path.normpath(file_path)
|
|
|
|
# FIRST LINE OF DEFENSE: Prevent modification of protected files
|
|
if VDFHandler.is_protected_file(file_path):
|
|
error_msg = f"CRITICAL SAFETY ERROR: Attempted to modify protected Steam file: {file_path}"
|
|
logger.error(error_msg)
|
|
raise ValueError(error_msg)
|
|
|
|
# SECOND LINE OF DEFENSE: Only allow saving to shortcuts.vdf
|
|
file_name = os.path.basename(file_path)
|
|
if file_name != "shortcuts.vdf":
|
|
error_msg = f"CRITICAL SAFETY ERROR: Only shortcuts.vdf can be modified, attempted: {file_path}"
|
|
logger.error(error_msg)
|
|
raise ValueError(error_msg)
|
|
|
|
# THIRD LINE OF DEFENSE: Create backup before saving
|
|
if os.path.exists(file_path):
|
|
# Create timestamped backup
|
|
timestamp = Path(file_path).stat().st_mtime
|
|
backup_path = f"{file_path}.{int(timestamp)}.bak"
|
|
|
|
# Also create a simple .bak file if it doesn't exist
|
|
simple_backup = f"{file_path}.bak"
|
|
|
|
try:
|
|
import shutil
|
|
# Create timestamped backup
|
|
shutil.copy2(file_path, backup_path)
|
|
logger.info(f"Created timestamped backup of {file_name} at {backup_path}")
|
|
|
|
# Create simple backup if it doesn't exist
|
|
if not os.path.exists(simple_backup):
|
|
shutil.copy2(file_path, simple_backup)
|
|
logger.info(f"Created backup of {file_name} at {simple_backup}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to create backup before modifying {file_path}: {e}")
|
|
return False
|
|
|
|
# Save the file
|
|
try:
|
|
# Additional safety: Verify we're only saving to shortcuts.vdf again
|
|
if not file_name == "shortcuts.vdf":
|
|
raise ValueError(f"Final safety check failed: Attempted to save to non-shortcuts file: {file_path}")
|
|
|
|
if binary:
|
|
with open(file_path, 'wb') as f:
|
|
vdf.binary_dumps(data, f)
|
|
else:
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
vdf.dump(data, f, pretty=True)
|
|
|
|
logger.info(f"Successfully saved changes to {file_path}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error saving VDF file {file_path}: {e}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def update_shortcuts(shortcuts_path: str, update_function) -> bool:
|
|
"""
|
|
Safely update shortcuts.vdf using a callback function.
|
|
|
|
Args:
|
|
shortcuts_path: Path to the shortcuts.vdf file
|
|
update_function: Callback function that takes shortcuts data and returns updated data
|
|
Signature: function(shortcuts_data) -> updated_shortcuts_data
|
|
|
|
Returns:
|
|
bool: True if update was successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check that we're only operating on shortcuts.vdf
|
|
if os.path.basename(shortcuts_path) != "shortcuts.vdf":
|
|
error_msg = f"Can only update shortcuts.vdf, not: {shortcuts_path}"
|
|
logger.error(error_msg)
|
|
raise ValueError(error_msg)
|
|
|
|
# Load the shortcuts file
|
|
logger.info(f"Loading shortcuts from: {shortcuts_path}")
|
|
shortcuts_data = VDFHandler.load(shortcuts_path, binary=True)
|
|
|
|
if not shortcuts_data:
|
|
logger.error(f"Failed to load shortcuts data from {shortcuts_path}")
|
|
return False
|
|
|
|
# Apply the update function
|
|
logger.info("Applying updates to shortcuts data")
|
|
updated_data = update_function(shortcuts_data)
|
|
|
|
if updated_data is None:
|
|
logger.error("Update function returned None")
|
|
return False
|
|
|
|
# Save the updated data
|
|
logger.info(f"Saving updated shortcuts to: {shortcuts_path}")
|
|
return VDFHandler.save(shortcuts_path, updated_data, binary=True)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating shortcuts: {e}")
|
|
return False |