v2.8.6 - fix: sanitize AST scanned dependencies, block invalid pip options, and refactor configparser I/O
fix: sanitize AST scanned dependencies and harden configparser handling - Scanner: Enforce case-insensitive matching for excluded directories to strictly avoid scanning virtual environments. - Scanner: Filter out legacy/pseudo modules (e.g., __main__, android, org) and invalid package names using VALID_IDENTIFIER_REGEX. - Build: Sanitize dedup_install_list before pip installation to prevent flags starting with '-' from being interpreted as CLI options. - Config: Refactor load_config to use standard configparser methods (add_section, set, has_option) instead of raw dict assignments. - Bump version to 2.8.6.
This commit is contained in:
1 parent
12d7ef32d3
commit
1eefb491a0
4 files changed
+109
-61
No files matched your search
@@ -6,6 +6,20 @@ QPyPack 项目的所有重要变更都将记录在此文件中。
|
||||
|
||||
--
|
||||
|
||||
## [2.8.6] - 2026-09-11
|
||||
|
||||
### 修复
|
||||
- 修复 AST 依赖扫描导致误闯虚拟环境的问题
|
||||
- 修复第三方存根包及伪模块(如 `--main--`、`-winreg`、`android`、`org` 等)被误判为依赖并传入 pip 引发 `no such option` 参数解析崩溃的问题
|
||||
- 修复配置文件初始化与更新时使用字典直接赋值可能触发的潜在冲突,重构为标准的 `config.has_section` / `config.set` API 调用
|
||||
|
||||
### 优化
|
||||
- 引入 Python 合法标识符正则校验(`VALID_IDENTIFIER_REGEX`),从 AST 提取与 pip 安装双层阶段彻底封堵非法包名与命令参数注入
|
||||
- 内置旧版本与跨平台伪模块黑名单(`PSEUDO_AND_LEGACY_MODULES`),避免将历史兼容或存根模块作为依赖安装
|
||||
- 优化配置加载容错机制,统一字段读写与小写去重,提升 `config.ini` 的稳定性
|
||||
|
||||
--
|
||||
|
||||
## [2.8.5] - 2026-09-10
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -6,6 +6,20 @@ All notable changes to the **QPyPack** project will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [2.8.6] - 2026-09-11
|
||||
|
||||
### Fixed
|
||||
- Fixed case-sensitive directory matching in AST scanner that allowed traversal into virtual environments (`.venv/Lib`).
|
||||
- Fixed phantom/stub packages (e.g. `--main--`, `-winreg`, `android`, `org`) being passed to pip as invalid flags, preventing `no such option` errors.
|
||||
- Refactored `load_config` initialization and updates to use standard `configparser` APIs (`has_section`, `has_option`, `set`), eliminating dict assignment issues.
|
||||
|
||||
### Improved
|
||||
- Implemented identifier regex validation (`VALID_IDENTIFIER_REGEX`) across AST analysis and pip installation stages to block parameter injection.
|
||||
- Added blacklist for legacy and cross-platform pseudo-modules (`PSEUDO_AND_LEGACY_MODULES`).
|
||||
- Hardened configuration file handling with normalized lowercase key synchronization and fallback handling.
|
||||
|
||||
---
|
||||
|
||||
## [2.8.5] - 2026-09-10
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "qpypack"
|
||||
version = "2.8.5"
|
||||
version = "2.8.6"
|
||||
description = "A cross-platform GUI packaging and compilation suite based on PyInstaller and Nuitka."
|
||||
readme = "README_EN.md"
|
||||
authors = [
|
||||
|
||||
+80
-60
@@ -125,7 +125,7 @@ except ImportError:
|
||||
HAS_QT_AUDIO = False
|
||||
|
||||
__app_name__ = "QPyPack"
|
||||
__version__ = "2.8.5"
|
||||
__version__ = "2.8.6"
|
||||
__author__ = "QwejayHuang"
|
||||
__company__ = "Qwesoft"
|
||||
__description__ = "Modern Cross-Platform Python Packaging GUI Powered by PyInstaller & Nuitka"
|
||||
@@ -897,13 +897,15 @@ def load_config(retry=True):
|
||||
default_mirror = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
default_backup = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
|
||||
clean_default_mappings = {}
|
||||
for k, v in DEFAULT_MAPPINGS.items():
|
||||
clean_default_mappings[k.lower()] = v
|
||||
clean_default_mappings = {k.lower(): v for k, v in DEFAULT_MAPPINGS.items()}
|
||||
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
config["Mappings"] = clean_default_mappings
|
||||
config["Settings"] = {
|
||||
config.add_section("Mappings")
|
||||
for k, v in clean_default_mappings.items():
|
||||
config.set("Mappings", k, v)
|
||||
|
||||
config.add_section("Settings")
|
||||
settings_defaults = {
|
||||
"language": "auto",
|
||||
"engine": "PyInstaller",
|
||||
"pip_index": default_mirror,
|
||||
@@ -934,6 +936,13 @@ def load_config(retry=True):
|
||||
"enable_backport_shield": "True",
|
||||
"lite_mode": "False",
|
||||
}
|
||||
for k, v in settings_defaults.items():
|
||||
config.set("Settings", k, v)
|
||||
|
||||
config.add_section("BackportRules")
|
||||
for k, v in DEFAULT_BACKPORT_RULES.items():
|
||||
config.set("BackportRules", k, str(v))
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
config.write(f)
|
||||
@@ -950,32 +959,22 @@ def load_config(retry=True):
|
||||
except Exception:
|
||||
pass
|
||||
return load_config(retry=False)
|
||||
else:
|
||||
if "Mappings" not in config:
|
||||
config["Mappings"] = clean_default_mappings
|
||||
if "Settings" not in config:
|
||||
config["Settings"] = {}
|
||||
if "BackportRules" not in config:
|
||||
config["BackportRules"] = DEFAULT_BACKPORT_RULES
|
||||
|
||||
if "Mappings" not in config:
|
||||
config["Mappings"] = clean_default_mappings
|
||||
else:
|
||||
updated_map = False
|
||||
for k, v in clean_default_mappings.items():
|
||||
if k not in config["Mappings"]:
|
||||
config["Mappings"][k] = v
|
||||
updated_map = True
|
||||
if updated_map:
|
||||
try:
|
||||
save_config(config)
|
||||
except Exception:
|
||||
pass
|
||||
updated_any = False
|
||||
|
||||
if not config.has_section("Mappings"):
|
||||
config.add_section("Mappings")
|
||||
updated_any = True
|
||||
|
||||
for k, v in clean_default_mappings.items():
|
||||
if not config.has_option("Mappings", k):
|
||||
config.set("Mappings", k, v)
|
||||
updated_any = True
|
||||
|
||||
if "Settings" not in config:
|
||||
config["Settings"] = {}
|
||||
if not config.has_section("Settings"):
|
||||
config.add_section("Settings")
|
||||
updated_any = True
|
||||
|
||||
updated = False
|
||||
default_updates = {
|
||||
"language": "auto",
|
||||
"keep_venv": "False",
|
||||
@@ -1003,34 +1002,29 @@ def load_config(retry=True):
|
||||
"lite_mode": "False",
|
||||
}
|
||||
for k, v in default_updates.items():
|
||||
if k not in config["Settings"]:
|
||||
config["Settings"][k] = v
|
||||
updated = True
|
||||
if not config.has_option("Settings", k):
|
||||
config.set("Settings", k, v)
|
||||
updated_any = True
|
||||
|
||||
if updated:
|
||||
if not config.has_section("BackportRules"):
|
||||
config.add_section("BackportRules")
|
||||
updated_any = True
|
||||
|
||||
for k, v in DEFAULT_BACKPORT_RULES.items():
|
||||
if not config.has_option("BackportRules", k):
|
||||
config.set("BackportRules", k, str(v))
|
||||
updated_any = True
|
||||
|
||||
if updated_any:
|
||||
try:
|
||||
save_config(config)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "BackportRules" not in config:
|
||||
config["BackportRules"] = DEFAULT_BACKPORT_RULES
|
||||
else:
|
||||
updated_rules = False
|
||||
for k, v in DEFAULT_BACKPORT_RULES.items():
|
||||
if k not in config["BackportRules"]:
|
||||
config["BackportRules"][k] = v
|
||||
updated_rules = True
|
||||
if updated_rules:
|
||||
try:
|
||||
save_config(config)
|
||||
except Exception:
|
||||
pass
|
||||
lang_pref = config["Settings"].get("language", "auto")
|
||||
lang_pref = config.get("Settings", "language", fallback="auto")
|
||||
I18N.set_language(lang_pref)
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config):
|
||||
import time
|
||||
|
||||
@@ -1189,6 +1183,14 @@ def is_cloud_sync_path(path_obj: Path) -> bool:
|
||||
]
|
||||
return any(kw in path_str for kw in cloud_keywords)
|
||||
|
||||
PSEUDO_AND_LEGACY_MODULES = {
|
||||
"__builtin__", "__main__", "java", "android", "org", "com", "dl",
|
||||
"thread", "dummy_thread", "htmlentitydefs", "htmlparser", "httplib",
|
||||
"urllib2", "urlparse", "xmlrpclib", "stringio", "cstringio",
|
||||
"exceptions", "commands", "ntlm", "typeshed"
|
||||
}
|
||||
VALID_IDENTIFIER_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
|
||||
|
||||
def extract_project_imports_via_ast(target_path, scan_dir: bool = False) -> set:
|
||||
imports = set()
|
||||
target_path = Path(target_path)
|
||||
@@ -1200,19 +1202,21 @@ def extract_project_imports_via_ast(target_path, scan_dir: bool = False) -> set:
|
||||
EXCLUDE_DIR_NAMES = {
|
||||
"__pycache__", ".git", ".hg", ".svn", "build", "dist",
|
||||
".venv", "venv", "env", ".env", ".conda", "envs",
|
||||
"site-packages", "dist-info", "egg-info", "Lib", "lib",
|
||||
"site-packages", "dist-info", "egg-info", "lib", "libs",
|
||||
"node_modules", ".tox", ".nox", ".pytest_cache", ".mypy_cache"
|
||||
}
|
||||
for root, dirs, files in os.walk(target_path):
|
||||
dirs[:] = [
|
||||
d for d in dirs
|
||||
if not d.startswith((".qpypack", "qpypack")) and d not in EXCLUDE_DIR_NAMES
|
||||
if not d.lower().startswith((".qpypack", "qpypack"))
|
||||
and d.lower() not in EXCLUDE_DIR_NAMES
|
||||
]
|
||||
|
||||
path_obj = Path(root)
|
||||
if any(p.startswith((".qpypack", "qpypack")) for p in path_obj.parts):
|
||||
path_parts_lower = {p.lower() for p in path_obj.parts}
|
||||
if any(p.startswith((".qpypack", "qpypack")) for p in path_parts_lower):
|
||||
continue
|
||||
if any(part in EXCLUDE_DIR_NAMES for part in path_obj.parts):
|
||||
if path_parts_lower & EXCLUDE_DIR_NAMES:
|
||||
continue
|
||||
|
||||
for file in files:
|
||||
@@ -1226,20 +1230,24 @@ def extract_project_imports_via_ast(target_path, scan_dir: bool = False) -> set:
|
||||
continue
|
||||
tree = ast.parse(code, filename=file_p.as_posix())
|
||||
for node in ast.walk(tree):
|
||||
names_to_check = []
|
||||
if isinstance(node, ast.Import):
|
||||
for n in node.names:
|
||||
top_pkg = n.name.split(".")[0].strip()
|
||||
if top_pkg and not top_pkg.startswith(("-", "_")):
|
||||
imports.add(top_pkg)
|
||||
names_to_check = [n.name for n in node.names]
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level == 0 and node.module:
|
||||
top_pkg = node.module.split(".")[0].strip()
|
||||
if top_pkg and not top_pkg.startswith(("-", "_")):
|
||||
imports.add(top_pkg)
|
||||
names_to_check = [node.module]
|
||||
|
||||
for raw_name in names_to_check:
|
||||
top_pkg = raw_name.split(".")[0].strip()
|
||||
if not top_pkg or not VALID_IDENTIFIER_REGEX.match(top_pkg):
|
||||
continue
|
||||
if top_pkg.lower() in PSEUDO_AND_LEGACY_MODULES:
|
||||
continue
|
||||
imports.add(top_pkg)
|
||||
except Exception:
|
||||
pass
|
||||
return imports
|
||||
|
||||
|
||||
def query_target_env_packages(python_exe: str) -> dict:
|
||||
if not python_exe or not os.path.exists(python_exe):
|
||||
return {}
|
||||
@@ -5924,7 +5932,19 @@ class PackingThread(QThread):
|
||||
dedup_install_list = [
|
||||
pkg for pkg in list(final_install_dict.values()) if re.split(r"[=><!~]", pkg)[0].strip().lower() not in target_std_libs
|
||||
]
|
||||
clean_dedup_list = []
|
||||
for pkg_spec in dedup_install_list:
|
||||
pkg_raw = pkg_spec.strip()
|
||||
if not pkg_raw or pkg_raw.startswith(("-", "_", ".")):
|
||||
continue
|
||||
base_name = re.split(r"[=><!~@;\[]", pkg_raw)[0].strip().lower()
|
||||
if not VALID_IDENTIFIER_REGEX.match(base_name.replace("-", "_")):
|
||||
continue
|
||||
if base_name in PSEUDO_AND_LEGACY_MODULES:
|
||||
continue
|
||||
clean_dedup_list.append(pkg_raw)
|
||||
|
||||
dedup_install_list = clean_dedup_list
|
||||
enable_shield = self.params.get("enable_backport_shield", True)
|
||||
user_backport_rules = self.params.get("backport_rules", {})
|
||||
|
||||
|
||||
Reference in new issue
Block a user