tools: Add pd, a test tool for libpldm

As per the comment in the implementation:

> `grep 'p.*l.*d.*m' /usr/share/dict/words` found 'palladium', which has
> element symbol Pd.

Change-Id: Idbfb15e3382cccb0fde48dc2d5ee8de14fc30816
Signed-off-by: Andrew Jeffery <andrew@codeconstruct.com.au>
diff --git a/meson.build b/meson.build
index 6f2b11f..f50975c 100644
--- a/meson.build
+++ b/meson.build
@@ -77,6 +77,7 @@
 
 subdir('include')
 subdir('src')
+subdir('tools')
 
 doxygen = find_program('doxygen', required: false)
 if doxygen.found()
diff --git a/tests/data/test.hex b/tests/data/test.hex
new file mode 100644
index 0000000..d53b202
--- /dev/null
+++ b/tests/data/test.hex
@@ -0,0 +1,10 @@
+12 44 d2 64 8d 7d 47 18 a0 30 fc 8a 56 58 7d 5a
+02 94 00 00 e9 07 03 0b 16 03 00 00 00 00 76 02
+08 00 01 04 74 65 73 74 01 18 00 01 00 00 00 00
+01 04 00 00 01 76 30 2e 31 01 00 04 00 9c 01 00
+00 01 18 00 01 00 00 00 00 01 04 00 00 02 76 31
+2e 30 01 00 04 00 9c 01 00 00 02 00 0a 00 00 00
+ff ff ff ff 00 00 01 00 94 00 00 00 01 00 00 00
+01 04 76 30 2e 32 0a 00 00 00 ff ff ff ff 00 00
+01 00 95 00 00 00 01 00 00 00 01 04 76 32 2e 30
+d3 5c 1c 8a 5a a5
diff --git a/tools/meson.build b/tools/meson.build
new file mode 100644
index 0000000..d047267
--- /dev/null
+++ b/tools/meson.build
@@ -0,0 +1,3 @@
+if get_option('abi').contains('testing')
+    executable('pd', 'pd.c', dependencies: [libpldm_dep])
+endif
diff --git a/tools/pd.c b/tools/pd.c
new file mode 100644
index 0000000..fe63a70
--- /dev/null
+++ b/tools/pd.c
@@ -0,0 +1,244 @@
+/* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */
+/* Copyright 2025 Code Construct */
+
+/*
+ * `grep 'p.*l.*d.*m' /usr/share/dict/words` found 'palladium', which has
+ * element symbol Pd.
+ */
+
+#include <assert.h>
+#include <err.h>
+#include <inttypes.h>
+#include <libpldm/firmware_update.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define PD_PACKAGE_BUFFER (1ul * 1024ul * 1024ul)
+
+static void pd_print_bytes(const char *head, const void *_buf, size_t len,
+			   const char *tail)
+{
+	const uint8_t *buf = _buf;
+
+	if (head) {
+		printf("%s", head);
+	}
+
+	while (len-- > 1) {
+		printf("%02" PRIx8 " ", *buf++);
+	}
+
+	printf("%02" PRIx8, *buf++);
+
+	if (tail) {
+		printf("%s", tail);
+	}
+}
+
+static void pd_print_variable_field(const char *head,
+				    const struct variable_field *field,
+				    const char *tail)
+{
+	if (head) {
+		printf("%s", head);
+	}
+
+	if (field && field->ptr) {
+		fwrite(field->ptr, 1, field->length, stdout);
+	}
+
+	if (tail) {
+		printf("%s", tail);
+	}
+}
+
+static void pd_print_typed_string(const char *head, size_t type,
+				  const struct variable_field *string,
+				  const char *tail)
+{
+	switch (type) {
+	case 1:
+		pd_print_variable_field(head, string, tail);
+		break;
+	default:
+		printf("Unsupported string type: %zu\n", type);
+		break;
+	}
+}
+
+static void pd_print_descriptor(const char *head,
+				const struct pldm_descriptor *desc,
+				const char *tail)
+{
+	if (head) {
+		printf("%s", head);
+	}
+
+	assert(desc);
+	switch (desc->descriptor_type) {
+	case 1: {
+		uint32_t pen;
+
+		memcpy(&pen, desc->descriptor_data, sizeof(pen));
+		pen = le32toh(pen);
+		printf("IANA PEN: %" PRIu32, pen);
+		break;
+	}
+	default:
+		printf("Unsupported descriptor type: %" PRIu16,
+		       desc->descriptor_type);
+		break;
+	}
+
+	if (tail) {
+		printf("%s", tail);
+	}
+}
+
+int main(void)
+{
+	struct pldm_package_downstream_device_id_record ddrec;
+	struct pldm_package_component_image_information info;
+	struct pldm_package_firmware_device_id_record fdrec;
+	DEFINE_PLDM_PACKAGE_FORMAT_PIN_FR02H(pin);
+	pldm_package_header_information_pad hdr;
+	struct pldm_package_iter iter;
+	size_t nr_fdrecs = 0;
+	size_t nr_ddrecs = 0;
+	size_t nr_infos = 0;
+	void *package;
+	int status;
+	size_t in;
+	int rc;
+
+	status = EXIT_SUCCESS;
+	package = calloc(PD_PACKAGE_BUFFER, 1);
+	if (!package) {
+		err(EXIT_FAILURE, "malloc");
+	}
+
+	in = fread(package, 1, PD_PACKAGE_BUFFER, stdin);
+	rc = decode_pldm_firmware_update_package(package, in, &pin, &hdr,
+						 &iter);
+	if (rc < 0) {
+		warnx("Failed to parse PLDM package: %s\n",
+		      strerrorname_np(-rc));
+		status = EXIT_FAILURE;
+		goto cleanup_package;
+	}
+
+	printf("Package header\n");
+	pd_print_bytes("\tIdentifier: 0x [ ", hdr.package_header_identifier,
+		       sizeof(hdr.package_header_identifier), " ]\n");
+	printf("\tFormat revision: %" PRIu8 "\n",
+	       hdr.package_header_format_revision);
+	fwrite("\n", 1, 1, stdout);
+
+	foreach_pldm_package_firmware_device_id_record(iter, fdrec, rc)
+	{
+		struct pldm_descriptor desc;
+
+		printf("Firmware device ID record: %zu\n", nr_fdrecs++);
+		printf("\tDevice update option flags: %#.8" PRIx32 "\n",
+		       fdrec.device_update_option_flags.value);
+		pd_print_typed_string(
+			"\tComponent image set version: ",
+			fdrec.component_image_set_version_string_type,
+			&fdrec.component_image_set_version_string, "\n");
+		pd_print_bytes("\tApplicable components: 0x [ ",
+			       fdrec.applicable_components.bitmap.ptr,
+			       fdrec.applicable_components.bitmap.length,
+			       " ]\n");
+
+		printf("\tDescriptors:\n");
+		foreach_pldm_package_firmware_device_id_record_descriptor(
+			iter, fdrec, desc, rc)
+		{
+			pd_print_descriptor("\t\t", &desc, "\n");
+		}
+		if (rc) {
+			warnx("Failed parsing firmware device ID record descriptors: %s\n",
+			      strerrorname_np(-rc));
+			status = EXIT_FAILURE;
+			goto cleanup_package;
+		}
+		fwrite("\n", 1, 1, stdout);
+	}
+	if (rc) {
+		warnx("Failed parsing firmware device ID records: %s\n",
+		      strerrorname_np(-rc));
+		status = EXIT_FAILURE;
+		goto cleanup_package;
+	}
+
+	foreach_pldm_package_downstream_device_id_record(iter, ddrec, rc)
+	{
+		struct pldm_descriptor desc;
+
+		printf("Downstream device ID record: %zu\n", nr_ddrecs++);
+		printf("\tDevice update option flags: %#.4" PRIx32 "\n",
+		       ddrec.update_option_flags.value);
+		pd_print_typed_string(
+			"\tSelf-contained activation min version: ",
+			ddrec.self_contained_activation_min_version_string_type,
+			&ddrec.self_contained_activation_min_version_string,
+			"\n");
+		pd_print_bytes("\tApplicable components: 0x [ ",
+			       ddrec.applicable_components.bitmap.ptr,
+			       ddrec.applicable_components.bitmap.length,
+			       " ]\n");
+		printf("\tDescriptors:\n");
+		foreach_pldm_package_downstream_device_id_record_descriptor(
+			iter, ddrec, desc, rc)
+		{
+			pd_print_descriptor("\t\t", &desc, "\n");
+		}
+		if (rc) {
+			warnx("Failed parsing downstream device ID record descriptors: %s\n",
+			      strerrorname_np(-rc));
+			status = EXIT_FAILURE;
+			goto cleanup_package;
+		}
+		fwrite("\n", 1, 1, stdout);
+	}
+	if (rc) {
+		warnx("Failed parsing downstream device ID records: %s\n",
+		      strerrorname_np(-rc));
+		status = EXIT_FAILURE;
+		goto cleanup_package;
+	}
+
+	foreach_pldm_package_component_image_information(iter, info, rc)
+	{
+		printf("Component image info: %zu\n", nr_infos++);
+		printf("\tComponent classification: %" PRIu16 "\n",
+		       info.component_classification);
+		printf("\tComponent identifier: %" PRIu16 "\n",
+		       info.component_identifier);
+		printf("\tComponent comparison stamp: %" PRIu32 "\n",
+		       info.component_comparison_stamp);
+		printf("\tComponent options: %#.4" PRIx16 "\n",
+		       info.component_options.value);
+		printf("\tRequested activation method: %#.4" PRIx16 "\n",
+		       info.requested_component_activation_method.value);
+		printf("\tComponent image: %p (%zu)\n",
+		       (void *)info.component_image.ptr,
+		       info.component_image.length);
+		pd_print_typed_string("\tComponent version: ",
+				      info.component_version_string_type,
+				      &info.component_version_string, "\n");
+		fwrite("\n", 1, 1, stdout);
+	}
+	if (rc) {
+		warnx("Failed parsing component image information: %s\n",
+		      strerrorname_np(-rc));
+		status = EXIT_FAILURE;
+		goto cleanup_package;
+	}
+
+cleanup_package:
+	free(package);
+
+	exit(status);
+}