From 8944566c1e43b98c0c46fb2a7bd9f1098e6af8d4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=AD=99=E6=98=A5=E9=98=B3?= Date: Tue, 14 Oct 2025 12:06:48 +0800 Subject: [PATCH] =?utf8?q?=E6=B7=BB=E5=8A=A0fwpkg=E8=A7=A3=E5=8E=8B?= =?utf8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- tools/scripts/unpack_fwpkg.py | 292 ++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 tools/scripts/unpack_fwpkg.py diff --git a/tools/scripts/unpack_fwpkg.py b/tools/scripts/unpack_fwpkg.py new file mode 100644 index 000000000..94cced959 --- /dev/null +++ b/tools/scripts/unpack_fwpkg.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +unpack_fwpkg.py - FWPKG固件升级包解析和解包工具 + +功能: +1. 解析fwpkg文件格式 +2. 提取fwpkg中的各个镜像文件 +3. 显示升级包的详细信息(包括烧录地址等) + +FWPKG格式: +- FWPKG_HEAD (12字节): flag + crc + imageNum + totalFileSize +- IMAGE_INFO (52字节 * imageNum): pathName + startIndex + imageSize + burnAddr + burnSize + type +- 文件数据区(每个文件后有16字节padding) + +用法: + python unpack_fwpkg.py [output_dir] + +参数: + fwpkg_file: 要解析的.fwpkg文件路径 + output_dir: 输出目录(可选,默认:同名目录) + +示例: + python unpack_fwpkg.py update.fwpkg + python unpack_fwpkg.py update.fwpkg ./extracted/ +""" + +import os +import sys +import struct + +# ==================== 主类 ==================== + +class FwpkgUnpacker: + """FWPKG解包器""" + + MAGIC = 0xEFBEADDF # 文件头魔数 + FWPKG_HEAD_SIZE = 12 # flag(4) + crc(2) + imageNum(2) + totalFileSize(4) + IMAGE_INFO_SIZE = 52 # pathName(32) + startIndex(4) + imageSize(4) + burnAddr(4) + burnSize(4) + type(4) + IMAGE_PADDING = 16 # 每个镜像后的padding + + def __init__(self, fwpkg_path, output_dir=None): + """ + 初始化解包器 + + Args: + fwpkg_path: fwpkg文件路径 + output_dir: 输出目录(可选) + """ + self.fwpkg_path = os.path.abspath(fwpkg_path) + + if not os.path.exists(self.fwpkg_path): + raise FileNotFoundError(f"文件不存在: {self.fwpkg_path}") + + # 设置输出目录 + if output_dir: + self.output_dir = os.path.abspath(output_dir) + else: + base_name = os.path.splitext(os.path.basename(self.fwpkg_path))[0] + self.output_dir = os.path.join(os.path.dirname(self.fwpkg_path), base_name + "_unpacked") + + # 读取整个文件 + with open(self.fwpkg_path, 'rb') as f: + self.data = f.read() + + self.file_size = len(self.data) + print(f"[INFO] 读取文件: {self.fwpkg_path}") + print(f"[INFO] 文件大小: {self.file_size:,} bytes ({self.file_size / 1024 / 1024:.2f} MB)") + + # 解析结果 + self.flag = 0 + self.crc = 0 + self.image_num = 0 + self.total_file_size = 0 + self.images = [] + + def parse(self): + """解析fwpkg文件""" + print("\n" + "=" * 70) + print("开始解析FWPKG文件") + print("=" * 70) + + # 1. 解析FWPKG_HEAD + print("\n[步骤1] 解析FWPKG头...") + if len(self.data) < self.FWPKG_HEAD_SIZE: + raise ValueError("文件太小,无法包含FWPKG头") + + # struct.unpack格式: 'IHHI' = I(uint32) H(uint16) H(uint16) I(uint32) + self.flag, self.crc, self.image_num, self.total_file_size = struct.unpack( + ' len(self.data): + raise ValueError(f"镜像{i}的IMAGE_INFO超出文件范围") + + # struct.unpack格式: '32sIIIII' = 32s(32字节字符串) + 5个I(uint32) + info_data = self.data[offset:offset + self.IMAGE_INFO_SIZE] + path_name_bytes, start_index, image_size, burn_addr, burn_size, img_type = struct.unpack( + '<32sIIIII', info_data + ) + + # 处理文件名(去除\x00) + path_name = path_name_bytes.split(b'\x00')[0].decode('utf-8', errors='ignore') + + image_info = { + 'index': i, + 'name': path_name, + 'start_index': start_index, + 'image_size': image_size, + 'burn_addr': burn_addr, + 'burn_size': burn_size, + 'type': img_type + } + self.images.append(image_info) + + print(f"\n [{i+1}] {path_name}") + print(f" 数据偏移: 0x{start_index:08X} ({start_index})") + print(f" 数据大小: {image_size:,} bytes ({image_size / 1024:.2f} KB)") + print(f" 烧录地址: 0x{burn_addr:08X}") + print(f" 烧录大小: 0x{burn_size:08X} ({burn_size} bytes)") + print(f" 类型: {img_type}") + + offset += self.IMAGE_INFO_SIZE + + print(f"\n✓ 解析完成!") + return True + + def print_summary(self): + """打印摘要信息""" + print("\n" + "=" * 70) + print("FWPKG摘要信息") + print("=" * 70) + + print("\n文件信息:") + print(f" 路径: {self.fwpkg_path}") + print(f" 大小: {self.file_size:,} bytes ({self.file_size / 1024 / 1024:.2f} MB)") + print(f" Magic: 0x{self.flag:08X}") + print(f" CRC16: 0x{self.crc:04X}") + + print(f"\n包含镜像数: {self.image_num}") + + print("\n镜像列表:") + print(f" {'序号':<4} {'文件名':<30} {'大小':<12} {'烧录地址':<12}") + print(" " + "-" * 68) + + total_size = 0 + for img in self.images: + size_mb = img['image_size'] / 1024 / 1024 + if size_mb >= 1.0: + size_str = f"{size_mb:.2f} MB" + else: + size_str = f"{img['image_size'] / 1024:.2f} KB" + + print(f" {img['index']+1:<4} {img['name']:<30} {size_str:<12} 0x{img['burn_addr']:08X}") + total_size += img['image_size'] + + print("\n统计信息:") + print(f" 总数据大小: {total_size:,} bytes ({total_size / 1024 / 1024:.2f} MB)") + header_size = self.FWPKG_HEAD_SIZE + self.image_num * self.IMAGE_INFO_SIZE + print(f" 头部大小: {header_size:,} bytes ({header_size / 1024:.2f} KB)") + padding_size = self.image_num * self.IMAGE_PADDING + print(f" Padding大小: {padding_size:,} bytes ({padding_size / 1024:.2f} KB)") + + def extract(self): + """提取所有镜像到输出目录""" + print("\n" + "=" * 70) + print("开始提取镜像文件") + print("=" * 70) + + # 创建输出目录 + os.makedirs(self.output_dir, exist_ok=True) + print(f"\n输出目录: {self.output_dir}") + + # 保存包信息 + info_file = os.path.join(self.output_dir, "package_info.txt") + with open(info_file, 'w', encoding='utf-8') as f: + f.write("=" * 70 + "\n") + f.write("FWPKG Package Information\n") + f.write("=" * 70 + "\n\n") + + f.write(f"Source File: {self.fwpkg_path}\n") + f.write(f"File Size: {self.file_size:,} bytes ({self.file_size / 1024 / 1024:.2f} MB)\n") + f.write(f"Magic: 0x{self.flag:08X}\n") + f.write(f"CRC16: 0x{self.crc:04X}\n") + f.write(f"Image Count: {self.image_num}\n\n") + + f.write("Images:\n") + for img in self.images: + f.write(f" [{img['index']+1}] {img['name']}\n") + f.write(f" Data Offset: 0x{img['start_index']:08X} ({img['start_index']})\n") + f.write(f" Image Size: {img['image_size']:,} bytes\n") + f.write(f" Burn Address: 0x{img['burn_addr']:08X}\n") + f.write(f" Burn Size: 0x{img['burn_size']:08X} ({img['burn_size']} bytes)\n") + f.write(f" Type: {img['type']}\n\n") + + print(f" ✓ 保存包信息: package_info.txt") + + # 提取每个镜像 + success_count = 0 + for img in self.images: + filename = img['name'] + start_index = img['start_index'] + image_size = img['image_size'] + + # 检查边界 + if start_index + image_size > self.file_size: + actual_size = self.file_size - start_index + print(f" ⚠ 警告: {filename} 数据超出文件范围") + print(f" 期望大小: {image_size:,}, 实际大小: {actual_size:,}") + if actual_size <= 0: + print(f" ✗ 跳过: {filename}") + continue + image_size = actual_size + + # 提取镜像数据 + image_data = self.data[start_index:start_index + image_size] + + # 保存镜像 + output_file = os.path.join(self.output_dir, filename) + with open(output_file, 'wb') as f: + f.write(image_data) + + file_size_mb = image_size / 1024 / 1024 + if file_size_mb >= 1.0: + size_str = f"{file_size_mb:.2f} MB" + else: + size_str = f"{image_size / 1024:.2f} KB" + + print(f" ✓ 提取: {filename:<30} {size_str:>12}") + success_count += 1 + + print(f"\n✓ 提取完成!成功 {success_count}/{self.image_num} 个文件") + print(f"输出目录: {self.output_dir}") + +def print_usage(): + """打印使用说明""" + print(__doc__) + +def main(): + """主函数""" + if len(sys.argv) < 2: + print_usage() + return 1 + + if sys.argv[1] in ['-h', '--help', 'help']: + print_usage() + return 0 + + fwpkg_file = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + try: + # 创建解包器 + unpacker = FwpkgUnpacker(fwpkg_file, output_dir) + + # 解析文件 + unpacker.parse() + + # 打印摘要 + unpacker.print_summary() + + # 提取文件 + unpacker.extract() + + print("\n" + "=" * 70) + print("✓ 所有操作完成!") + print("=" * 70) + + return 0 + + except Exception as e: + print(f"\n✗ 错误: {str(e)}") + import traceback + traceback.print_exc() + return 1 + +if __name__ == '__main__': + sys.exit(main()) -- 2.17.1