4. 差分补丁生成工具开发

任务目标

开发一个PC端Python工具,输入旧固件与新版固件二进制文件,输出差分补丁文件(.patch)。该补丁文件包含版本信息、数据块操作指令(写入/跳过)及新增数据,供后续Bootloader端解析并完成OTA升级。

环境依赖

仅需Python 3.8+标准库,无第三方依赖。工具采用bsdiff风格的分块差分算法,以固定块大小(默认256字节)对比新旧固件,生成紧凑的补丁结构。

核心实现

import struct
import hashlib
import os
import sys

BLOCK_SIZE = 256
MAGIC = b'DFOTA'
VERSION = 1

def generate_patch(old_path, new_path, patch_path):
    with open(old_path, 'rb') as f:
        old_data = f.read()
    with open(new_path, 'rb') as f:
        new_data = f.read()

    # 对齐到块大小
    old_len = (len(old_data) + BLOCK_SIZE - 1) // BLOCK_SIZE * BLOCK_SIZE
    new_len = (len(new_data) + BLOCK_SIZE - 1) // BLOCK_SIZE * BLOCK_SIZE
    old_data = old_data.ljust(old_len, b'\xff')
    new_data = new_data.ljust(new_len, b'\xff')

    old_blocks = [old_data[i:i+BLOCK_SIZE] for i in range(0, old_len, BLOCK_SIZE)]
    new_blocks = [new_data[i:i+BLOCK_SIZE] for i in range(0, new_len, BLOCK_SIZE)]

    # 生成操作指令列表
    ops = []  # (block_index, type, data) type: 0=skip, 1=write
    max_blocks = max(len(old_blocks), len(new_blocks))
    for i in range(max_blocks):
        old_blk = old_blocks[i] if i < len(old_blocks) else b'\xff' * BLOCK_SIZE
        new_blk = new_blocks[i] if i < len(new_blocks) else b'\xff' * BLOCK_SIZE
        if old_blk == new_blk:
            ops.append((i, 0, b''))
        else:
            ops.append((i, 1, new_blk))

    # 序列化补丁文件
    with open(patch_path, 'wb') as f:
        f.write(MAGIC)
        f.write(struct.pack('<H', VERSION))
        f.write(struct.pack('<I', len(old_data)))
        f.write(struct.pack('<I', len(new_data)))
        f.write(hashlib.md5(old_data).digest())
        f.write(hashlib.md5(new_data).digest())

        # 压缩连续skip操作
        compressed_ops = []
        skip_count = 0
        for idx, op_type, data in ops:
            if op_type == 0:
                skip_count += 1
            else:
                if skip_count > 0:
                    compressed_ops.append((0, skip_count, b''))
                    skip_count = 0
                compressed_ops.append((1, 1, data))
        if skip_count > 0:
            compressed_ops.append((0, skip_count, b''))

        f.write(struct.pack('<I', len(compressed_ops)))
        for op_type, count, data in compressed_ops:
            f.write(struct.pack('<B', op_type))
            f.write(struct.pack('<I', count))
            if op_type == 1:
                f.write(data)

def verify_patch(patch_path, old_path, new_path):
    """验证补丁可正确还原新固件"""
    with open(patch_path, 'rb') as f:
        assert f.read(5) == MAGIC
        ver = struct.unpack('<H', f.read(2))[0]
        old_len = struct.unpack('<I', f.read(4))[0]
        new_len = struct.unpack('<I', f.read(4))[0]
        old_md5 = f.read(16)
        new_md5 = f.read(16)
        op_count = struct.unpack('<I', f.read(4))[0]

        with open(old_path, 'rb') as of:
            old_data = of.read().ljust(old_len, b'\xff')
        new_data = bytearray(new_len)

        pos = 0
        for _ in range(op_count):
            op_type = struct.unpack('<B', f.read(1))[0]
            count = struct.unpack('<I', f.read(4))[0]
            if op_type == 0:
                pos += count * BLOCK_SIZE
            else:
                for _ in range(count):
                    block = f.read(BLOCK_SIZE)
                    new_data[pos:pos+BLOCK_SIZE] = block
                    pos += BLOCK_SIZE

        new_data = bytes(new_data[:new_len])
        assert hashlib.md5(new_data).digest() == new_md5, 'MD5 mismatch'
        with open(new_path, 'rb') as nf:
            assert new_data == nf.read(), 'Data mismatch'
        print('验证通过:补丁可正确还原新固件')

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print('用法: python diff_patch.py <旧固件> <新固件> <输出补丁>')
        sys.exit(1)
    generate_patch(sys.argv[1], sys.argv[2], sys.argv[3])
    print(f'补丁已生成: {sys.argv[3]}')
    # 自动验证
    verify_patch(sys.argv[3], sys.argv[1], sys.argv[2])

运行与验证

  1. 准备两个测试固件文件(如old.bin和new.bin),其中new.bin相对old.bin有部分字节修改。
  2. 执行命令:python diff_patch.py old.bin new.bin upgrade.patch
  3. 程序自动完成补丁生成与回验,输出验证通过:补丁可正确还原新固件即表示成功。

补丁文件结构:文件头(魔数+版本+长度+MD5)+操作指令序列。操作类型0表示跳过count个块(旧数据不变),类型1表示写入count个新数据块。连续跳过操作已合并压缩,减少补丁体积。

判定标准

  • 生成补丁后自动验证通过,无异常输出。
  • 补丁文件大小远小于新固件(当新旧固件差异较小时)。
  • 使用不同内容的新旧固件重复测试,验证逻辑均通过。