diff --git a/.gitignore b/.gitignore index e261543..431ad4e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ wheels/ .python-version output.md log/ -*.mb \ No newline at end of file +*.mb +*.mbank +dual_protection_ +*.db \ No newline at end of file diff --git a/README.md b/README.md index d846fc9..5b2440d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # Python Module Bank - SQLite 模块打包系统 -一个将Python模块打包到SQLite数据库,并支持从数据库直接导入模块的工具系统。 +一个将 Python 模块打包到 SQLite 数据库,并支持从数据库直接导入模块的工具系统。支持 **AES 字节码加密** 和 **PyArmor 代码混淆** 双重保护机制。 ## 🌟 特性 -- **单文件分发** - 将所有模块打包到单个SQLite数据库文件 -- **源代码保护** - 模块以编译后的字节码形式存储 -- **动态导入** - 运行时直接从数据库加载模块,无需文件系统 +- **单文件分发** - 将所有模块打包到单个 SQLite 数据库文件 +- **AES 字节码加密** - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储 +- **PyArmor 代码混淆** - 支持使用 PyArmor 对 Python 源代码进行混淆保护 +- **双层保护** - 可同时使用 PyArmor 混淆 + AES 字节码加密,提供双重安全保障 +- **动态导入** - 运行时直接从数据库加载模块,无缝集成 Python 导入系统 - **完整包支持** - 支持包结构和子模块导入 -- **CLI工具** - 提供完整的命令行接口 -- **导入钩子** - 无缝集成Python导入系统 +- **CLI 工具** - 提供完整的命令行接口 ## 📦 安装 @@ -21,69 +22,154 @@ cd module-bank pip install -e . ``` -### 依赖要求 +### 使用 uv(推荐) -- Python 3.7+ -- 无需额外依赖(仅使用标准库) +```bash +git clone http://124.71.68.6:3000/chakcy/module_bank.git +cd module-bank +uv sync +``` + +### 可选依赖 + +- **PyArmor** (强烈推荐): `uv add pyarmor` 或 `pip install pyarmor` + - 为模块提供 Python 源代码级别的混淆保护 + - 商业版可去除试用版水印 ## 🚀 快速开始 ### 1. 创建示例模块 ```python -# my_package/my_module.py +# my_package/__init__.py def hello(): - print("Hello from my_module!") + print("Hello from my_package!") return "success" + +VERSION = "1.0.0" +``` + +```python +# my_package/utils.py +def add(a, b): + return a + b ``` ### 2. 打包模块到数据库 -```python -# pack_example.py -from module_bank import PythonToSQLite -from module_bank.encryption import Encryption +#### 使用 AES 加密(推荐) -key = Encryption.generate_key() +```bash +# 生成密钥并打包 +mbank pack my_package --db my_package.mbank --key "$(python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())")" +``` -with open("my_package.key", "w") as key_file: - key_file.write(key) +#### 使用 PyArmor 混淆 -packer = PythonToSQLite("my_package.mbank", key) -packer.pack_directory("my_package") +```bash +# 使用 PyArmor 保护源代码 +mbank pack my_package --db my_package.mbank --pyarmor +``` +#### 使用双层保护 + +```bash +# PyArmor 混淆 + AES 加密 +mbank pack my_package --db my_package.mbank --pyarmor --key "your-encryption-key" ``` ### 3. 从数据库导入 ```python -from module_bank import PythonToSQLite - -with open("my_package.key") as f: - key = f.read() +# import_example.py +from module_bank.python_to_sqlite import PythonToSQLite packer = PythonToSQLite("my_package.mbank") -finder = packer.install_importer(key) +finder = packer.install_importer(key="your-encryption-key") # 如果加密了需要提供密钥 -from my_package import my_module +from my_package import utils +from my_package import hello -print(my_module) -my_module.hello() +print(hello()) +print(utils.add(3, 4)) +``` + +## 🛡️ 安全保护机制 + +### 第一层:PyArmor 代码混淆 + +PyArmor 对 Python 源代码进行混淆,将代码转换为难以阅读和逆向的形式: + +- 混淆后的代码依赖运行时二进制文件(`.pyd`/`.so`),无法直接运行 +- 支持混淆级别控制(代码混淆、模块混淆) +- 运行时文件会自动存储在数据库中,加载时自动部署 + +```bash +# 高级混淆选项 +mbank pack my_module.py --name my_module --db modules.db \ + --pyarmor \ + --pyarmor-obf-code 1 \ + --pyarmor-obf-mod 1 +``` + +### 第二层:AES 字节码加密 + +编译后的字节码使用 Fernet (AES-128-CBC) 加密: + +```bash +# 生成密钥 +python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())" > my_key.key + +# 使用密钥打包 +mbank pack my_module.py --name my_module --db modules.db --key "$(cat my_key.key)" + +# 使用时提供密钥 +mbank install --db modules.db --key "$(cat my_key.key)" +``` + +### 安全建议 + +1. **密钥管理**:加密密钥不要硬编码在代码中,使用环境变量或密钥管理服务 +2. **删除源代码**:打包后可从数据库删除源代码,仅保留混淆后的代码: + +```bash +# 删除所有模块的源代码 +mbank delete_source --db modules.db + +# 删除指定模块的源代码 +mbank delete_source --db modules.db --module my_module ``` ## 📖 详细使用 ### 命令行工具 -```python -# 打包模块或目录 -mbank pack my_package --db modules.db --key my_package_key +```bash +# 打包单个模块 +mbank pack my_module.py --name my_module --db modules.db + +# 打包整个目录(自动识别包结构) +mbank pack my_package/ --db modules.db + +# 使用 PyArmor 混淆打包 +mbank pack my_module.py --name my_module --db modules.db --pyarmor + +# 使用 AES 加密打包 +mbank pack my_module.py --name my_module --db modules.db --key "your-key" + +# 双层保护 +mbank pack my_module.py --name my_module --db modules.db --pyarmor --key "your-key" # 列出数据库中的模块 mbank list --db modules.db +# 删除源代码 +mbank delete_source --db modules.db +mbank delete_source --db modules.db --module my_module + # 安装导入器并进入交互模式 -mbank install --db modules.db --key my_package_key +mbank install --db modules.db +mbank install --db modules.db --key "your-key" ``` ### 编程接口 @@ -92,8 +178,21 @@ mbank install --db modules.db --key my_package_key ```python from module_bank import PythonToSQLite +from module_bank.encryption import Encryption -packer = PythonToSQLite("modules.mbank") +# 生成加密密钥 +key = Encryption.generate_key() + +# 使用 AES 加密打包 +packer = PythonToSQLite("modules.mbank", key=key) + +# 或使用 PyArmor 混淆打包 +packer = PythonToSQLite( + "modules.mbank", + pyarmor_obfuscate=True, + pyarmor_obf_code=1, + pyarmor_obf_mod=1, +) # 打包单个模块 packer.pack_module("module.py", "module_name") @@ -109,21 +208,25 @@ packer.verify_package_structure() ```python from module_bank import PythonToSQLite -import sys +# 如果数据库中的模块被 AES 加密了,需要提供密钥 packer = PythonToSQLite("modules.mbank") -# 安装导入器到sys.meta_path -finder = packer.install_importer() +# 安装导入器到 sys.meta_path +finder = packer.install_importer(key="your-key") # 列出所有可用模块 modules = packer.list_modules() for module in modules: - print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}") + package_flag = " [包]" if module["is_package"] else "" + print(f" - {module['module_name']}{package_flag}") -# 导入数据库中的模块 +# 直接导入数据库中的模块(就像普通模块一样) import my_package import my_package.submodule + +print(my_package) +my_package.hello() ``` ## 🏗️ 架构设计 @@ -132,37 +235,49 @@ import my_package.submodule ```python src/module_bank/ -├── python_to_sqlite.py # 主打包类 -├── sqlite_module_importer.py # 数据库存储管理器 -├── sqlite_meta_path_finder.py # 元路径查找器 -├── sqlite_module_loader.py # 模块加载器 -├── encryption.py # 密钥管理 -├── cli.py # 命令行接口 -└── __init__.py # 模块导出 +├── python_to_sqlite.py # 主打包类 +├── sqlite_module_importer.py # 数据库存储管理器 +├── sqlite_meta_path_finder.py # 元路径查找器 +├── sqlite_module_loader.py # 模块加载器 +├── encryption.py # AES 密钥管理 +├── pyarmor_encryption.py # PyArmor 混淆封装 +├── cli.py # 命令行接口 +└── __init__.py # 模块导出 ``` ### 数据流 ```text 1. 打包阶段: - .py文件 → 编译为字节码 → 存储到SQLite数据库 + .py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密) → 存储到 SQLite 2. 导入阶段: - 导入请求 → MetaPathFinder查找 → ModuleLoader加载 → 执行模块 + 导入请求 → MetaPathFinder 查找 → ModuleLoader 加载 → (AES 解密) → (PyArmor 运行时部署) → 执行模块 ``` ### 数据库模式 ```sql +-- 模块表 CREATE TABLE python_modules ( module_name TEXT PRIMARY KEY, - source_code TEXT, -- 源代码(可选) - bytecode BLOB, -- 编译后的字节码 - is_package BOOLEAN, -- 是否是包 - metadata TEXT, -- 元数据(JSON格式) - encrypted INTEGER DEFAULT 0, -- 是否已加密 + source_code TEXT, -- 源代码(PyArmor 混淆后) + bytecode BLOB, -- 编译后的字节码(可选 AES 加密) + is_package BOOLEAN, -- 是否是包 + metadata TEXT, -- 元数据(JSON 格式) + encrypted INTEGER DEFAULT 0, -- AES 是否已加密 + pyarmor_protected INTEGER DEFAULT 0, -- 是否已 PyArmor 混淆 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) +); + +-- PyArmor 运行时存储表 +CREATE TABLE pyarmor_runtime ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + runtime_name TEXT UNIQUE NOT NULL, + pyd_data BLOB, -- 原生二进制文件(.pyd/.so) + init_data TEXT, -- __init__.py 内容 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); ``` ## 🔧 高级功能 @@ -173,7 +288,7 @@ CREATE TABLE python_modules ( # 打包时排除特定文件 packer.pack_directory( "my_project/", - exclude_patterns=["*_test.py", "*.pyc", "__pycache__"] + exclude_patterns=["*_test.py", "*.pyc", "__pycache__"], ) ``` @@ -185,42 +300,56 @@ packer.importer.add_module( "my_module", source_code, is_package=False, - metadata={"version": "1.0", "author": "me"} + metadata={"version": "1.0", "author": "me"}, ) ``` ### 混合导入 ```python -# 可以同时使用文件系统和数据库导入 -# 数据库导入器优先级更高 +# 数据库导入器默认插入到 meta_path 开头,优先级高于文件系统 import sys from module_bank import PythonToSQLite packer = PythonToSQLite("modules.db") -finder = packer.install_importer() # 插入到meta_path开头 +finder = packer.install_importer() -# 如果需要文件系统优先,可以调整插入位置 +# 如果需要文件系统优先,将查找器追加到末尾 +sys.meta_path.remove(finder) sys.meta_path.append(finder) ``` +### 验证运行时完整性 + +```python +from module_bank.sqlite_module_importer import SQLiteModuleImporter + +importer = SQLiteModuleImporter("modules.db") +runtimes = importer.list_runtimes() +for rt in runtimes: + print(f"运行时: {rt['runtime_name']} (创建于: {rt['created_at']})") +``` + ## ⚠️ 注意事项 ### 安全性 - 模块字节码直接执行,确保数据库来源可信 -- 生产环境建议添加代码签名验证 +- AES 加密密钥必须安全保管,丢失后将无法解密模块 +- PyArmor 混淆提供源代码级别的保护,但商业版才能去除试用版水印 +- 生产环境建议 PyArmor 混淆 + AES 加密同时使用 ### 兼容性 -- 字节码不跨Python版本兼容 -- 不支持C扩展模块 -- 不支持需要文件系统资源的模块(如__file__依赖) +- 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载) +- 不支持 C 扩展模块 +- PyArmor 运行时文件与操作系统相关(Windows 用 `.pyd`,Linux 用 `.so`) +- 运行时文件需在目标系统上重新打包 ### 性能 -- **启动时**:有一次性数据库查询和反序列化开销 -- **运行时**:与传统导入性能相同(使用sys.modules缓存) +- **启动时**:有一次性数据库查询和反序列化/运行时部署开销 +- **运行时**:与传统导入性能相同(使用 sys.modules 缓存) - **最佳适用**:长期运行的服务、桌面应用 ### 更新模块 @@ -249,11 +378,15 @@ cp modules.backup.mbank modules.mbank ## 📚 应用场景 -1. 商业软件分发 - 保护源代码知识产权 -2. 插件系统 - 动态加载数据库中的插件模块 -3. 教育平台 - 安全分发练习代码 -4. 微服务 - 打包多个服务模块到单个文件 -5. 嵌入式系统 - 减少文件系统依赖 +1. **商业软件分发** - PyArmor 混淆保护源代码知识产权 +2. **安全敏感环境** - AES 加密确保即使数据库泄露也无法还原代码 +3. **插件系统** - 动态加载数据库中的加密/混淆插件模块 +4. **教育平台** - 安全分发练习代码,防止学生抄袭 +5. **微服务** - 打包多个服务模块到单个加密文件 +6. **嵌入式系统** - 减少文件系统依赖,提高安全性 --- + **注意**: 本工具主要用于模块分发和部署场景,不适合开发阶段的频繁修改。 + +**许可证**: MIT License diff --git a/examples/pyarmor_aes_dual_protection.py b/examples/pyarmor_aes_dual_protection.py new file mode 100644 index 0000000..7c6cd67 --- /dev/null +++ b/examples/pyarmor_aes_dual_protection.py @@ -0,0 +1,202 @@ +""" +PyArmor + AES 双层保护完整示例 + +本示例演示如何同时使用 PyArmor 混淆和 AES 字节码加密 +来提供双重保护。 +""" + +import sys +import os +import tempfile +import shutil + +# 添加项目源码路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +# ========== 1. 创建一个需要保护的模块 ========== + +SECRET_MODULE_CODE = """ +# 这是需要保护的敏感代码(支付逻辑、授权验证等) +import hashlib +import hmac + +SECRET_KEY = "this-is-a-top-secret-key" + +def verify_license(license_key: str) -> bool: + \"\"\"验证许可证密钥(敏感业务逻辑)\"\"\" + expected = hmac.new( + SECRET_KEY.encode(), + license_key.encode(), + hashlib.sha256 + ).hexdigest() + return expected.startswith("a1b2") + +def calculate_commission(amount: float, level: int) -> float: + \"\"\"计算佣金(商业机密算法)\"\"\" + rates = {1: 0.1, 2: 0.15, 3: 0.25, 4: 0.35} + rate = rates.get(level, 0.05) + # 敏感计算公式 + commission = amount * rate * (1.0 - 0.05) + return round(commission, 2) + +class PaymentProcessor: + \"\"\"支付处理器(核心商业逻辑)\"\"\" + def __init__(self, merchant_id: str): + self.merchant_id = merchant_id + + def process(self, order_id: str, amount: float) -> dict: + # 模拟支付处理 + return { + "status": "success", + "order_id": order_id, + "amount": amount, + "merchant": self.merchant_id, + "transaction_id": hashlib.sha256( + f"{order_id}{self.merchant_id}".encode() + ).hexdigest()[:16] + } + +VERSION = "2.1.0" +BUILD = "2026-05-04" +""" + + +def main(): + print("=" * 70) + print("PyArmor + AES 双层保护完整示例") + print("=" * 70) + + # 创建临时工作目录 + # work_dir = tempfile.mkdtemp(prefix="dual_protection_") + # db_path = os.path.join(work_dir, "protected_modules.db") + db_path = os.path.join("dual_protection_", "protected_modules.db") + key_file = os.path.join("dual_protection_", "encryption_key.txt") + + try: + # ========== 步骤 1: 引入模块 ========== + from module_bank.encryption import Encryption + from module_bank.python_to_sqlite import PythonToSQLite + + # ========== 步骤 2: 生成 AES 加密密钥 ========== + print("\n[步骤 1/5] 生成 AES 加密密钥...") + aes_key = Encryption.generate_key() + with open(key_file, "w") as f: + f.write(aes_key) + print(f" AES 密钥已生成: {aes_key[:20]}...") + + # ========== 步骤 3: 使用双层保护打包模块 ========== + print("\n[步骤 2/5] 使用 PyArmor + AES 双层保护打包模块...") + packer = PythonToSQLite( + db_path, + key=aes_key, # AES 字节码加密 + pyarmor_obfuscate=True, # PyArmor 源代码混淆 + pyarmor_obf_code=1, # 启用代码混淆 + pyarmor_obf_mod=1, # 启用模块混淆 + ) + + packer.importer.add_module( + "payment_system", + SECRET_MODULE_CODE, + is_package=False, + metadata={ + "version": "2.1.0", + "protection": "pyarmor_obfuscation + aes_encryption", + }, + ) + print(" ✓ 模块已使用双层保护存储到数据库") + + # ========== 步骤 4: 验证数据库中的保护状态 ========== + print("\n[步骤 3/5] 验证数据库中的保护状态...") + cursor = packer.importer.connection.execute( + """SELECT module_name, encrypted, pyarmor_protected, source_code, + LENGTH(bytecode) as bytecode_size, + LENGTH(source_code) as source_size + FROM python_modules""" + ) + row = cursor.fetchone() + print(f" 模块名: {row['module_name']}") + print(f" AES 加密: {'✓ 是' if row['encrypted'] else '✗ 否'}") + print(f" PyArmor 混淆: {'✓ 是' if row['pyarmor_protected'] else '✗ 否'}") + print(f" 字节码大小: {row['bytecode_size']} 字节") + print(f" 源代码大小: {row['source_size']} 字节") + + # 验证 PyArmor 运行时也已存储 + runtimes = packer.importer.list_runtimes() + for rt in runtimes: + print(f" PyArmor 运行时: {rt['runtime_name']}") + + # 显示数据库中存储的混淆代码片段 + print("\n[验证] 数据库中的混淆代码(前200字符):") + obfuscated = row["source_code"] + print(f" {obfuscated[:200]}...") + + # ========== 步骤 5: 从数据库导入并执行 ========== + print("\n[步骤 4/5] 从数据库导入模块(需要提供 AES 密钥)...") + # 清除之前的导入 + if "payment_system" in sys.modules: + del sys.modules["payment_system"] + + # 使用密钥安装导入器 + finder = packer.install_importer(key=aes_key) + + # 导入模块 + import importlib + + mod = importlib.import_module("payment_system") + + print(f" ✓ 模块导入成功: {mod}") + print(f" 模块文件: {mod.__file__}") + + # ========== 步骤 6: 验证模块功能正常 ========== + print("\n[步骤 5/5] 验证模块功能...") + + # 调用验证许可证函数 + result = mod.verify_license("TEST-1234-5678") + print(f" verify_license('TEST-1234-5678') → {result}") + + # 调用佣金计算 + commission = mod.calculate_commission(10000.0, 3) + print(f" calculate_commission(10000, 3) → ¥{commission}") + + # 使用支付处理器 + processor = mod.PaymentProcessor("MERCHANT_001") + payment_result = processor.process("ORDER_20260504", 299.00) + print(f" PaymentProcessor.process() → {payment_result['status']}") + print(f" 交易ID: {payment_result['transaction_id']}") + + # 访问模块常量 + print(f"\n 模块版本: {mod.VERSION}") + print(f" 构建日期: {mod.BUILD}") + + # ========== 安全验证 ========== + print("\n" + "=" * 70) + print("安全验证") + print("=" * 70) + print(""" + 1. PyArmor 混淆: 源代码已转换为不可读的混淆代码 + 2. AES 加密: 字节码已使用 AES-128-CBC 加密 + 3. 运行时保护: PyArmor 运行时二进制文件无法直接加载 + 4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行 + + 即使有人获取了数据库文件: + - 没有 AES 密钥 → 无法解密字节码 + - 没有 PyArmor 运行时 → 无法执行混淆代码 + - 两者同时具备 → 才能解密和执行 + """) + + except Exception as e: + print(f"\n❌ 错误: {e}") + import traceback + + traceback.print_exc() + finally: + # 清理 + print("\n[清理] 清除临时文件...") + # shutil.rmtree(work_dir, ignore_errors=True) + if "payment_system" in sys.modules: + del sys.modules["payment_system"] + print(" 完成!") + + +if __name__ == "__main__": + main() diff --git a/examples/verify.py b/examples/verify.py new file mode 100644 index 0000000..e084771 --- /dev/null +++ b/examples/verify.py @@ -0,0 +1,57 @@ +from module_bank import PythonToSQLite + +with open("./dual_protection_/encryption_key.txt") as f: + key = f.read() + +packer = PythonToSQLite( + "./dual_protection_/protected_modules.db", + key=key, + pyarmor_obfuscate=True, + pyarmor_obf_code=1, + pyarmor_obf_mod=1, +) + +packer.install_importer() + +import payment_system # type: ignore + +mod = payment_system +print(f" ✓ 模块导入成功: {mod}") +print(f" 模块文件: {mod.__file__}") + +# ========== 步骤 6: 验证模块功能正常 ========== +print("\n[步骤 5/5] 验证模块功能...") + +# 调用验证许可证函数 +result = mod.verify_license("TEST-1234-5678") +print(f" verify_license('TEST-1234-5678') → {result}") + +# 调用佣金计算 +commission = mod.calculate_commission(10000.0, 3) +print(f" calculate_commission(10000, 3) → ¥{commission}") + +# 使用支付处理器 +processor = mod.PaymentProcessor("MERCHANT_001") +payment_result = processor.process("ORDER_20260504", 299.00) +print(f" PaymentProcessor.process() → {payment_result['status']}") +print(f" 交易ID: {payment_result['transaction_id']}") + +# 访问模块常量 +print(f"\n 模块版本: {mod.VERSION}") +print(f" 构建日期: {mod.BUILD}") + +# ========== 安全验证 ========== +print("\n" + "=" * 70) +print("安全验证") +print("=" * 70) +print(""" + 1. PyArmor 混淆: 源代码已转换为不可读的混淆代码 + 2. AES 加密: 字节码已使用 AES-128-CBC 加密 + 3. 运行时保护: PyArmor 运行时二进制文件无法直接加载 + 4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行 + + 即使有人获取了数据库文件: + - 没有 AES 密钥 → 无法解密字节码 + - 没有 PyArmor 运行时 → 无法执行混淆代码 + - 两者同时具备 → 才能解密和执行 + """) diff --git a/pyproject.toml b/pyproject.toml index b13cffe..6e1368f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [project] dependencies = [ "cryptography>=45.0.7", + "pyarmor>=9.2.4", ] description = "Add your description here" name = "module_bank" readme = "README.md" requires-python = ">=3.7" -version = "0.2.3" +version = "0.3.0" [[tool.uv.index]] default = true diff --git a/scripts/test_integration.py b/scripts/test_integration.py new file mode 100644 index 0000000..62cbceb --- /dev/null +++ b/scripts/test_integration.py @@ -0,0 +1,295 @@ +""" +集成测试:测试 PyArmor 加密打包 -> 存储 -> 加载 -> 执行 全流程 +""" + +import sys +import os +import tempfile +import shutil + +# 添加项目模块路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +TEST_DB = os.path.join(os.path.dirname(__file__), "test_pyarmor_integration.db") + + +def create_test_module(): + """创建测试用的 Python 模块""" + return """ +def greet(name): + return f"Hello, {name}! This is a PyArmor protected module." + +class Calculator: + def add(self, a, b): + return a + b + + def multiply(self, a, b): + return a * b + +VERSION = "1.0.0" +""" + + +def test_pyarmor_obfuscate(): + """测试 PyArmor 混淆功能""" + print("=" * 60) + print("测试 1: PyArmor 混淆功能") + print("=" * 60) + + from module_bank.pyarmor_encryption import PyArmorEncryption + + enc = PyArmorEncryption(obf_code=1, obf_mod=1) + + # 检查 PyArmor 是否可用 + if not enc.check_pyarmor_available(): + print("ERROR: PyArmor 未安装或不可用") + return False + + source = create_test_module() + print(f"原始代码长度: {len(source)} 字节") + + try: + obfuscated_code, runtime_data = enc.obfuscate_source( + source, "test_protected_module" + ) + print(f"混淆后代码长度: {len(obfuscated_code)} 字节") + print(f"运行时文件: {'有' if runtime_data else '无'}") + print(f"运行时数据大小: {len(runtime_data) if runtime_data else 0} 字节") + + # 验证是否被保护 + assert enc.is_pyarmor_protected(obfuscated_code), "代码应被标记为 PyArmor 保护" + print(f"PyArmor 保护标记: 正确") + + # 提取运行时模块名 + runtime_name = enc.extract_runtime_info(obfuscated_code) + print(f"运行时模块名: {runtime_name}") + + print("测试 1 通过!\n") + return True + except Exception as e: + print(f"ERROR: {e}") + import traceback + + traceback.print_exc() + return False + + +def test_pack_with_pyarmor(): + """测试使用 PyArmor 打包到 SQLite""" + print("=" * 60) + print("测试 2: 使用 PyArmor 打包到 SQLite") + print("=" * 60) + + # 清理旧的测试数据库 + if os.path.exists(TEST_DB): + os.remove(TEST_DB) + + try: + from module_bank.python_to_sqlite import PythonToSQLite + + # 使用 PyArmor 创建打包器 + packer = PythonToSQLite( + TEST_DB, + pyarmor_obfuscate=True, + pyarmor_obf_code=1, + pyarmor_obf_mod=1, + ) + + source = create_test_module() + packer.importer.add_module("test_protected_module", source, is_package=False) + print("模块已打包到数据库") + + # 验证模块已存储 + modules = packer.list_modules() + module_names = [m["module_name"] for m in modules] + assert "test_protected_module" in module_names, "模块应在数据库中存在" + print(f"数据库中的模块: {module_names}") + + # 验证运行时已存储 + runtimes = packer.importer.list_runtimes() + print(f"数据库中的运行时: {[r['runtime_name'] for r in runtimes]}") + + # 验证 pyarmor_protected 标记 + cursor = packer.importer.connection.execute( + "SELECT pyarmor_protected FROM python_modules WHERE module_name = ?", + ("test_protected_module",), + ) + row = cursor.fetchone() + assert row["pyarmor_protected"] == 1, "模块应标记为 pyarmor_protected" + print("PyArmor 保护标记: 已正确设置") + + print("测试 2 通过!\n") + return True + except Exception as e: + print(f"ERROR: {e}") + import traceback + + traceback.print_exc() + return False + + +def test_import_pyarmor_module(): + """测试从 SQLite 导入并执行 PyArmor 保护的模块""" + print("=" * 60) + print("测试 3: 从 SQLite 导入并执行 PyArmor 保护模块") + print("=" * 60) + + try: + from module_bank.python_to_sqlite import PythonToSQLite + from module_bank.sqlite_meta_path_finder import SQLiteMetaPathFinder + + packer = PythonToSQLite(TEST_DB) + + # 安装导入器 + finder = packer.install_importer() + + # 尝试导入模块 + import importlib + + mod = importlib.import_module("test_protected_module") + print(f"模块导入成功: {mod}") + print(f"模块名: {mod.__name__}") + print(f"模块文件: {mod.__file__}") + + # 调用模块函数 + result = mod.greet("World") + print(f"调用 greet('World'): {result}") + assert result == "Hello, World! This is a PyArmor protected module." + + # 使用模块类 + calc = mod.Calculator() + assert calc.add(3, 4) == 7 + assert calc.multiply(3, 4) == 12 + print(f"Calculator.add(3, 4) = {calc.add(3, 4)}") + print(f"Calculator.multiply(3, 4) = {calc.multiply(3, 4)}") + + # 访问模块变量 + print(f"VERSION = {mod.VERSION}") + assert mod.VERSION == "1.0.0" + + print("测试 3 通过!\n") + + # 清理 + sys.meta_path.remove(finder) + if "test_protected_module" in sys.modules: + del sys.modules["test_protected_module"] + + return True + except Exception as e: + print(f"ERROR: {e}") + import traceback + + traceback.print_exc() + return False + + +def test_cli_with_pyarmor(): + """测试 CLI 的 PyArmor 参数""" + print("=" * 60) + print("测试 4: CLI --pyarmor 参数") + print("=" * 60) + + try: + import tempfile + + # 创建临时源文件 + tmpdir = tempfile.mkdtemp() + src_file = os.path.join(tmpdir, "hello_module.py") + with open(src_file, "w", encoding="utf-8") as f: + f.write("def hello():\n return 'Hello from PyArmor CLI!'\n") + + db_path = os.path.join(tmpdir, "test_cli.db") + + # 测试解析 --pyarmor 参数 + from module_bank.cli import main as cli_main + + # 保存原始argv + old_argv = sys.argv + try: + sys.argv = [ + "mbank", + "pack", + src_file, + "--db", + db_path, + "--name", + "hello_cli_module", + "--pyarmor", + ] + cli_main() + print("CLI pack with --pyarmor 执行成功") + + # 验证模块已用 PyArmor 保护 + from module_bank.sqlite_module_importer import SQLiteModuleImporter + + importer = SQLiteModuleImporter(db_path) + cursor = importer.connection.execute( + "SELECT pyarmor_protected FROM python_modules WHERE module_name = ?", + ("hello_cli_module",), + ) + row = cursor.fetchone() + assert row and row["pyarmor_protected"] == 1, "CLI 打包的模块应启用 PyArmor" + print("CLI 打包的模块已正确启用 PyArmor 保护") + + # 验证运行时存储 + runtimes = importer.list_runtimes() + assert len(runtimes) > 0, "CLI 打包后应有运行时文件存储在数据库中" + print(f"运行时已存储: {[r['runtime_name'] for r in runtimes]}") + + finally: + sys.argv = old_argv + shutil.rmtree(tmpdir, ignore_errors=True) + + print("测试 4 通过!\n") + return True + except Exception as e: + print(f"ERROR: {e}") + import traceback + + traceback.print_exc() + return False + + +def cleanup(): + """清理测试文件""" + if os.path.exists(TEST_DB): + os.remove(TEST_DB) + print(f"已清理测试数据库: {TEST_DB}") + + +if __name__ == "__main__": + print("\n" + "=" * 60) + print("PyArmor 集成测试套件") + print("=" * 60 + "\n") + + tests = [ + ("PyArmor 混淆功能", test_pyarmor_obfuscate), + ("PyArmor 打包到 SQLite", test_pack_with_pyarmor), + ("从 SQLite 导入 PyArmor 模块", test_import_pyarmor_module), + ("CLI --pyarmor 参数", test_cli_with_pyarmor), + ] + + passed = 0 + failed = 0 + + for name, func in tests: + print(f"\n>>> 运行测试: {name}") + try: + if func(): + passed += 1 + else: + failed += 1 + except Exception as e: + print(f"测试异常: {e}") + import traceback + + traceback.print_exc() + failed += 1 + + cleanup() + + print("\n" + "=" * 60) + print(f"测试完成: {passed} 通过, {failed} 失败") + print("=" * 60) + + sys.exit(1 if failed > 0 else 0) diff --git a/scripts/test_output/pyarmor_runtime_000000/__init__.py b/scripts/test_output/pyarmor_runtime_000000/__init__.py new file mode 100644 index 0000000..e0eb39b --- /dev/null +++ b/scripts/test_output/pyarmor_runtime_000000/__init__.py @@ -0,0 +1,2 @@ +# Pyarmor 9.2.4 (trial), 000000, 2026-05-04T17:52:33.319452 +from .pyarmor_runtime import __pyarmor__ diff --git a/scripts/test_output/pyarmor_runtime_000000/pyarmor_runtime.pyd b/scripts/test_output/pyarmor_runtime_000000/pyarmor_runtime.pyd new file mode 100644 index 0000000..8a21ac1 Binary files /dev/null and b/scripts/test_output/pyarmor_runtime_000000/pyarmor_runtime.pyd differ diff --git a/scripts/test_output/test_pyarmor.py b/scripts/test_output/test_pyarmor.py new file mode 100644 index 0000000..2ee659c --- /dev/null +++ b/scripts/test_output/test_pyarmor.py @@ -0,0 +1,3 @@ +# Pyarmor 9.2.4 (trial), 000000, non-profits, 2026-05-04T17:52:33.361906 +from pyarmor_runtime_000000 import __pyarmor__ +__pyarmor__(__name__, __file__, b'PY000000\x00\x03\x0c\x00\xcb\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00$\r\x00\x00\x12\t\x04\x00\xfd\xdd\x01\xac|\xe4\xb5\xc5\xb1\x8d>\x8aPK"O\x00\x00\x00\x00\x00\x00\x00\x00\xeb\xcd\xb7y\x06\x1d\x85\x87\xb3\xe6\x16q\xb5\xaf\'L\x87\x1fk\x02\\!g6\xe7Y\x08\xec\xcc\xd8F\xb5g\xd9\xfa\xe8|Y\x86s\x1b\x15\xbfzxd\x8b\xee\x9f\xb99\x1dN\xdf\xc4!+\xac+=Cp\xf3\\\xa7\xbcu\xe7\xa4Q\xb5\xf9\xbbw\x90\x0c\xc2f\x98T\xcfR\x13x\x0e\xfa\xc6\xe3\xc8&\x0e\xf0\xcf\x99\x03\xd9\xc2\x0b\x1d\xc1\xc3\xe5\xa5\xa8\xa5\xe7\x8f\xb9V\x90\xc4\xcc\x0c\xef\xe2T\x0eyAf0\x9da\x1f8\x9a\x92Y\x8f\xea\xf7\x80\xe5\xd2b{\xe6uJ7\xa3\x17A\xd4\x84"\xe2A\x03\\\xbc %\xa4 \xe7,\xccxEf{\x10\x85\x88\xa0\xb2\xbaK\x10\xfbZO\x8f}!:n\xc8\x80\x9b\x8d8\xa9;\x9b\x93\x01\\-\xce\xc3\x10\x0eX\x046^1\xecp\x1a}\xc1\'\xc1*\xf0:}!\x13p\xf4<\xfb\x8cz\x88c\x14\xb6C\xd4\x8e4\r\xf0=\xd8\xf4\xb4\x87Ih)\x90\xc7\x98\x8ck\xdcS\x08\xa2\x18O?t\xc7;\x0fB\xab\xe9#\xa1B}Q2\xad) w\xca\x91\xa8\xdd\x15\xfa\xd6\xef\xa1\x90\xb7\x1c\x7f\x9d\x8b\x834\xd1:\xb1q\x19\x0b\xf9\n+\xfe\xc2\x9d\x8d\xd2\xe7\xae(\x9bk\xe5<^\xa6m\x8a\xac\xa4\xe6\x95\xc8x!\xfd\x9az\xb1\xe5aT\x15y\xf6\xa7\xc3\xd6K\xe8\x0cK\xbb\x85}o1\xa1\xaa\xc8z\xb85\xc2\x11\x92\xe6\xfe\xe8\x92\x08\x94\x82\xab\xe4A\xc8\x9cg\xe9\xb6&\x8aR\x7f\x08w\x17\xd8\xca\xd6I\xd9\xf7\xdcP\xe7\x8a\xd5\xdb\xe4\xae <\xf7\x1a`\xa0/\x9c\xd7QH\x0b?B\x8a\xf6\x8fp\xfd2\n\xcc\xdd\x9fG\x008k\xe7H\xe3N\xef\x1f\xee7\xda-s\xc4\xfa\xdd\xdb\x9eCTB\x7fc\xc6\x0cA1\xaf\xe7dl`\xe2\xc1\x83\xb217\xf4\xbc\xc8\x98\xb7}7\x87\xf5\xed\xff]\xf8f\x7f\xb1\xda\xd4\xdfM\xc7\xd4\xd5Tg<\xec2\xadR\xccm\xaf\xae\xdf?\xe3#\xc8\x0c\xf2\n\x935\x11QZ\xb5\x9bM#z\x9a\nds\x0fg\xd7\x03\x9f=\xbe{\xb5\xd6\x19_\xbe\'\x17\xd7\xd7\xb5\xd8s,\x0c\xafh\x0f4a\x98\xd9\xf41\xff\xf6\x06\xc1\xc1z\xfcmlm\nV\xc2\xd7\xc1v\xa0\xbaR\xfb\xd7T4\xb0\x0e\x99]\xe2\x90\x01\xb3\xdax\x16\xd7\xcf\x04^#\x88\x9c\xb9\xf2\x18\xe1\xb0\xcc\x1as\'\xd4\xcf\xd5{\x04.\xcf\x99\xd5\x92)\x07f\xa1\x95H\x93\x95C#\x92 \xe1A\xb2\xe1\x8c\xa0\xb0\x95\x91\xafNN6\xce\xa2\xb7l:\xcc)\xc9\xab\xcd\x19\xf7#\x1e\x86\x8f\xae\xd5R\xf2\xc76\xbeM9\xb4D\x18t\x0f(k\x15)\xd0\x8d\xc2\x1fPGr2t\xebR\xcfj\xc8\x13\xa5hFE\x073\xb4\xab\xb3\xf9\xb8\x15\xd7>\x8c\x0e\xe1G\xacD}\xbf\xe5#H}\xf1\xba\x0e\x8c\xc1\x9a1@\xf8\xb4\xc5Fy\xc9G\x0b\x8eY\xccBa\x04\xdbx6m\xca\xf8x\xdf;K\x11GF\x8c\xd1\xc5V1\xca\xdf\xd3\xe9\x1e\xd9]\xa0\xc8!Q;\xf0\xbd\x82\x97\x1b\xb7\xc6\xa6\x1b\x86W\x82\x9f\xf7\x12R\xc5\x81A\x8b\xf6\x06M\xbd\x88\xa6\xc5\xad\x94\t7\x9c\xaaZ\xc8\n\x9b\xa8&\xe6\xf9d\xb8\x86\x85\xc0E\xc4s\x8e\xa7\xd1\xd1\xe2\xc6.\x98\x8e\xd7Y\xbct!#e 3\xb9(\x1e!\x98l\xf4\xfd\x05\xf8\\\xa9\xc2\xfd\x0f\xf4\x98\x11\xed\x14\x0f\xe9\x8a\xb3\xc55r\\I\xd5*\xdf4\x18\x00\x07\xee\x8ci\x9e\x92A\xfb\x85\x95+\xa114j\x1d"Zb\xf4YA\xa4A~\xb8R\xe1\xb0\xccyv\xba\x8c\xb1"{\xd3\xd4[\x96N\xf0\xf2TNdw\xecW\xe1.\xb4\xd5\xabY\x95t\xe9\x90A\x18$(\xbd\'\xdc)\x1dtTPT\xcc\xb8\xb3\xce\xac\x92\x92\xf9iI>U\xd9N\xfa%\n\xcc\xf5\xf5\xf2\x05\x11p\xba\x8c\xe7\xf47\x9e,yZ_\x8d\x1du\x15\xfb\x9c\xc3\xd2\xde\x82i>\x1f\xa1\xd9\xff\x89\xfb\x81\x80\x8c\x89V|\xbd\x90\xa1pV{\xf1\xd5\xcf2r\xdf&\x13\xc5\x1eMD\xfdt\'arw\x8e\xe1O\xc9\x84\x84 \x95O"t\x94\xce\x81\x1a*\xf9ju\x82\xde\x8c\x02\x0bs\xc3\xd0*~Zx\x9f1\x95\x99\xda\xea\xfd%h\x12-\xa0t\xff-?\xeb&9g\xad\x8d5\x7f\x83\xb5\xc4\x018\xd2A\x8f\x08o;\xc0\x91\xebf\xe0@\xce\xc1\xa7\x0f\xce\x96\n<\xd8\x17o\x9d\x058\xfe`\x10\xb9%U6\xdf\x97r\x05g\x16\xa4UU}%\x12\'7\xac\xbbWV\x80\xa6z\x11R\x19\xc8\xdcv\xe6\x1b?\n|\xe7\x15\t]ak\xa8\xc6\xc4\x93)\xa8<\x12M\xfe\xfe\x07m\xc8Y\xe4\xe1\x89/\xecv~N\x06\xfb^\x8dg(\xcf\xe6\xeay\x19\xcb\'\xe9\x0e5\x85F>\'\xfbz\x92>d\\:1h\xb2}a\x1bkJS\xe2\xa6p\xfb\xbb\xde\x9ao\xf7:\xb8\xf5Q\xf5,\xe5n\xd6;,\xd00\xf0#\xe4\x8a\xba:Y:\xc3\x10\x16\xa8\xb6\xbas\xcc\xc9\x15\xf0\xb3A\xec\x1fH?\xe24\xf1\xd0,4\xb4o\xa4\xb0\'M`\xa3\x00\xd8\x0c\xd4\xfa\x15\xea\x8a\x87\xb1\x87V\x015\xa0|s\x7fyies\x0b\x03\x83\xf0\x084\x10\xae\x8c\xf4\x19O\x17p\xb6\x12s\x8a\xc1^\xd3q$\xce\x9a\xab<\x14\xe2\xbd\xb4%\x99=3\xf4Q\xa65\xb1\x94f\xc4q\xdd\xe6\xfe\xb7\x13r{\xb6{5\x14i\xa1E\xa5$B\xc87\xb4\xabf\x90P\xc8$\x9fC\xdd\x0b\x8c+\xbb\xd5\x8f\xd2:\x18\xa38\x103.ZN\xef\x1f+?\xadp\xcdM2\xbfe\xa0\xdb\x9f\xf2\xf64B+\xa6&\x89\x04\xce\xa6#?\x8d\xc9o\x84Y\xbe\xd2\xc1w\xe0\xeb(!\x8e\xb8\xa7\x86%\xe7\xac\x862\xa7\xba\xc3w\x8f\xc9\x05\xa2s\x82\xb7\xe9\xf1ED}\xd2OZ-aW\xc2\x9aK\x1d\xd4v\x17\x89\x19\xa5}\x025*\t~\xf7\x87qJQ\xe7H_4\x91\x17\x1c\xcf\x06\xbb\\.\xe6\xa8\x95/ \x02\x9e6\xd4\x93e\x833\xf6\x8f\xbdj\xc2\xa6,\xe0\xd3\xd0\xa6\xf6\x04{5}R\xb5\xbd\x8b\xe8\x0eN\xbf\x8d:\xac_\xc4>\x05u\x99\xe5\x7f\xc0\x89\xeaES`\xde\xb2\xf6q\xc0a\xb2\x0f{#|.\x19X\x95\xde6\xbc\x16\x9c{8\x8a\xfe8X\xf3G\xb5\x8f\xf2Y\xe4\xaf"[o\xfb\xe5<6\xa2\xa22\x06\'\xeb\xc8\xb5\xcb\xf1]\xa8m\xa4\x9b0\xb6\xda+\xaem\xabwg\x7f\x8d\xf0dr\x99\xd42\xb9GSe\x96\xb0\xb8k\xb0\x19\x17Y\n!A8\xb8\x03\xbdG@yJ\xc5\xe8\xdf\xe8ZU\xf3\xf6\xfe\xe6\x11\x8eU\xe5r\x7fEuI\xdeWy\xaeW\x89\x81;\xe29\xce^y\xab%\xf2\xfa\xc9\x06\xb4\xed\xda}\x8fM\x15\xeb\x17\xe9\xfbT\xda\xa4\xbc\xa0\xfa\x7f_\xc38\x85\xce}\xb5\xaa\x01\xc1*\x8c\xdbC\xf0Cp-\xec\xc6\xab&v_\xc1(w\x10\xf0\xef\xc6WD\x18\x87\xb8\xf6I|\x06\x1e\xf6\x1b\x9e\x1c!\xee\xec\xc7;\xdd\xf3\xb6~e\xfb}\xb2\xf8\x15\\L<\n\xd4\xd2\xdcQ\xcc\xa0\xd6\xa2\xc2n\x8en\xdb\xcd\xb5\xdb\xb3W\xaat\x197bP&\xd6\xdf>~\xf9\x8d\xd3\x84h%\xcd\x91\x8a4\x97\xd2o\xbc\xf9G8\xce\x0ep\xa1\xdfk94\'\xee\xf3\xa4t\x9d\xef\xe4\xde\xde\xd4\xa1\xe3\x95q\xfd\xc8\xd3\xa1\xdc\x82\xdc\xf2\xf1\xb9\xe5I\x97\x1d\x01C\xaf?&\xa4\xf1\xe3\x8fH\xb0\xe55U\x0e\x8a]\xa4\xf6}+\x07\xf2t\xed"\xb9\xfa|i\xcb\xcf\x0e^z\xe4\xb7\t\xd9\x05\x90\x19\xc5\xa3\xe9^\x9b\xcc\xccC\xeb&Z\x16r\x94`AQ\x98I\x08\x03\xf7?\xabA\x8f\xb26;\xd9\x8aD\x1a\xe5\x1d\x92\xd2\xb4b.\x83\xa9,\x0ca\xa6\xbdw\x13\xd1\x15\x00P\xb0G\xc4\xff\x0e\xf9g\xebf\xa20H\xec\x82Q\xcb\x9d\xd6\xb2\x02\xa9M\xd2?\x08\xa3\xb98\xcax\x8d\x1fN^\x95d\xc3o\x9f\x19\xc1y\xa5\x8a4)he#b\x89~\xe9\xda\x13\x9e\xf3\xf7%\xf7^\xa2@w\x9b@\xc2-\x9eA_\xd6\xce\x88\xf1\xfe\xc3\xd8TT\xafh\xce=y\xa1\x068b\xb8K\x05\x04\xc3\x91\xa9a\x8c_H\xba\xa03G.\x82m-\xb3\x9fo\x19 \x8f\x82\n\xb4\x8e\xc6\xe2!m=\xb4_3:\x12\xce\x8bF\xc1\x8f\xb8^\xaaZsZy\x8f\x8a\xca\xec\xf1u\xa0\xa3=\x9b\x067\xff\xe4\xe2k-\xcb\xe0:>\x18\xc1>\x9b\xa3\xc5]\x84$\xcd\x98\x85\x16\xec\xe2\xaaj\xdd\x08*\nK\xdc\xae-V\xc38\x9b\xec\x01\xa9*/\x9f/\xf2L\x90+\xbe\x1c\xbf=\xe8\x8c\xa7#\xcc\x0fF\xe1cWF\xfa\xd7\x7f\xe8%7\x1b\xaea\x0b\x81\x96=x\x97\x98\xd5\x03\x01\x8a\xd5@\x8b\x1d&\xd3w\x83\x19\xb1\xbe\xe1\n\xa7j\xc6`C\xca\xb7{\x87\x9a\xf2\x88\xe1G\xc3\xa8b\x88\x03\xc6\x04\xca\xa6\x01\x1b\xa9\x03\x81?tRD\xb8\x11Q\xd8^0prF\xecp\x0f\x154\n\xabv\xbe\xab\xde\xde[\x01Wq\xf7\x88A\x92S\xcd\xe7O\xa3y;0\xc2I^T\xd0Z\x7f(\xa6J\xd9\x0f\xae\x9e\x19\xada\xd1\xceu\xc1X\xb0\xd9\xb5ir\xf3K\xc3 \x9c\xf8\xae\x94\xa5\x95n\xbd\xe3o\xc5T\x19\xb0\x83u9\xef\x07\x8c\xb2$\x86\xc1\xb5H\xfcO\x0f\xed\xcc\xe4>\xdao\xf3%\x80\xed,!\xe1k\'\xe9\xd4\xe9:\x00\xc2\xcb]\x99\xde\xab\x81\xc5\xe6{d\xd1\xea\x8a\xe0b\xbd(6#o`\x93\x14!\x18\x8f\x86\x1c1\\8\x18u\x8c\xfd?\x0f\xd8\x04\x14\x08t$\xb4\x0c\xd0\x00F\x90u\x93\xbcU\x80\x1f\x98cv\xdcz\xdf\xad\x95\x94j\xbb"r\x0b\xdc\xeb\xff\xe3\xcfJdx\xca5\xaa\xf9\x06[\xfb\xb5m_\x84\x13\x17{X\xfc\x8f\xdeR\xd5\xd3\xe6q:\xa9\x01\x10i\xd04r\x18\x97\xc9\xd3$\xbc\x04\xc7\xb3s4\x0c\xd2\xbdJ\x15\x03?>\xc32\xe30\xd4\xe8le3\x94\xc3]\x17!A\x083\xb7\xb4\x19%\xd1\x83\x9af\'\xd9@\xc05\x9bA\x88\x8cH_o\xbeB\x88KgsO\xce$;k\xa5\x1e,r\x99\xd4\xc4\xff\x86\xaf\x06\xd2\x87\xee\xfa>\xc4z\x8d\xcfa\xbetw\x14Lk2\xc5,\xd2\xbd\xcd\x83 mN\x16\x06\xde3\x02\tn(\xbe\xa7\x8a\xbe\xc4\xd9\xf6\x82\xd0\xa4h\xa7{B\x0ed=\xe1\xdc\xa6]s\xa4\xe5\xde\xf9\xee;!\x00~\x95\xb9r\xd6\xa0\x10\xdcD9\xf1\xd1V$\x9a\x16\xa2\x83\x98\xb4\xb6\xbc\x9a\xb8\x86\xbcW\x11z\xae\x9b\xfb\xa4\xdf\xf8$\x8d\xc4!\xe0\x02\xe03IX\xf2\x1c\x04\xab]\x10\x7f\xd7\xa27\x08\x8d\x18\x90\xf6\x17u\x1b\x0e\xd1\x89\xa9s\x8e\xe6I\xae8r\xff\x0f-\xf8\xd7\xc6\xa1\xc3\x151\xfaIK\x85\x89\xed$\xa5\xcf\x02\xc4\xfaa\x9c\x1f\xa2\xf6i\\\x89d\x9f\x0b\xa0\xdcA\x80"\xac\x14\xe5\r\xf7@\xa6\x81\x85\xc6\xaf\x9b\xbe\x7f\xd9\x8d\x18\x1a>xC71\xa9\x84\xaa\xf8\xfd\x14\xfdI\xce\x81\x91"p(\x0cw\xff\xb8[\x8f\xfe\xcb\x95:\xdc]-\x1d\xadt\xcb\x81\xe0\xe5uZ\xa9\x03\x96\xba\x97u\x06\xe9\xb8_c\x95\x9c\xb7o\xa0\x8dN$\xb3\x00\xb1\x85t\x0f\x80~C!\xf9\x01\xb0\xc9\xc5\xcc\x14\xd6Q\xadZ\x93\xc3\xa3C\xf3\x0e\xfe\xc6\xa67Q}\xfe\x1c\'\x87\x12m\x16.\x14\xaf\xa8\x83\xe1C\x996\x1b\xf8wZ\xcb\xf7\x08\x02b\xcfc\xd5v\xab6\xc7\'\xff\x9c\xc9=t\x97\xe0!\xb9\xfe\xedj\xde\xec\x85\xe8\xc7Z\t*\xfb\xed\x8d\x1d\x98u\xaa\xae\xe5\r,\xae9&p\xdby\xb4\x96g\x11\xfdS\xcb\xfdn{\x0b\xf4\xccs\x9a\xb2\xa6\xcb\xe6\x83\x96\xd2\xac\xf8\'\xcb\xae\n\x84>\xd5\xbf+H$>\x93\xbf\x19\xd9\x00\x04\xeb\xed\xf4\'}\xb0L\xd8\xa9\xb0\xc5\x19\xe4\x17\x10\xbb\xbaP-s;\x8b@V\xbe\x13\xb7R9\xef|6B\xe4\xfd8\x13\'0A\xf0_\xb0\xfe\xf0J\x82\x0cV\x19\xae$\x8d\xa5\x11\t\x04\x17\xdfi\xe6Z`\xaen\xde\x96d\xe7\x8d\x1bO\x86\x05\xae2\x02\xeb/\x83?\xf5\xec\xac\xb9<\xd2\xca\x04\xa1\xad\xc1T\x15*\xdb\x88\xbc\xb9\x9bfl7I\xc0\xb3\xf3#\xdc\x90%*\x84\xd2\xce\xd3L(\xa9"\x0b*\'\xc0\x8fH\xd0\xbe\xbc\x88\x8f\x0e_\xb2\xab\x9d\x08\xa2\xd5B\xb2\x01\xda\xcf\xed\x03\x0e\xed\xe8\xa2\x82\x8f\xa02\x8c\x83\\\xdd\x9b\x96\x95\x9e\x9a\x9c\x14Lw\x9e\n\x15\xef\xaal\xb2\x9eQ\xfa@\x87&\x1f&\xf9\xc7_\xe5,\xce3\xf7\x0e\xdd\xffD\x8f1\xb4\xdf?I\xeb\xc0k\xee:\x10\x0e\xb4d\x08J\xbf\x9d\xe4\x854\x027%\x06\xcac2\x9b\xc1\xba\xc4\xa3m\xa1gs|\xcb\xd3*\xedM\n\xed\xf3,Z\xe2 \x03\x0b\xc64\x12\x13\x88B/~\x94bj\xcd\tR\xc0,\x05h\x04\x11\xee-\x90%\xcc\xb4\xb4\xbb\xf0\x16\xbc\x90\xa8B\x15\xbfe\x9e\xb0m~t\xce\x1a\xaf\xfdh\x878\x112O\x0c\n<<\xf6f\xb6\xd5m\xef\x8a.\x99\x1f\x9f\x89\xdd\xde\x06\x9f\x97\x9a\x12Q\x06\x81\x02\xad\xdf\xb9\xf2Byd\x11\x81\xc7\x82\xb0t\'C\xa9\xe7\xe0p\xe4\xb1\xac\xff\xe9\xddL\xf0G\xe3\xc3') diff --git a/scripts/test_pyarmor.py b/scripts/test_pyarmor.py new file mode 100644 index 0000000..f2280e6 --- /dev/null +++ b/scripts/test_pyarmor.py @@ -0,0 +1,53 @@ +"""测试 PyArmor 加密功能""" + +import tempfile +import os +import sys +import shutil + +# 确保能导入项目模块 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + + +def test_pyarmor_encrypt(): + """测试 PyArmor encrypt_script 功能""" + import pyarmor.pyarmor as pa + + tmpdir = tempfile.mkdtemp() + try: + src = os.path.join(tmpdir, "test_mod.py") + with open(src, "w", encoding="utf-8") as f: + f.write('def hello():\n print("Hello from PyArmor!")\n') + + dest = os.path.join(tmpdir, "dist", "test_mod.py") + os.makedirs(os.path.dirname(dest), exist_ok=True) + + # Check capsule + capsule = pa.DEFAULT_CAPSULE + print(f"Capsule: {capsule}") + print(f"Capsule exists: {os.path.exists(capsule)}") + + # Try encrypt + result = pa.encrypt_script( + pubkey=capsule, + filename=src, + destname=dest, + ) + print(f"Result: {result}") + + # Check output + if os.path.exists(dest): + with open(dest, "r", encoding="utf-8") as f: + content = f.read() + print(f"Output length: {len(content)} bytes") + print(f"First 300 chars:\n{content[:300]}") + print(f"\n--- Full content ({len(content)} bytes) ---") + print(content) + else: + print(f"ERROR: Output file not created at {dest}") + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +if __name__ == "__main__": + test_pyarmor_encrypt() diff --git a/src/module_bank/__init__.py b/src/module_bank/__init__.py index 2bbef38..b1478ef 100644 --- a/src/module_bank/__init__.py +++ b/src/module_bank/__init__.py @@ -2,10 +2,12 @@ from .python_to_sqlite import PythonToSQLite from .sqlite_module_importer import SQLiteModuleImporter from .sqlite_module_loader import SQLiteModuleLoader from .sqlite_meta_path_finder import SQLiteMetaPathFinder +from .pyarmor_encryption import PyArmorEncryption __all__ = [ "PythonToSQLite", "SQLiteModuleImporter", "SQLiteModuleLoader", "SQLiteMetaPathFinder", + "PyArmorEncryption", ] diff --git a/src/module_bank/cli.py b/src/module_bank/cli.py index 258d2fd..dc20bf0 100644 --- a/src/module_bank/cli.py +++ b/src/module_bank/cli.py @@ -14,6 +14,25 @@ def main(): pack_parser.add_argument( "--key", help="加密密钥(Fernet格式,32字节base64),若不提供则不加密" ) + pack_parser.add_argument( + "--pyarmor", + action="store_true", + help="使用 PyArmor 混淆保护源代码", + ) + pack_parser.add_argument( + "--pyarmor-obf-code", + type=int, + default=1, + choices=[0, 1], + help="PyArmor 代码混淆级别 (0=禁用, 1=启用, 默认: 1)", + ) + pack_parser.add_argument( + "--pyarmor-obf-mod", + type=int, + default=1, + choices=[0, 1], + help="PyArmor 模块混淆级别 (0=禁用, 1=启用, 默认: 1)", + ) # 列出命令 list_parser = subparsers.add_parser("list", help="列出数据库中的模块") @@ -40,7 +59,13 @@ def main(): if args.command == "pack": from .python_to_sqlite import PythonToSQLite - packer = PythonToSQLite(args.db, key=args.key) + packer = PythonToSQLite( + args.db, + key=args.key, + pyarmor_obfuscate=args.pyarmor, + pyarmor_obf_code=args.pyarmor_obf_code, + pyarmor_obf_mod=args.pyarmor_obf_mod, + ) if Path(args.source).is_dir(): packer.pack_directory(args.source) @@ -49,6 +74,17 @@ def main(): packer.pack_module(args.source, args.name) print(f"已打包模块: {args.source}") + # 如果启用了 PyArmor,显示运行时信息 + if args.pyarmor: + from .sqlite_module_importer import SQLiteModuleImporter + + importer = SQLiteModuleImporter(args.db) + runtimes = importer.list_runtimes() + if runtimes: + print(f"\nPyArmor 运行时信息:") + for rt in runtimes: + print(f" - {rt['runtime_name']} (创建于: {rt['created_at']})") + elif args.command == "list": from .python_to_sqlite import PythonToSQLite diff --git a/src/module_bank/pyarmor_encryption.py b/src/module_bank/pyarmor_encryption.py new file mode 100644 index 0000000..9feb726 --- /dev/null +++ b/src/module_bank/pyarmor_encryption.py @@ -0,0 +1,202 @@ +""" +PyArmor 加密支持模块 + +为 module_bank 提供 PyArmor 代码混淆/加密能力。 +使用 PyArmor CLI 对 Python 源代码进行混淆保护, +并将混淆后的源代码和运行时文件存储到 SQLite 数据库中。 +""" + +import os +import sys +import tempfile +import shutil +import subprocess +import re +from pathlib import Path +from typing import Optional, Tuple + + +class PyArmorEncryption: + """PyArmor 加密处理器,用于混淆/保护 Python 源代码""" + + def __init__(self, key: Optional[str] = None, obf_code: int = 1, obf_mod: int = 1): + """ + 初始化 PyArmor 加密处理器 + + Args: + key: 加密密钥(可选,PyArmor 不使用此参数,保留接口兼容性) + obf_code: 代码混淆级别 (0/1) + obf_mod: 模块混淆级别 (0/1) + """ + self.key = key + self.obf_code = obf_code + self.obf_mod = obf_mod + + def obfuscate_source( + self, source_code: str, module_name: str + ) -> Tuple[str, Optional[dict]]: + """ + 使用 PyArmor 混淆源代码 + + Args: + source_code: 原始 Python 源代码 + module_name: 模块名 + + Returns: + (混淆后的源代码, PyArmor 运行时数据字典) + 运行时数据字典包含: { + "runtime_name": "pyarmor_runtime_000000", + "pyd_data": bytes, + "init_data": str, + } + 为 None 表示没有运行时文件 + """ + if not self.check_pyarmor_available(): + raise ImportError( + "PyArmor 未安装。请使用 'uv add pyarmor' 或 'pip install pyarmor' 安装。" + ) + + temp_dir = tempfile.mkdtemp(prefix=f"pyarmor_pack_") + try: + # 创建临时源文件 + src_filename = f"{module_name.replace('.', '_')}.py" + src_file = os.path.join(temp_dir, src_filename) + with open(src_file, "w", encoding="utf-8") as f: + f.write(source_code) + + # 输出目录 + output_dir = os.path.join(temp_dir, "dist") + os.makedirs(output_dir, exist_ok=True) + + # 调用 pyarmor gen 命令进行混淆 + cmd = [ + sys.executable, + "-m", + "pyarmor.cli", + "gen", + "-O", + output_dir, + "-i", + src_file, + ] + + # 添加混淆级别参数 + if self.obf_code == 0: + cmd.extend(["--obf-code", "0"]) + if self.obf_mod == 0: + cmd.extend(["--obf-mod", "0"]) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=temp_dir, + ) + + if result.returncode != 0: + raise RuntimeError(f"PyArmor 混淆失败:\n{result.stderr}") + + # 读取混淆后的代码 + obfuscated_file = os.path.join(output_dir, src_filename) + if not os.path.exists(obfuscated_file): + raise RuntimeError(f"PyArmor 未生成输出文件: {obfuscated_file}") + + with open(obfuscated_file, "r", encoding="utf-8") as f: + obfuscated_code = f.read() + + # 查找并读取运行时文件 + runtime_data = self._find_and_read_runtime(output_dir) + + return obfuscated_code, runtime_data + + except Exception as e: + if isinstance(e, RuntimeError): + raise + raise RuntimeError(f"PyArmor 混淆失败 ({module_name}): {e}") from e + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + def _find_and_read_runtime(self, output_dir: str) -> Optional[dict]: + """ + 查找并读取 PyArmor 运行时文件 + + Args: + output_dir: PyArmor 输出目录 + + Returns: + 包含运行时文件的字典: { + "runtime_name": "pyarmor_runtime_000000", + "pyd_data": bytes, # 原生二进制文件 + "init_data": str, # __init__.py 内容 + } + 如果没有找到则返回 None + """ + output_path = Path(output_dir) + # 查找 pyarmor_runtime_* 目录 + runtime_dirs = list(output_path.glob("pyarmor_runtime_*")) + if not runtime_dirs: + return None + + runtime_dir = runtime_dirs[0] + runtime_name = runtime_dir.name + + result = {"runtime_name": runtime_name, "pyd_data": None, "init_data": None} + + # 查找 .pyd 或 .so 文件 + runtime_files = list(runtime_dir.glob("pyarmor_runtime.*")) + if runtime_files: + with open(runtime_files[0], "rb") as f: + result["pyd_data"] = f.read() + + # 读取 __init__.py + init_file = runtime_dir / "__init__.py" + if init_file.exists(): + with open(init_file, "r", encoding="utf-8") as f: + result["init_data"] = f.read() + + return result + + @staticmethod + def extract_runtime_info(obfuscated_code: str) -> Optional[str]: + """ + 从混淆后的代码中提取运行时模块名 + + Args: + obfuscated_code: 混淆后的源代码 + + Returns: + 运行时模块名,如 'pyarmor_runtime_000000' + """ + match = re.search( + r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__", + obfuscated_code, + ) + return match.group(1) if match else None + + @staticmethod + def is_pyarmor_protected(source_code: str) -> bool: + """ + 检测源代码是否已被 PyArmor 保护 + + Args: + source_code: 要检测的源代码 + + Returns: + 是否为 PyArmor 保护的代码 + """ + markers = [ + "pyarmor_runtime", + "__pyarmor__", + "from pyarmor_runtime", + ] + return any(marker in source_code for marker in markers) + + @staticmethod + def check_pyarmor_available() -> bool: + """检查 PyArmor 是否可用""" + try: + import pyarmor # noqa: F401 + + return True + except ImportError: + return False diff --git a/src/module_bank/python_to_sqlite.py b/src/module_bank/python_to_sqlite.py index 57fd275..fcebd0d 100644 --- a/src/module_bank/python_to_sqlite.py +++ b/src/module_bank/python_to_sqlite.py @@ -8,10 +8,26 @@ from .sqlite_meta_path_finder import SQLiteMetaPathFinder class PythonToSQLite: """Python到SQLite的打包工具""" - def __init__(self, db_path: str = "python_modules.db", key: Optional[str] = None): + def __init__( + self, + db_path: str = "python_modules.db", + key: Optional[str] = None, + pyarmor_obfuscate: bool = False, + pyarmor_obf_code: int = 1, + pyarmor_obf_mod: int = 1, + ): self.db_path = db_path self.key = key - self.importer = SQLiteModuleImporter(db_path, key) + self.pyarmor_obfuscate = pyarmor_obfuscate + self.pyarmor_obf_code = pyarmor_obf_code + self.pyarmor_obf_mod = pyarmor_obf_mod + self.importer = SQLiteModuleImporter( + db_path, + key, + pyarmor_obfuscate=pyarmor_obfuscate, + pyarmor_obf_code=pyarmor_obf_code, + pyarmor_obf_mod=pyarmor_obf_mod, + ) def pack_directory( self, @@ -146,12 +162,13 @@ class PythonToSQLite: (module_name, source_code, bytecode, is_package, metadata, encrypted) VALUES (?, ?, ?, ?, ?, ?) """, - ("key", key, None, None, None, None), + ("key", None, key, None, None, None), ) self.importer.connection.commit() def get_key(self): cursor = self.importer.connection.execute(""" - SELECT source_code FROM python_modules WHERE module_name = 'key' + SELECT bytecode FROM python_modules WHERE module_name = 'key' """) - return cursor.fetchone() + row = cursor.fetchone() + return row["bytecode"] if row else None diff --git a/src/module_bank/sqlite_module_importer.py b/src/module_bank/sqlite_module_importer.py index c2033dd..004bd92 100644 --- a/src/module_bank/sqlite_module_importer.py +++ b/src/module_bank/sqlite_module_importer.py @@ -1,25 +1,37 @@ import sqlite3 import marshal import base64 +import os +import json from pathlib import Path -from typing import Dict, Optional +from typing import Dict, Optional, Tuple from .encryption import Encryption class SQLiteModuleImporter: """从SQLite数据库导入Python模块的导入器""" - def __init__(self, db_path: str, key: Optional[str] = None): + def __init__( + self, + db_path: str, + key: Optional[str] = None, + pyarmor_obfuscate: bool = False, + pyarmor_obf_code: int = 1, + pyarmor_obf_mod: int = 1, + ): self.db_path = db_path self.connection = sqlite3.connect(db_path) self.connection.row_factory = sqlite3.Row self.encryption = Encryption(key) + self.pyarmor_obfuscate = pyarmor_obfuscate + self.pyarmor_obf_code = pyarmor_obf_code + self.pyarmor_obf_mod = pyarmor_obf_mod + self._pyarmor_enc = None self._create_tables() def _create_tables(self): """创建存储模块的数据库表""" - self.connection.execute( - """ + self.connection.execute(""" CREATE TABLE IF NOT EXISTS python_modules ( module_name TEXT PRIMARY KEY, source_code TEXT, @@ -28,17 +40,46 @@ class SQLiteModuleImporter: metadata TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) - """ - ) + """) try: self.connection.execute( "ALTER TABLE python_modules ADD COLUMN encrypted INTEGER DEFAULT 0" ) except sqlite3.OperationalError: - # 列可能已存在,忽略错误 pass + + # 添加 pyarmor_protected 列(用于标记 PyArmor 混淆过的模块) + try: + self.connection.execute( + "ALTER TABLE python_modules ADD COLUMN pyarmor_protected INTEGER DEFAULT 0" + ) + except sqlite3.OperationalError: + pass + + # 创建 PyArmor 运行时存储表(支持二进制文件和文本文件的存储) + self.connection.execute(""" + CREATE TABLE IF NOT EXISTS pyarmor_runtime ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + runtime_name TEXT UNIQUE NOT NULL, + pyd_data BLOB, + init_data TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) self.connection.commit() + def _get_pyarmor_enc(self): + """惰性获取 PyArmor 加密器""" + if self._pyarmor_enc is None: + from .pyarmor_encryption import PyArmorEncryption + + self._pyarmor_enc = PyArmorEncryption( + key=None, + obf_code=self.pyarmor_obf_code, + obf_mod=self.pyarmor_obf_mod, + ) + return self._pyarmor_enc + def add_module( self, module_name: str, @@ -47,6 +88,24 @@ class SQLiteModuleImporter: metadata: Dict | None = None, ): """添加模块到数据库""" + pyarmor_protected = 0 + + # 如果启用了 PyArmor 混淆,则先混淆源代码 + if self.pyarmor_obfuscate: + pyarmor_enc = self._get_pyarmor_enc() + if pyarmor_enc.check_pyarmor_available(): + obfuscated_code, runtime_dict = pyarmor_enc.obfuscate_source( + source_code, module_name + ) + source_code = obfuscated_code + pyarmor_protected = 1 + + # 存储运行时文件 + if runtime_dict is not None: + self._store_runtime(runtime_dict) + else: + print(f"警告: PyArmor 未安装,跳过模块 '{module_name}' 的 PyArmor 混淆") + # 编译为字节码 bytecode = compile(source_code, f"", "exec") bytecode_blob = marshal.dumps(bytecode) @@ -62,8 +121,8 @@ class SQLiteModuleImporter: self.connection.execute( """ INSERT OR REPLACE INTO python_modules - (module_name, source_code, bytecode, is_package, metadata, encrypted) - VALUES (?, ?, ?, ?, ?, ?) + (module_name, source_code, bytecode, is_package, metadata, encrypted, pyarmor_protected) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( module_name, @@ -72,10 +131,49 @@ class SQLiteModuleImporter: is_package, metadata_str, encrypted_flag, + pyarmor_protected, ), ) self.connection.commit() + def _store_runtime(self, runtime_dict: dict): + """存储 PyArmor 运行时文件到数据库""" + self.connection.execute( + """ + INSERT OR REPLACE INTO pyarmor_runtime + (runtime_name, pyd_data, init_data) + VALUES (?, ?, ?) + """, + ( + runtime_dict.get("runtime_name"), + runtime_dict.get("pyd_data"), + runtime_dict.get("init_data"), + ), + ) + self.connection.commit() + + def get_runtime(self, runtime_name: str) -> Optional[dict]: + """从数据库获取 PyArmor 运行时文件数据""" + cursor = self.connection.execute( + "SELECT runtime_name, pyd_data, init_data FROM pyarmor_runtime WHERE runtime_name = ?", + (runtime_name,), + ) + row = cursor.fetchone() + if row: + return { + "runtime_name": row["runtime_name"], + "pyd_data": row["pyd_data"], + "init_data": row["init_data"], + } + return None + + def list_runtimes(self): + """列出数据库中的所有 PyArmor 运行时""" + cursor = self.connection.execute( + "SELECT runtime_name, created_at FROM pyarmor_runtime ORDER BY id" + ) + return [dict(row) for row in cursor.fetchall()] + def add_module_from_file(self, file_path_str: str, module_name: str | None = None): """从文件添加模块""" file_path = Path(file_path_str) diff --git a/src/module_bank/sqlite_module_loader.py b/src/module_bank/sqlite_module_loader.py index ef70945..c3f90bf 100644 --- a/src/module_bank/sqlite_module_loader.py +++ b/src/module_bank/sqlite_module_loader.py @@ -3,6 +3,11 @@ import importlib import importlib.abc import marshal import types +import os +import sys +import tempfile +import shutil +from pathlib import Path from .encryption import Encryption @@ -21,6 +26,7 @@ class SQLiteModuleLoader(importlib.abc.Loader): self.db_path = db_path self.db_connection.row_factory = sqlite3.Row self.encryption = Encryption(key) + self._runtime_temp_dir = None def create_module(self, spec): """创建模块对象""" @@ -44,7 +50,7 @@ class SQLiteModuleLoader(importlib.abc.Loader): def exec_module(self, module): # type: ignore """执行模块代码""" cursor = self.db_connection.execute( - "SELECT bytecode, is_package, encrypted FROM python_modules WHERE module_name = ?", + "SELECT bytecode, is_package, encrypted, pyarmor_protected, source_code FROM python_modules WHERE module_name = ?", (self.module_name,), ) row = cursor.fetchone() @@ -52,6 +58,12 @@ class SQLiteModuleLoader(importlib.abc.Loader): if not row: raise ImportError(f"No module named '{self.module_name}' in database") + pyarmor_protected = row["pyarmor_protected"] + + # 如果是 PyArmor 保护的模块,通过源代码执行(需要先部署运行时) + if pyarmor_protected: + return self._exec_pyarmor_module(module, row) + # 从字节码加载 bytecode_blob = row["bytecode"] encrypted = row["encrypted"] @@ -80,3 +92,103 @@ class SQLiteModuleLoader(importlib.abc.Loader): module.__file__ = f"sqlite://{self.db_path}/{self.module_name}" return module + + def _exec_pyarmor_module(self, module, row): + """ + 执行 PyArmor 保护的模块 + + PyArmor 混淆后的代码需要在运行时导入 pyarmor_runtime_xxx 包。 + 该包包含 __init__.py 和原生 .pyd/.so 文件。 + 我们需要从数据库提取这些文件并重建包结构到临时目录。 + """ + source_code = row["source_code"] + if not source_code: + raise ImportError( + f"PyArmor protected module '{self.module_name}' has no source code available" + ) + + # 提取运行时模块名 + import re + + match = re.search( + r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__", + source_code, + ) + if not match: + # 没有找到运行时引用,直接执行源代码 + bytecode = compile( + source_code, f"", "exec" + ) + exec(bytecode, module.__dict__) + module.__file__ = f"sqlite://{self.db_path}/{self.module_name}" + return module + + runtime_name = match.group(1) + + # 从数据库获取运行时包数据 + runtime_dict = None + if self.db_path is not None: + try: + from .sqlite_module_importer import SQLiteModuleImporter + + importer = SQLiteModuleImporter(self.db_path) + runtime_dict = importer.get_runtime(runtime_name) + except Exception: + pass + + if runtime_dict: + # 创建临时目录来存放运行时包 + self._runtime_temp_dir = tempfile.mkdtemp(prefix="pyarmor_runtime_") + runtime_pkg_dir = os.path.join(self._runtime_temp_dir, runtime_name) + os.makedirs(runtime_pkg_dir, exist_ok=True) + + # 确定运行时二进制文件名(.pyd for Windows, .so for Linux/Mac) + pyd_data = runtime_dict.get("pyd_data") + if pyd_data: + if sys.platform == "win32": + so_filename = f"pyarmor_runtime.pyd" + elif sys.platform == "darwin": + so_filename = f"pyarmor_runtime.dylib" + else: + so_filename = f"pyarmor_runtime.so" + + so_path = os.path.join(runtime_pkg_dir, so_filename) + with open(so_path, "wb") as f: + f.write(pyd_data) + + # 写入 __init__.py + init_data = runtime_dict.get("init_data") + if init_data: + init_path = os.path.join(runtime_pkg_dir, "__init__.py") + with open(init_path, "w", encoding="utf-8") as f: + f.write(init_data) + + # 将临时目录添加到 sys.path,使运行时包可被导入 + if self._runtime_temp_dir not in sys.path: + sys.path.insert(0, self._runtime_temp_dir) + + # 设置 __file__ 属性(PyArmor 的 __pyarmor__() 需要它) + module_file = f"sqlite://{self.db_path}/{self.module_name}" + module.__file__ = module_file + + # 如果是包,设置__path__和__package__属性 + if row["is_package"]: + module.__path__ = [] + module.__package__ = self.module_name + + # 执行混淆后的代码 + bytecode = compile(source_code, f"", "exec") + exec(bytecode, module.__dict__) + + return module + + def cleanup_runtime(self): + """清理 PyArmor 运行时临时文件""" + if self._runtime_temp_dir and os.path.exists(self._runtime_temp_dir): + try: + if self._runtime_temp_dir in sys.path: + sys.path.remove(self._runtime_temp_dir) + shutil.rmtree(self._runtime_temp_dir, ignore_errors=True) + except Exception: + pass + self._runtime_temp_dir = None diff --git a/uv.lock b/uv.lock index b019880..cf5e8f5 100644 --- a/uv.lock +++ b/uv.lock @@ -891,11 +891,12 @@ wheels = [ [[package]] name = "module-bank" -version = "0.2.2" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" }, { name = "cryptography", version = "46.0.3", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "pyarmor" }, ] [package.dev-dependencies] @@ -906,7 +907,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "cryptography", specifier = ">=45.0.7" }] +requires-dist = [ + { name = "cryptography", specifier = ">=45.0.7" }, + { name = "pyarmor", specifier = ">=9.2.4" }, +] [package.metadata.requires-dev] dev = [{ name = "twine", specifier = ">=4.0.2" }] @@ -999,6 +1003,114 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392, upload-time = "2024-03-03T08:34:18.891Z" }, ] +[[package]] +name = "pyarmor" +version = "9.2.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "pyarmor-cli-core" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/58/6f51b2a14fe14e9a98807cee85055ffba91b3991420a884f606bbdae6b76/pyarmor-9.2.4.tar.gz", hash = "sha256:09e9b6dddb65d287c07aa428933d8760821fa05614a25c30b5595222e777d60d", size = 3012328, upload-time = "2026-03-18T02:01:38.759Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/d9/9d324a9acbe2daa37893895beeac201503d3f822196107185da16f21ed1b/pyarmor-9.2.4-py3-none-any.whl", hash = "sha256:2d7acea204739a5b390dcb9010b18ac747d646d1eac31d5f03d7b5670be11f8f", size = 3071639, upload-time = "2026-03-18T02:01:45.165Z" }, +] + +[[package]] +name = "pyarmor-cli-core" +version = "8.1.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/26/3fd196462de9c66551cc03a5def656c1a27a64148421cc5528de3429b48f/pyarmor_cli_core-8.1.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a5dc6923cc379a797d3265161cddc4db713577bdb3cfbb0f3663812cd945e4b6", size = 510402, upload-time = "2026-04-09T01:26:07.223Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/0d/2961d4ed2c8d582b1e078bda1e80689a5394955e06da915c26d222c56399/pyarmor_cli_core-8.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe0c6782b4b0b26aa1bdc68e8f4cf772198c2ea548d4568d2334fcc30fa7f34b", size = 402418, upload-time = "2026-04-09T01:26:12.769Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/a8/8c415f417299d7490151096ee78be4d97eef956e66a771b19a5161a93bb1/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:943d584627fb48975b567f442a3ac1b71473582eec17606dd18240921d52b263", size = 481568, upload-time = "2026-04-09T01:26:17.196Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/ba/fd5acb8f325304ac918f14b765988622dbf526d5eed5c17b1f5b1f1fa64f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f2a1777205bcc3737594e9fa111552ccd7877efc29ab2e2b13e4769f7eba3aff", size = 452540, upload-time = "2026-04-09T01:26:21.782Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/92/cbc1184315524e6bb4226c962c3fc11326c500d1f333a734a9fbea8f724f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d8f6d9d916cb83a1ac3256576158528d5bfb02404f324713ffe942d743a759d6", size = 598833, upload-time = "2026-04-09T01:26:26.116Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/d9/229e1fda8127e8558653b69e5245823aa16e770110897fceb712e621b41f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux2014_armv7l.whl", hash = "sha256:869ff6e51b8dd951620a22b5174d1d0795628f02c76df566d6790e7fbd68b6c9", size = 994953, upload-time = "2026-04-09T01:26:31.203Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/87/9f1849c2e44c93dbd04f685f76aaaac4731800750451d52622389747de59/pyarmor_cli_core-8.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d16c0069fec6daba35e384228e7d493ff7a83bd402bf3465527e2b8a1c1ad0", size = 643939, upload-time = "2026-04-09T01:26:36.522Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/16/4f48172435e6b91a323da5b97cda8fa77620d5e934b7ae5eb90690ecf80a/pyarmor_cli_core-8.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3dab4883b1ce7fcfa4212b94b7f5a538cc45a8a06742235caaf1b01f93b2b925", size = 439787, upload-time = "2026-04-09T01:26:41.738Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/2e/96233f232008021b1977c3a356da55af287a7f36760a28d60f7061616a1b/pyarmor_cli_core-8.1.1-cp310-cp310-win32.whl", hash = "sha256:00cc0266c245d946bdaf2e0f85047a5a4813bee2f26971d45faa833e2bf67500", size = 454720, upload-time = "2026-04-09T01:26:46.266Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/42/fa668d204c631c03157f71865514b14d0d400b7804f2ba9c4858d1379bac/pyarmor_cli_core-8.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:07584b1251aadc9540d47747bed0193131411399501a9d42ae74c7555dfb6a9e", size = 414449, upload-time = "2026-04-09T01:26:51.043Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/e3/173a37ad0d0ded7302037965c0ee747260b939a95299e9ebf0898c1e14bb/pyarmor_cli_core-8.1.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:852fd36f9854dacafa20f26a375adf0b94160cbff68d6a3b6970f5a79e7d3629", size = 511673, upload-time = "2026-04-09T01:26:57.916Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/33/2c23d8dfb7b6a5dd39899f3d298826ca02cdc231746966196666bbe33384/pyarmor_cli_core-8.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a31b7ec9a79c2286898830365d7cc7e275af4e3cc7f99e1717e622c797165fbe", size = 403721, upload-time = "2026-04-09T01:27:02.812Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/db/7aac19704813770f4acbcd6708d8afe181c21935fea1daed7621133a0fc6/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux1_i686.whl", hash = "sha256:be3911a12c318b4cd93f7ffb492f3a8517c7e7c13b48dc53ea12988ef106ebb9", size = 484036, upload-time = "2026-04-09T01:27:07.129Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/70/73a9b625901da86b2590f2fd874ea6c1efd8f5ff195763261ba7e6248253/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:8fba7ea6f2884ef78df89018bc7ca67fd3aba989dc4d3e6d88fc5e069a88ef9b", size = 451883, upload-time = "2026-04-09T01:27:12.465Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/dc/6afbee56cdfb8b1257bb9943782ce076c52bee546a532cb84541ec3179f5/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:68d1e9bc7bc811a318965483dc079ce7da670799fc5ddb8ad1d3727216c437e3", size = 599055, upload-time = "2026-04-09T01:27:17.707Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/07/39a8cc9af7aabbe0d70ea62ec14d438bba5e88a69cc1be2f0dfa95bd8676/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux2014_armv7l.whl", hash = "sha256:883c0ed2dfa19dab5a0c5edc8d8b4ad3b820491b1b4bbc4eaf5d4218a09caff5", size = 995541, upload-time = "2026-04-09T01:27:22.626Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/a4/efb2ebd451ee6bdda5c6ca359ab8a4bf7a0098cabc7d2d68d998ac9410b9/pyarmor_cli_core-8.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fedd378a0131d6df79e74e2cde4d039c7f19c14627301a7cba5a12ed200d55ab", size = 644557, upload-time = "2026-04-09T01:27:28Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/65/cc33d6423712a36aa6b133e9bb9483a2936ee44c1a40e0b99658cff45f2a/pyarmor_cli_core-8.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3dd40308a8bd0ccd8a4e7c162fd8093de6a6e1fefb5ded2825c339e58f15a996", size = 441387, upload-time = "2026-04-09T01:27:33.25Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/49/63509bc1b5d6bdb94306906a77638b4d4a2fdf6605244288990942b6ac50/pyarmor_cli_core-8.1.1-cp311-cp311-win32.whl", hash = "sha256:e5ce2e9442ff1a046cdd4a5c54c3df23640f37981877789420bf46d80943430c", size = 455036, upload-time = "2026-04-09T01:27:38.055Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/df/e0fd7f46bf412596d5cc3ffe6165e37d950b58aea4976bedc21e8c2a2419/pyarmor_cli_core-8.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d3f90b32d6b83c3bf73ff0f3d23f0045fc94671d6f467c9d50c641cea39795c", size = 414904, upload-time = "2026-04-09T01:27:42.651Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/b4/33fd5192cedc4600e9de8c9c7519205f421840da1b0df6e9491afca0a072/pyarmor_cli_core-8.1.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:4a6e09545fe74dc304992a0b002cc8d426c16ac3670d6f602d51838f080a2f31", size = 515480, upload-time = "2026-04-09T01:27:47.282Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/e5/3310cb2e29bab80bd923d7e0749c2163550effe1d1a6c6d5d95f5c58d183/pyarmor_cli_core-8.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df035854a6603d4861a50156484027a03c01a7b9385b5365cabea4871ac72e3e", size = 407703, upload-time = "2026-04-09T01:27:51.869Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/66/1aa494d360a05fd90677db8c4b8385dedd2083fe96fe6607ea555f800c87/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux1_i686.whl", hash = "sha256:ec304457ff31b4939c8527640d7f525e78e9c91cc8307a11f068ef1291710b40", size = 489280, upload-time = "2026-04-09T01:27:56.492Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/ad/5f246234ce778dd3b7bd4faa2bded71690bbcddbf8f29506cba131ae4f85/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:9b65a62a71a88fb7534002067b972aac2f363e871ad34563579550fec1a260cb", size = 457685, upload-time = "2026-04-09T01:28:01.313Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/5a/cbaa2088483837ded4bb6301993de16743c633f230bf29947cdc10f7c307/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:d1c4e623dbb8bd93fc4273a7ca79d192618ca36cf74aceaeca9d36c83608ed77", size = 604340, upload-time = "2026-04-09T01:28:06.227Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/39/2686a96a3d80c5ef61fea10066677ddf3678bdd48afcec04a0d268203fa0/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux2014_armv7l.whl", hash = "sha256:0c10ad18ca2cb285b4b6c2bfdba6bc0234e84272f52bf692dce25ae7c6a21d56", size = 1001029, upload-time = "2026-04-09T01:28:11.426Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/e9/da903ea517387ac3d560b324fd03a7f50267cc2f68e99f84934fd9ee3ef9/pyarmor_cli_core-8.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:812201887aa2fb90f8b865a5dd443aa841cb951bd25d14dad843ce11b3ceef57", size = 649535, upload-time = "2026-04-09T01:28:16.373Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/eb/fcd742456b3a475e16eb5c3468132a9950095169cf1e6a64e4fb4bac208d/pyarmor_cli_core-8.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88e129603512326ce1440e3aa105506304424ee40366cc19f0a72a9aeeabecf7", size = 446443, upload-time = "2026-04-09T01:28:21.054Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/87/b58aed090a6a9debf65045f37e93b779e330c22106aa4cbb9aaaf7bb12b8/pyarmor_cli_core-8.1.1-cp312-cp312-win32.whl", hash = "sha256:b645d69c88f55a08d34fdd547759079ac1722e246d8e994ce3eb07937bdf318b", size = 460155, upload-time = "2026-04-09T01:28:25.665Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/b8/1dc8b15bc1607a27ed78025d5f50c97c78542930cab15a6d6d457b664bb3/pyarmor_cli_core-8.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:09df045973b5de85ecb950f3fffc23686caa5b43749d4d966ccd150719639e2b", size = 420620, upload-time = "2026-04-09T01:28:30.36Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/35/4b2969ebaea72869a5ba13847d47682ec77a1ff894fc83450e23e7257845/pyarmor_cli_core-8.1.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:86a576252f26752cfed5a1f0d6a12352f7ca8f38aadfd7e1df44453d933bf70c", size = 517354, upload-time = "2026-04-09T01:28:35.086Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/0f/c688f09d158d5d4bf3c83289b9d16d454d5ac001ae962a7501d4209dbcad/pyarmor_cli_core-8.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef4cd7ecf1aeca5748d221e548a6203f58ede46e3b1f31cc1fbba5170f1dc5c5", size = 409747, upload-time = "2026-04-09T01:28:40.159Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/f1/c97c393fa0f6431ac6537acd21f117025488b1254c255ae42da1809d2ae3/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux1_i686.whl", hash = "sha256:e95cfbaafee2eec3a3ef918c80f6ff6d6e26535c0b9f5ea615a97cf419a45603", size = 489761, upload-time = "2026-04-09T01:28:45.023Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/78/5874a8cecc9123475bcf5884037a6abd5859e4b93e89bc5eacc06dfd493f/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:2f42447c95f0de7df586a5899b395dcbf3193b5288a290f77dde90d4459700a6", size = 458105, upload-time = "2026-04-09T01:28:49.719Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/a0/73eb8557f0dd6f9910dfb1ff70ad31550d5b037dff78f80a006732efb085/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:635144791ddcf3a7bbd31ac10181828d40636e4b6a477ffefa0c6f4073524604", size = 604874, upload-time = "2026-04-09T01:28:54.871Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/6c/3eeab3e37748ae5354eb629ed5e03ee09c85c6abac8d77926d4d27507f52/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux2014_armv7l.whl", hash = "sha256:248bfb04d86f38851bdc13652d5e447e0415128c0cdfbb04498802e624a88475", size = 1001331, upload-time = "2026-04-09T01:29:00.195Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/f3/9736f5049e0fff5a333a8e14058e5b6b89ef68f469bcc23f9001fd47ab42/pyarmor_cli_core-8.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a71b3b4ab8d07259b82c9fdad18e099d86b0880bce352949b834db825cbd697", size = 651229, upload-time = "2026-04-09T01:29:05.072Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/e5/988ebde164710de231a93127a1df25a278307bb8cd31e75b207266a3ae8b/pyarmor_cli_core-8.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:bc51089e37602a4e4c75b990448c44e25d7b1c7048805ad789863c889e80793f", size = 447196, upload-time = "2026-04-09T01:29:09.99Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/5c/4be2fe6a77477c6a82b776c147487030978fb3c6c825dfcecafe4cdaf1d1/pyarmor_cli_core-8.1.1-cp313-cp313-win32.whl", hash = "sha256:689356f95fec04a53f91a624b0d1f4bfc8547e2ddede6799fb1c728377a51e19", size = 460848, upload-time = "2026-04-09T01:29:15.702Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/d1/80a1085e94a8bcc3f1d8c6d1ef703c1fc35afe9a8f2b5f0676d74fc1a80d/pyarmor_cli_core-8.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e7b259c4dda700360030cf212b2cca625aaf9e7498dc9bfcbee9ef85acfec9b", size = 421343, upload-time = "2026-04-09T01:29:20.354Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/57/12936c3ea69e88481e71d4f61a878aa49b26181c0fed9282b590f46b9132/pyarmor_cli_core-8.1.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:4bfbfe564881bd467b2103b47b4fdfe883746e39651545280f4c815d409e45f7", size = 522708, upload-time = "2026-04-09T01:29:25.471Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/4c/ba326b377d0d698fa80864a478822e56acd0b1dd74db7b2645ca66e3c732/pyarmor_cli_core-8.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0ff3494acdacbec1565a1d98065ab2a2b47683778ad02b89b643008a1ffe90bd", size = 414399, upload-time = "2026-04-09T01:29:30.291Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/15/f6121674cca8487665bc4803da745a1f4e7f98d233bf33e659ca3dbfec29/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux1_i686.whl", hash = "sha256:2d905427cd23226f86998a28f6c79c849136dfb85e569e32b208ad7d75b24853", size = 495367, upload-time = "2026-04-09T01:29:35.223Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/6a/67121dc6f489120322bf4742774a3ebd05eb4b6d1ebcf971b34ea91ec2b4/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:4de138aacd465e0a0d4601b290d0555d47657f59b86d5eeec1bc660fd01e149a", size = 462060, upload-time = "2026-04-09T01:29:39.799Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/87/09d5c023474495dd3dbb789ecf538af1bcbe53c49cd07a703cfa5da03bde/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:1777c7c2c7130a34b7c7125ddd828569e3f938fc91efc0db99fb0ee1eba88aa4", size = 609493, upload-time = "2026-04-09T01:29:44.322Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/55/c6f403c7a33900d1a194839e55677814a35ab33d10c4234394f7ad12c311/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux2014_armv7l.whl", hash = "sha256:5172681e95bfbf4eb250d53e1ede75b270e7ec7c29a4accfb0acf766bc7b6351", size = 1004962, upload-time = "2026-04-09T01:29:49.616Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/bc/ce7146830a08306884929b53f785bb708df7c360a73a2a3687ff73533918/pyarmor_cli_core-8.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:06de0a47cb8ae86fe9270f3930d8e245fff73f87bd3c49cdc2af47c8ce1a5b06", size = 655381, upload-time = "2026-04-09T01:29:54.994Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/05/0fac5b9e9d37e1ea46ceae2733ab07d348b199257d99f69d290eae363ac0/pyarmor_cli_core-8.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7ae1c265c6560e869d355373fd86c2880a951db6bbf2bbdffec698d47c269be9", size = 450987, upload-time = "2026-04-09T01:30:00.065Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/84/173c89753b13037ad45fd6c220aeb12e055de261c8453025df9dc0c82b2c/pyarmor_cli_core-8.1.1-cp314-cp314-win32.whl", hash = "sha256:18a7f8aa4054395be9fe5b051727f2c32bdce74d609ef75cb869a9fd0396da90", size = 465415, upload-time = "2026-04-09T01:30:04.793Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/ce/32fb60e0971791c7e102ab87140d8c5da444e8b22b05614d7b63b7a90113/pyarmor_cli_core-8.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:9e50864097734b039060074b4913ffca73069162ff3c32556f3b594888129661", size = 424573, upload-time = "2026-04-09T01:30:09.704Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/02/7632897343d0b1ac3faa6e882f62880c4910d1331a46f1796816b99e9f8d/pyarmor_cli_core-8.1.1-cp315-cp315-macosx_10_14_x86_64.whl", hash = "sha256:eb3a63691a67aca10e2af7937baf0c88effc459c78c3c4ef1c48cf562376b830", size = 522819, upload-time = "2026-04-09T01:30:14.265Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/aa/09cce8ed725419d8a3c1a4a4b588b5f369094ef0b9768bc7e2d7fe855897/pyarmor_cli_core-8.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:75328d808fb790546272ba92440c8ae9e80f3c84cce3f423fefb00a9af230af0", size = 414398, upload-time = "2026-04-09T01:30:18.603Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/a9/844a2fcb09dc30994e03b5469ed80d6066e794828cd48af437da33029f8d/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux1_i686.whl", hash = "sha256:e0e0965e8a1d779485f747b93d6f4c7a20395553851921b0fd22ea89fb09287e", size = 495322, upload-time = "2026-04-09T01:30:23.59Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/1b/b169c06628dc9ac375c5d3f270cd742e344ddfa6dab5e00216a984ccfa01/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux1_x86_64.whl", hash = "sha256:3562142724c8cd48a6b161b35a7b4ebfc30820f08ca0adc28f8729077531c3f4", size = 461861, upload-time = "2026-04-09T01:30:28.655Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/98/8ab28d635e03d269234243c4ac2fd82d722435640d9be89e78cb6f4ba15f/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux2014_aarch64.whl", hash = "sha256:27c6e269131da709bcf1aac04ed8285eaa8a328f7682e98c4e93a81f97e2e4db", size = 609340, upload-time = "2026-04-09T01:30:33.652Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/68/f6e381d81e988077c6fdddcfa659ed30953640e863928575886815e1eefa/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux2014_armv7l.whl", hash = "sha256:0cc8d7c337663eb9d108649ba296973284ff8b6fc0f83ae0ce347697533a983d", size = 1004837, upload-time = "2026-04-09T01:30:38.944Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/83/4958e7eb4a4dd27b024a034443636e7db418d06ee743eb09eecab2360e18/pyarmor_cli_core-8.1.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:00cccda40da819c45550625c910f836b9c768699ea40b04c058c3221270b514b", size = 655276, upload-time = "2026-04-09T01:30:44.053Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/d8/c8992fc6f66fcd8b29f8497ca352165bd9e0972a1f5371eb8c01aaca7a3e/pyarmor_cli_core-8.1.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:488be5b88a0c434fb17457972f6b7f681b38650ce078c3f19dced5b15091146d", size = 450926, upload-time = "2026-04-09T01:30:49.022Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/61/6cb081493c75662dd979a6cf8f6092d81605a9a59c2439a2ab77f3abfc1b/pyarmor_cli_core-8.1.1-cp315-cp315-win32.whl", hash = "sha256:a0f087fe8c388d178859dbc2a14e11e12bf8efc694f035e41d1dfc5b4a630052", size = 465352, upload-time = "2026-04-09T01:30:53.861Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/87/9daf968e6e6add10d089eef86b46a26c9908bfae74820bc97aacbf37d081/pyarmor_cli_core-8.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:c1b96471bb1d9fc575ba526841829c6bf03cd9db5caabf5688518a29ed389870", size = 424475, upload-time = "2026-04-09T01:30:58.457Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/5c/e653770e60de273502616202ffcf769011a50c744442bd63e01506ee61d6/pyarmor_cli_core-8.1.1-cp37-cp37-macosx_10_14_x86_64.whl", hash = "sha256:6d41880b098558f3905a18ffa3c9498e95ec71d93ddb3bc2f23701947e174862", size = 509125, upload-time = "2026-04-09T01:31:03.266Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/71/da506483c853b23eb45e63678da67919c113894649f6fd7fa08a602447ce/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux1_i686.whl", hash = "sha256:05d9c9c54636b09fcbb37a95c97ccb1df9718f0cad88e03eb72f12bf18922092", size = 479615, upload-time = "2026-04-09T01:31:08.596Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/fa/249684e417a9fcdd369672232901dfad7fe84b8a7ed0e3c12457ef6f108e/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux1_x86_64.whl", hash = "sha256:245e837ab2543ce7e2eaa39a80c3ef093ff97a51ced18ddb9052512859f8db72", size = 449406, upload-time = "2026-04-09T01:31:13.597Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/06/c41929814430426aa4f9a24ef736b210e8b2482d7bcc84ca0fa64bd577c6/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux2014_aarch64.whl", hash = "sha256:bf023d2f6c0cf69013eb80da97da87e8a67c0b2dd1e6d3375f02e0f195b3524f", size = 592931, upload-time = "2026-04-09T01:31:19.038Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/fe/74bc6a0d3a3f41696f4a944872bca0c73aa124ddef2b8af8168fb11337c1/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux2014_armv7l.whl", hash = "sha256:2a66cfed9f7dd72043389ed9b933ae57ffb00d28a8195f4d6aa446a8eda0a787", size = 989241, upload-time = "2026-04-09T01:31:24.261Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f2/343507785aa9d742589a57006cecb5fe8f5552bd05c3076712efffeedd30/pyarmor_cli_core-8.1.1-cp37-cp37-musllinux_1_1_aarch64.whl", hash = "sha256:183555f51b42d3be95b7fa248800ddfec6bcdc7ea843cfb4982be2671df8c39f", size = 639618, upload-time = "2026-04-09T01:31:29.486Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/04/ae296dabb653962116cd9c7bb453b9d12fac8efa49bb67e2e98c15aec062/pyarmor_cli_core-8.1.1-cp37-cp37-musllinux_1_1_x86_64.whl", hash = "sha256:6575a73f3928782e3394bd4d74942fa60f926a3f78bcca5bb920b33e49647910", size = 435876, upload-time = "2026-04-09T01:31:34.288Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/b5/f3647e1d1d7d58d12257d23bec0ee37838bc359a77b051fb724002283fc4/pyarmor_cli_core-8.1.1-cp37-cp37-win32.whl", hash = "sha256:6129c315fac01be1ceae09231816a42396d00928a96f537faf1ba3435263ee67", size = 447840, upload-time = "2026-04-09T01:31:39.095Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/a0/b767e2303491a5028be332c5c38656e1e6953c224c11f51ef7ac98f0d3a9/pyarmor_cli_core-8.1.1-cp37-cp37-win_amd64.whl", hash = "sha256:84a8ad19400f52e75451c35a04e41d8034bbfdf8743a306346a800a995a5f470", size = 407502, upload-time = "2026-04-09T01:31:44.021Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/9c/dc11b51400a3c9ae97d092b169dbcbe5d3cdd7a48efa8253f173cec336a0/pyarmor_cli_core-8.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:db8b83e5d656ce2310a252ecb009f84ebeaf8c60be6a9b5fb6a7e08ac3dc0064", size = 510970, upload-time = "2026-04-09T01:31:48.658Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/84/c2a1832659e83ced41974a64b3317d80c72126b38a61314419dc0c3f578d/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8b397fcd7febfcf692daa5641ee175377840304cbc0456929c84bf0eeefc634e", size = 483313, upload-time = "2026-04-09T01:31:53.644Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/0b/df3cd01216f01d6122447e7d69b8562973a27e9df15c2cf0840872e30763/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b487b9e93a5e63f3e1ff7da2af6ef0db79aa4a92234613d8e7c93ca8239f525a", size = 460867, upload-time = "2026-04-09T01:31:58.436Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/33/800fbb62b8a7193ddba3445a117900782125ec678b086bc786c0ff4ff6c9/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0838f354a4c299bf6204a5be510b193c37aa4b4bd2ed2ebd23046b0b75a345db", size = 597415, upload-time = "2026-04-09T01:32:03.52Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/7e/927a29f880ec35eef6499470642c079417d1ef6caa4c887a05d982a1cf22/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux2014_armv7l.whl", hash = "sha256:a2dd5be9b02b3b4cdc300423bdb38453e34bef7c5397d36981c35280d0ea13bd", size = 993141, upload-time = "2026-04-09T01:32:08.932Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/fb/d33f4128745823235f3d6f2488f1161f43d9f855c730abd598978e97b7c0/pyarmor_cli_core-8.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:08bb1e1071cb4356ce85ef8e7089cf85853cd3204dfb146a00f9401bca80a3a1", size = 644146, upload-time = "2026-04-09T01:32:13.936Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/ba/addbf681e1fcb006a9590b721beac1bbe663553dc151d3b146a5a51cabc0/pyarmor_cli_core-8.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a15fc2e4af49ffca703391a7adf29f39c8975f09afafd2461ad808dc2aeaf215", size = 439049, upload-time = "2026-04-09T01:32:19.879Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/97/a92a86b584499d4d8dc3ccd94ca92048892242d2d6fce84ba27bf66344f3/pyarmor_cli_core-8.1.1-cp38-cp38-win32.whl", hash = "sha256:b685e5cdd68660dc6fbcf1502b79a93ccd7969a5d411837d3dc4fa8f266df81a", size = 452679, upload-time = "2026-04-09T01:32:24.782Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/fc/b933559c1fccacc063bc04cd88d7cc48c84a1fb707ec03fe0b106e34337e/pyarmor_cli_core-8.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:a83d4d571012bffe2196a1b65f56abf04ee57dd09aa8d063ced99b0cbc21c341", size = 413687, upload-time = "2026-04-09T01:32:29.485Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/48/07a43938c9fb667d1b487aedf4ef691914cb049c398eb355585ad7d691ad/pyarmor_cli_core-8.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a7dee28cefefc8e2c183e8b56a12d4029b9517f4f860ec17bed3cca5f495be62", size = 510309, upload-time = "2026-04-09T01:32:34.533Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/ed/ce279cdd805d22c0f315eebe1c41d541e0e3bd5f58057297102912fc38de/pyarmor_cli_core-8.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4eedb4151c13c8fa55a0c2050a1f06513a8099c78983a99f5176b962892bf85", size = 402366, upload-time = "2026-04-09T01:32:39.294Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/9e/841f564ad60e0d9a0ba05c416f1220b95038fe643f08bc5871c932d3549d/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3122cb3b9049d8f71bc459abdd7baa3a588d49a7cb2d20aaaf9d01e8b16ea84b", size = 481480, upload-time = "2026-04-09T01:32:44.122Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/bf/d019a15c35f2c06a971c4140c471249d4da523afb37aed70855d80bf9e12/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fa41bb83dd13fdac97f11703afabe241253906ed233c39d5f2248a64a17a5efc", size = 461013, upload-time = "2026-04-09T01:32:49.154Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/c2/8c4d7314e1fc5262fa47ff1ae61eec421261e1160329cd940f4f463a590e/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0817f5493745b0521f437ad26713bdf892a102e86f2e482b73f73fdd1432c605", size = 598571, upload-time = "2026-04-09T01:32:53.983Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/19/21dd64c879c3c15a960393ffbce3b6c15c8e760e1d7d99a284f19aabfab1/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux2014_armv7l.whl", hash = "sha256:ccf8db789066281475faedd5b06253b28bed3316a1bce5716e84166604b3ca10", size = 994367, upload-time = "2026-04-09T01:32:59.448Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/2e/1e2fcd7779f376af4c3a8ede0c9e9ae9981d6e735a8525eee566c9d80329/pyarmor_cli_core-8.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b98390156ab4226d77dcc4e7b6ea79ce82ffd553aaf12b402b965468d0c30a4e", size = 643749, upload-time = "2026-04-09T01:33:04.734Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/35/4ee9c4315aef4f28f9cfb47a50d4e48bfec220d77f5e1e86a36d50302b33/pyarmor_cli_core-8.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27d776ab86890c84381b3eed7c5906550f929d11d3ed63b4d981e0b4670a7f94", size = 439652, upload-time = "2026-04-09T01:33:09.45Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/0e/06f20cb41f14e703cf78b68e37a25f2fa163f87a82914404d9c916437a92/pyarmor_cli_core-8.1.1-cp39-cp39-win32.whl", hash = "sha256:5d7a3b90c559a29596db18d7f99342da1ba4634d526e08cbbe4743d4c77e3679", size = 454449, upload-time = "2026-04-09T01:33:14.388Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/75/681dfabc50249cb38f452c0139006d6df9302dc5e32925972506f31ec1fa/pyarmor_cli_core-8.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:8dcd779236cb32cae03f2c71c469fbb945e45c6ef04e38a7ad39e412fe7a8df0", size = 414029, upload-time = "2026-04-09T01:33:19.46Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/90/89c95f00699600a2c476b1c80e97df5ea1f09b995a8abfc0a8f118f0bf38/pyarmor_cli_core-8.1.1-py3-none-any.whl", hash = "sha256:06eb9771c7d1f0ad7c00884f13ea42edfbffab8b28ef718ce197c0b4c69d97d5", size = 5177, upload-time = "2026-04-09T01:33:22.702Z" }, +] + [[package]] name = "pycparser" version = "2.21"