blob: a85a5279e471154c65255e072f205e97f8d326f5 [file] [edit]
#!/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()