--- /dev/null
+#!/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 <fwpkg_file> [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(
+ '<IHHI', self.data[0:self.FWPKG_HEAD_SIZE]
+ )
+
+ print(f" ✓ Magic: 0x{self.flag:08X}")
+ if self.flag != self.MAGIC:
+ raise ValueError(f"无效的Magic Number: 0x{self.flag:08X}, 期望: 0x{self.MAGIC:08X}")
+
+ print(f" ✓ CRC16: 0x{self.crc:04X}")
+ print(f" ✓ Image Count: {self.image_num}")
+ print(f" ✓ Total Size: {self.total_file_size:,} bytes ({self.total_file_size / 1024 / 1024:.2f} MB)")
+
+ # 2. 解析IMAGE_INFO列表
+ print(f"\n[步骤2] 解析镜像信息表 ({self.image_num} 个镜像)...")
+ offset = self.FWPKG_HEAD_SIZE
+
+ for i in range(self.image_num):
+ if offset + self.IMAGE_INFO_SIZE > 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())