(gui): fix security and stability issues

- add null-safety for path variables (USER_WORK_PATH, DATA_PATH)
- fix potential crash in handle_process_output with uninitialized process
- improve env vars and executable path handling
- enhance .desktop files and menu processing
This commit is contained in:
Sergey Palcheh
2026-01-22 18:03:04 +06:00
parent 3e18a73383
commit a5e396eead

View File

@@ -1310,7 +1310,8 @@ class CreatePrefixDialog(QDialog):
ErrorReporter.show_error(self, "Ошибка", error_msg)
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
if os.path.exists(prefix_path):
error_msg = f"Префикс с именем '{prefix_name}' уже существует."
ErrorReporter.show_error(self, "Ошибка", error_msg)
@@ -1589,7 +1590,7 @@ class WineHelperGUI(QMainWindow):
"""
# Основные переменные
self.winehelper_path: str = Var.RUN_SCRIPT
self.winehelper_path: str = Var.RUN_SCRIPT or ""
self.process: Optional[QProcess] = None
self.current_script: Optional[str] = None
self.install_process: Optional[QProcess] = None
@@ -2051,7 +2052,8 @@ class WineHelperGUI(QMainWindow):
"""
button_index = 0
for script in scripts_list:
script_path = os.path.join(Var.DATA_PATH, script_folder, script)
data_path = Var.DATA_PATH or ""
script_path = os.path.join(data_path, script_folder, script)
prog_name = ScriptParser.extract_prog_name_from_script(script_path)
# Создаем кнопку, только если для скрипта указано имя программы
@@ -2059,7 +2061,8 @@ class WineHelperGUI(QMainWindow):
continue
icon_names = ScriptParser.extract_icons_from_script(script_path)
icon_paths = [os.path.join(Var.DATA_PATH, "image", f"{name}.png") for name in icon_names]
data_path = Var.DATA_PATH or ""
icon_paths = [os.path.join(data_path, "image", f"{name}.png") for name in icon_names]
# Выбираем стиль в зависимости от папки
if script_folder == 'testinstall':
@@ -2486,7 +2489,8 @@ class WineHelperGUI(QMainWindow):
def _get_current_prefixes(self):
"""Возвращает множество имен существующих префиксов."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
user_work_path = Var.USER_WORK_PATH or ""
prefixes_root_path = os.path.join(user_work_path, "prefixes")
if not os.path.isdir(prefixes_root_path):
return set()
try:
@@ -2506,7 +2510,8 @@ class WineHelperGUI(QMainWindow):
def _load_created_prefixes(self):
"""Загружает и обновляет список созданных префиксов в выпадающем списке."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
user_work_path = Var.USER_WORK_PATH or ""
prefixes_root_path = os.path.join(user_work_path, "prefixes")
has_prefixes_dir = os.path.isdir(prefixes_root_path)
if not has_prefixes_dir:
return
@@ -2667,7 +2672,8 @@ class WineHelperGUI(QMainWindow):
if not prefix_name:
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
if os.path.isdir(prefix_path):
try:
subprocess.Popen(['xdg-open', prefix_path])
@@ -2706,7 +2712,8 @@ class WineHelperGUI(QMainWindow):
self.fsync_button.setChecked(False)
return
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
user_work_path = Var.USER_WORK_PATH or ""
last_conf_path = os.path.join(user_work_path, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
self.prefix_info_display.setHtml(f"<p>Файл конфигурации last.conf не найден для префикса '{prefix_name}'.</p>")
@@ -2751,7 +2758,8 @@ class WineHelperGUI(QMainWindow):
self.fsync_button.blockSignals(False)
# --- Чтение и отображение установленных компонентов Winetricks ---
winetricks_log_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "winetricks.log")
user_work_path = Var.USER_WORK_PATH or ""
winetricks_log_path = os.path.join(user_work_path, "prefixes", prefix_name, "winetricks.log")
installed_verbs = []
if os.path.exists(winetricks_log_path):
try:
@@ -2872,7 +2880,8 @@ class WineHelperGUI(QMainWindow):
if not prefix_name:
return None
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
user_work_path = Var.USER_WORK_PATH or ""
last_conf_path = os.path.join(user_work_path, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
return None
@@ -2953,7 +2962,8 @@ class WineHelperGUI(QMainWindow):
return
# Определяем архитектуру префикса
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
last_conf_path = os.path.join(prefix_path, "last.conf")
architecture = "win64" # По умолчанию
if os.path.exists(last_conf_path):
@@ -3009,7 +3019,8 @@ class WineHelperGUI(QMainWindow):
)
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name))
user_work_path = Var.USER_WORK_PATH or ""
env.insert("WINEPREFIX", os.path.join(user_work_path, "prefixes", prefix_name))
self.command_process.setProcessEnvironment(env)
args = ["change-wine", new_version]
@@ -3019,7 +3030,8 @@ class WineHelperGUI(QMainWindow):
def run_component_install_command(self, prefix_name, command, version):
"""Выполняет команду установки компонента (DXVK/VKD3D) через winehelper."""
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
self.command_dialog = QDialog(self)
self.command_dialog.setWindowTitle(f"Выполнение: {command} {version}")
@@ -3106,7 +3118,8 @@ class WineHelperGUI(QMainWindow):
def run_update_associations_command(self, prefix_name, new_associations):
"""Выполняет команду обновления ассоциаций файлов."""
# --- Прямое редактирование last.conf, чтобы обойти перезапись переменных в winehelper ---
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
user_work_path = Var.USER_WORK_PATH or ""
last_conf_path = os.path.join(user_work_path, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
QMessageBox.critical(self, "Ошибка", f"Файл конфигурации last.conf не найден для префикса '{prefix_name}'.")
return
@@ -3139,7 +3152,8 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка записи", f"Не удалось обновить файл last.conf: {str(e)}")
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
def on_finished(exit_code, exit_status):
self._handle_component_install_finished(prefix_name, exit_code, exit_status)
@@ -3167,7 +3181,8 @@ class WineHelperGUI(QMainWindow):
QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.")
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
drive_c_path = os.path.join(prefix_path, "drive_c")
if not os.path.isdir(drive_c_path):
@@ -3402,7 +3417,8 @@ class WineHelperGUI(QMainWindow):
if self.created_prefix_selector.count() > 0:
self.created_prefix_selector.setCurrentIndex(-1)
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
self.command_dialog = QDialog(self)
self.command_dialog.setWindowTitle(f"Создание префикса: {prefix_name}")
@@ -3486,12 +3502,13 @@ class WineHelperGUI(QMainWindow):
widget.deleteLater()
self.installed_buttons.clear()
if not os.path.exists(Var.USER_WORK_PATH):
os.makedirs(Var.USER_WORK_PATH, exist_ok=True)
user_work_path = Var.USER_WORK_PATH or ""
if not os.path.exists(user_work_path):
os.makedirs(user_work_path, exist_ok=True)
return
desktop_files = []
for entry in os.scandir(Var.USER_WORK_PATH):
for entry in os.scandir(user_work_path):
if entry.is_file() and entry.name.endswith('.desktop'):
desktop_files.append(entry.path)
@@ -3829,13 +3846,14 @@ class WineHelperGUI(QMainWindow):
ErrorReporter.show_error(self, "Менеджер Winetricks", error_msg)
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
if not os.path.isdir(prefix_path):
error_msg = f"Каталог префикса не найден:\n{prefix_path}"
ErrorReporter.show_error(self, "Ошибка", error_msg)
return
winetricks_path = Var.WH_WINETRICKS
winetricks_path = Var.WH_WINETRICKS or ""
wine_executable = self._get_wine_executable_for_prefix(prefix_name)
dialog = WinetricksManagerDialog(prefix_path, winetricks_path, self, wine_executable=wine_executable)
dialog.installation_complete.connect(lambda: self.update_prefix_info_display(prefix_name))
@@ -3843,7 +3861,8 @@ class WineHelperGUI(QMainWindow):
def _get_wine_executable_for_prefix(self, prefix_name):
"""Определяет и возвращает путь к исполняемому файлу wine для указанного префикса."""
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
last_conf_path = os.path.join(prefix_path, "last.conf")
wh_wine_use = None
@@ -3866,7 +3885,8 @@ class WineHelperGUI(QMainWindow):
print(f"Предупреждение: не удалось прочитать или обработать {last_conf_path}: {e}")
if wh_wine_use and not wh_wine_use.startswith('system'):
local_wine_path = os.path.join(Var.USER_WORK_PATH, "dist", wh_wine_use, "bin", "wine")
user_work_path = Var.USER_WORK_PATH or ""
local_wine_path = os.path.join(user_work_path, "dist", wh_wine_use, "bin", "wine")
if os.path.exists(local_wine_path):
return local_wine_path
QMessageBox.warning(self, "Предупреждение",
@@ -3880,7 +3900,8 @@ class WineHelperGUI(QMainWindow):
if not prefix_name:
return
last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf")
user_work_path = Var.USER_WORK_PATH or ""
last_conf_path = os.path.join(user_work_path, "prefixes", prefix_name, "last.conf")
if not os.path.exists(last_conf_path):
error_msg = f"Файл last.conf не найден для префикса '{prefix_name}'."
ErrorReporter.show_error(self, "Ошибка", error_msg)
@@ -3928,7 +3949,8 @@ class WineHelperGUI(QMainWindow):
ErrorReporter.show_error(self, "Ошибка", error_msg)
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
if not os.path.isdir(prefix_path):
error_msg = f"Каталог префикса не найден:\n{prefix_path}"
ErrorReporter.show_error(self, "Ошибка", error_msg)
@@ -3993,7 +4015,8 @@ class WineHelperGUI(QMainWindow):
process.kill()
return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
wine_executable = self._get_wine_executable_for_prefix(prefix_name)
wineserver_path = os.path.join(os.path.dirname(wine_executable), "wineserver")
@@ -4256,7 +4279,8 @@ class WineHelperGUI(QMainWindow):
if msg_box.clickedButton() == yes_button:
try:
# Полный путь к префиксу
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
user_work_path = Var.USER_WORK_PATH or ""
prefix_path = os.path.join(user_work_path, "prefixes", prefix_name)
# 1. Сначала собираем ВСЕ .desktop файлы, связанные с этим префиксом
all_desktop_files = set()
@@ -4265,7 +4289,7 @@ class WineHelperGUI(QMainWindow):
all_desktop_files.add(self.current_selected_app['desktop_path'])
desktop_locations = [
Var.USER_WORK_PATH,
user_work_path,
os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper"),
os.path.join(os.path.expanduser("~"), "Desktop"),
os.path.join(os.path.expanduser("~"), "Рабочий стол"),
@@ -4324,16 +4348,16 @@ class WineHelperGUI(QMainWindow):
".local/share/desktop-directories/WineHelper.directory"),
os.path.join(os.path.expanduser("~"), ".config/menus/applications-merged/WineHelper.menu")
]
for f in menu_files:
if os.path.exists(f):
for menu_file in menu_files:
if os.path.exists(menu_file):
try:
os.remove(f)
os.remove(menu_file)
except FileNotFoundError:
print(f"Файл меню не найден для удаления: {f}")
print(f"Файл меню не найден для удаления: {menu_file}")
except PermissionError:
print(f"Нет доступа к файлу меню для удаления: {f}")
print(f"Нет доступа к файлу меню для удаления: {menu_file}")
except Exception as e:
print(f"Ошибка удаления файла меню {f}: {str(e)}")
print(f"Ошибка удаления файла меню {menu_file}: {str(e)}")
except FileNotFoundError:
print(f"Директория меню не найдена для удаления: {menu_dir}")
except PermissionError:
@@ -4418,13 +4442,16 @@ class WineHelperGUI(QMainWindow):
# Определяем виджеты и действия в зависимости от типа скрипта
if script_name in self.autoinstall_scripts:
script_path = os.path.join(Var.DATA_PATH, "autoinstall", script_name)
data_path = Var.DATA_PATH or ""
script_path = os.path.join(data_path, "autoinstall", script_name)
tab_type = 'auto'
if not os.path.exists(script_path): # Проверяем в testinstall, если не нашли в autoinstall
script_path = os.path.join(Var.DATA_PATH, "testinstall", script_name)
data_path = Var.DATA_PATH or ""
script_path = os.path.join(data_path, "testinstall", script_name)
self.manual_install_path_widget.setVisible(False)
else:
script_path = os.path.join(Var.DATA_PATH, "manualinstall", script_name)
data_path = Var.DATA_PATH or ""
script_path = os.path.join(data_path, "manualinstall", script_name)
tab_type = 'manual'
self.manual_install_path_widget.setVisible(True)
@@ -4454,7 +4481,8 @@ class WineHelperGUI(QMainWindow):
if icon_names:
# Для заголовка используем первую иконку из списка
icon_path = os.path.join(Var.DATA_PATH, "image", f"{icon_names[0]}.png")
data_path = Var.DATA_PATH or ""
icon_path = os.path.join(data_path, "image", f"{icon_names[0]}.png")
if os.path.exists(icon_path):
self.script_title.setPixmap(QPixmap(icon_path).scaled(64, 64, Qt.KeepAspectRatio))
else:
@@ -4592,11 +4620,13 @@ class WineHelperGUI(QMainWindow):
self._reset_log_state() # Сбрасываем состояние для обработки лога
winehelper_path = self.winehelper_path
script_path = os.path.join(Var.DATA_PATH,
"autoinstall" if os.path.exists(os.path.join(Var.DATA_PATH, "autoinstall", self.current_script))
else "testinstall" if os.path.exists(os.path.join(Var.DATA_PATH, "testinstall", self.current_script))
data_path = Var.DATA_PATH or ""
current_script = self.current_script or ""
script_path = os.path.join(data_path,
"autoinstall" if os.path.exists(os.path.join(data_path, "autoinstall", current_script))
else "testinstall" if os.path.exists(os.path.join(data_path, "testinstall", current_script))
else "manualinstall",
self.current_script)
current_script)
if not os.path.exists(winehelper_path):
error_msg = f"winehelper не найден по пути:\n{winehelper_path}"
@@ -4769,6 +4799,8 @@ class WineHelperGUI(QMainWindow):
def handle_process_output(self):
"""Обрабатывает вывод процесса, корректно отображая однострочный прогресс."""
if self.install_process is None:
return
new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore')
self.output_buffer += new_data