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 in components:
219        if component["ComponentIdentifier"] in applicable_components_list:
220            applicable_components[components.index(component)] = 1
221    return applicable_components
222
223
224def prepare_record_descriptors(descriptors):
225    """
226    This function processes the Descriptors and prepares the RecordDescriptors
227    section of the the firmware device ID record.
228
229        Parameters:
230            descriptors: Descriptors entry
231
232        Returns:
233            RecordDescriptors, DescriptorCount
234    """
235    record_descriptors = bytearray()
236    vendor_defined_desc_type = 65535
237    vendor_desc_title_str_type_len = 1
238    vendor_desc_title_str_len_len = 1
239    descriptor_count = 0
240
241    for descriptor in descriptors:
242        descriptor_type = descriptor["DescriptorType"]
243        if descriptor_count == 0:
244            if (
245                initial_descriptor_type_name_length.get(descriptor_type)
246                is None
247            ):
248                sys.exit("ERROR: Initial descriptor type not supported")
249        else:
250            if (
251                descriptor_type_name_length.get(descriptor_type) is None
252                and descriptor_type != vendor_defined_desc_type
253            ):
254                sys.exit("ERROR: Descriptor type not supported")
255
256        if descriptor_type == vendor_defined_desc_type:
257            vendor_desc_title_str = descriptor[
258                "VendorDefinedDescriptorTitleString"
259            ]
260            vendor_desc_data = descriptor["VendorDefinedDescriptorData"]
261            check_string_length(vendor_desc_title_str)
262            vendor_desc_title_str_type = string_types["ASCII"]
263            descriptor_length = (
264                vendor_desc_title_str_type_len
265                + vendor_desc_title_str_len_len
266                + len(vendor_desc_title_str)
267                + len(bytearray.fromhex(vendor_desc_data))
268            )
269            format_string = "<HHBB" + str(len(vendor_desc_title_str)) + "s"
270            record_descriptors.extend(
271                struct.pack(
272                    format_string,
273                    descriptor_type,
274                    descriptor_length,
275                    vendor_desc_title_str_type,
276                    len(vendor_desc_title_str),
277                    vendor_desc_title_str.encode("ascii"),
278                )
279            )
280            record_descriptors.extend(bytearray.fromhex(vendor_desc_data))
281            descriptor_count += 1
282        else:
283            descriptor_type = descriptor["DescriptorType"]
284            descriptor_data = descriptor["DescriptorData"]
285            descriptor_length = len(bytearray.fromhex(descriptor_data))
286            if (
287                descriptor_length
288                != descriptor_type_name_length.get(descriptor_type)[1]
289            ):
290                err_string = (
291                    "ERROR: Descriptor type - "
292                    + descriptor_type_name_length.get(descriptor_type)[0]
293                    + " length is incorrect"
294                )
295                sys.exit(err_string)
296            format_string = "<HH"
297            record_descriptors.extend(
298                struct.pack(format_string, descriptor_type, descriptor_length)
299            )
300            record_descriptors.extend(bytearray.fromhex(descriptor_data))
301            descriptor_count += 1
302    return record_descriptors, descriptor_count
303
304
305def write_fw_device_identification_area(
306    pldm_fw_up_pkg, metadata, component_bitmap_bit_length
307):
308    """
309    Write firmware device ID records into the PLDM package header
310
311    This function writes the DeviceIDRecordCount and the
312    FirmwareDeviceIDRecords into the firmware update package by processing the
313    metadata JSON. Currently there is no support for optional
314    FirmwareDevicePackageData.
315
316        Parameters:
317            pldm_fw_up_pkg: PLDM FW update package
318            metadata: metadata about PLDM FW update package
319            component_bitmap_bit_length: length of the ComponentBitmapBitLength
320    """
321    # The spec limits the number of firmware device ID records to 255
322    max_device_id_record_count = 255
323    devices = metadata["FirmwareDeviceIdentificationArea"]
324    device_id_record_count = len(devices)
325    if device_id_record_count > max_device_id_record_count:
326        sys.exit(
327            "ERROR: there can be only upto 255 entries in the                "
328            " FirmwareDeviceIdentificationArea section"
329        )
330
331    # DeviceIDRecordCount
332    pldm_fw_up_pkg.write(struct.pack("<B", device_id_record_count))
333
334    for device in devices:
335        # RecordLength size
336        record_length = 2
337
338        # DescriptorCount
339        record_length += 1
340
341        # DeviceUpdateOptionFlags
342        device_update_option_flags = bitarray(32, endian="little")
343        device_update_option_flags.setall(0)
344        # Continue component updates after failure
345        supported_device_update_option_flags = [0]
346        for option in device["DeviceUpdateOptionFlags"]:
347            if option not in supported_device_update_option_flags:
348                sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry")
349            device_update_option_flags[option] = 1
350        record_length += 4
351
352        # ComponentImageSetVersionStringType supports only ASCII for now
353        component_image_set_version_string_type = string_types["ASCII"]
354        record_length += 1
355
356        # ComponentImageSetVersionStringLength
357        component_image_set_version_string = device[
358            "ComponentImageSetVersionString"
359        ]
360        check_string_length(component_image_set_version_string)
361        record_length += len(component_image_set_version_string)
362        record_length += 1
363
364        # Optional FirmwareDevicePackageData not supported now,
365        # FirmwareDevicePackageDataLength is set to 0x0000
366        fw_device_pkg_data_length = 0
367        record_length += 2
368
369        # ApplicableComponents
370        components = metadata["ComponentImageInformationArea"]
371        applicable_components = get_applicable_components(
372            device, components, component_bitmap_bit_length
373        )
374        applicable_components_bitfield_length = round(
375            len(applicable_components) / 8
376        )
377        record_length += applicable_components_bitfield_length
378
379        # RecordDescriptors
380        descriptors = device["Descriptors"]
381        record_descriptors, descriptor_count = prepare_record_descriptors(
382            descriptors
383        )
384        record_length += len(record_descriptors)
385
386        format_string = (
387            "<HBIBBH"
388            + str(applicable_components_bitfield_length)
389            + "s"
390            + str(len(component_image_set_version_string))
391            + "s"
392        )
393        pldm_fw_up_pkg.write(
394            struct.pack(
395                format_string,
396                record_length,
397                descriptor_count,
398                ba2int(device_update_option_flags),
399                component_image_set_version_string_type,
400                len(component_image_set_version_string),
401                fw_device_pkg_data_length,
402                applicable_components.tobytes(),
403                component_image_set_version_string.encode("ascii"),
404            )
405        )
406        pldm_fw_up_pkg.write(record_descriptors)
407
408
409def get_component_comparison_stamp(component):
410    """
411    Get component comparison stamp from metadata file.
412
413    This function checks if ComponentOptions field is having value 1. For
414    ComponentOptions 1, ComponentComparisonStamp value from metadata file
415    is used and Default value 0xFFFFFFFF is used for other Component Options.
416
417    Parameters:
418        component: Component image info
419    Returns:
420        component_comparison_stamp: Component Comparison stamp
421    """
422    component_comparison_stamp = 0xFFFFFFFF
423    if (
424        int(ComponentOptions.UseComponentCompStamp)
425        in component["ComponentOptions"]
426    ):
427        # Use FD vendor selected value from metadata file
428        if "ComponentComparisonStamp" not in component.keys():
429            sys.exit(
430                "ERROR: ComponentComparisonStamp is required"
431                " when value '1' is specified in ComponentOptions field"
432            )
433        else:
434            try:
435                tmp_component_cmp_stmp = int(
436                    component["ComponentComparisonStamp"], 16
437                )
438                if 0 < tmp_component_cmp_stmp < 0xFFFFFFFF:
439                    component_comparison_stamp = tmp_component_cmp_stmp
440                else:
441                    sys.exit(
442                        "ERROR: Value for ComponentComparisonStamp "
443                        " should be  [0x01 - 0xFFFFFFFE] when "
444                        "ComponentOptions bit is set to"
445                        "'1'(UseComponentComparisonStamp)"
446                    )
447            except ValueError:  # invalid hext format
448                sys.exit("ERROR: Invalid hex for ComponentComparisonStamp")
449    return component_comparison_stamp
450
451
452def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files):
453    """
454    Write component image information area into the PLDM package header
455
456    This function writes the ComponentImageCount and the
457    ComponentImageInformation into the firmware update package by processing
458    the metadata JSON.
459
460    Parameters:
461        pldm_fw_up_pkg: PLDM FW update package
462        metadata: metadata about PLDM FW update package
463        image_files: component images
464    """
465    components = metadata["ComponentImageInformationArea"]
466    # ComponentImageCount
467    pldm_fw_up_pkg.write(struct.pack("<H", len(components)))
468    component_location_offsets = []
469    # ComponentLocationOffset position in individual component image
470    # information
471    component_location_offset_pos = 12
472
473    for component in components:
474        # Record the location of the ComponentLocationOffset to be updated
475        # after appending images to the firmware update package
476        component_location_offsets.append(
477            pldm_fw_up_pkg.tell() + component_location_offset_pos
478        )
479
480        # ComponentClassification
481        component_classification = component["ComponentClassification"]
482        if component_classification < 0 or component_classification > 0xFFFF:
483            sys.exit(
484                "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]"
485            )
486
487        # ComponentIdentifier
488        component_identifier = component["ComponentIdentifier"]
489        if component_identifier < 0 or component_identifier > 0xFFFF:
490            sys.exit("ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]")
491
492        # ComponentComparisonStamp
493        component_comparison_stamp = get_component_comparison_stamp(component)
494
495        # ComponentOptions
496        component_options = bitarray(16, endian="little")
497        component_options.setall(0)
498        supported_component_options = [0, 1, 2]
499        for option in component["ComponentOptions"]:
500            if option not in supported_component_options:
501                sys.exit(
502                    "ERROR: unsupported ComponentOption in                   "
503                    " ComponentImageInformationArea section"
504                )
505            component_options[option] = 1
506
507        # RequestedComponentActivationMethod
508        requested_component_activation_method = bitarray(16, endian="little")
509        requested_component_activation_method.setall(0)
510        supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5]
511        for option in component["RequestedComponentActivationMethod"]:
512            if option not in supported_requested_component_activation_method:
513                sys.exit(
514                    "ERROR: unsupported RequestedComponent                    "
515                    "    ActivationMethod entry"
516                )
517            requested_component_activation_method[option] = 1
518
519        # ComponentLocationOffset
520        component_location_offset = 0
521        # ComponentSize
522        component_size = 0
523        # ComponentVersionStringType
524        component_version_string_type = string_types["ASCII"]
525        # ComponentVersionStringlength
526        # ComponentVersionString
527        component_version_string = component["ComponentVersionString"]
528        check_string_length(component_version_string)
529
530        format_string = "<HHIHHIIBB" + str(len(component_version_string)) + "s"
531        pldm_fw_up_pkg.write(
532            struct.pack(
533                format_string,
534                component_classification,
535                component_identifier,
536                component_comparison_stamp,
537                ba2int(component_options),
538                ba2int(requested_component_activation_method),
539                component_location_offset,
540                component_size,
541                component_version_string_type,
542                len(component_version_string),
543                component_version_string.encode("ascii"),
544            )
545        )
546
547    index = 0
548    pkg_header_checksum_size = 4
549    start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size
550    # Update ComponentLocationOffset and ComponentSize for all the components
551    for offset in component_location_offsets:
552        file_size = os.stat(image_files[index]).st_size
553        pldm_fw_up_pkg.seek(offset)
554        pldm_fw_up_pkg.write(struct.pack("<II", start_offset, file_size))
555        start_offset += file_size
556        index += 1
557    pldm_fw_up_pkg.seek(0, os.SEEK_END)
558
559
560def write_pkg_header_checksum(pldm_fw_up_pkg):
561    """
562    Write PackageHeaderChecksum into the PLDM package header.
563
564        Parameters:
565            pldm_fw_up_pkg: PLDM FW update package
566    """
567    pldm_fw_up_pkg.seek(0)
568    package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read())
569    pldm_fw_up_pkg.seek(0, os.SEEK_END)
570    pldm_fw_up_pkg.write(struct.pack("<I", package_header_checksum))
571
572
573def update_pkg_header_size(pldm_fw_up_pkg):
574    """
575    Update PackageHeader in the PLDM package header. The package header size
576    which is the count of all bytes in the PLDM package header structure is
577    calculated once the package header contents is complete.
578
579        Parameters:
580            pldm_fw_up_pkg: PLDM FW update package
581    """
582    pkg_header_checksum_size = 4
583    file_size = pldm_fw_up_pkg.tell() + pkg_header_checksum_size
584    pkg_header_size_offset = 17
585    # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision
586    pldm_fw_up_pkg.seek(pkg_header_size_offset)
587    pldm_fw_up_pkg.write(struct.pack("<H", file_size))
588    pldm_fw_up_pkg.seek(0, os.SEEK_END)
589
590
591def append_component_images(pldm_fw_up_pkg, image_files):
592    """
593    Append the component images to the firmware update package.
594
595        Parameters:
596            pldm_fw_up_pkg: PLDM FW update package
597            image_files: component images
598    """
599    for image in image_files:
600        with open(image, "rb") as file:
601            for line in file:
602                pldm_fw_up_pkg.write(line)
603
604
605def main():
606    """Create PLDM FW update (DSP0267) package based on a JSON metadata file"""
607    parser = argparse.ArgumentParser()
608    parser.add_argument(
609        "pldmfwuppkgname", help="Name of the PLDM FW update package"
610    )
611    parser.add_argument("metadatafile", help="Path of metadata JSON file")
612    parser.add_argument(
613        "images",
614        nargs="+",
615        help=(
616            "One or more firmware image paths, in the same order as           "
617            " ComponentImageInformationArea entries"
618        ),
619    )
620
621    args = parser.parse_args()
622    image_files = args.images
623    with open(args.metadatafile) as file:
624        try:
625            metadata = json.load(file)
626        except ValueError:
627            sys.exit("ERROR: Invalid metadata JSON file")
628
629    # Validate the number of component images
630    if len(image_files) != len(metadata["ComponentImageInformationArea"]):
631        sys.exit(
632            "ERROR: number of images passed != number of entries            "
633            " in ComponentImageInformationArea"
634        )
635
636    try:
637        with open(args.pldmfwuppkgname, "w+b") as pldm_fw_up_pkg:
638            component_bitmap_bit_length = write_pkg_header_info(
639                pldm_fw_up_pkg, metadata
640            )
641            write_fw_device_identification_area(
642                pldm_fw_up_pkg, metadata, component_bitmap_bit_length
643            )
644            write_component_image_info_area(
645                pldm_fw_up_pkg, metadata, image_files
646            )
647            update_pkg_header_size(pldm_fw_up_pkg)
648            write_pkg_header_checksum(pldm_fw_up_pkg)
649            append_component_images(pldm_fw_up_pkg, image_files)
650            pldm_fw_up_pkg.close()
651    except BaseException:
652        pldm_fw_up_pkg.close()
653        os.remove(args.pldmfwuppkgname)
654        raise
655
656
657if __name__ == "__main__":
658    main()
659