| #!/usr/bin/env python3 |
| |
| """ |
| Script to unpack PLDM FW update package (DSP0267) |
| Reverses the logic of pldm_fwup_pkg_creator.py |
| """ |
| |
| import argparse |
| import binascii |
| import json |
| import os |
| import struct |
| import sys |
| from datetime import datetime |
| |
| def read_bytes(f, length): |
| data = f.read(length) |
| if len(data) != length: |
| raise ValueError(f"Unexpected EOF: expected {length} bytes, got {len(data)}") |
| return data |
| |
| def parse_bitfield(value, length): |
| """Convert an integer to a list of set bit indices (0-based).""" |
| flags = [] |
| for i in range(length): |
| if (value >> i) & 1: |
| flags.append(i) |
| return flags |
| |
| def decode_pldm_string(str_data, str_type): |
| """Decodes bytes based on PLDM string type.""" |
| try: |
| # Strip null terminators for cleaner JSON |
| if str_type == 1: # ASCII |
| return str_data.decode('ascii').strip('\x00') |
| elif str_type == 2: # UTF8 |
| return str_data.decode('utf-8').strip('\x00') |
| elif str_type in [3, 4]: # UTF16/LE |
| return str_data.decode('utf-16le').strip('\x00') |
| elif str_type == 5: # UTF16BE |
| return str_data.decode('utf-16be').strip('\x00') |
| else: |
| return str_data.hex() |
| except UnicodeDecodeError: |
| return str_data.hex() |
| |
| def parse_contiguous_pldm_string(f): |
| """Reads Type, Length, and String data assuming they are contiguous.""" |
| b_type = read_bytes(f, 1) |
| b_len = read_bytes(f, 1) |
| |
| str_type = struct.unpack("<B", b_type)[0] |
| str_len = struct.unpack("<B", b_len)[0] |
| |
| if str_len == 0: |
| return "" |
| |
| str_data = read_bytes(f, str_len) |
| return decode_pldm_string(str_data, str_type) |
| |
| def parse_timestamp(f): |
| """Parses the 13-byte PLDM timestamp.""" |
| data = read_bytes(f, 13) |
| us = int.from_bytes(data[2:5], byteorder='little') |
| sec = data[5] |
| minute = data[6] |
| hour = data[7] |
| day = data[8] |
| month = data[9] |
| year = int.from_bytes(data[10:12], byteorder='little') |
| |
| try: |
| dt = datetime(year, month, day, hour, minute, sec, us) |
| return dt.strftime("%Y-%m-%d %H:%M:%S") |
| except ValueError: |
| return "Invalid DateTime" |
| |
| def unpack_package(pkg_path, output_dir): |
| if not os.path.exists(output_dir): |
| os.makedirs(output_dir) |
| |
| metadata = { |
| "PackageHeaderInformation": {}, |
| "FirmwareDeviceIdentificationArea": [], |
| "ComponentImageInformationArea": [] |
| } |
| |
| with open(pkg_path, "rb") as f: |
| # --- 1. Parse Package Header --- |
| uuid_bytes = read_bytes(f, 16) |
| metadata["PackageHeaderInformation"]["PackageHeaderIdentifier"] = uuid_bytes.hex().upper() |
| |
| fmt_ver = struct.unpack("<B", read_bytes(f, 1))[0] |
| metadata["PackageHeaderInformation"]["PackageHeaderFormatVersion"] = fmt_ver |
| |
| pkg_header_size = struct.unpack("<H", read_bytes(f, 2))[0] |
| |
| timestamp = parse_timestamp(f) |
| metadata["PackageHeaderInformation"]["PackageReleaseDateTime"] = timestamp |
| |
| comp_bitmap_len = struct.unpack("<H", read_bytes(f, 2))[0] |
| |
| pkg_ver_str = parse_contiguous_pldm_string(f) |
| metadata["PackageHeaderInformation"]["PackageVersionString"] = pkg_ver_str |
| |
| # --- 2. Parse Firmware Device Identification Area --- |
| dev_rec_count = struct.unpack("<B", read_bytes(f, 1))[0] |
| |
| for i in range(dev_rec_count): |
| dev_record = {} |
| |
| # Track start for alignment check |
| record_start_pos = f.tell() |
| |
| # RecordLength - 2 bytes |
| rec_len = struct.unpack("<H", read_bytes(f, 2))[0] |
| |
| # DescriptorCount - 1 byte |
| desc_count = struct.unpack("<B", read_bytes(f, 1))[0] |
| |
| # DeviceUpdateOptionFlags - 4 bytes |
| opt_flags_int = struct.unpack("<I", read_bytes(f, 4))[0] |
| dev_record["DeviceUpdateOptionFlags"] = parse_bitfield(opt_flags_int, 32) |
| |
| # --- CORRECTION START --- |
| # The creator script packs: [Type][Len][PkgDataLen][AppComp][StringData] |
| |
| # 1. Read Version String Type (1B) and Length (1B) |
| ver_str_type = struct.unpack("<B", read_bytes(f, 1))[0] |
| ver_str_len = struct.unpack("<B", read_bytes(f, 1))[0] |
| |
| # 2. Read FirmwareDevicePackageDataLength (2 bytes) |
| pkg_data_len = struct.unpack("<H", read_bytes(f, 2))[0] |
| |
| # 3. Read ApplicableComponents (Bitmap) |
| app_comp_byte_len = (comp_bitmap_len + 7) // 8 |
| app_comp_bytes = read_bytes(f, app_comp_byte_len) |
| app_comp_int = int.from_bytes(app_comp_bytes, byteorder='little') |
| dev_record["ApplicableComponents"] = parse_bitfield(app_comp_int, comp_bitmap_len) |
| |
| # 4. NOW Read the Version String Data (ver_str_len bytes) |
| if ver_str_len > 0: |
| ver_str_data = read_bytes(f, ver_str_len) |
| dev_record["ComponentImageSetVersionString"] = decode_pldm_string(ver_str_data, ver_str_type) |
| else: |
| dev_record["ComponentImageSetVersionString"] = "" |
| |
| # 5. Handle Package Data (if any) - Creator sets len to 0 usually |
| if pkg_data_len > 0: |
| read_bytes(f, pkg_data_len) |
| |
| # --- CORRECTION END --- |
| |
| # Descriptors |
| descriptors = [] |
| for _ in range(desc_count): |
| desc = {} |
| d_type = struct.unpack("<H", read_bytes(f, 2))[0] |
| d_len = struct.unpack("<H", read_bytes(f, 2))[0] |
| |
| if d_type == 0xFFFF: # Vendor Defined |
| title_type = struct.unpack("<B", read_bytes(f, 1))[0] |
| title_len = struct.unpack("<B", read_bytes(f, 1))[0] |
| |
| if title_len > 0: |
| title_str = read_bytes(f, title_len).decode('ascii').strip('\x00') |
| else: |
| title_str = "" |
| |
| data_len = d_len - (1 + 1 + title_len) |
| vendor_data = read_bytes(f, data_len) |
| |
| desc["DescriptorType"] = d_type |
| desc["VendorDefinedDescriptorTitleString"] = title_str |
| desc["VendorDefinedDescriptorData"] = vendor_data.hex().upper() |
| else: |
| d_data = read_bytes(f, d_len) |
| desc["DescriptorType"] = d_type |
| desc["DescriptorData"] = d_data.hex().upper() |
| |
| descriptors.append(desc) |
| |
| dev_record["Descriptors"] = descriptors |
| metadata["FirmwareDeviceIdentificationArea"].append(dev_record) |
| |
| # Force alignment to ensure robustness |
| bytes_consumed = f.tell() - record_start_pos |
| if bytes_consumed != rec_len: |
| print(f" [Info] Re-aligning Record {i}. Consumed {bytes_consumed} / Expected {rec_len}") |
| f.seek(record_start_pos + rec_len) |
| |
| # --- 3. Parse Component Image Information Area --- |
| |
| comp_count = struct.unpack("<H", read_bytes(f, 2))[0] |
| print(f"Found {comp_count} components.") |
| |
| extraction_list = [] |
| |
| for i in range(comp_count): |
| comp_info = {} |
| comp_info["ComponentClassification"] = struct.unpack("<H", read_bytes(f, 2))[0] |
| comp_id = struct.unpack("<H", read_bytes(f, 2))[0] |
| comp_info["ComponentIdentifier"] = comp_id |
| |
| stamp = struct.unpack("<I", read_bytes(f, 4))[0] |
| comp_info["ComponentComparisonStamp"] = f"0x{stamp:08X}" |
| |
| opts_int = struct.unpack("<H", read_bytes(f, 2))[0] |
| comp_info["ComponentOptions"] = parse_bitfield(opts_int, 16) |
| |
| act_int = struct.unpack("<H", read_bytes(f, 2))[0] |
| comp_info["RequestedComponentActivationMethod"] = parse_bitfield(act_int, 16) |
| |
| loc_offset = struct.unpack("<I", read_bytes(f, 4))[0] |
| size = struct.unpack("<I", read_bytes(f, 4))[0] |
| |
| # For Component Info, Creator packs contiguous (Type, Len, String) |
| comp_info["ComponentVersionString"] = parse_contiguous_pldm_string(f) |
| |
| metadata["ComponentImageInformationArea"].append(comp_info) |
| extraction_list.append((loc_offset, size, comp_id, i)) |
| |
| # --- 4. Verify Checksum --- |
| # Read Checksum to advance pointer, but we trust the unpacking logic |
| _ = read_bytes(f, 4) |
| print(f"Header parsed successfully. Final Offset: {f.tell()}") |
| |
| # --- 5. Extract Images --- |
| print(f"Extracting {len(extraction_list)} component images...") |
| |
| for offset, size, comp_id, idx in extraction_list: |
| f.seek(offset) |
| image_data = read_bytes(f, size) |
| |
| filename = f"component_{idx}_{comp_id}.bin" |
| filepath = os.path.join(output_dir, filename) |
| |
| with open(filepath, 'wb') as img_f: |
| img_f.write(image_data) |
| print(f" Extracted: {filepath} (Size: {size} bytes)") |
| |
| # --- 6. Write Metadata JSON --- |
| json_path = os.path.join(output_dir, "metadata.json") |
| with open(json_path, 'w') as json_f: |
| json.dump(metadata, json_f, indent=4) |
| |
| print(f"Metadata written to: {json_path}") |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Unpack PLDM FW Update Package") |
| parser.add_argument("package_file", help="Path to the .pldm package file") |
| parser.add_argument("-o", "--output", default="output", help="Output directory") |
| |
| args = parser.parse_args() |
| |
| try: |
| unpack_package(args.package_file, args.output) |
| except Exception as e: |
| print(f"Error: {e}") |
| sys.exit(1) |
| |
| if __name__ == "__main__": |
| main() |