差分文件制作小工具
一、功能模块
-
界面布局
- 采用垂直布局(
QVBoxLayout
)嵌套水平布局(QHBoxLayout
),包含以下控件:- 文件选择:旧文件(
oldFile
)与新文件(newFile
)路径输入框,通过QFileDialog
实现文件浏览。 - 参数配置:压缩字典大小(
dictSize
)输入框,支持自定义数值(如32k
、1m
)。 - 信息显示:输出日志(
QTextEdit
)展示命令执行结果,命令显示框实时显示生成的hdiffi
命令及文件处理信息。 - 操作按钮:“创建补丁”触发差分包生成,“清空”按钮重置所有输入。
- 文件选择:旧文件(
- 采用垂直布局(
-
默认配置
- 初始预设测试文件路径(
V1.00.31.bin
与32.bin
),便于快速验证功能。
- 初始预设测试文件路径(
二、技术实现
-
命令执行
- 使用
subprocess.run
调用hdiffi
命令行工具,关键参数包括:python
command = ['hdiffi', f'-c-tuz-{compress_dict_size}', old_file, new_file, out_diff_file]
- 捕获标准输出(
stdout
)与错误(stderr
),通过check=True
自动检测非零返回码并抛出异常。
- 使用
-
差分包后处理
- 文件头注入:将差分包大小转换为 4 字节小端格式(
patch_size_bytes
),写入文件开头。 - 128 字节对齐:计算
(patch_size + 34) % 128
的余数,不足则补零(b'\x00'
),确保文件长度符合传输协议要求。 - 追加元数据:读取旧文件末 30 字节,以十六进制格式追加至差分包尾部,用于校验或版本标识。
- 文件头注入:将差分包大小转换为 4 字节小端格式(
-
日志管理
- 输出框实时显示命令执行状态、错误信息及文件处理详情(如补丁大小、对齐操作)。
三、核心逻辑与注意事项
-
关键流程
选择文件 → 构建 hdiffi 命令 → 执行命令 → 后处理 → 显示结果
-
技术亮点
- GUI 封装:将命令行操作转化为可视化界面,降低使用门槛。
- 自动化处理:通过文件大小计算、字节操作实现差分包格式标准化。
- 错误鲁棒性:捕获
subprocess.CalledProcessError
并显示详细错误信息。
-
潜在改进点
- 路径安全:未处理含空格路径(建议用
shlex.quote
包裹路径)。 - 参数校验:缺少压缩字典大小格式验证(如
32k
是否合法)。 - 性能优化:大文件处理时可能因内存加载导致卡顿,可增加进度条提示。
- 代码复用:后处理逻辑(补零、追加字节)可封装为独立函数提升可维护性。
- 路径安全:未处理含空格路径(建议用
总结
该程序通过 PyQt5 实现了 hdiffi.exe
的图形化调用,具备差分包生成、文件格式处理及日志反馈功能,适用于自动化固件升级场景。其核心价值在于将复杂的命令行操作简化为用户友好的界面交互,同时通过后处理确保差分包符合特定协议要求。后续可通过增强异常处理、参数校验及性能优化进一步提升可靠性。
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QFileDialog, QTextEdit
from PyQt5.QtCore import Qt
import subprocess
import os
class PatchCreatorApp(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 设置窗口标题和大小
self.setWindowTitle('Patch Creator')
self.setGeometry(100, 100, 600, 400)
# 创建布局
layout = QVBoxLayout()
# 创建原始文件选择框
old_file_label = QLabel('旧文件 (oldFile):')
self.old_file_edit = QLineEdit()
self.old_file_edit.setReadOnly(True)
old_file_button = QPushButton('选择文件')
old_file_button.clicked.connect(self.select_old_file)
old_file_layout = QHBoxLayout()
old_file_layout.addWidget(old_file_label)
old_file_layout.addWidget(self.old_file_edit)
old_file_layout.addWidget(old_file_button)
layout.addLayout(old_file_layout)
# 创建新文件选择框
new_file_label = QLabel('新文件 (newFile):')
self.new_file_edit = QLineEdit()
self.new_file_edit.setReadOnly(True)
new_file_button = QPushButton('选择文件')
new_file_button.clicked.connect(self.select_new_file)
new_file_layout = QHBoxLayout()
new_file_layout.addWidget(new_file_label)
new_file_layout.addWidget(self.new_file_edit)
new_file_layout.addWidget(new_file_button)
layout.addLayout(new_file_layout)
# 创建压缩字典大小输入框
compress_dict_label = QLabel('压缩字典大小 (dictSize):')
self.compress_dict_edit = QLineEdit()
self.compress_dict_edit.setPlaceholderText('例如: 32, 64k, 1m, 1g')
compress_dict_layout = QHBoxLayout()
compress_dict_layout.addWidget(compress_dict_label)
compress_dict_layout.addWidget(self.compress_dict_edit)
layout.addLayout(compress_dict_layout)
# 创建输出框
output_label = QLabel('输出信息:')
self.output_edit = QTextEdit()
self.output_edit.setReadOnly(True)
output_layout = QVBoxLayout()
output_layout.addWidget(output_label)
output_layout.addWidget(self.output_edit)
layout.addLayout(output_layout)
# 创建命令显示框
command_label = QLabel('生成的命令:')
self.command_edit = QTextEdit()
self.command_edit.setReadOnly(True)
command_layout = QVBoxLayout()
command_layout.addWidget(command_label)
command_layout.addWidget(self.command_edit)
layout.addLayout(command_layout)
# 创建按钮
create_patch_button = QPushButton('创建补丁')
clear_button = QPushButton('清空')
button_layout = QHBoxLayout()
button_layout.addWidget(create_patch_button)
button_layout.addWidget(clear_button)
layout.addLayout(button_layout)
# 设置布局
self.setLayout(layout)
# 连接按钮事件
create_patch_button.clicked.connect(self.create_patch)
clear_button.clicked.connect(self.clear_fields)
def select_old_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, '选择旧文件', '', 'All Files (*);;Text Files (*.txt)')
if file_path:
self.old_file_edit.setText(file_path)
def select_new_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, '选择新文件', '', 'All Files (*);;Text Files (*.txt)')
if file_path:
self.new_file_edit.setText(file_path)
def create_patch(self):
old_file = self.old_file_edit.text().strip()
new_file = self.new_file_edit.text().strip()
if not new_file:
self.output_edit.append("请确保选择了新文件。")
return
# 生成补丁文件路径
new_file_dir = os.path.dirname(new_file)
new_file_name = os.path.basename(new_file)
patch_file_name = f"patch-tuz-{self.compress_dict_edit.text().strip()}-{new_file_name}"
out_diff_file = os.path.join(new_file_dir, patch_file_name)
# 获取压缩字典大小
compress_dict_size = self.compress_dict_edit.text().strip()
if not compress_dict_size:
compress_dict_size = '32k' # 默认值
command = ['hdiffi', f'-c-tuz-{compress_dict_size}', old_file, new_file, out_diff_file]
# 显示生成的命令
# self.command_edit.setText(f"执行命令: {' '.join(command)}")
self.output_edit.append(f"执行命令: {' '.join(command)}")
try:
result = subprocess.run(command, check=True, text=True, capture_output=True)
self.output_edit.append("命令输出:")
self.output_edit.append(result.stdout)
self.output_edit.append("命令返回码: 0")
self.output_edit.append(f"补丁文件已生成: {out_diff_file}")
except subprocess.CalledProcessError as e:
self.output_edit.append("命令执行出错:")
self.output_edit.append(e.stderr)
self.output_edit.append(f"命令返回码: {e.returncode}")
# print(f"路径:{out_diff_file}")
patch_size = os.path.getsize(out_diff_file)
# print(f"分输出的差分文件大小: {patch_size} 字节")
self.command_edit.append(f"差分输出的差分文件大小: {patch_size} 字节\n")
# 将patch_size转化为16进制的格式并输出在命令显示框中
# 将 patch_size 转换为4个字节的字节数组
patch_size_bytes = patch_size.to_bytes(4, byteorder='little')
hex_data1 = ' '.join(f'{byte:02X}' for byte in patch_size_bytes)
self.command_edit.append(f"4字节: {hex_data1} 字节\n")
# 我想将hex_data1添加在out_diff_file文件的最前面
with open(out_diff_file, 'rb') as file:
file_content = file.read()
with open(out_diff_file, 'wb') as file:
file.write(patch_size_bytes)
file.write(file_content)
# 计算patch_size_bytes对128取余的结果
remainder = (patch_size+30+4) % 128
if remainder == 0:
self.command_edit.append(f"patch_size对128取余的结果为0,不需要补0\n")
else:
self.command_edit.append(f"patch_size对128取余的结果为{remainder},需要补0\n")
# 将patch_size_bytes对128取余的结果补0
for i in range(128-remainder):
with open(out_diff_file, 'ab') as file:
file.write(b'\x00')
# 获取旧文件路径
old_file_path = self.new_file_edit.text().strip()
print(old_file_path)
# 获取旧文件内容
with open(old_file_path, 'rb') as file:
old_file_content = file.read()
# 获取旧文件最后30个字节的内容
last_30_bytes = old_file_content[-30:]
self.command_edit.append(f"旧文件最后30个字节的内容: {last_30_bytes}\n")
# 以16进制的格式显示last_30_bytes数据
hex_data = ' '.join(f'{byte:02X}' for byte in last_30_bytes)
self.command_edit.append(f"旧文件最后30个字节的内容: {hex_data}\n")
# 将最后30个字节添加到out_diff_file这个文件的最后面
with open(out_diff_file, 'ab') as file:
file.write(last_30_bytes)
patch_size = os.path.getsize(out_diff_file)
self.command_edit.append(f"差分输出的差分文件大小: {patch_size} 字节\n")
def clear_fields(self):
self.old_file_edit.clear()
self.new_file_edit.clear()
self.compress_dict_edit.clear()
self.output_edit.clear()
self.command_edit.clear()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = PatchCreatorApp()
ex.show()
sys.exit(app.exec_())