1#!/usr/bin/env python3
2
3"""Script to create PLDM FW update package"""
4
5import argparse
6import binascii
7import enum
8import json
9import math
10import os
11import struct
12import sys
13from datetime import datetime
14
15from bitarray import bitarray
16from bitarray.util import ba2int
17
18string_types = dict(
19    [
20        ("Unknown", 0),
21        ("ASCII", 1),
22        ("UTF8", 2),
23        ("UTF16", 3),
24        ("UTF16LE", 4),
25        ("UTF16BE", 5),
26    ]
27)
28
29initial_descriptor_type_name_length = {
30    0x0000: ["PCI Vendor ID", 2],
31    0x0001: ["IANA Enterprise ID", 4],
32    0x0002: ["UUID", 16],
33    0x0003: ["PnP Vendor ID", 3],
34    0x0004: ["ACPI Vendor ID", 4],
35}
36
37descriptor_type_name_length = {
38    0x0000: ["PCI Vendor ID", 2],
39    0x0001: ["IANA Enterprise ID", 4],
40    0x0002: ["UUID", 16],
41    0x0003: ["PnP Vendor ID", 3],
42    0x0004: ["ACPI Vendor ID", 4],
43    0x0100: ["PCI Device ID", 2],
44    0x0101: ["PCI Subsystem Vendor ID", 2],
45    0x0102: ["PCI Subsystem ID", 2],
46    0x0103: ["PCI Revision ID", 1],
47    0x0104: ["PnP Product Identifier", 4],
48    0x0105: ["ACPI Product Identifier", 4],
49}
50
51
52class ComponentOptions(enum.IntEnum):
53    """
54    Enum to represent ComponentOptions
55    """
56
57    ForceUpdate = 0
58    UseComponentCompStamp = 1
59
60
61def check_string_length(string):
62    """Check if the length of the string is not greater than 255."""
63    if len(string) > 255:
64        sys.exit("ERROR: Max permitted string length is 255")
65
66
67def write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time):
68    """
69    Write the timestamp into the package header. The timestamp is formatted as
70    series of 13 bytes defined in DSP0240 specification.
71
72        Parameters:
73            pldm_fw_up_pkg: PLDM FW update package
74            release_date_time: Package Release Date Time
75    """
76    time = release_date_time.time()
77    date = release_date_time.date()
78    us_bytes = time.microsecond.to_bytes(3, byteorder="little")
79    pldm_fw_up_pkg.write(
80        struct.pack(
81            "<hBBBBBBBBHB",
82            0,
83            us_bytes[0],
84            us_bytes[1],
85            us_bytes[2],
86            time.second,
87            time.minute,
88            time.hour,
89            date.day,
90            date.month,
91            date.year,
92            0,
93        )
94    )
95
96
97def write_package_version_string(pldm_fw_up_pkg, metadata):
98    """
99    Write PackageVersionStringType, PackageVersionStringLength and
100    PackageVersionString to the package header.
101
102        Parameters:
103            pldm_fw_up_pkg: PLDM FW update package
104            metadata: metadata about PLDM FW update package
105    """
106    # Hardcoded string type to ASCII
107    string_type = string_types["ASCII"]
108    package_version_string = metadata["PackageHeaderInformation"][
109        "PackageVersionString"
110    ]
111    check_string_length(package_version_string)
112    format_string = "<BB" + str(len(package_version_string)) + "s"
113    pldm_fw_up_pkg.write(
114        struct.pack(
115            format_string,
116            string_type,
117            len(package_version_string),
118            package_version_string.encode("ascii"),
119        )
120    )
121
122
123def write_component_bitmap_bit_length(pldm_fw_up_pkg, metadata):
124    """
125    ComponentBitmapBitLength in the package header indicates the number of bits
126    that will be used represent the bitmap in the ApplicableComponents field
127    for a matching device. The value shall be a multiple of 8 and be large
128    enough to contain a bit for each component in the package. The number of
129    components in the JSON file is used to populate the bitmap length.
130
131        Parameters:
132            pldm_fw_up_pkg: PLDM FW update package
133            metadata: metadata about PLDM FW update package
134
135        Returns:
136            ComponentBitmapBitLength: number of bits that will be used
137            represent the bitmap in the ApplicableComponents field for a
138            matching device
139    """
140    # The script supports upto 32 components now
141    max_components = 32
142    bitmap_multiple = 8
143
144    num_components = len(metadata["ComponentImageInformationArea"])
145    if num_components > max_components:
146        sys.exit("ERROR: only upto 32 components supported now")
147    component_bitmap_bit_length = bitmap_multiple * math.ceil(
148        num_components / bitmap_multiple
149    )
150    pldm_fw_up_pkg.write(struct.pack("<H", int(component_bitmap_bit_length)))
151    return component_bitmap_bit_length
152
153
154def write_pkg_header_info(pldm_fw_up_pkg, metadata):
155    """
156    ComponentBitmapBitLength in the package header indicates the number of bits
157    that will be used represent the bitmap in the ApplicableComponents field
158    for a matching device. The value shall be a multiple of 8 and be large
159    enough to contain a bit for each component in the package. The number of
160    components in the JSON file is used to populate the bitmap length.
161
162        Parameters:
163            pldm_fw_up_pkg: PLDM FW update package
164            metadata: metadata about PLDM FW update package
165
166        Returns:
167            ComponentBitmapBitLength: number of bits that will be used
168            represent the bitmap in the ApplicableComponents field for a
169            matching device
170    """
171    uuid = metadata["PackageHeaderInformation"]["PackageHeaderIdentifier"]
172    package_header_identifier = bytearray.fromhex(uuid)
173    pldm_fw_up_pkg.write(package_header_identifier)
174
175    package_header_format_revision = metadata["PackageHeaderInformation"][
176        "PackageHeaderFormatVersion"
177    ]
178    # Size will be computed and updated subsequently
179    package_header_size = 0
180    pldm_fw_up_pkg.write(
181        struct.pack("<BH", package_header_format_revision, package_header_size)
182    )
183
184    try:
185        release_date_time = datetime.strptime(
186            metadata["PackageHeaderInformation"]["PackageReleaseDateTime"],
187            "%d/%m/%Y %H:%M:%S",
188        )
189        write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time)
190    except KeyError:
191        write_pkg_release_date_time(pldm_fw_up_pkg, datetime.now())
192
193    component_bitmap_bit_length = write_component_bitmap_bit_length(
194        pldm_fw_up_pkg, metadata
195    )
196    write_package_version_string(pldm_fw_up_pkg, metadata)
197    return component_bitmap_bit_length
198
199
200def get_applicable_components(device, components, component_bitmap_bit_length):
201    """
202    This function figures out the components applicable for the device and sets
203    the ApplicableComponents bitfield accordingly.
204
205        Parameters:
206            device: device information
207            components: list of components in the package
208            component_bitmap_bit_length: length of the ComponentBitmapBitLength
209
210        Returns:
211            The ApplicableComponents bitfield
212    """
213    applicable_components_list = device["ApplicableComponents"]
214    applicable_components = bitarray(
215        component_bitmap_bit_length, endian="little"
216    )
217    applicable_components.setall(0)
218    for component_index in applicable_components_list:
219        if 0 <= component_index < len(components):
220            applicable_components[component_index] = 1
221        else:
222            sys.exit("ERROR: Applicable Component index not found.")
223    return applicable_components
224
225
226def prepare_record_descriptors(descriptors):
227    """
228    This function processes the Descriptors and prepares the RecordDescriptors
229    section of the the firmware device ID record.
230
231        Parameters:
232            descriptors: Descriptors entry
233
234        Returns:
235            RecordDescriptors, DescriptorCount
236    """
237    record_descriptors = bytearray()
238    vendor_defined_desc_type = 65535
239    vendor_desc_title_str_type_len = 1
240    vendor_desc_title_str_len_len = 1
241    descriptor_count = 0
242
243    for descriptor in descriptors:
244        descriptor_type = descriptor["DescriptorType"]
245        if descriptor_count == 0:
246            if (
247                initial_descriptor_type_name_length.get(descriptor_type)
248                is None
249            ):
250                sys.exit("ERROR: Initial descriptor type not supported")
251        else:
252            if (
253                descriptor_type_name_length.get(descriptor_type) is None
254                and descriptor_type != vendor_defined_desc_type
255            ):
256                sys.exit("ERROR: Descriptor type not supported")
257
258        if descriptor_type == vendor_defined_desc_type:
259            vendor_desc_title_str = descriptor[
260                "VendorDefinedDescriptorTitleString"
261            ]
262            vendor_desc_data = descriptor["VendorDefinedDescriptorData"]
263            check_string_length(vendor_desc_title_str)
264            vendor_desc_title_str_type = string_types["ASCII"]
265            descriptor_length = (
266                vendor_desc_title_str_type_len
267                + vendor_desc_title_str_len_len
268                + len(vendor_desc_title_str)
269                + len(bytearray.fromhex(vendor_desc_data))
270            )
271            format_string = "<HHBB" + str(len(vendor_desc_title_str)) + "s"
272            record_descriptors.extend(
273                struct.pack(
274                    format_string,
275                    descriptor_type,
276                    descriptor_length,
277                    vendor_desc_title_str_type,
278                    len(vendor_desc_title_str),
279                    vendor_desc_title_str.encode("ascii"),
280                )
281            )
282            record_descriptors.extend(bytearray.fromhex(vendor_desc_data))
283            descriptor_count += 1
284        else:
285            descriptor_type = descriptor["DescriptorType"]
286            descriptor_data = descriptor["DescriptorData"]
287            descriptor_length = len(bytearray.fromhex(descriptor_data))
288            if (
289                descriptor_length
290                != descriptor_type_name_length.get(descriptor_type)[1]
291            ):
292                err_string = (
293                    "ERROR: Descriptor type - "
294                    + descriptor_type_name_length.get(descriptor_type)[0]
295                    + " length is incorrect"
296                )
297                sys.exit(err_string)
298            format_string = "<HH"
299            record_descriptors.extend(
300                struct.pack(format_string, descriptor_type, descriptor_length)
301            )
302            record_descriptors.extend(bytearray.fromhex(descriptor_data))
303            descriptor_count += 1
304    return record_descriptors, descriptor_count
305
306
307def write_fw_device_identification_area(
308    pldm_fw_up_pkg, metadata, component_bitmap_bit_length
309):
310    """
311    Write firmware device ID records into the PLDM package header
312
313    This function writes the DeviceIDRecordCount and the
314    FirmwareDeviceIDRecords into the firmware update package by processing the
315    metadata JSON. Currently there is no support for optional
316    FirmwareDevicePackageData.
317
318        Parameters:
319            pldm_fw_up_pkg: PLDM FW update package
320            metadata: metadata about PLDM FW update package
321            component_bitmap_bit_length: length of the ComponentBitmapBitLength
322    """
323    # The spec limits the number of firmware device ID records to 255
324    max_device_id_record_count = 255
325    devices = metadata["FirmwareDeviceIdentificationArea"]
326    device_id_record_count = len(devices)
327    if device_id_record_count > max_device_id_record_count:
328        sys.exit(
329            "ERROR: there can be only upto 255 entries in the                "
330            " FirmwareDeviceIdentificationArea section"
331        )
332
333    # DeviceIDRecordCount
334    pldm_fw_up_pkg.write(struct.pack("<B", device_id_record_count))
335
336    for device in devices:
337        # RecordLength size
338        record_length = 2
339
340        # DescriptorCount
341        record_length += 1
342
343        # DeviceUpdateOptionFlags
344        device_update_option_flags = bitarray(32, endian="little")
345        device_update_option_flags.setall(0)
346        # Continue component updates after failure
347        supported_device_update_option_flags = [0]
348        for option in device["DeviceUpdateOptionFlags"]:
349            if option not in supported_device_update_option_flags:
350                sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry")
351            device_update_option_flags[option] = 1
352        record_length += 4
353
354        # ComponentImageSetVersionStringType supports only ASCII for now
355        component_image_set_version_string_type = string_types["ASCII"]
356        record_length += 1
357
358        # ComponentImageSetVersionStringLength
359        component_image_set_version_string = device[
360            "ComponentImageSetVersionString"
361        ]
362        check_string_length(component_image_set_version_string)
363        record_length += len(component_image_set_version_string)
364        record_length += 1
365
366        # Optional FirmwareDevicePackageData not supported now,
367        # FirmwareDevicePackageDataLength is set to 0x0000
368        fw_device_pkg_data_length = 0
369        record_length += 2
370
371        # ApplicableComponents
372        components = metadata["ComponentImageInformationArea"]
373        applicable_components = get_applicable_components(
374            device, components, component_bitmap_bit_length
375        )
376        applicable_components_bitfield_length = round(
377            len(applicable_components) / 8
378        )
379        record_length += applicable_components_bitfield_length
380
381        # RecordDescriptors
382        descriptors = device["Descriptors"]
383        record_descriptors, descriptor_count = prepare_record_descriptors(
384            descriptors
385        )
386        record_length += len(record_descriptors)
387
388        format_string = (
389            "<HBIBBH"
390            + str(applicable_components_bitfield_length)
391            + "s"
392            + str(len(component_image_set_version_string))
393            + "s"
394        )
395        pldm_fw_up_pkg.write(
396            struct.pack(
397                format_string,
398                record_length,
399                descriptor_count,
400                ba2int(device_update_option_flags),
401                component_image_set_version_string_type,
402                len(component_image_set_version_string),
403                fw_device_pkg_data_length,
404                applicable_components.tobytes(),
405                component_image_set_version_string.encode("ascii"),
406            )
407        )
408        pldm_fw_up_pkg.write(record_descriptors)
409
410
411def get_component_comparison_stamp(component):
412    """
413    Get component comparison stamp from metadata file.
414
415    This function checks if ComponentOptions field is having value 1. For
416    ComponentOptions 1, ComponentComparisonStamp value from metadata file
417    is used and Default value 0xFFFFFFFF is used for other Component Options.
418
419    Parameters:
420        component: Component image info
421    Returns:
422        component_comparison_stamp: Component Comparison stamp
423    """
424    component_comparison_stamp = 0xFFFFFFFF
425    if (
426        int(ComponentOptions.UseComponentCompStamp)
427        in component["ComponentOptions"]
428    ):
429        # Use FD vendor selected value from metadata file
430        if "ComponentComparisonStamp" not in component.keys():
431            sys.exit(
432                "ERROR: ComponentComparisonStamp is required"
433                " when value '1' is specified in ComponentOptions field"
434            )
435        else:
436            try:
437                tmp_component_cmp_stmp = int(
438                    component["ComponentComparisonStamp"], 16
439                )
440                if 0 < tmp_component_cmp_stmp < 0xFFFFFFFF:
441                    component_comparison_stamp = tmp_component_cmp_stmp
442                else:
443                    sys.exit(
444                        "ERROR: Value for ComponentComparisonStamp "
445                        " should be  [0x01 - 0xFFFFFFFE] when "
446                        "ComponentOptions bit is set to"
447                        "'1'(UseComponentComparisonStamp)"
448                    )
449            except ValueError:  # invalid hext format
450                sys.exit("ERROR: Invalid hex for ComponentComparisonStamp")
451    return component_comparison_stamp
452
453
454def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files):
455    """
456    Write component image information area into the PLDM package header
457
458    This function writes the ComponentImageCount and the
459    ComponentImageInformation into the firmware update package by processing
460    the metadata JSON.
461
462    Parameters:
463        pldm_fw_up_pkg: PLDM FW update package
464        metadata: metadata about PLDM FW update package
465        image_files: component images
466    """
467    components = metadata["ComponentImageInformationArea"]
468    # ComponentImageCount
469    pldm_fw_up_pkg.write(struct.pack("<H", len(components)))
470    component_location_offsets = []
471    # ComponentLocationOffset position in individual component image
472    # information
473    component_location_offset_pos = 12
474
475    for component in components:
476        # Record the location of the ComponentLocationOffset to be updated
477        # after appending images to the firmware update package
478        component_location_offsets.append(
479            pldm_fw_up_pkg.tell() + component_location_offset_pos
480        )
481
482        # ComponentClassification
483        component_classification = component["ComponentClassification"]
484        if component_classification < 0 or component_classification > 0xFFFF:
485            sys.exit(
486                "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]"
487            )
488
489        # ComponentIdentifier
490        component_identifier = component["ComponentIdentifier"]
491        if component_identifier < 0 or component_identifier > 0xFFFF:
492            sys.exit("ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]")
493
494        # ComponentComparisonStamp
495        component_comparison_stamp = get_component_comparison_stamp(component)
496
497        # ComponentOptions
498        component_options = bitarray(16, endian="little")
499        component_options.setall(0)
500        supported_component_options = [0, 1, 2]
501        for option in component["ComponentOptions"]:
502            if option not in supported_component_options:
503                sys.exit(
504                    "ERROR: unsupported ComponentOption in                   "
505                    " ComponentImageInformationArea section"
506                )
507            component_options[option] = 1
508
509        # RequestedComponentActivationMethod
510        requested_component_activation_method = bitarray(16, endian="little")
511        requested_component_activation_method.setall(0)
512        supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5]
513        for option in component["RequestedComponentActivationMethod"]:
514            if option not in supported_requested_component_activation_method:
515                sys.exit(
516                    "ERROR: unsupported RequestedComponent                    "
517                    "    ActivationMethod entry"
518                )
519            requested_component_activation_method[option] = 1
520
521        # ComponentLocationOffset
522        component_location_offset = 0
523        # ComponentSize
524        component_size = 0
525        # ComponentVersionStringType
526        component_version_string_type = string_types["ASCII"]
527        # ComponentVersionStringlength
528        # ComponentVersionString
529        component_version_string = component["ComponentVersionString"]
530        check_string_length(component_version_string)
531
532        format_string = "<HHIHHIIBB" + str(len(component_version_string)) + "s"
533        pldm_fw_up_pkg.write(
534            struct.pack(
535                format_string,
536                component_classification,
537                component_identifier,
538                component_comparison_stamp,
539                ba2int(component_options),
540                ba2int(requested_component_activation_method),
541                component_location_offset,
542                component_size,
543                component_version_string_type,
544                len(component_version_string),
545                component_version_string.encode("ascii"),
546            )
547        )
548
549    index = 0
550    pkg_header_checksum_size = 4
551    start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size
552    # Update ComponentLocationOffset and ComponentSize for all the components
553    for offset in component_location_offsets:
554        file_size = os.stat(image_files[index]).st_size
555        pldm_fw_up_pkg.seek(offset)
556        pldm_fw_up_pkg.write(struct.pack("<II", start_offset, file_size))
557        start_offset += file_size
558        index += 1
559    pldm_fw_up_pkg.seek(0, os.SEEK_END)
560
561
562def write_pkg_header_checksum(pldm_fw_up_pkg):
563    """
564    Write PackageHeaderChecksum into the PLDM package header.
565
566        Parameters:
567            pldm_fw_up_pkg: PLDM FW update package
568    """
569    pldm_fw_up_pkg.seek(0)
570    package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read())
571    pldm_fw_up_pkg.seek(0, os.SEEK_END)
572    pldm_fw_up_pkg.write(struct.pack("<I", package_header_checksum))
573
574
575def update_pkg_header_size(pldm_fw_up_pkg):
576    """
577    Update PackageHeader in the PLDM package header. The package header size
578    which is the count of all bytes in the PLDM package header structure is
579    calculated once the package header contents is complete.
580
581        Parameters:
582            pldm_fw_up_pkg: PLDM FW update package
583    """
584    pkg_header_checksum_size = 4
585    file_size = pldm_fw_up_pkg.tell() + pkg_header_checksum_size
586    pkg_header_size_offset = 17
587    # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision
588    pldm_fw_up_pkg.seek(pkg_header_size_offset)
589    pldm_fw_up_pkg.write(struct.pack("<H", file_size))
590    pldm_fw_up_pkg.seek(0, os.SEEK_END)
591
592
593def append_component_images(pldm_fw_up_pkg, image_files):
594    """
595    Append the component images to the firmware update package.
596
597        Parameters:
598            pldm_fw_up_pkg: PLDM FW update package
599            image_files: component images
600    """
601    for image in image_files:
602        with open(image, "rb") as file:
603            for line in file:
604                pldm_fw_up_pkg.write(line)
605
606
607def main():
608    """Create PLDM FW update (DSP0267) package based on a JSON metadata file"""
609    parser = argparse.ArgumentParser()
610    parser.add_argument(
611        "pldmfwuppkgname", help="Name of the PLDM FW update package"
612    )
613    parser.add_argument("metadatafile", help="Path of metadata JSON file")
614    parser.add_argument(
615        "images",
616        nargs="+",
617        help=(
618            "One or more firmware image paths, in the same order as           "
619            " ComponentImageInformationArea entries"
620        ),
621    )
622
623    args = parser.parse_args()
624    image_files = args.images
625    with open(args.metadatafile) as file:
626        try:
627            metadata = json.load(file)
628        except ValueError:
629            sys.exit("ERROR: Invalid metadata JSON file")
630
631    # Validate the number of component images
632    if len(image_files) != len(metadata["ComponentImageInformationArea"]):
633        sys.exit(
634            "ERROR: number of images passed != number of entries            "
635            " in ComponentImageInformationArea"
636        )
637
638    try:
639        with open(args.pldmfwuppkgname, "w+b") as pldm_fw_up_pkg:
640            component_bitmap_bit_length = write_pkg_header_info(
641                pldm_fw_up_pkg, metadata
642            )
643            write_fw_device_identification_area(
644                pldm_fw_up_pkg, metadata, component_bitmap_bit_length
645            )
646            write_component_image_info_area(
647                pldm_fw_up_pkg, metadata, image_files
648            )
649            update_pkg_header_size(pldm_fw_up_pkg)
650            write_pkg_header_checksum(pldm_fw_up_pkg)
651            append_component_images(pldm_fw_up_pkg, image_files)
652            pldm_fw_up_pkg.close()
653    except BaseException:
654        pldm_fw_up_pkg.close()
655        os.remove(args.pldmfwuppkgname)
656        raise
657
658
659if __name__ == "__main__":
660    main()
661