添加fwpkg解压脚本 2501/1
author孙春阳 <snickersun@163.com>
Tue, 14 Oct 2025 04:06:48 +0000 (12:06 +0800)
committer孙春阳 <snickersun@163.com>
Tue, 14 Oct 2025 04:06:48 +0000 (12:06 +0800)
tools/scripts/unpack_fwpkg.py [new file with mode: 0644]

diff --git a/tools/scripts/unpack_fwpkg.py b/tools/scripts/unpack_fwpkg.py
new file mode 100644 (file)
index 0000000..94cced9
--- /dev/null
@@ -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 <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())