[libvirt PATCH v3 1/1] Add a PCI/PCIe device VPD Capability

Dmitrii Shcherbakov dmitrii.shcherbakov at canonical.com
Fri Sep 17 10:54:49 UTC 2021


Add support for deserializing the binary PCI/PCIe VPD format and
exposing VPD resources as XML elements in a new nested capability
of PCI/PCIe devices called 'vpd'.

The VPD format is specified in "I.3. VPD Definitions" in PCI specs
(2.2+) and "6.28.1 VPD Format" PCIe 4.0. As section 6.28 in PCIe 4.0
notes, the PCI Local Bus and PCIe VPD formats are binary compatible
and PCIe 4.0 merely started incorporating what was already present in
PCI specs.

Linux kernel exposes a binary blob in the VPD format via sysfs since
v2.6.26 (commit 94e6108803469a37ee1e3c92dafdd1d59298602f) which requires
a parser to interpret.

Signed-off-by: Dmitrii Shcherbakov <dmitrii.shcherbakov at canonical.com>
---
 build-aux/syntax-check.mk                     |   4 +-
 docs/drvnodedev.html.in                       |  46 ++
 docs/formatnode.html.in                       |  24 +-
 docs/schemas/nodedev.rng                      |  40 +
 include/libvirt/libvirt-nodedev.h             |   1 +
 po/POTFILES.in                                |   1 +
 src/conf/node_device_conf.c                   | 258 ++++++
 src/conf/node_device_conf.h                   |   6 +-
 src/conf/virnodedeviceobj.c                   |   7 +-
 src/libvirt_private.syms                      |  17 +
 src/node_device/node_device_driver.c          |   2 +
 src/node_device/node_device_udev.c            |   2 +
 src/util/meson.build                          |   1 +
 src/util/virpci.c                             |  60 ++
 src/util/virpci.h                             |   3 +
 src/util/virpcivpd.c                          | 771 +++++++++++++++++
 src/util/virpcivpd.h                          | 106 +++
 src/util/virpcivpdpriv.h                      |  42 +
 tests/meson.build                             |   1 +
 .../pci_0000_42_00_0_vpd.xml                  |  33 +
 .../pci_0000_42_00_0_vpd.xml                  |   1 +
 tests/nodedevxml2xmltest.c                    |   1 +
 tests/testutils.c                             |  51 ++
 tests/testutils.h                             |   6 +
 tests/virpcitest.c                            |   3 +
 tests/virpcivpdtest.c                         | 777 ++++++++++++++++++
 tools/virsh-nodedev.c                         |   3 +
 27 files changed, 2262 insertions(+), 5 deletions(-)
 create mode 100644 src/util/virpcivpd.c
 create mode 100644 src/util/virpcivpd.h
 create mode 100644 src/util/virpcivpdpriv.h
 create mode 100644 tests/nodedevschemadata/pci_0000_42_00_0_vpd.xml
 create mode 120000 tests/nodedevxml2xmlout/pci_0000_42_00_0_vpd.xml
 create mode 100644 tests/virpcivpdtest.c

diff --git a/build-aux/syntax-check.mk b/build-aux/syntax-check.mk
index cb54c8ba36..a428831380 100644
--- a/build-aux/syntax-check.mk
+++ b/build-aux/syntax-check.mk
@@ -775,9 +775,9 @@ sc_prohibit_windows_special_chars_in_filename:
 	{ echo '$(ME): Windows special chars in filename not allowed' 1>&2; echo exit 1; } || :
 
 sc_prohibit_mixed_case_abbreviations:
-	@prohibit='Pci|Usb|Scsi' \
+	@prohibit='Pci|Usb|Scsi|Vpd' \
 	in_vc_files='\.[ch]$$' \
-	halt='Use PCI, USB, SCSI, not Pci, Usb, Scsi' \
+	halt='Use PCI, USB, SCSI, VPD, not Pci, Usb, Scsi, Vpd' \
 	  $(_sc_search_regexp)
 
 # Require #include <locale.h> in all files that call setlocale()
diff --git a/docs/drvnodedev.html.in b/docs/drvnodedev.html.in
index 70f7e6717d..438d20f2dd 100644
--- a/docs/drvnodedev.html.in
+++ b/docs/drvnodedev.html.in
@@ -185,6 +185,52 @@
   </capability>
 </device></pre>
 
+    <h3><a id="VPDCap">VPD capability</a></h3>
+    <p>
+      A device that exposes a PCI/PCIe VPD capability will include a nested
+      capability <code>vpd</code> which presents all resources (string, read-only
+      and read-write keyword resources) that may be contained in a valid VPD
+      entry. The XML format follows the binary format described in
+      "I.3. VPD Definitions" in PCI Local Bus (2.2+) and the identical format
+      in PCIe 4.0+. At the time of writing, the support for exposing this capability
+      is only present on Linux-based systems (kernel version v2.6.26 is the first one
+      to expose VPD via sysfs which Libvirt relies on). Reading the VPD contents requires
+      root privileges, therefore, <code>virsh nodedev-dumpxml</code> must be executed
+      accordingly.
+      A description of the XML format for the <code>vpd</code> capability can
+      be found <a href="formatnode.html#VPDCap">here</a>.
+    </p>
+    <p>
+      The following example shows a VPD representation for a device that exposes the
+      VPD capability with a string resource and a VPD-R resource. Among other things,
+      the VPD of this particular device includes a unique board serial number in the
+      "SN" field.
+    </p>
+<pre>
+<device>
+  <!-- ... -->
+  <driver>
+    <name>mlx5_core</name>
+  </driver>
+  <capability type='pci'>
+    <!-- ... -->
+    <capability type='vpd'>
+      <resource type='string'>BlueField-2 DPU 25GbE Dual-Port SFP56, 16GB on-board DDR, 1GbE OOB management, Tall Bracket</resource>
+      <resource type='vpd-r'>
+        <field keyword='SN'>MT2113X00000</field>
+        <field keyword='V3'>d644e35a61d0eb11e000e8cef671bf3e</field>
+        <field keyword='VA'>MLX:MN=MLNX:CSKU=V2:UUID=V3:PCI=V0:MODL=BF2H332A</field>
+        <field keyword='PN'>MBF2H332A-AEEOT</field>
+        <field keyword='V2'>MBF2H332A-AEEOT</field>
+        <field keyword='EC'>B1</field>
+        <field keyword='V0'>PCIeGen4 x8</field>
+      </resource>
+    </capability>
+  <!-- ... -->
+  </capability>
+</device>
+</pre>
+
     <h2><a id="MDEV">Mediated devices (MDEVs)</a></h2>
     <p>
       Mediated devices (<span class="since">Since 3.2.0</span>) are software
diff --git a/docs/formatnode.html.in b/docs/formatnode.html.in
index 3b3c3105d4..3e8c771398 100644
--- a/docs/formatnode.html.in
+++ b/docs/formatnode.html.in
@@ -162,7 +162,13 @@
                     This device is capable of creating mediated devices.
                     The sub-elements are summarized in
                     <a href="#MDEVTypesCap">mdev_types capability</a>.
-                 </dd>
+                  </dd>
+                  <dt><code><a id="VPDCapPCI">vpd</a></code></dt>
+                  <dd>
+                    This device exposes a VPD PCI/PCIe capability.
+                    The sub-elements are summarized in
+                    <a href="#VPDCap">vpd capability</a>.
+                  </dd>
                 </dl>
               </dd>
 
@@ -592,5 +598,21 @@
 </device>
     </pre>
 
+    <h3><a id="VPDCap">vpd capability</a></h3>
+
+    <p>
+      <a href="#VPDCapPCI">PCI</a> devices can expose a VPD capability which
+      is optional per PCI Local Bus 2.2+ and PCIe 4.0+ specifications. If
+      the VPD capability is present, then the parent <code>capability</code>
+      element with the <code>vpd</code> type will contain a list of
+      <code>resource</code> elements with contents applicable to a
+      particular resource type (string or keyword resources). The
+      <code>resource</code> element will either contain a string
+      resource value or a list of <code>field</code> elements in
+      the case of keyword resources. <code>field</code> elements
+      contain a <code>keyword</code> attribute corresponding to the
+      2-byte VPD keyword and text as element data representing the
+      field value.
+    </p>
   </body>
 </html>
diff --git a/docs/schemas/nodedev.rng b/docs/schemas/nodedev.rng
index 777227c38a..b68669d771 100644
--- a/docs/schemas/nodedev.rng
+++ b/docs/schemas/nodedev.rng
@@ -222,6 +222,10 @@
       <ref name="mdev_types"/>
     </optional>
 
+    <optional>
+      <ref name="vpd"/>
+    </optional>
+
     <optional>
       <element name="iommuGroup">
         <attribute name="number">
@@ -757,6 +761,42 @@
     </element>
   </define>
 
+  <define name="vpd">
+    <element name="capability">
+      <attribute name="type">
+        <value>vpd</value>
+      </attribute>
+      <oneOrMore>
+        <choice>
+          <element name="resource">
+            <attribute name="type">
+              <choice>
+                <value>string</value>
+              </choice>
+            </attribute>
+            <text/>
+          </element>
+          <element name="resource">
+            <attribute name="type">
+              <choice>
+                <value>vpd-r</value>
+                <value>vpd-w</value>
+              </choice>
+            </attribute>
+            <oneOrMore>
+              <element name="field">
+                <attribute name="keyword">
+                  <data type="string"/>
+                </attribute>
+                <text/>
+              </element>
+            </oneOrMore>
+          </element>
+        </choice>
+      </oneOrMore>
+    </element>
+  </define>
+
   <define name="apDomainRange">
     <choice>
       <data type="string">
diff --git a/include/libvirt/libvirt-nodedev.h b/include/libvirt/libvirt-nodedev.h
index e492634217..245365b07f 100644
--- a/include/libvirt/libvirt-nodedev.h
+++ b/include/libvirt/libvirt-nodedev.h
@@ -84,6 +84,7 @@ typedef enum {
     VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_CARD       = 1 << 18, /* s390 AP Card device */
     VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_QUEUE      = 1 << 19, /* s390 AP Queue */
     VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_MATRIX     = 1 << 20, /* s390 AP Matrix */
+    VIR_CONNECT_LIST_NODE_DEVICES_CAP_VPD           = 1 << 21, /* Device with VPD */
 
     /* filter the devices by active state */
     VIR_CONNECT_LIST_NODE_DEVICES_INACTIVE          = 1 << 30, /* Inactive devices */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c200d7452a..4be4139529 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -302,6 +302,7 @@
 @SRCDIR at src/util/virnvme.c
 @SRCDIR at src/util/virobject.c
 @SRCDIR at src/util/virpci.c
+ at SRCDIR@src/util/virpcivpd.c
 @SRCDIR at src/util/virperf.c
 @SRCDIR at src/util/virpidfile.c
 @SRCDIR at src/util/virpolkit.c
diff --git a/src/conf/node_device_conf.c b/src/conf/node_device_conf.c
index 9bbff97ffd..0108026308 100644
--- a/src/conf/node_device_conf.c
+++ b/src/conf/node_device_conf.c
@@ -36,6 +36,7 @@
 #include "virrandom.h"
 #include "virlog.h"
 #include "virfcp.h"
+#include "virpcivpd.h"
 
 #define VIR_FROM_THIS VIR_FROM_NODEDEV
 
@@ -70,6 +71,7 @@ VIR_ENUM_IMPL(virNodeDevCap,
               "ap_card",
               "ap_queue",
               "ap_matrix",
+              "vpd",
 );
 
 VIR_ENUM_IMPL(virNodeDevNetCap,
@@ -240,6 +242,78 @@ virNodeDeviceCapMdevTypesFormat(virBuffer *buf,
     }
 }
 
+static void
+virNodeDeviceCapVPDStringResourceFormat(virBuffer *buf, virPCIVPDResource *res)
+{
+    virBufferAdjustIndent(buf, 2);
+    virBufferEscapeString(buf, "%s", virPCIVPDStringResourceGetValue((virPCIVPDStringResource *)res));
+    virBufferAdjustIndent(buf, -2);
+}
+
+static void
+virNodeDeviceCapVPDKeywordResourceFormat(virBuffer *buf, virPCIVPDResource *res)
+{
+    virPCIVPDKeywordResource *keyword_res = NULL;
+    g_autofree GHashTableIter *iter = NULL;
+    const gchar *key, *val;
+
+    virBufferAddLit(buf, "\n");
+    virBufferAdjustIndent(buf, 2);
+    keyword_res = (virPCIVPDKeywordResource*)res;
+
+    while ((iter = virPCIVPDKeywordResourceNextField(keyword_res, iter, &key, &val,
+                VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+            virBufferEscapeString(buf, "<field keyword='%s'>", key);
+            virBufferEscapeString(buf, "%s", val);
+            virBufferEscapeString(buf, "</field>\n", key);
+    }
+    virBufferAdjustIndent(buf, -2);
+}
+
+static void
+virNodeDeviceCapVPDResourceFormat(virBuffer *buf, virPCIVPDResource *res)
+{
+    GEnumValue *res_type = NULL;
+    void (*res_format_func)(virBuffer *buf, virPCIVPDResource *res);
+
+    if (G_TYPE_CHECK_INSTANCE_TYPE(res, VIR_TYPE_PCI_VPD_STRING_RESOURCE)) {
+        res_format_func = virNodeDeviceCapVPDStringResourceFormat;
+    } else if (G_TYPE_CHECK_INSTANCE_TYPE(res, VIR_TYPE_PCI_VPD_KEYWORD_RESOURCE))  {
+        res_format_func = virNodeDeviceCapVPDKeywordResourceFormat;
+    } else {
+        // Unexpected resource type. This should not happen unless the PCI(e) specs
+        // change and new resource types are introduced into util.virpcivpd. Either way,
+        // we can only return the control back to the caller here.
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("Unexpected VPD resource type encountered during formatting"));
+        return;
+    }
+    virBufferAdjustIndent(buf, 2);
+
+    res_type = virPCIVPDResourceGetResourceType(res);
+    virBufferEscapeString(buf, "<resource type='%s'>", res_type->value_nick);
+    // Format the resource in a type-specific way.
+    res_format_func(buf, res);
+    virBufferAddLit(buf, "</resource>\n");
+
+    virBufferAdjustIndent(buf, -2);
+}
+
+static void
+virNodeDeviceCapVPDFormat(virBuffer *buf, GList *vpd_resources)
+{
+    if (!g_list_length(vpd_resources)) {
+        return;
+    }
+
+    virBufferAddLit(buf, "<capability type='vpd'>\n");
+    while (vpd_resources) {
+        GList *next = vpd_resources->next;
+        virNodeDeviceCapVPDResourceFormat(buf, vpd_resources->data);
+        vpd_resources = next;
+    }
+    virBufferAddLit(buf, "</capability>\n");
+}
 
 static void
 virNodeDeviceCapPCIDefFormat(virBuffer *buf,
@@ -315,6 +389,9 @@ virNodeDeviceCapPCIDefFormat(virBuffer *buf,
                                         data->pci_dev.mdev_types,
                                         data->pci_dev.nmdev_types);
     }
+    if (data->pci_dev.flags & VIR_NODE_DEV_CAP_FLAG_PCI_VPD) {
+        virNodeDeviceCapVPDFormat(buf, data->pci_dev.vpd_resources);
+    }
     if (data->pci_dev.nIommuGroupDevices) {
         virBufferAsprintf(buf, "<iommuGroup number='%d'>\n",
                           data->pci_dev.iommuGroupNumber);
@@ -673,6 +750,7 @@ virNodeDeviceDefFormat(const virNodeDeviceDef *def)
         case VIR_NODE_DEV_CAP_MDEV_TYPES:
         case VIR_NODE_DEV_CAP_FC_HOST:
         case VIR_NODE_DEV_CAP_VPORTS:
+        case VIR_NODE_DEV_CAP_VPD:
         case VIR_NODE_DEV_CAP_LAST:
             break;
         }
@@ -859,6 +937,130 @@ virNodeDevCapMdevTypesParseXML(xmlXPathContextPtr ctxt,
     return ret;
 }
 
+static GHashTable *
+virNodeDeviceCapVPDParseXMLKeywordResource(xmlXPathContextPtr ctxt)
+{
+    int nfields = -1;
+    g_autofree gchar* field_value = NULL;
+    g_autofree gchar* field_keyword = NULL;
+    g_autoptr(GHashTable) resource_fields = NULL;
+    g_autofree xmlNodePtr *nodes = NULL;
+    xmlNodePtr orignode = NULL;
+    size_t i = 0;
+
+    resource_fields = g_hash_table_new_full(
+            g_str_hash, g_str_equal, g_free, g_free);
+
+    if ((nfields = virXPathNodeSet("./field[@keyword]", ctxt, &nodes)) < 0) {
+        virReportError(VIR_ERR_XML_ERROR, "%s",
+                _("no VPD <field> elements with a keyword specified found"));
+        ctxt->node = orignode;
+        return NULL;
+    }
+
+    orignode = ctxt->node;
+    for (i = 0; i < nfields; i++) {
+        ctxt->node = nodes[i];
+        if (!(field_keyword = virXPathString("string(./@keyword[1])", ctxt))) {
+            virReportError(VIR_ERR_XML_ERROR, "%s",
+                    _("VPD resource field keyword parsing has failed"));
+            continue;
+        }
+        if (!(field_value = virXPathString("string(./text())", ctxt))) {
+            virReportError(VIR_ERR_XML_ERROR, "%s",
+                    _("VPD resource field keyword parsing has failed"));
+            continue;
+        }
+        g_hash_table_insert(resource_fields, g_steal_pointer(&field_keyword),
+                g_steal_pointer(&field_value));
+    }
+    ctxt->node = orignode;
+    return g_steal_pointer(&resource_fields);
+}
+
+static int
+virNodeDeviceCapVPDParseXML(xmlXPathContextPtr ctxt, GList **vpd_resources)
+{
+    xmlNodePtr orignode = NULL;
+    g_autofree gchar* res_text = NULL;
+    g_autofree GHashTable *resource_fields = NULL;
+    g_autofree xmlNodePtr *nodes = NULL;
+    int nresources = -1;
+    g_autofree gchar* res_type_str = NULL;
+    virPCIVPDResourceType type = VIR_PCI_VPD_RESOURCE_TYPE_LAST;
+    GEnumClass *class;
+    size_t i = 0;
+
+    if ((nresources = virXPathNodeSet("./resource[@type]", ctxt, &nodes)) < 0) {
+        virReportError(VIR_ERR_XML_ERROR, "%s",
+                _("no VPD <resource> elements with a type specified found"));
+        return -1;
+    }
+
+    orignode = ctxt->node;
+    for (i = 0; i < nresources; i++) {
+        ctxt->node = nodes[i];
+        if (!(res_type_str = virXPathString("string(./@type[1])", ctxt))) {
+            virReportError(VIR_ERR_XML_ERROR, "%s",
+                    _("VPD resource type parsing has failed"));
+            ctxt->node = orignode;
+            return -1;
+        }
+
+        class = g_type_class_ref(VIR_TYPE_PCI_VPD_RESOURCE_TYPE);
+        type = g_enum_get_value_by_nick(class, res_type_str)->value;
+        g_type_class_unref(class);
+        if (!type) {
+            virReportError(VIR_ERR_XML_ERROR, "%s",
+                    _("Unexpected VPD resource type encountered"));
+            // Skip resources with unknown types.
+            continue;
+        }
+
+        // Create in-memory representations of resources based on their type. If a resource is not
+        // valid, report an error and skip to parsing another resource.
+        switch (type) {
+            case VIR_PCI_VPD_RESOURCE_TYPE_STRING:
+                if (!(res_text = virXPathString("string(./text())", ctxt))) {
+                    virReportError(VIR_ERR_XML_ERROR, "%s",
+                            _("Could not read a string resource text"));
+                    continue;
+                }
+                *vpd_resources = g_list_append(*vpd_resources, virPCIVPDStringResourceNew(
+                            g_steal_pointer(&res_text)));
+                break;
+            case VIR_PCI_VPD_RESOURCE_TYPE_VPD_R:
+                if (!(resource_fields = virNodeDeviceCapVPDParseXMLKeywordResource(ctxt))) {
+                    virReportError(VIR_ERR_XML_ERROR,
+                            _("Could not parse %s VPD resource fields"), res_type_str);
+                    continue;
+                }
+                *vpd_resources = g_list_append(*vpd_resources,
+                        virPCIVPDKeywordResourceNew(g_steal_pointer(&resource_fields), true));
+                break;
+            case VIR_PCI_VPD_RESOURCE_TYPE_VPD_W:
+                if (!(resource_fields = virNodeDeviceCapVPDParseXMLKeywordResource(ctxt))) {
+                    virReportError(VIR_ERR_XML_ERROR,
+                            _("Could not parse %s VPD resource fields"), res_type_str);
+                    continue;
+                }
+                *vpd_resources = g_list_append(*vpd_resources,
+                        virPCIVPDKeywordResourceNew(resource_fields, false));
+                break;
+            case VIR_PCI_VPD_RESOURCE_TYPE_LAST:
+            default:
+                // If the VPD module is aware of a resource type and it has been serialized into
+                // XML while the parser does not then this is a bug.
+                virReportError(VIR_ERR_XML_ERROR,
+                        _("The XML parser has encountered an unsupported resource type %s"),
+                        res_type_str);
+                ctxt->node = orignode;
+                return -1;
+        }
+    }
+    ctxt->node = orignode;
+    return 0;
+}
 
 static int
 virNodeDevAPMatrixCapabilityParseXML(xmlXPathContextPtr ctxt,
@@ -1718,6 +1920,11 @@ virNodeDevPCICapabilityParseXML(xmlXPathContextPtr ctxt,
                                            &pci_dev->nmdev_types) < 0)
             return -1;
         pci_dev->flags |= VIR_NODE_DEV_CAP_FLAG_PCI_MDEV;
+    } else if (STREQ(type, "vpd")) {
+        if (virNodeDeviceCapVPDParseXML(ctxt, &pci_dev->vpd_resources) < 0) {
+            return -1;
+        }
+        pci_dev->flags |= VIR_NODE_DEV_CAP_FLAG_PCI_VPD;
     } else {
         int hdrType = virPCIHeaderTypeFromString(type);
 
@@ -2024,6 +2231,7 @@ virNodeDevCapsDefParseXML(xmlXPathContextPtr ctxt,
     case VIR_NODE_DEV_CAP_VPORTS:
     case VIR_NODE_DEV_CAP_SCSI_GENERIC:
     case VIR_NODE_DEV_CAP_VDPA:
+    case VIR_NODE_DEV_CAP_VPD:
     case VIR_NODE_DEV_CAP_LAST:
         virReportError(VIR_ERR_INTERNAL_ERROR,
                        _("unknown capability type '%d' for '%s'"),
@@ -2287,6 +2495,8 @@ virNodeDevCapsDefFree(virNodeDevCapsDef *caps)
         for (i = 0; i < data->pci_dev.nmdev_types; i++)
             virMediatedDeviceTypeFree(data->pci_dev.mdev_types[i]);
         g_free(data->pci_dev.mdev_types);
+        g_list_free_full(g_steal_pointer(&data->pci_dev.vpd_resources),
+                g_object_unref);
         break;
     case VIR_NODE_DEV_CAP_USB_DEV:
         g_free(data->usb_dev.product_name);
@@ -2352,6 +2562,7 @@ virNodeDevCapsDefFree(virNodeDevCapsDef *caps)
     case VIR_NODE_DEV_CAP_VDPA:
     case VIR_NODE_DEV_CAP_AP_CARD:
     case VIR_NODE_DEV_CAP_AP_QUEUE:
+    case VIR_NODE_DEV_CAP_VPD:
     case VIR_NODE_DEV_CAP_LAST:
         /* This case is here to shutup the compiler */
         break;
@@ -2418,6 +2629,7 @@ virNodeDeviceUpdateCaps(virNodeDeviceDef *def)
         case VIR_NODE_DEV_CAP_VDPA:
         case VIR_NODE_DEV_CAP_AP_CARD:
         case VIR_NODE_DEV_CAP_AP_QUEUE:
+        case VIR_NODE_DEV_CAP_VPD:
         case VIR_NODE_DEV_CAP_LAST:
             break;
         }
@@ -2489,6 +2701,10 @@ virNodeDeviceCapsListExport(virNodeDeviceDef *def,
                 MAYBE_ADD_CAP(VIR_NODE_DEV_CAP_MDEV_TYPES);
                 ncaps++;
             }
+            if (flags & VIR_NODE_DEV_CAP_FLAG_PCI_VPD) {
+                MAYBE_ADD_CAP(VIR_NODE_DEV_CAP_VPD);
+                ncaps++;
+            }
         }
 
         if (caps->data.type == VIR_NODE_DEV_CAP_CSS_DEV) {
@@ -2749,6 +2965,44 @@ virNodeDeviceGetMdevTypesCaps(const char *sysfspath,
 }
 
 
+/**
+ * virNodeDeviceGetPCIVPDDynamicCap:
+ * @pci_dev: a virNodeDevCapPCIDev for which to add VPD resources.
+ *
+ * While VPD has a read-only portion, there may be a read-write portion per
+ * the specs which may change dynamically.
+ *
+ * Returns: 0 if the operation was successful (even if VPD is not present for
+ * that device since it is optional in the specs, -1 otherwise.
+ */
+static int
+virNodeDeviceGetPCIVPDDynamicCap(virNodeDevCapPCIDev *pci_dev)
+{
+    g_autoptr(virPCIDevice) pciDev = NULL;
+    virPCIDeviceAddress devAddr;
+    g_autolist(virPCIVPDResource) vpd_resource_list = NULL;
+
+    devAddr.domain = pci_dev->domain;
+    devAddr.bus = pci_dev->bus;
+    devAddr.slot = pci_dev->slot;
+    devAddr.function = pci_dev->function;
+
+    if (!(pciDev = virPCIDeviceNew(&devAddr)))
+        return -1;
+
+    if (virPCIDeviceHasVPD(pciDev)) {
+        // VPD is optional in PCI(e) specs. If it is there, attempt to add it.
+        if ((vpd_resource_list = virPCIDeviceGetVPDResources(pciDev))) {
+            pci_dev->flags |= VIR_NODE_DEV_CAP_FLAG_PCI_VPD;
+            pci_dev->vpd_resources = g_steal_pointer(&vpd_resource_list);
+        } else {
+            g_list_free_full(pci_dev->vpd_resources, g_object_unref);
+        }
+    }
+    return 0;
+}
+
+
 /* virNodeDeviceGetPCIDynamicCaps() get info that is stored in sysfs
  * about devices related to this device, i.e. things that can change
  * without this device itself changing. These must be refreshed
@@ -2759,6 +3013,7 @@ int
 virNodeDeviceGetPCIDynamicCaps(const char *sysfsPath,
                                virNodeDevCapPCIDev *pci_dev)
 {
+
     if (virNodeDeviceGetPCISRIOVCaps(sysfsPath, pci_dev) < 0 ||
         virNodeDeviceGetPCIIOMMUGroupCaps(pci_dev) < 0)
         return -1;
@@ -2771,6 +3026,9 @@ virNodeDeviceGetPCIDynamicCaps(const char *sysfsPath,
     if (pci_dev->nmdev_types > 0)
         pci_dev->flags |= VIR_NODE_DEV_CAP_FLAG_PCI_MDEV;
 
+    if (virNodeDeviceGetPCIVPDDynamicCap(pci_dev) < 0)
+        return -1;
+
     return 0;
 }
 
diff --git a/src/conf/node_device_conf.h b/src/conf/node_device_conf.h
index 5a4d9c7a55..32e59fa52a 100644
--- a/src/conf/node_device_conf.h
+++ b/src/conf/node_device_conf.h
@@ -69,6 +69,7 @@ typedef enum {
     VIR_NODE_DEV_CAP_AP_CARD,           /* s390 AP Card device */
     VIR_NODE_DEV_CAP_AP_QUEUE,          /* s390 AP Queue */
     VIR_NODE_DEV_CAP_AP_MATRIX,         /* s390 AP Matrix device */
+    VIR_NODE_DEV_CAP_VPD,               /* Device provides VPD */
 
     VIR_NODE_DEV_CAP_LAST
 } virNodeDevCapType;
@@ -103,6 +104,7 @@ typedef enum {
     VIR_NODE_DEV_CAP_FLAG_PCI_VIRTUAL_FUNCTION      = (1 << 1),
     VIR_NODE_DEV_CAP_FLAG_PCIE                      = (1 << 2),
     VIR_NODE_DEV_CAP_FLAG_PCI_MDEV                  = (1 << 3),
+    VIR_NODE_DEV_CAP_FLAG_PCI_VPD                   = (1 << 4),
 } virNodeDevPCICapFlags;
 
 typedef enum {
@@ -181,6 +183,7 @@ struct _virNodeDevCapPCIDev {
     int hdrType; /* enum virPCIHeaderType or -1 */
     virMediatedDeviceType **mdev_types;
     size_t nmdev_types;
+    GList *vpd_resources;
 };
 
 typedef struct _virNodeDevCapUSBDev virNodeDevCapUSBDev;
@@ -418,7 +421,8 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(virNodeDevCapsDef, virNodeDevCapsDefFree);
                  VIR_CONNECT_LIST_NODE_DEVICES_CAP_VDPA          | \
                  VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_CARD       | \
                  VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_QUEUE      | \
-                 VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_MATRIX)
+                 VIR_CONNECT_LIST_NODE_DEVICES_CAP_AP_MATRIX     | \
+                 VIR_CONNECT_LIST_NODE_DEVICES_CAP_VPD)
 
 #define VIR_CONNECT_LIST_NODE_DEVICES_FILTERS_ACTIVE \
     VIR_CONNECT_LIST_NODE_DEVICES_ACTIVE | \
diff --git a/src/conf/virnodedeviceobj.c b/src/conf/virnodedeviceobj.c
index 9a9841576a..165ec1f1dd 100644
--- a/src/conf/virnodedeviceobj.c
+++ b/src/conf/virnodedeviceobj.c
@@ -701,6 +701,9 @@ virNodeDeviceObjHasCap(const virNodeDeviceObj *obj,
             if (type == VIR_NODE_DEV_CAP_MDEV_TYPES &&
                 (cap->data.pci_dev.flags & VIR_NODE_DEV_CAP_FLAG_PCI_MDEV))
                 return true;
+            if (type == VIR_NODE_DEV_CAP_VPD &&
+                (cap->data.pci_dev.flags & VIR_NODE_DEV_CAP_FLAG_PCI_VPD))
+                return true;
             break;
 
         case VIR_NODE_DEV_CAP_SCSI_HOST:
@@ -742,6 +745,7 @@ virNodeDeviceObjHasCap(const virNodeDeviceObj *obj,
         case VIR_NODE_DEV_CAP_VDPA:
         case VIR_NODE_DEV_CAP_AP_CARD:
         case VIR_NODE_DEV_CAP_AP_QUEUE:
+        case VIR_NODE_DEV_CAP_VPD:
         case VIR_NODE_DEV_CAP_LAST:
             break;
         }
@@ -899,7 +903,8 @@ virNodeDeviceObjMatch(virNodeDeviceObj *obj,
               MATCH_CAP(VDPA)          ||
               MATCH_CAP(AP_CARD)       ||
               MATCH_CAP(AP_QUEUE)      ||
-              MATCH_CAP(AP_MATRIX)))
+              MATCH_CAP(AP_MATRIX)     ||
+              MATCH_CAP(VPD)))
             return false;
     }
 
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index ace35d709f..5bb4f28153 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -2986,7 +2986,9 @@ virPCIDeviceGetReprobe;
 virPCIDeviceGetStubDriver;
 virPCIDeviceGetUnbindFromStub;
 virPCIDeviceGetUsedBy;
+virPCIDeviceGetVPDResources;
 virPCIDeviceHasPCIExpressLink;
+virPCIDeviceHasVPD;
 virPCIDeviceIsAssignable;
 virPCIDeviceIsPCIExpress;
 virPCIDeviceListAdd;
@@ -3570,6 +3572,21 @@ virVHBAManageVport;
 virVHBAPathExists;
 
 
+# util/virpcivpd.h
+
+virPCIVPDKeywordResourceNew;
+virPCIVPDKeywordResourceNextField;
+virPCIVPDParse;
+virPCIVPDParseVPDLargeResourceFields;
+virPCIVPDParseVPDLargeResourceString;
+virPCIVPDReadVPDBytes;
+virPCIVPDResourceGetFieldValueFormat;
+virPCIVPDResourceGetResourceType;
+virPCIVPDResourceIsExpectedKeyword;
+virPCIVPDResourceIsValidTextValue;
+virPCIVPDStringResourceGetValue;
+virPCIVPDStringResourceNew;
+
 # util/virvsock.h
 virVsockAcquireGuestCid;
 virVsockSetGuestCid;
diff --git a/src/node_device/node_device_driver.c b/src/node_device/node_device_driver.c
index 3bc6eb1c11..d19ed7d948 100644
--- a/src/node_device/node_device_driver.c
+++ b/src/node_device/node_device_driver.c
@@ -708,6 +708,7 @@ nodeDeviceObjFormatAddress(virNodeDeviceObj *obj)
         case VIR_NODE_DEV_CAP_VDPA:
         case VIR_NODE_DEV_CAP_AP_CARD:
         case VIR_NODE_DEV_CAP_AP_QUEUE:
+        case VIR_NODE_DEV_CAP_VPD:
         case VIR_NODE_DEV_CAP_LAST:
             break;
         }
@@ -1983,6 +1984,7 @@ int nodeDeviceDefValidate(virNodeDeviceDef *def,
             case VIR_NODE_DEV_CAP_AP_CARD:
             case VIR_NODE_DEV_CAP_AP_QUEUE:
             case VIR_NODE_DEV_CAP_AP_MATRIX:
+            case VIR_NODE_DEV_CAP_VPD:
             case VIR_NODE_DEV_CAP_LAST:
                 break;
         }
diff --git a/src/node_device/node_device_udev.c b/src/node_device/node_device_udev.c
index 71f0bef827..7c3bb762b3 100644
--- a/src/node_device/node_device_udev.c
+++ b/src/node_device/node_device_udev.c
@@ -42,6 +42,7 @@
 #include "virnetdev.h"
 #include "virmdev.h"
 #include "virutil.h"
+#include "virpcivpd.h"
 
 #include "configmake.h"
 
@@ -1397,6 +1398,7 @@ udevGetDeviceDetails(struct udev_device *device,
     case VIR_NODE_DEV_CAP_AP_MATRIX:
         return udevProcessAPMatrix(device, def);
     case VIR_NODE_DEV_CAP_MDEV_TYPES:
+    case VIR_NODE_DEV_CAP_VPD:
     case VIR_NODE_DEV_CAP_SYSTEM:
     case VIR_NODE_DEV_CAP_FC_HOST:
     case VIR_NODE_DEV_CAP_VPORTS:
diff --git a/src/util/meson.build b/src/util/meson.build
index 05934f6841..24350a3e67 100644
--- a/src/util/meson.build
+++ b/src/util/meson.build
@@ -105,6 +105,7 @@ util_sources = [
   'virutil.c',
   'viruuid.c',
   'virvhba.c',
+  'virpcivpd.c',
   'virvsock.c',
   'virxml.c',
 ]
diff --git a/src/util/virpci.c b/src/util/virpci.c
index f307580a53..60c6e20906 100644
--- a/src/util/virpci.c
+++ b/src/util/virpci.c
@@ -37,6 +37,7 @@
 #include "virkmod.h"
 #include "virstring.h"
 #include "viralloc.h"
+#include "virpcivpd.h"
 
 VIR_LOG_INIT("util.pci");
 
@@ -2640,6 +2641,52 @@ virPCIGetVirtualFunctionInfo(const char *vf_sysfs_device_path,
     return 0;
 }
 
+
+gboolean virPCIDeviceHasVPD(virPCIDevice *dev)
+{
+    g_autofree char *vpd_path = NULL;
+    vpd_path = virPCIFile(dev->name, "vpd");
+    if (!virFileExists(vpd_path)) {
+        VIR_INFO("Device VPD file does not exist %s", vpd_path);
+        return false;
+    } else if (!virFileIsRegular(vpd_path)) {
+        VIR_WARN("VPD path does not point to a regular file %s", vpd_path);
+        return false;
+    }
+    return true;
+}
+
+/**
+ * virPCIDeviceGetVPDResources:
+ * @dev: a PCI device to get a list of VPD resources for.
+ *
+ * Obtain resources stored in the PCI device's Vital Product Data (VPD).
+ * VPD is optional in both PCI Local Bus and PCIe specifications so there is
+ * no guarantee it will be there for a particular device.
+ *
+ * Returns: a pointer to a list of VPDResource types which needs to be freed by the caller or
+ * NULL if getting it failed for some reason.
+ */
+GList *
+virPCIDeviceGetVPDResources(virPCIDevice *dev)
+{
+    g_autofree char *vpd_path = NULL;
+    int fd;
+
+    vpd_path = virPCIFile(dev->name, "vpd");
+    if (!virPCIDeviceHasVPD(dev)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, _("Device %s does not have a VPD"),
+                virPCIDeviceGetName(dev));
+        return NULL;
+    }
+    if ((fd = open(vpd_path, O_RDONLY)) < 0) {
+        virReportSystemError(-fd,
+                             _("Failed to open a VPD file '%s'"), vpd_path);
+        return NULL;
+    }
+    return virPCIVPDParse(fd);
+}
+
 #else
 static const char *unsupported = N_("not supported on non-linux platforms");
 
@@ -2713,6 +2760,19 @@ virPCIGetVirtualFunctionInfo(const char *vf_sysfs_device_path G_GNUC_UNUSED,
     virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _(unsupported));
     return -1;
 }
+
+gboolean virPCIDeviceHasVPD(virPCIDevice *dev)
+{
+    virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _(unsupported));
+    return NULL;
+}
+
+GList *
+virPCIDeviceGetVPDResources(virPCIDevice *dev)
+{
+    virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _(unsupported));
+    return NULL;
+}
 #endif /* __linux__ */
 
 int
diff --git a/src/util/virpci.h b/src/util/virpci.h
index 9a3db6c6d8..a89561496b 100644
--- a/src/util/virpci.h
+++ b/src/util/virpci.h
@@ -269,6 +269,9 @@ int virPCIGetVirtualFunctionInfo(const char *vf_sysfs_device_path,
                                  char **pfname,
                                  int *vf_index);
 
+gboolean virPCIDeviceHasVPD(virPCIDevice *dev);
+GList * virPCIDeviceGetVPDResources(virPCIDevice *dev);
+
 int virPCIDeviceUnbind(virPCIDevice *dev);
 int virPCIDeviceRebind(virPCIDevice *dev);
 int virPCIDeviceGetDriverPathAndName(virPCIDevice *dev,
diff --git a/src/util/virpcivpd.c b/src/util/virpcivpd.c
new file mode 100644
index 0000000000..417c7a11d2
--- /dev/null
+++ b/src/util/virpcivpd.c
@@ -0,0 +1,771 @@
+/*
+ * virpcivpd.c: helper APIs for working with the PCI/PCIe VPD capability
+ *
+ * Copyright (C) 2021 Canonical Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <unistd.h>
+
+#define LIBVIRT_VIRPCIVPDPRIV_H_ALLOW
+
+#include "virpcivpdpriv.h"
+#include "virlog.h"
+#include "virerror.h"
+#include "virfile.h"
+
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+VIR_LOG_INIT("util.pcivpd");
+
+GType vir_pci_vpd_resource_type_get_type(void)
+{
+  static GType vir_pci_vpd_resource_type;
+
+  static const GEnumValue resource_types[] = {
+    {VIR_PCI_VPD_RESOURCE_TYPE_STRING, "String resource", "string"},
+    {VIR_PCI_VPD_RESOURCE_TYPE_VPD_R, "Read-only resource", "vpd-r"},
+    {VIR_PCI_VPD_RESOURCE_TYPE_VPD_W, "Read-write resource", "vpd-w"},
+    {VIR_PCI_VPD_RESOURCE_TYPE_LAST, "last", "last"},
+    {0, NULL, NULL},
+  };
+
+  if (!vir_pci_vpd_resource_type) {
+    vir_pci_vpd_resource_type = g_enum_register_static("virPCIVPDResourceType", resource_types);
+  }
+  return vir_pci_vpd_resource_type;
+}
+
+static gboolean virPCIVPDResourceIsVendorKeyword(const gchar* keyword)
+{
+    return g_str_has_prefix(keyword, "V");
+}
+
+static gboolean virPCIVPDResourceIsSystemKeyword(const gchar* keyword)
+{
+    // Special-case the system-specific keywords since they share the "Y" prefix with "YA".
+    return g_str_has_prefix(keyword, "Y") && STRNEQ(keyword, "YA");
+}
+
+static gchar* virPCIVPDResourceGetKeywordPrefix(const gchar* keyword)
+{
+    g_autofree gchar *key = NULL;
+
+    // Keywords must have a length of 2 bytes.
+    if (strlen(keyword) != 2) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                _("The keyword length is not 2 bytes: %s"), keyword);
+        return NULL;
+    } else if (!(g_ascii_isalnum(keyword[0]) && g_ascii_isalnum(keyword[1]))) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                _("The keyword contains non-alphanumeric ASCII characters"));
+        return NULL;
+    }
+    // Special-case the system-specific keywords since they share the "Y" prefix with "YA".
+    if (virPCIVPDResourceIsSystemKeyword(keyword) || virPCIVPDResourceIsVendorKeyword(keyword)) {
+        key = g_strndup(keyword, 1);
+    } else {
+        key = g_strndup(keyword, 2);
+    }
+    return g_steal_pointer(&key);
+}
+
+/**
+ * virPCIVPDResourceGetFieldValueFormat:
+ * @keyword: A keyword for which to get a value type
+ *
+ * Returns: a virPCIVPDResourceFieldValueFormat value which specifies the field value type for
+ * a provided keyword based on the static information from PCI(e) specs.
+ */
+virPCIVPDResourceFieldValueFormat
+virPCIVPDResourceGetFieldValueFormat(const gchar* keyword)
+{
+    static GHashTable *fieldValueFormats;
+    g_autofree gchar *key = NULL;
+    gpointer key_val = NULL;
+    virPCIVPDResourceFieldValueFormat format = VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST;
+
+    // Keywords are expected to be 2 bytes in length which is defined in the specs.
+    if (strlen(keyword) != 2) {
+        return VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST;
+    }
+
+    if (!fieldValueFormats) {
+        /* Initialize a hash table once with static format metadata coming from the PCI(e) specs.
+           The VPD format does not embed format metadata into the resource records so it is not
+           possible to do format discovery without static information. Legacy PICMIG keywords
+           are not included.
+           */
+        fieldValueFormats = g_hash_table_new(g_str_hash, g_str_equal);
+        // Extended capability. Contains binary data per PCI(e) specs.
+        g_hash_table_insert(fieldValueFormats, g_strdup("CP"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_BINARY));
+        // Engineering Change Level of an Add-in Card.
+        g_hash_table_insert(fieldValueFormats, g_strdup("EC"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        // Manufacture ID
+        g_hash_table_insert(fieldValueFormats, g_strdup("MN"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        // Add-in Card Part Number
+        g_hash_table_insert(fieldValueFormats, g_strdup("PN"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        // Checksum and Reserved
+        g_hash_table_insert(fieldValueFormats, g_strdup("RV"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD));
+        // Remaining Read/Write Area
+        g_hash_table_insert(fieldValueFormats, g_strdup("RW"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR));
+        // Serial Number
+        g_hash_table_insert(fieldValueFormats, g_strdup("SN"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        // Asset Tag Identifier
+        g_hash_table_insert(fieldValueFormats, g_strdup("YA"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        /* This is a vendor specific item and the characters are alphanumeric. The second
+           character (x) of the keyword can be 0 through Z so only the first one is stored. */
+        g_hash_table_insert(fieldValueFormats, g_strdup("V"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+        /* This is a system specific item and the characters are alphanumeric.
+           The second character (x) of the keyword can be 0 through 9 and B through Z. */
+        g_hash_table_insert(fieldValueFormats, g_strdup("Y"),
+                GINT_TO_POINTER(VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT));
+    }
+
+    // The system and vendor-specific keywords have a variable part - lookup
+    // the prefix significant for determining the value format.
+    key = virPCIVPDResourceGetKeywordPrefix(keyword);
+    if (key) {
+        key_val = g_hash_table_lookup(fieldValueFormats, key);
+        if (key_val) {
+            format = GPOINTER_TO_INT(key_val);
+        }
+    }
+    return format;
+}
+
+/**
+ * virPCIVPDResourceIsExpectedKeyword:
+ * @keyword: A keyword to assess.
+ * @read_only: A parameter indicating whether the resource is read-only or not.
+ *
+ * Returns: a boolean indicating whether this keyword is expected to be in a
+ * read-only or read-write keyword resource or not. The expectations are based
+ * on the keywords specified in relevant sections of PCI(e) specifications
+ * ("I.3. VPD Definitions" in PCI specs, "6.28.1 VPD Format" PCIe 4.0).
+ */
+gboolean virPCIVPDResourceIsExpectedKeyword(const gchar* keyword, gboolean read_only)
+{
+    g_autofree gchar *key = NULL;
+    static const char *expected_readonly_keys[] = {
+        "CP", "EC", "FG", "LC", "MN",
+        "PG", "PN", "RV", "SN", "V", NULL};
+    static const char *expected_readwrite_keys[] = {"V", "Y", "YA", "RW", NULL};
+
+    key = virPCIVPDResourceGetKeywordPrefix(keyword);
+    if (!key) {
+        return false;
+    }
+    if (read_only) {
+        return g_strv_contains(expected_readonly_keys, key);
+    } else {
+        return g_strv_contains(expected_readwrite_keys, key);
+    }
+}
+
+/**
+ * virPCIVPDResourceIsValidTextValue:
+ * @value: A NULL-terminated string to assess.
+ *
+ * Returns: a boolean indicating whether this value is a valid string resource
+ * value or text field value. The expectations are based on the keywords specified
+ * in relevant sections of PCI(e) specifications
+ * ("I.3. VPD Definitions" in PCI specs, "6.28.1 VPD Format" PCIe 4.0).
+ */
+gboolean virPCIVPDResourceIsValidTextValue(const gchar* value)
+{
+    /*
+     * The PCI(e) specs mention alphanumeric characters when talking about text fields
+     * and the string resource but also include spaces and dashes in the provided example.
+     * Dots, commas, equal signs have also been observed in values used by major device vendors.
+     * The specs do not specify a full set of allowed code points and for Libvirt it is important
+     * to keep values in the ranges allowed within XML elements (mainly excluding less-than,
+     * greater-than and ampersand).
+     */
+    if (!g_regex_match_simple("^[a-zA-Z0-9\\-_\\s,.:=]*$", value, 0, 0)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                _("The provided value contains invalid characters: %s"), value);
+        return false;
+    }
+    return true;
+}
+
+// virPCIVPDResource
+
+typedef enum
+{
+  PROP_RESOURCE_TYPE = 1,
+  N_PROPERTIES
+} virPCIVPDResourceProperty;
+
+typedef struct _virPCIVPDResourcePrivate {
+    GObject parent;
+    virPCIVPDResourceType resource_type;
+} virPCIVPDResourcePrivate;
+
+
+static void vir_pci_vpd_resource_class_init(virPCIVPDResourceClass *klass);
+static void vir_pci_vpd_resource_init(virPCIVPDResource *res);
+static void vir_pci_vpd_resource_finalize(GObject *object);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(virPCIVPDResource, vir_pci_vpd_resource, G_TYPE_OBJECT)
+
+static void vir_pci_vpd_resource_init(virPCIVPDResource *self)
+{
+  virPCIVPDResourcePrivate *priv;
+  priv = vir_pci_vpd_resource_get_instance_private(self);
+  priv->resource_type = VIR_PCI_VPD_RESOURCE_TYPE_LAST;
+}
+
+static void
+vir_pci_vpd_resource_set_property(GObject *object, guint property_id,
+        const GValue *value, GParamSpec *pspec)
+{
+  virPCIVPDResource *self = VIR_PCI_VPD_RESOURCE(object);
+  virPCIVPDResourcePrivate *priv;
+  priv = vir_pci_vpd_resource_get_instance_private(self);
+
+  switch ((virPCIVPDResourceProperty) property_id) {
+    case PROP_RESOURCE_TYPE:
+        priv->resource_type = g_value_get_enum(value);
+      break;
+    case N_PROPERTIES:
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+vir_pci_vpd_resource_get_property(GObject *object, guint property_id,
+        GValue *value, GParamSpec *pspec)
+{
+  virPCIVPDResource *self = VIR_PCI_VPD_RESOURCE(object);
+  virPCIVPDResourcePrivate *priv;
+  priv = vir_pci_vpd_resource_get_instance_private(self);
+
+  switch ((virPCIVPDResourceProperty) property_id) {
+    case PROP_RESOURCE_TYPE:
+      g_value_set_enum(value, priv->resource_type);
+      break;
+    case N_PROPERTIES:
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+      break;
+    }
+}
+
+static void vir_pci_vpd_resource_class_init(virPCIVPDResourceClass *klass)
+{
+    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+    obj_class->set_property = vir_pci_vpd_resource_set_property;
+    obj_class->get_property = vir_pci_vpd_resource_get_property;
+
+    g_object_class_install_property(obj_class, PROP_RESOURCE_TYPE,
+            g_param_spec_enum("resource_type", "Resource type",
+                "A VPD resource type per PCI(e) specifications.",
+                VIR_TYPE_PCI_VPD_RESOURCE_TYPE, VIR_PCI_VPD_RESOURCE_TYPE_LAST,
+                G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
+    obj_class->finalize = vir_pci_vpd_resource_finalize;
+}
+
+static void vir_pci_vpd_resource_finalize(GObject *object)
+{
+    G_OBJECT_CLASS(vir_pci_vpd_resource_parent_class)->finalize(object);
+}
+
+GEnumValue *
+virPCIVPDResourceGetResourceType(virPCIVPDResource *res)
+{
+    GValue gval = G_VALUE_INIT;
+    GEnumValue *enum_val;
+    GEnumClass *class;
+
+    g_object_get_property(G_OBJECT(res), "resource_type", &gval);
+
+    class = g_type_class_ref(VIR_TYPE_PCI_VPD_RESOURCE_TYPE);
+
+    enum_val = g_enum_get_value(class, g_value_get_enum(&gval));
+    g_type_class_unref(class);
+    return enum_val;
+}
+
+
+// virPCIVPDStringResource
+
+struct _virPCIVPDStringResource {
+    virPCIVPDResource parent;
+    gchar *value;
+};
+G_DEFINE_TYPE(virPCIVPDStringResource, vir_pci_vpd_string_resource, VIR_TYPE_PCI_VPD_RESOURCE)
+
+static void vir_pci_vpd_string_resource_class_init(virPCIVPDStringResourceClass *klass);
+static void vir_pci_vpd_string_resource_init(virPCIVPDStringResource *res);
+static void vir_pci_vpd_string_resource_finalize(GObject *object);
+
+static void vir_pci_vpd_string_resource_init(virPCIVPDStringResource *res)
+{
+    res->value = NULL;
+}
+
+static void vir_pci_vpd_string_resource_class_init(virPCIVPDStringResourceClass *klass)
+{
+    GObjectClass *obj = G_OBJECT_CLASS(klass);
+    obj->finalize = vir_pci_vpd_string_resource_finalize;
+}
+
+static void vir_pci_vpd_string_resource_finalize(GObject *object)
+{
+    virPCIVPDStringResource *res = VIR_PCI_VPD_STRING_RESOURCE(object);
+    g_free(res->value);
+    G_OBJECT_CLASS(vir_pci_vpd_resource_parent_class)->finalize(object);
+}
+
+virPCIVPDStringResource *
+virPCIVPDStringResourceNew(gchar *value)
+{
+    g_autoptr(virPCIVPDStringResource) res = NULL;
+
+    res = VIR_PCI_VPD_STRING_RESOURCE(g_object_new(VIR_TYPE_PCI_VPD_STRING_RESOURCE,
+                "resource_type",
+                VIR_PCI_VPD_RESOURCE_TYPE_STRING, NULL));
+    res->value = value;
+    return g_steal_pointer(&res);
+}
+
+const gchar*
+virPCIVPDStringResourceGetValue(const virPCIVPDStringResource *res)
+{
+    return res->value;
+}
+
+// virPCIVPDKeywordResource
+
+struct _virPCIVPDKeywordResource {
+    virPCIVPDResource parent;
+    GHashTable *resource_fields;
+};
+
+G_DEFINE_TYPE(virPCIVPDKeywordResource, vir_pci_vpd_keyword_resource, VIR_TYPE_PCI_VPD_RESOURCE)
+
+static void vir_pci_vpd_keyword_resource_class_init(virPCIVPDKeywordResourceClass *klass);
+static void vir_pci_vpd_keyword_resource_init(virPCIVPDKeywordResource *res);
+static void vir_pci_vpd_keyword_resource_finalize(GObject *object);
+
+static void vir_pci_vpd_keyword_resource_class_init(virPCIVPDKeywordResourceClass *klass)
+{
+    GObjectClass *obj = G_OBJECT_CLASS(klass);
+    obj->finalize = vir_pci_vpd_keyword_resource_finalize;
+}
+
+static void vir_pci_vpd_keyword_resource_init(virPCIVPDKeywordResource *res)
+{
+    res->resource_fields = NULL;
+}
+
+static void vir_pci_vpd_keyword_resource_finalize(GObject *object)
+{
+    virPCIVPDKeywordResource *res = VIR_PCI_VPD_KEYWORD_RESOURCE(object);
+    g_hash_table_destroy(g_steal_pointer(&res->resource_fields));
+    G_OBJECT_CLASS(vir_pci_vpd_resource_parent_class)->finalize(object);
+}
+
+virPCIVPDKeywordResource *
+virPCIVPDKeywordResourceNew(GHashTable *resource_fields, bool read_only)
+{
+    g_autoptr(virPCIVPDKeywordResource) res = NULL;
+    virPCIVPDResourceType t;
+
+    t = read_only ? VIR_PCI_VPD_RESOURCE_TYPE_VPD_R : VIR_PCI_VPD_RESOURCE_TYPE_VPD_W;
+    // Create an instance and set a property specifying its VPD resource type taking the
+    // read_only parameter into account.
+    res = VIR_PCI_VPD_KEYWORD_RESOURCE(
+            g_object_new(VIR_TYPE_PCI_VPD_KEYWORD_RESOURCE,
+                "resource_type", t, NULL));
+    res->resource_fields = g_hash_table_ref(resource_fields);
+    return g_steal_pointer(&res);
+}
+
+/**
+ * virPCIVPDKeywordResourceNextField:
+ * @iter: An iterator containing the current iteration state or NULL. Must be freed by the caller.
+ * @key: A pointer to a pointer to a memory area where a field key will be stored.
+ * @value: A pointer to a pointer to a memory area where a field value will be stored.
+ * @format: A field value format of interest. Fields of other formats are skipped.
+ *
+ * A helper function to iterate over keyword resources. The caller can use the returned
+ * iterator to obtain pointers to the next field's key and value. Initially, the iterator
+ * value can be NULL to start iteration from the beginning. A field value type of interest
+ * can be specified so that iteration only happens over fields of that value type. Key and value
+ * data needs to be duplicated by the caller if their usage past the keyword resource lifetime is
+ * intended.
+ *
+ * Returns: a GHashTableIter pointer advanced to the next field or NULL if the end was
+ * reached. Modifies the key and value pointers such that they point to the respective
+ * field's key and value const gchar pointers.
+ */
+ GHashTableIter *
+ virPCIVPDKeywordResourceNextField(
+    virPCIVPDKeywordResource *res, GHashTableIter *iter,
+    const gchar **const key, const gchar **const value, virPCIVPDResourceFieldValueFormat format) {
+    gpointer k, v;
+    gboolean end_reached = false;
+    virPCIVPDResourceFieldValueFormat found_format = VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST;
+
+    if (!res) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                _("Invalid resource pointer provided - must be a non-NULL pointer"));
+        return NULL;
+    }
+    if (!key || !value) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                _("key and value parameters must be non-NULL pointers: key: %p, value %p"),
+                key, value);
+        return NULL;
+    }
+    // Searching for a value with this format is incorrect.
+    if (format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST) {
+        return NULL;
+    }
+
+    if (!iter) {
+        // Allocate a new iterator and initialize it if not provided by the caller.
+        // The caller is expected to free it after use.
+        iter = g_new0(GHashTableIter, 1);
+        g_hash_table_iter_init(iter, res->resource_fields);
+    }
+    do {
+        end_reached = !g_hash_table_iter_next(iter, &k, &v);
+        found_format = end_reached ? VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST :
+            virPCIVPDResourceGetFieldValueFormat(k);
+    } while (!end_reached && found_format != format);
+
+    if (end_reached) {
+        g_free(g_steal_pointer(&iter));
+        return NULL;
+    }
+    // Types are known based on how this type of a resource is constructed.
+    *key = (const gchar *const)k;
+    *value = (const gchar *const)v;
+    return g_steal_pointer(&iter);
+}
+
+/**
+ * virPCIVPDReadVPDBytes:
+ * @vpdFileFd: A file descriptor associated with a file containing PCI device VPD.
+ * @buf: An allocated buffer to use for storing VPD bytes read.
+ * @count: The number of bytes to read from the VPD file descriptor.
+ * @offset: The offset at which bytes need to be read.
+ * @csum: A pointer to a byte containing the current checksum value. Mutated by this function.
+ *
+ * Returns: the number of VPD bytes read from the specified file descriptor. The csum value is
+ * also modified as bytes are read. If an error occurs while reading data from the VPD file
+ * descriptor, it is reported and -1 is returned to the caller. If EOF is occurred, 0 is returned
+ * to the caller.
+ */
+size_t
+virPCIVPDReadVPDBytes(int vpdFileFd, uint8_t *buf, size_t count, off_t offset, uint8_t *csum)
+{
+    ssize_t num_read = pread(vpdFileFd, buf, count, offset);
+    if (num_read == -1) {
+        VIR_DEBUG("Unable to read %zu bytes at offset %ld from fd: %d",
+                count, offset, vpdFileFd);
+    } else if (num_read) {
+        // Update the checksum for every byte read. Per the PCI(e) specs
+        // the checksum is correct if the sum of all bytes in VPD from
+        // VPD address 0 up to and including the VPD-R RV field's first
+        // data byte is zero.
+        while (count--) {
+            *csum += *buf;
+            buf++;
+        }
+    }
+    return num_read;
+}
+
+/**
+ * virPCIVPDParseVPDLargeResourceFields:
+ * @vpdFileFd: A file descriptor associated with a file containing PCI device VPD.
+ * @res_pos: A position where the resource data bytes begin in a file descriptor.
+ * @res_data_len: A length of the data portion of a resource.
+ * @read_only: A boolean showing whether the resource type is VPD-R or VPD-W.
+ * @csum: A pointer to a 1-byte checksum.
+ *
+ * Returns: a pointer to a VPDResource which needs to be freed by the caller or
+ * NULL if getting it failed for some reason.
+ */
+virPCIVPDKeywordResource *
+virPCIVPDParseVPDLargeResourceFields(
+        int vpdFileFd, uint16_t res_pos, uint16_t res_data_len, bool read_only, uint8_t *csum)
+{
+    g_autofree char *field_keyword = NULL;
+    g_autofree char *field_value = NULL;
+    virPCIVPDResourceFieldValueFormat field_format = VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST;
+    /* A buffer of up to one resource record field size (plus a zero byte) is needed. */
+    g_autofree uint8_t *buf = g_malloc0(PCI_VPD_MAX_FIELD_SIZE + 1);
+    uint16_t field_data_len = 0, bytes_to_read = 0;
+    uint16_t field_pos = res_pos;
+    g_autoptr(GHashTable) res_field_table = g_hash_table_new_full(
+            g_str_hash, g_str_equal, g_free, g_free);
+    bool has_checksum = false;
+
+    while (field_pos + 3 < res_pos + res_data_len) {
+        /* Keyword resources consist of keywords (2 ASCII bytes per the spec) and 1-byte length. */
+        if (virPCIVPDReadVPDBytes(vpdFileFd, buf, 3, field_pos, csum) != 3) {
+            /* Invalid field encountered which means the resource itself is invalid too. Report
+               That VPD has invalid format and bail. */
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("Could not read a resource field header - VPD has invalid format"));
+            return NULL;
+        }
+        field_data_len = buf[2];
+        // Change the position to the field's data portion skipping the keyword and length bytes.
+        field_pos += 3;
+        field_keyword = g_strndup((char *)buf, 2);
+
+        if (!virPCIVPDResourceIsExpectedKeyword(field_keyword, read_only)) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("Unexpected keyword encountered - VPD has invalid format"));
+            return NULL;
+        }
+        field_format = virPCIVPDResourceGetFieldValueFormat(field_keyword);
+
+        if (field_format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD) {
+            if (!field_data_len) {
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("The RV field has a 0 length - VPD has invalid format."));
+                return NULL;
+            }
+            // Only need one byte to be read and accounted towards the checksum calculation.
+            bytes_to_read = 1;
+        } else {
+            bytes_to_read = field_data_len;
+        }
+        if (virPCIVPDReadVPDBytes(
+                    vpdFileFd, buf, bytes_to_read, field_pos, csum) != bytes_to_read) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("Could not parse a resource field data - VPD has invalid format"));
+            return NULL;
+        }
+        // Advance the position to the first byte of the next field.
+        field_pos += field_data_len;
+
+        if (field_format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT) {
+            // Trim whitespace around a retrieved value and set it to be a field's value. Cases
+            // where unnecessary whitespace was present around a field value have been encountered
+            // in the wild.
+            field_value = g_strstrip(g_strndup((char *)buf, field_data_len));
+            if (!virPCIVPDResourceIsValidTextValue(field_value)) {
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("Field value contains invalid characters"));
+                return NULL;
+            }
+        } else if (field_format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD) {
+            if (*csum) {
+                // All bytes up to and including the checksum byte should add up to 0.
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                        _("Checksum validation has failed"));
+                return NULL;
+            }
+            has_checksum = true;
+            g_free(g_steal_pointer(&field_keyword));
+            g_free(g_steal_pointer(&field_value));
+            continue;
+        } else if (field_format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR ||
+                field_format == VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST) {
+            // Skip fields which are not known in advance or the read-write space field since
+            // it is not being used.
+            g_free(g_steal_pointer(&field_keyword));
+            g_free(g_steal_pointer(&field_value));
+            continue;
+        } else {
+            field_value = g_malloc(field_data_len);
+            memcpy(field_value, buf, field_data_len);
+        }
+        // At this point it is determined that the keyword is expected and field format
+        // is known and acceptable.
+        g_hash_table_insert(res_field_table,
+                g_steal_pointer(&field_keyword), g_steal_pointer(&field_value));
+    }
+    if (read_only && !has_checksum) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                _("A VPD-R resource does not contain the mandatory RV field with a checksum"));
+        return NULL;
+    }
+    return virPCIVPDKeywordResourceNew(g_steal_pointer(&res_field_table), read_only);
+}
+
+/**
+ * virPCIVPDParseVPDLargeResourceString:
+ * @vpdFileFd: A file descriptor associated with a file containing PCI device VPD.
+ * @res_pos: A position where the resource data bytes begin in a file descriptor.
+ * @res_data_len: A length of the data portion of a resource.
+ * @csum: A pointer to a 1-byte checksum.
+ *
+ * Returns: a pointer to a VPDResource which needs to be freed by the caller or
+ * NULL if getting it failed for some reason.
+ */
+virPCIVPDStringResource *
+virPCIVPDParseVPDLargeResourceString(
+        int vpdFileFd, uint16_t res_pos,
+        uint16_t res_data_len, uint8_t *csum)
+{
+    g_autofree char *res_value = NULL;
+    // The resource value is not NULL-terminated so add one more byte.
+    g_autofree char *buf = g_malloc0(res_data_len + 1);
+
+    if (virPCIVPDReadVPDBytes(vpdFileFd, (uint8_t *)buf, res_data_len,
+                res_pos, csum) != res_data_len) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                _("Could not read a part of a resource - VPD has invalid format"));
+        return NULL;
+    }
+    res_value = g_strdup(g_strstrip(buf));
+    if (!virPCIVPDResourceIsValidTextValue(res_value)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                _("The string resource has invalid characters in its value"));
+        return NULL;
+    }
+    return virPCIVPDStringResourceNew(g_steal_pointer(&res_value));
+}
+
+/**
+ * virPCIVPDParse:
+ * @vpdFileFd: a file descriptor associated with a file containing PCI device VPD.
+ *
+ * Parse a PCI device's Vital Product Data (VPD) contained in a file descriptor.
+ *
+ * Returns: a pointer to a GList of VPDResource types which needs to be freed by the caller or
+ * NULL if getting it failed for some reason.
+ */
+GList *
+virPCIVPDParse(int vpdFileFd)
+{
+    /* A checksum which is calculated as a sum of all bytes from VPD byte 0 up to
+     * the checksum byte in the RV field's value. The RV field is only present in the
+     * VPD-R resource and the checksum byte there is the first byte of the field's value.
+     * The checksum byte in RV field is actually a two's complement of the sum of all bytes
+     * of VPD that come before it so adding the two together must produce 0 if data
+     * was not corrupted and VPD storage is intact.
+     */
+    uint8_t csum = 0;
+    uint8_t header_buf[2];
+    g_autolist(virPCIVPDResource) res_list = NULL;
+    uint16_t res_pos = 0, res_data_len;
+    uint8_t tag = 0;
+    bool end_res_reached = false, has_vpd_r = false;
+    g_autoptr(virPCIVPDResource) res = NULL;
+
+    while (res_pos <= PCI_VPD_ADDR_MASK) {
+        // Read the resource data type tag.
+        if (virPCIVPDReadVPDBytes(vpdFileFd, &tag, 1, res_pos, &csum) != 1) {
+            break;
+        }
+        // 0x80 == 0b10000000 - the large resource data type flag.
+        if (tag & PCI_VPD_LARGE_RESOURCE_FLAG) {
+            if (res_pos > PCI_VPD_ADDR_MASK + 1 - 3) {
+                // Bail if the large resource starts at the position where the end tag should be.
+                break;
+            }
+            // Read the two length bytes of the large resource record.
+            if (virPCIVPDReadVPDBytes(vpdFileFd, header_buf, 2, res_pos + 1, &csum) != 2) {
+                break;
+            }
+            res_data_len = header_buf[0] + (header_buf[1] << 8);
+            // Change the position to the byte following the tag and length bytes.
+            res_pos += 3;
+        } else {
+            // Handle a small resource record.
+            // 0xxxxyyy & 00000111, where xxxx - resource data type bits, yyy - length bits.
+            res_data_len = tag & 7;
+            // 0xxxxyyy >> 3 == 0000xxxx
+            tag >>= 3;
+            // Change the position to the byte past the byte containing tag and length bits.
+            res_pos += 1;
+        }
+        if (tag == PCI_VPD_RESOURCE_END_TAG) {
+            // Stop VPD traversal since the end tag was encountered.
+            end_res_reached = true;
+            break;
+        }
+        if (res_data_len > PCI_VPD_ADDR_MASK + 1 - res_pos) {
+            // Bail if the resource is too long to fit into the VPD address space.
+            break;
+        }
+
+        switch (tag) {
+            // Large resource type which is also a string: 0x80 | 0x02 = 0x82
+            case PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_STRING_RESOURCE_FLAG:
+                res = (virPCIVPDResource *)virPCIVPDParseVPDLargeResourceString(
+                        vpdFileFd, res_pos, res_data_len, &csum);
+                break;
+            // Large resource type which is also a VPD-R: 0x80 | 0x10 == 0x90
+            case PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG:
+                res = (virPCIVPDResource *)virPCIVPDParseVPDLargeResourceFields(
+                        vpdFileFd, res_pos, res_data_len, true, &csum);
+                // Encountered the VPD-R tag. The resource record parsing also validates
+                // the presence of the required checksum in the RV field.
+                has_vpd_r = true;
+                break;
+            // Large resource type which is also a VPD-W: 0x80 | 0x11 == 0x91
+            case PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_WRITE_LARGE_RESOURCE_FLAG:
+                res = (virPCIVPDResource *)virPCIVPDParseVPDLargeResourceFields(
+                        vpdFileFd, res_pos, res_data_len, false, &csum);
+                break;
+            default:
+                // While we cannot parse unknown resource types, they can still be skipped
+                // based on the header and data length.
+                VIR_DEBUG("Encountered an unexpected VPD resource tag: %#x", tag);
+                res_pos += res_data_len;
+                continue;
+        }
+
+        if (!res) {
+            VIR_DEBUG("Encountered an invalid VPD");
+            return NULL;
+        }
+
+        res_list = g_list_append(res_list, g_steal_pointer(&res));
+        // Continue processing other resource records.
+        res_pos += res_data_len;
+    }
+
+    if (VIR_CLOSE(vpdFileFd) < 0) {
+        virReportSystemError(errno,
+                _("Unable to close the VPD file, fd: %d"),
+                vpdFileFd);
+        return NULL;
+    }
+    if (!has_vpd_r) {
+        VIR_DEBUG("Encountered an invalid VPD: does not have a VPD-R record");
+        return NULL;
+    } else if (!(end_res_reached && g_list_length(res_list) > 0)) {
+        // Does not have an end tag or there are not any other resources.
+        VIR_DEBUG("Encountered an invalid VPD");
+        return NULL;
+    }
+    return g_steal_pointer(&res_list);
+}
diff --git a/src/util/virpcivpd.h b/src/util/virpcivpd.h
new file mode 100644
index 0000000000..a844ce8a9e
--- /dev/null
+++ b/src/util/virpcivpd.h
@@ -0,0 +1,106 @@
+/*
+ * virpcivpd.h: helper APIs for working with the PCI/PCIe VPD capability
+ *
+ * Copyright (C) 2021 Canonical Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "internal.h"
+
+G_BEGIN_DECLS
+
+/*
+ * PCI Local bus (2.2+, Appendix I) and PCIe 4.0+ (7.9.19 VPD Capability) define
+ * the VPD capability structure (8 bytes in total) and VPD registers that can be used to access
+ * VPD data including:
+ * bit 31 of the first 32-bit DWORD: data transfer completion flag (between the VPD data register
+ * and the VPD data storage hardware);
+ * bits 30:16 of the first 32-bit DWORD: VPD address of the first VPD data byte to be accessed;
+ * bits 31:0 of the second 32-bit DWORD: VPD data bytes with LSB being pointed to by the VPD address.
+ *
+ * Given that only 15 bits (30:16) are allocated for VPD address its mask is 0x7fff.
+*/
+#define PCI_VPD_ADDR_MASK 0x7FFF
+/*
+ * VPD data consists of small and large resource data types. Information within a resource type
+ * consists of a 2-byte keyword, 1-byte length and data bytes (up to 255).
+*/
+#define PCI_VPD_MAX_FIELD_SIZE 255
+#define PCI_VPD_LARGE_RESOURCE_FLAG 0x80
+#define PCI_VPD_STRING_RESOURCE_FLAG 0x02
+#define PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG 0x10
+#define PCI_VPD_READ_WRITE_LARGE_RESOURCE_FLAG 0x11
+#define PCI_VPD_RESOURCE_END_TAG 0x0F
+#define PCI_VPD_RESOURCE_END_VAL PCI_VPD_RESOURCE_END_TAG << 3
+
+typedef enum {
+      VIR_PCI_VPD_RESOURCE_TYPE_STRING = 1,
+      VIR_PCI_VPD_RESOURCE_TYPE_VPD_R,
+      VIR_PCI_VPD_RESOURCE_TYPE_VPD_W,
+      VIR_PCI_VPD_RESOURCE_TYPE_LAST
+  } virPCIVPDResourceType;
+
+GType vir_pci_vpd_resource_type_get_type (void);
+#define VIR_TYPE_PCI_VPD_RESOURCE_TYPE vir_pci_vpd_resource_type_get_type()
+
+typedef enum {
+      VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT = 1,
+      VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_BINARY,
+      VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD,
+      VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR,
+      VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST
+  } virPCIVPDResourceFieldValueFormat;
+
+virPCIVPDResourceFieldValueFormat
+virPCIVPDResourceGetFieldValueFormat(const gchar* value);
+
+#define VIR_TYPE_PCI_VPD_RESOURCE vir_pci_vpd_resource_get_type()
+G_DECLARE_DERIVABLE_TYPE(virPCIVPDResource, vir_pci_vpd_resource, VIR, PCI_VPD_RESOURCE, GObject)
+struct _virPCIVPDResourceClass {
+        GObjectClass parent_class;
+};
+
+GEnumValue *
+virPCIVPDResourceGetResourceType(virPCIVPDResource *res);
+
+#define VIR_TYPE_PCI_VPD_STRING_RESOURCE vir_pci_vpd_string_resource_get_type()
+G_DECLARE_FINAL_TYPE(virPCIVPDStringResource, vir_pci_vpd_string_resource, VIR, PCI_VPD_STRING_RESOURCE, virPCIVPDResource)
+struct _virPCIVPDStringResourceClass {
+        virPCIVPDResourceClass parent_class;
+};
+
+virPCIVPDStringResource *
+virPCIVPDStringResourceNew(gchar *value);
+
+const gchar*
+virPCIVPDStringResourceGetValue(const virPCIVPDStringResource *res);
+
+#define VIR_TYPE_PCI_VPD_KEYWORD_RESOURCE vir_pci_vpd_keyword_resource_get_type()
+G_DECLARE_FINAL_TYPE(virPCIVPDKeywordResource, vir_pci_vpd_keyword_resource, VIR, PCI_VPD_KEYWORD_RESOURCE, virPCIVPDResource)
+
+virPCIVPDKeywordResource *
+virPCIVPDKeywordResourceNew(GHashTable *resource_fields, bool read_only);
+
+GHashTableIter *
+virPCIVPDKeywordResourceNextField(
+    virPCIVPDKeywordResource *res, GHashTableIter *iter,
+    const gchar **const key, const gchar **const value, virPCIVPDResourceFieldValueFormat format);
+
+GList *virPCIVPDParse(int vpdFileFd);
+
+G_END_DECLS
diff --git a/src/util/virpcivpdpriv.h b/src/util/virpcivpdpriv.h
new file mode 100644
index 0000000000..1c4dbafe0a
--- /dev/null
+++ b/src/util/virpcivpdpriv.h
@@ -0,0 +1,42 @@
+/*
+ * virpcivpdpriv.h: helper APIs for working with the PCI/PCIe VPD capability
+ *
+ * Copyright (C) 2021 Canonical Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library;  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBVIRT_VIRPCIVPDPRIV_H_ALLOW
+# error "virpcivpdpriv.h may only be included by virpcivpd.c or test suites"
+#endif /* LIBVIRT_VIRPCIVPDPRIV_H_ALLOW */
+
+#pragma once
+
+#include "virpcivpd.h"
+
+size_t
+virPCIVPDReadVPDBytes(int vpdFileFd, uint8_t *buf, size_t count, off_t offset, uint8_t *csum);
+
+virPCIVPDKeywordResource *
+virPCIVPDParseVPDLargeResourceFields(
+                int vpdFileFd, uint16_t res_pos, uint16_t res_data_len,
+                bool read_only, uint8_t *csum);
+
+virPCIVPDStringResource *
+virPCIVPDParseVPDLargeResourceString(int vpdFileFd, uint16_t res_pos,
+                uint16_t res_data_len, uint8_t *csum);
+
+gboolean virPCIVPDResourceIsExpectedKeyword(const gchar* keyword, gboolean read_only);
+gboolean virPCIVPDResourceIsValidTextValue(const gchar* value);
diff --git a/tests/meson.build b/tests/meson.build
index dfbc2c01e2..1948c07ae3 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -336,6 +336,7 @@ tests += [
   { 'name': 'virtimetest' },
   { 'name': 'virtypedparamtest' },
   { 'name': 'viruritest' },
+  { 'name': 'virpcivpdtest' },
   { 'name': 'vshtabletest', 'link_with': [ libvirt_shell_lib ] },
   { 'name': 'virmigtest' },
 ]
diff --git a/tests/nodedevschemadata/pci_0000_42_00_0_vpd.xml b/tests/nodedevschemadata/pci_0000_42_00_0_vpd.xml
new file mode 100644
index 0000000000..3dec23f886
--- /dev/null
+++ b/tests/nodedevschemadata/pci_0000_42_00_0_vpd.xml
@@ -0,0 +1,33 @@
+<device>
+  <name>pci_0000_42_00_0</name>
+  <capability type='pci'>
+    <class>0x020000</class>
+    <domain>0</domain>
+    <bus>66</bus>
+    <slot>0</slot>
+    <function>0</function>
+    <product id='0xa2d6'>MT42822 BlueField-2 integrated ConnectX-6 Dx network controller</product>
+    <vendor id='0x15b3'>Mellanox Technologies</vendor>
+    <capability type='virt_functions' maxCount='16'/>
+    <capability type='vpd'>
+      <resource type='string'>BlueField-2 DPU 25GbE Dual-Port SFP56, Crypto Enabled, 16GB on-board DDR, 1GbE OOB management, Tall Bracket</resource>
+      <resource type='vpd-r'>
+        <field keyword='SN'>MT2113X00000</field>
+        <field keyword='V3'>3c53d07eec484d8aab34dabd24fe575aa</field>
+        <field keyword='VA'>MLX:MN=MLNX:CSKU=V2:UUID=V3:PCI=V0:MODL=BF2H332A</field>
+        <field keyword='PN'>MBF2H332A-AEEOT</field>
+        <field keyword='V2'>MBF2H332A-AEEOT</field>
+        <field keyword='EC'>B1</field>
+        <field keyword='V0'>PCIeGen4 x8</field>
+      </resource>
+    </capability>
+    <iommuGroup number='65'>
+      <address domain='0x0000' bus='0x42' slot='0x00' function='0x0'/>
+    </iommuGroup>
+    <numa node='0'/>
+    <pci-express>
+      <link validity='cap' port='0' speed='16' width='8'/>
+      <link validity='sta' speed='8' width='8'/>
+    </pci-express>
+  </capability>
+</device>
diff --git a/tests/nodedevxml2xmlout/pci_0000_42_00_0_vpd.xml b/tests/nodedevxml2xmlout/pci_0000_42_00_0_vpd.xml
new file mode 120000
index 0000000000..a0b5372ca0
--- /dev/null
+++ b/tests/nodedevxml2xmlout/pci_0000_42_00_0_vpd.xml
@@ -0,0 +1 @@
+../nodedevschemadata/pci_0000_42_00_0_vpd.xml
\ No newline at end of file
diff --git a/tests/nodedevxml2xmltest.c b/tests/nodedevxml2xmltest.c
index 9e32e7d553..557347fb07 100644
--- a/tests/nodedevxml2xmltest.c
+++ b/tests/nodedevxml2xmltest.c
@@ -121,6 +121,7 @@ mymain(void)
     DO_TEST("pci_0000_02_10_7_sriov_pf_vfs_all_header_type");
     DO_TEST("drm_renderD129");
     DO_TEST("pci_0000_02_10_7_mdev_types");
+    DO_TEST("pci_0000_42_00_0_vpd");
     DO_TEST("mdev_3627463d_b7f0_4fea_b468_f1da537d301b");
     DO_TEST("ccw_0_0_ffff");
     DO_TEST("css_0_0_ffff");
diff --git a/tests/testutils.c b/tests/testutils.c
index d071abd6d7..e47db344c6 100644
--- a/tests/testutils.c
+++ b/tests/testutils.c
@@ -38,6 +38,12 @@
 #include "virprocess.h"
 #include "virstring.h"
 
+#ifdef __linux__
+#if WITH_MMAP
+# include <sys/mman.h>
+#endif
+#endif
+
 #define VIR_FROM_THIS VIR_FROM_NONE
 
 VIR_LOG_INIT("tests.testutils");
@@ -1143,3 +1149,48 @@ virTestStablePath(const char *path)
 
     return g_strdup(path);
 }
+
+#ifdef __linux__
+#if WITH_MMAP
+/**
+ * virCreateAnonymousFile:
+ * @data: a pointer to data to be written into a new file.
+ * @len: the length of data to be written (in bytes).
+ *
+ * Create a fake fd, write initial data to it and seal it for modification.
+ *
+ */
+int
+virCreateAnonymousFile(const uint8_t *data, size_t len)
+{
+    int fd;
+    if ((fd = memfd_create("testutils-memfd-file", MFD_CLOEXEC | MFD_ALLOW_SEALING)) == -1) {
+        VIR_TEST_DEBUG("%s: %s", "failed to create an anonymous file using memfd_create",
+                g_strerror(errno));
+        return -1;
+    }
+
+    if (len) {
+        if (ftruncate(fd, len) != 0) {
+            VIR_TEST_DEBUG("%s: %s", "failed to ftruncate an anonymous file",
+                    g_strerror(errno));
+            return -1;
+        }
+        if (safewrite(fd, data, len) != len) {
+            VIR_TEST_DEBUG("%s: %s", "failed to write to an anonymous file",
+                    g_strerror(errno));
+            return -1;
+        }
+    }
+
+    if (fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK) != 0) {
+        VIR_TEST_DEBUG("%s: %s", "failed to seal an anonymous file",
+                g_strerror(errno));
+        return -1;
+    }
+
+    return fd;
+}
+
+#endif
+#endif
diff --git a/tests/testutils.h b/tests/testutils.h
index 27d135fc02..e4092d6a5c 100644
--- a/tests/testutils.h
+++ b/tests/testutils.h
@@ -173,3 +173,9 @@ int testCompareDomXML2XMLFiles(virCaps *caps,
 
 char *
 virTestStablePath(const char *path);
+
+#ifdef __linux__
+#if WITH_MMAP
+int virCreateAnonymousFile(const uint8_t *data, size_t len);
+#endif
+#endif
diff --git a/tests/virpcitest.c b/tests/virpcitest.c
index 6fe9b7d13a..1af42c80bb 100644
--- a/tests/virpcitest.c
+++ b/tests/virpcitest.c
@@ -16,6 +16,8 @@
  * <http://www.gnu.org/licenses/>.
  */
 
+#define LIBVIRT_VIRPCIPRIV_H_ALLOW
+
 #include <config.h>
 
 #include "testutils.h"
@@ -25,6 +27,7 @@
 # include <sys/types.h>
 # include <sys/stat.h>
 # include <fcntl.h>
+# include <unistd.h>
 # include <virpci.h>
 
 # define VIR_FROM_THIS VIR_FROM_NONE
diff --git a/tests/virpcivpdtest.c b/tests/virpcivpdtest.c
new file mode 100644
index 0000000000..f4bce91cd4
--- /dev/null
+++ b/tests/virpcivpdtest.c
@@ -0,0 +1,777 @@
+/*
+ * Copyright (C) 2021 Canonical Ltd.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library;  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include "testutils.h"
+#include "virpcivpd.h"
+
+#define LIBVIRT_VIRPCIVPDPRIV_H_ALLOW
+
+#include "virpcivpdpriv.h"
+#include "virlog.h"
+
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+VIR_LOG_INIT("tests.vpdtest");
+
+
+static int testPCIVPDStringResourceBasic(const void* data G_GNUC_UNUSED)
+{
+    g_autoptr(virPCIVPDStringResource) str_res = NULL;
+    g_autofree char *val = g_strdup("testval");
+    GEnumValue *res_type = NULL;
+
+    str_res = virPCIVPDStringResourceNew(g_steal_pointer(&val));
+
+    res_type = virPCIVPDResourceGetResourceType((virPCIVPDResource *)str_res);
+
+    if (res_type->value != VIR_PCI_VPD_RESOURCE_TYPE_STRING) {
+        VIR_DEBUG("Expected '%d' got '%d'", VIR_PCI_VPD_RESOURCE_TYPE_STRING, res_type->value);
+        return -1;
+    }
+    if (!STREQ_NULLABLE(virPCIVPDStringResourceGetValue(str_res), "testval")) {
+        return -1;
+    }
+
+    return 0;
+}
+
+static int testPCIVPDKeywordResourceBasic(const void* data G_GNUC_UNUSED)
+{
+    g_autoptr(virPCIVPDKeywordResource) kw_res = NULL;
+    g_autoptr(GHashTable) res_field_table = g_hash_table_new_full(g_str_hash, g_str_equal,
+            g_free, g_free);
+    GEnumValue *res_type = NULL;
+    g_autofree GHashTableIter *iter = NULL;
+    const gchar *key, *val;
+
+    g_hash_table_insert(res_field_table, g_strdup("SN"), g_strdup("DEADBEEFCAFE"));
+
+    kw_res = virPCIVPDKeywordResourceNew(g_steal_pointer(&res_field_table), true);
+    res_type = virPCIVPDResourceGetResourceType((virPCIVPDResource *)kw_res);
+
+    if (res_type->value != VIR_PCI_VPD_RESOURCE_TYPE_VPD_R) {
+        return -1;
+    }
+
+    iter = virPCIVPDKeywordResourceNextField(kw_res, iter, &key, &val,
+            VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT);
+    if (STRNEQ_NULLABLE(key, "SN") || STRNEQ_NULLABLE(val, "DEADBEEFCAFE")) {
+        VIR_DEBUG("Unexpected keyword resource field encountered: %s: %s; expected: %s: %s",
+                key, val, "SN", "DEADBEEFCAFE");
+        return -1;
+    }
+
+    g_free(g_steal_pointer(&iter));
+    g_object_unref(g_steal_pointer(&kw_res));
+
+    res_field_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+    g_hash_table_insert(res_field_table, g_strdup("SN"), g_strdup("CAFEDEADBEEF"));
+
+    kw_res = virPCIVPDKeywordResourceNew(g_steal_pointer(&res_field_table), false);
+    res_type = virPCIVPDResourceGetResourceType((virPCIVPDResource *)kw_res);
+
+    if (res_type->value != VIR_PCI_VPD_RESOURCE_TYPE_VPD_W) {
+        return -1;
+    }
+
+    iter = virPCIVPDKeywordResourceNextField(kw_res, iter, &key, &val,
+            VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT);
+    if (STRNEQ_NULLABLE(key, "SN") || STRNEQ_NULLABLE(val, "CAFEDEADBEEF")) {
+        VIR_DEBUG("Unexpected keyword resource field encountered: %s: %s; expected: %s: %s",
+                key, val, "SN", "DEADBEEFCAFE");
+        return -1;
+    }
+
+    return 0;
+}
+
+typedef struct _TestPCIVPDKeyValue {
+    const char *keyword;
+    const char *value;
+} TestPCIVPDKeyValue;
+
+
+/*
+ * testPCIVPDKeywordResourceLoop:
+ *
+ * Validate the logic of iteration over keyword VPD resources. Static metadata about
+ * a field type for a particular keyword from the PCI(e) standards should be respected
+ * and only keywords known to have a given type need to be returned.
+ * */
+static int testPCIVPDKeywordResourceLoop(const void* data G_GNUC_UNUSED)
+{
+    g_autoptr(virPCIVPDKeywordResource) kw_res = NULL;
+    g_autoptr(GHashTable) res_field_table = g_hash_table_new_full(g_str_hash, g_str_equal,
+            g_free, g_free);
+    g_autofree GHashTableIter *iter = NULL;
+    const gchar *key = NULL, *val = NULL;
+    size_t i = 0, j = 0;
+
+    const char *expected_keys[] = {"SN", "EC", "V2", "V3", NULL};
+    size_t expected_keys_count = (sizeof(expected_keys) / sizeof(char*)) - 1;
+    const TestPCIVPDKeyValue test_table[] = {
+        {"SN", "DEADBEEFCAFE"},
+        {"EC", "42"},
+        {"CP", "BINARYCPBLOB"},
+        {"V3", "0f18b72f31934503b7aeef4957619bd8"},
+        {"V2", "SOMESKU"},
+        {"RV", "BINARYRVBLOB"},
+        // There is no validation of whether a key is expected in a read-only or
+        // read-write case during virPCIVPDKeywordResource creation in a generic case
+        // while the parser checks for that. This test case has a different purpose.
+        {"RW", "BINARYRWBLOB"},
+    };
+
+    for (i = 0; i < sizeof(test_table) / sizeof(test_table[0]); ++i) {
+        g_hash_table_insert(res_field_table, g_strdup(test_table[i].keyword),
+                g_strdup(test_table[i].value));
+    }
+
+    kw_res = virPCIVPDKeywordResourceNew(res_field_table, true);
+
+    while ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter, &key, &val,
+                    VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        if (!g_strv_contains(expected_keys, key)) {
+            VIR_DEBUG("Unexpected keyword occurred while iterating over text fields: %s", key);
+            return -1;
+        }
+        j++;
+    }
+    if (j != expected_keys_count) {
+        VIR_DEBUG("The number of iterations does not match the number of expected keys: %zu/%zu",
+                j, expected_keys_count);
+        return -1;
+    }
+
+    return 0;
+}
+
+/*
+ * testPCIVPDKeywordResourceLoopInvalid:
+ *
+ * Test that invalid parameters are properly handled during Keyword resource iteration.
+ * */
+static int testPCIVPDKeywordResourceLoopInvalid(const void* data G_GNUC_UNUSED)
+{
+    g_autoptr(virPCIVPDKeywordResource) kw_res = NULL;
+    g_autoptr(GHashTable) res_field_table = g_hash_table_new_full(g_str_hash, g_str_equal,
+            g_free, g_free);
+    g_autofree GHashTableIter *iter = NULL;
+    const gchar *key = NULL, *val = NULL;
+
+    g_hash_table_insert(res_field_table, g_strdup("foo"), g_strdup("bar"));
+
+    kw_res = virPCIVPDKeywordResourceNew(res_field_table, true);
+
+    // Invalid resource pointer.
+    if ((iter = virPCIVPDKeywordResourceNextField(NULL, iter,
+                    &key, &val, VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        VIR_DEBUG("A non-NULL iterator got returned for an invalid set of input parameters");
+        return -1;
+    }
+
+    // Invalid key and value pointers.
+    if ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter,
+                    NULL, NULL, VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        VIR_DEBUG("A non-NULL iterator got returned for an invalid set of input parameters");
+        return -1;
+    }
+
+    // Invalid key pointer.
+    if ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter,
+                    NULL, &val, VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        VIR_DEBUG("A non-NULL iterator got returned for an invalid set of input parameters");
+        return -1;
+    }
+
+    // Invalid value pointer.
+    if ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter,
+                    &key, NULL, VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        VIR_DEBUG("A non-NULL iterator got returned for an invalid set of input parameters");
+        return -1;
+    }
+
+    // Invalid resource field value format requested for lookup.
+    if ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter,
+                    &key, &val, VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST))) {
+        VIR_DEBUG("A non-NULL iterator got returned for an invalid set of input parameters");
+        return -1;
+    }
+    return 0;
+}
+typedef struct _TestPCIVPDExpectedString {
+    const char *keyword;
+    gboolean expected;
+} TestPCIVPDExpectedString;
+
+/*
+ * testPCIVPDIsExpectedKeywordReadOnly:
+ *
+ * Test expected keyword validation. Static metadata about expected
+ * keywords is taken from the PCI(e) standards should be respected
+ * and only keywords known to have a given type need to be returned.
+ * */
+static int testPCIVPDIsExpectedKeywordReadOnly(const void* data G_GNUC_UNUSED)
+{
+    size_t i = 0;
+    const TestPCIVPDExpectedString readonly_cases[] = {
+        {"CP", true},
+        {"EC", true},
+        {"FG", true},
+        {"LC", true},
+        {"MN", true},
+        {"PG", true},
+        {"PN", true},
+        {"RV", true},
+        {"SN", true},
+        {"V0", true},
+        {"VG", true},
+        {"IV", false},
+        {"YA", false},
+        {"YB", false},
+        {"RW", false},
+        // Invalid Keywords:
+        // Empty:
+        {"", false},
+        // 1-byte:
+        {"Y", false},
+        // 3-byte:
+        {"FOO", false},
+        // Not present in the spec:
+        {"42", false},
+        {"4A", false},
+    };
+    for (i = 0; i < sizeof(readonly_cases) / sizeof(readonly_cases[0]); ++i) {
+        if (virPCIVPDResourceIsExpectedKeyword(
+                    readonly_cases[i].keyword, true) != readonly_cases[i].expected) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
+/*
+ * testPCIVPDIsExpectedKeywordReadWrite:
+ *
+ * Test expected keyword validation. Static metadata about expected
+ * keywords is taken from the PCI(e) standards should be respected
+ * and only keywords known to have a given type need to be returned.
+ * */
+static int testPCIVPDIsExpectedKeywordReadWrite(const void* data G_GNUC_UNUSED)
+{
+    size_t i = 0;
+    const TestPCIVPDExpectedString readwrite_cases[] = {
+        {"CP", false},
+        {"EC", false},
+        {"FG", false},
+        {"LC", false},
+        {"MN", false},
+        {"PG", false},
+        {"PN", false},
+        {"RV", false},
+        {"SN", false},
+        {"V0", true},
+        {"VG", true},
+        {"IV", false},
+        {"YA", true},
+        {"YB", true},
+        {"RW", true},
+        // Empty:
+        {"", false},
+        // 1-byte:
+        {"Y", false},
+        // 3-byte:
+        {"FOO", false},
+        // Not present in the spec:
+        {"42", false},
+        {"4A", false},
+    };
+    for (i = 0; i < sizeof(readwrite_cases) / sizeof(readwrite_cases[0]); ++i) {
+        if (virPCIVPDResourceIsExpectedKeyword(
+                    readwrite_cases[i].keyword, false) != readwrite_cases[i].expected) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
+
+
+/*
+ * testPCIVPDIsValidTextValue:
+ *
+ * Test expected text value validation. Static metadata about possible values is taken
+ * from the PCI(e) standards and based on some real-world hardware examples.
+ * */
+static int testPCIVPDIsValidTextValue(const void* data G_GNUC_UNUSED)
+{
+    size_t i = 0;
+    const TestPCIVPDExpectedString text_value_cases[] = {
+        // Numbers
+        {"42", true},
+        // Alphanumeric
+        {"DCM1001008FC52101008FC53201008FC54301008FC5", true},
+        // Dots
+        {"DSV1028VPDR.VER1.0", true},
+        // Whitespace presence
+        {"NMVIntel Corp", true},
+        // Comma and spaces
+        {"BlueField-2 DPU 25GbE Dual-Port SFP56, Tall Bracket", true},
+        // Equal signs and colons.
+        {"MLX:MN=MLNX:CSKU=V2:UUID=V3:PCI=V0:MODL=BF2H332A", true},
+        // Dashes
+        {"MBF2H332A-AEEOT", true},
+        {"under_score_example", true},
+        {"", true},
+        {";", false},
+        {"\\42", false},
+        {"/42", false},
+    };
+    for (i = 0; i < sizeof(text_value_cases) / sizeof(text_value_cases[0]); ++i) {
+        if (virPCIVPDResourceIsValidTextValue(
+                    text_value_cases[i].keyword) != text_value_cases[i].expected) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
+/*
+ * testPCIVPDGetFieldValueFormat:
+ *
+ * A simple test to assess the functionality of the
+ * virPCIVPDResourceGetFieldValueFormat function.
+ * */
+static int testPCIVPDGetFieldValueFormat(const void* data G_GNUC_UNUSED)
+{
+    typedef struct _TestPCIVPDExpectedFieldValueFormat {
+        const char *keyword;
+        virPCIVPDResourceFieldValueFormat expected;
+    } TestPCIVPDExpectedFieldValueFormat;
+
+    size_t i = 0;
+    const TestPCIVPDExpectedFieldValueFormat value_format_cases[] = {
+        {"SN", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"RV", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD},
+        {"RW", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR},
+        {"VA", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"YA", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"YZ", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"CP", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_BINARY},
+        // Invalid keywords.
+        {"", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"4", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"Y", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"V", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        // 2 bytes but not present in the spec.
+        {"EX", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        // Many numeric bytes.
+        {"4242", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        // Many letters.
+        {"EXAMPLE", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+    };
+    for (i = 0; i < sizeof(value_format_cases) / sizeof(value_format_cases[0]); ++i) {
+        if (virPCIVPDResourceGetFieldValueFormat(
+                    value_format_cases[i].keyword) != value_format_cases[i].expected) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
+#define VPD_STRING_RESOURCE_EXAMPLE_HEADER \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_STRING_RESOURCE_FLAG, 0x08, 0x00
+
+#define VPD_STRING_RESOURCE_EXAMPLE_DATA \
+    't', 'e', 's', 't', 'n', 'a', 'm', 'e'
+
+#define VPD_R_FIELDS_EXAMPLE_HEADER \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x16, 0x00
+
+#define VPD_R_EXAMPLE_VALID_RV_FIELD \
+    'R', 'V', 0x02, 0x31, 0x00
+
+#define VPD_R_EXAMPLE_INVALID_RV_FIELD \
+    'R', 'V', 0x02, 0xFF, 0x00
+
+#define VPD_R_EXAMPLE_FIELDS \
+    'P', 'N', 0x02, '4', '2', \
+    'E', 'C', 0x04, '4', '2', '4', '2', \
+    'V', 'A', 0x02, 'E', 'X'
+
+#define VPD_R_FIELDS_EXAMPLE_DATA \
+    VPD_R_EXAMPLE_FIELDS, \
+    VPD_R_EXAMPLE_VALID_RV_FIELD
+
+#define VPD_W_FIELDS_EXAMPLE_HEADER \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_WRITE_LARGE_RESOURCE_FLAG, 0x19, 0x00
+
+#define VPD_W_EXAMPLE_FIELDS \
+    'V', 'Z', 0x02, '4', '2', \
+    'Y', 'A', 0x04, 'I', 'D', '4', '2', \
+    'Y', 'F', 0x02, 'E', 'X', \
+    'Y', 'E', 0x00, \
+    'R', 'W', 0x02, 0x00, 0x00
+
+static int
+testVirPCIVPDReadVPDBytes(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    g_autofree uint8_t *buf = NULL;
+    uint8_t csum = 0;
+    size_t read_bytes = 0;
+    size_t data_len = 0;
+    // An example of a valid VPD record with one VPD-R resource and 2 fields.
+    uint8_t full_vpd_example[] = {
+        VPD_STRING_RESOURCE_EXAMPLE_HEADER, VPD_STRING_RESOURCE_EXAMPLE_DATA,
+        VPD_R_FIELDS_EXAMPLE_HEADER, VPD_R_FIELDS_EXAMPLE_DATA,
+        PCI_VPD_RESOURCE_END_VAL
+    };
+    data_len = sizeof(full_vpd_example) / sizeof(uint8_t) - 2;
+    buf = g_malloc0(data_len);
+
+    fd = virCreateAnonymousFile(full_vpd_example, data_len);
+
+    read_bytes = virPCIVPDReadVPDBytes(fd, buf, data_len, 0, &csum);
+
+    if (read_bytes != data_len) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                "The number of bytes read %zu is lower than expected %zu ",
+                read_bytes, data_len);
+        return -1;
+    }
+
+    if (csum) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                "The sum of all VPD bytes up to and including the checksum byte"
+                "is equal to zero: 0x%02x", csum);
+        return -1;
+    }
+    return 0;
+}
+
+static int
+testVirPCIVPDParseVPDStringResource(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    uint8_t csum = 0;
+    size_t data_len = 0;
+    g_autoptr(virPCIVPDStringResource) str_res = NULL;
+    const gchar *expected_value = "testname", *actual_value = NULL;
+    const uint8_t string_res_example[] = {
+        VPD_STRING_RESOURCE_EXAMPLE_DATA
+    };
+
+    data_len = sizeof(string_res_example) / sizeof(uint8_t);
+    fd = virCreateAnonymousFile(string_res_example, data_len);
+    str_res = virPCIVPDParseVPDLargeResourceString(fd, 0, data_len, &csum);
+    VIR_FORCE_CLOSE(fd);
+
+    actual_value = virPCIVPDStringResourceGetValue(str_res);
+    if (STRNEQ(expected_value, actual_value)) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                    "Unexpected string resource value: %s, expected: %s",
+                    actual_value, expected_value);
+            return -1;
+    }
+    return 0;
+}
+
+static gboolean
+testVirPCIVPDValidateKeywordResource(virPCIVPDKeywordResource *kw_res, GHashTable *expected_kw)
+{
+    g_autofree GHashTableIter *iter = NULL;
+    const gchar *key;
+    uint8_t *val = NULL, *expected_val = NULL;
+    size_t num_fields = 0;
+
+    while ((iter = virPCIVPDKeywordResourceNextField(kw_res, iter, &key, (const char **)(&val),
+            VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT))) {
+        expected_val = g_hash_table_lookup(expected_kw, key);
+        if (!expected_val) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                    "Unexpected key encountered after parsing: %s", key);
+            return false;
+        } else if (*val != *expected_val) {
+            return false;
+        }
+        ++num_fields;
+    }
+    // Check that the number of actual fields observed equals the expected number
+    // which also validates that the "RV" field is not present after parsing.
+    if (num_fields != g_hash_table_size(expected_kw)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                "Unexpected key encountered after parsing: %s", key);
+        return false;
+    }
+    return true;
+}
+
+
+static int
+testVirPCIVPDParseFullVPD(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    const int EXPECTED_RES_LIST_LENGTH = 3;
+    size_t data_len = 0;
+    g_autolist(virPCIVPDResource) res_list = NULL;
+    GList *list_iter = NULL;
+    virPCIVPDResource *res = NULL;
+
+    const uint8_t full_vpd_example[] = {
+        VPD_STRING_RESOURCE_EXAMPLE_HEADER, VPD_STRING_RESOURCE_EXAMPLE_DATA,
+        VPD_R_FIELDS_EXAMPLE_HEADER, VPD_R_FIELDS_EXAMPLE_DATA,
+        VPD_W_FIELDS_EXAMPLE_HEADER, VPD_W_EXAMPLE_FIELDS,
+        PCI_VPD_RESOURCE_END_VAL
+    };
+    const gchar *expected_value = "testname", *actual_value = NULL;
+
+    g_autoptr(GHashTable) expected_read_only_kw = NULL, expected_read_write_kw = NULL;
+
+    expected_read_only_kw = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+    g_hash_table_insert(expected_read_only_kw, g_strdup("PN"), g_strdup("42"));
+    g_hash_table_insert(expected_read_only_kw, g_strdup("EC"), g_strdup("4242"));
+    g_hash_table_insert(expected_read_only_kw, g_strdup("VA"), g_strdup("EX"));
+
+    expected_read_write_kw = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+    g_hash_table_insert(expected_read_write_kw, g_strdup("VZ"), g_strdup("42"));
+    g_hash_table_insert(expected_read_write_kw, g_strdup("YA"), g_strdup("ID42"));
+    g_hash_table_insert(expected_read_write_kw, g_strdup("YF"), g_strdup("EX"));
+    g_hash_table_insert(expected_read_write_kw, g_strdup("YE"), g_strdup(""));
+
+    data_len = sizeof(full_vpd_example) / sizeof(uint8_t);
+    fd = virCreateAnonymousFile(full_vpd_example, data_len);
+    res_list = virPCIVPDParse(fd);
+    VIR_FORCE_CLOSE(fd);
+
+    if (!res_list) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                "The resource list is empty after parsing which is unexpected");
+        return -1;
+    } else if (g_list_length(res_list) != EXPECTED_RES_LIST_LENGTH) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                "The resource list length is not equal to: %d", EXPECTED_RES_LIST_LENGTH);
+        return -1;
+    }
+
+    for (list_iter = res_list; list_iter; list_iter = g_list_next(list_iter)) {
+        res = list_iter->data;
+        switch (virPCIVPDResourceGetResourceType(res)->value) {
+            case VIR_PCI_VPD_RESOURCE_TYPE_STRING:
+                actual_value = virPCIVPDStringResourceGetValue((virPCIVPDStringResource *)res);
+                if (STRNEQ(expected_value, actual_value)) {
+                    virReportError(VIR_ERR_INTERNAL_ERROR,
+                            "Unexpected string resource value: %s, expected: %s",
+                            actual_value, expected_value);
+                    return -1;
+                }
+                break;
+            case VIR_PCI_VPD_RESOURCE_TYPE_VPD_R:
+                if (!testVirPCIVPDValidateKeywordResource(
+                            (virPCIVPDKeywordResource *)res, expected_read_only_kw)) {
+                    virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                            "Keyword resource fields do not match the expected ones");
+                    return -1;
+                }
+                break;
+            case VIR_PCI_VPD_RESOURCE_TYPE_VPD_W:
+                if (!testVirPCIVPDValidateKeywordResource(
+                            (virPCIVPDKeywordResource *)res, expected_read_write_kw)) {
+                    virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                            "Keyword resource fields do not match the expected ones");
+                    return -1;
+                }
+                break;
+            default:
+                virReportError(VIR_ERR_INTERNAL_ERROR,
+                        "%s", "unexpected resource type encountered");
+                return -1;
+        }
+    }
+    return 0;
+}
+
+static int
+testVirPCIVPDParseFullVPDInvalid(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    size_t data_len = 0;
+
+#define VPD_INVALID_ZERO_BYTE \
+    0x00
+
+#define VPD_INVALID_STRING_HEADER_DATA_LONG \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_STRING_RESOURCE_FLAG, 0x04, 0x00, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x05, 0x00, \
+    'R', 'V', 0x02, 0xDA, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_INVALID_STRING_HEADER_DATA_SHORT \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_STRING_RESOURCE_FLAG, 0x0A, 0x00, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x05, 0x00, \
+    'R', 'V', 0x02, 0xD4, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_NO_VPD_R \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_NO_RV \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    VPD_R_FIELDS_EXAMPLE_HEADER, \
+    VPD_R_EXAMPLE_FIELDS, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_INVALID_RV \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    VPD_R_FIELDS_EXAMPLE_HEADER, \
+    VPD_R_EXAMPLE_FIELDS, \
+    VPD_R_EXAMPLE_INVALID_RV_FIELD, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_INVALID_RV_ZERO_LENGTH \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x14, 0x00, \
+    VPD_R_EXAMPLE_FIELDS, \
+    'R', 'V', 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+// The RW key is not expected in a VPD-R record.
+#define VPD_R_UNEXPECTED_KEY \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x1B, 0x00, \
+    VPD_R_EXAMPLE_FIELDS, \
+    'R', 'W', 0x02, 0x00, 0x00, \
+    'R', 'V', 0x02, 0x8F, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_INVALID_KEY_FIRST_BYTE \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x1B, 0x00, \
+    VPD_R_EXAMPLE_FIELDS, \
+    0x07, 'A', 0x02, 0x00, 0x00, \
+    'R', 'V', 0x02, 0xE2, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_INVALID_KEY_SECOND_BYTE \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x1B, 0x00, \
+    VPD_R_EXAMPLE_FIELDS, \
+    'V', 0x07, 0x02, 0x00, 0x00, \
+    'R', 'V', 0x02, 0xCD, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_R_INVALID_FIELD_VALUE \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    VPD_STRING_RESOURCE_EXAMPLE_DATA, \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x0A, 0x00, \
+    'S', 'N', 0x02, 0x04, 0x02, \
+    'R', 'V', 0x02, 0x28, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define VPD_INVALID_STRING_RESOURCE_VALUE \
+    VPD_STRING_RESOURCE_EXAMPLE_HEADER, \
+    't', 0x03, 's', 't', 'n', 'a', 'm', 'e', \
+    PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 0x0A, 0x00, \
+    'S', 'N', 0x02, 0x04, 0x02, \
+    'R', 'V', 0x02, 0x8A, 0x00, \
+    PCI_VPD_RESOURCE_END_VAL
+
+#define TEST_INVALID_VPD(invalid_vpd) \
+    do { \
+        const uint8_t test_case[] = { invalid_vpd }; \
+        data_len = sizeof(test_case) / sizeof(uint8_t); \
+        fd = virCreateAnonymousFile(test_case, data_len); \
+        if (virPCIVPDParse(fd)) { \
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s", \
+                    "Successfully parsed an invalid VPD - this is not expected"); \
+            return -1; \
+        } \
+        VIR_FORCE_CLOSE(fd); \
+    } while (0);
+
+    TEST_INVALID_VPD(VPD_INVALID_ZERO_BYTE);
+    TEST_INVALID_VPD(VPD_INVALID_STRING_HEADER_DATA_SHORT);
+    TEST_INVALID_VPD(VPD_INVALID_STRING_HEADER_DATA_LONG);
+    TEST_INVALID_VPD(VPD_NO_VPD_R);
+    TEST_INVALID_VPD(VPD_R_NO_RV);
+    TEST_INVALID_VPD(VPD_R_INVALID_RV);
+    TEST_INVALID_VPD(VPD_R_INVALID_RV_ZERO_LENGTH);
+    TEST_INVALID_VPD(VPD_R_UNEXPECTED_KEY);
+    TEST_INVALID_VPD(VPD_R_INVALID_KEY_FIRST_BYTE);
+    TEST_INVALID_VPD(VPD_R_INVALID_KEY_SECOND_BYTE);
+    TEST_INVALID_VPD(VPD_R_INVALID_FIELD_VALUE);
+    TEST_INVALID_VPD(VPD_INVALID_STRING_RESOURCE_VALUE);
+
+    return 0;
+}
+
+static int
+mymain(void)
+{
+    int ret = 0;
+
+    if (virTestRun("String resource (basic test) ", testPCIVPDStringResourceBasic, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Keyword resource (basic test) ", testPCIVPDKeywordResourceBasic, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Keyword resource (text field loop test) ", testPCIVPDKeywordResourceLoop,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Invalid parameter usage during keyword resource iteration ",
+                testPCIVPDKeywordResourceLoopInvalid,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Expected read-only resource keywords ", testPCIVPDIsExpectedKeywordReadOnly,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Expected read-write resource keywords ", testPCIVPDIsExpectedKeywordReadWrite,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Valid text values ", testPCIVPDIsValidTextValue, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Valid text values ", testPCIVPDGetFieldValueFormat, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Reading VPD bytes ", testVirPCIVPDReadVPDBytes,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Parsing VPD string resources ", testVirPCIVPDParseVPDStringResource,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Parsing a full VPD resources ", testVirPCIVPDParseFullVPD,
+                NULL) < 0)
+        ret = -1;
+    if (virTestRun("Parsing invalid VPD records ", testVirPCIVPDParseFullVPDInvalid,
+                NULL) < 0)
+        ret = -1;
+
+    return ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+VIR_TEST_MAIN(mymain)
diff --git a/tools/virsh-nodedev.c b/tools/virsh-nodedev.c
index f1b4eb94bf..e2f299d809 100644
--- a/tools/virsh-nodedev.c
+++ b/tools/virsh-nodedev.c
@@ -480,6 +480,9 @@ cmdNodeListDevices(vshControl *ctl, const vshCmd *cmd G_GNUC_UNUSED)
         case VIR_NODE_DEV_CAP_MDEV:
             flags |= VIR_CONNECT_LIST_NODE_DEVICES_CAP_MDEV;
             break;
+        case VIR_NODE_DEV_CAP_VPD:
+            flags |= VIR_CONNECT_LIST_NODE_DEVICES_CAP_VPD;
+            break;
         case VIR_NODE_DEV_CAP_CCW_DEV:
             flags |= VIR_CONNECT_LIST_NODE_DEVICES_CAP_CCW_DEV;
             break;
-- 
2.30.2





More information about the libvir-list mailing list