utils: add pldm_fwup_pkg_parser.py It is a script generated by Gemini to reverse the pack process done by pldm_fwup_pkg_creator.py. It's helpful to extract the meta data out from a PLDM firmware package from vendor and perform modification and repackage if necessary. Tested: extract the metadata.json and component bin, repack with pldm_fwup_pkg_creator.py, tested PLDM type5 download with the re-packed firmware package. Usage: ./pldm_fwup_pkg_parser.py pldm-fw-package.fwpkg -o extract Found 1 components. Header parsed successfully. Final Offset: 161 Extracting 1 component images... Extracted: extract/component_0_1.bin (Size: 9395200 bytes) Metadata written to: extract/metadata.json Change-Id: Ibb05cbe351d428ee30353578ff495257f36a371e Signed-off-by: Jinliang Wang <jinliangw@google.com>
diff --git a/tools/fw-update/pldm_fwup_pkg_parser.py b/tools/fw-update/pldm_fwup_pkg_parser.py new file mode 100755 index 0000000..a85a527 --- /dev/null +++ b/tools/fw-update/pldm_fwup_pkg_parser.py
@@ -0,0 +1,264 @@ +#!/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()