Add support for deserializing the binary PCI/PCIe VPD format and storing
results in memory.

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.shcherba...@canonical.com>
---
 build-aux/syntax-check.mk |   4 +-
 po/POTFILES.in            |   1 +
 src/libvirt_private.syms  |  18 +
 src/util/meson.build      |   1 +
 src/util/virpcivpd.c      | 754 +++++++++++++++++++++++++++++++++++
 src/util/virpcivpd.h      |  76 ++++
 src/util/virpcivpdpriv.h  |  83 ++++
 tests/meson.build         |   1 +
 tests/testutils.c         |  35 ++
 tests/testutils.h         |   4 +
 tests/virpcivpdtest.c     | 809 ++++++++++++++++++++++++++++++++++++++
 11 files changed, 1784 insertions(+), 2 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/virpcivpdtest.c

diff --git a/build-aux/syntax-check.mk b/build-aux/syntax-check.mk
index cb12b64532..2a6e2f86a1 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/po/POTFILES.in b/po/POTFILES.in
index b554cf08ca..8a726f624e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -302,6 +302,7 @@
 @SRCDIR@src/util/virnvme.c
 @SRCDIR@src/util/virobject.c
 @SRCDIR@src/util/virpci.c
+@SRCDIR@src/util/virpcivpd.c
 @SRCDIR@src/util/virperf.c
 @SRCDIR@src/util/virpidfile.c
 @SRCDIR@src/util/virpolkit.c
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index c5d788285e..444b51c880 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -3576,6 +3576,24 @@ virVHBAManageVport;
 virVHBAPathExists;
 
 
+# util/virpcivpd.h
+
+virPCIVPDParse;
+virPCIVPDParseVPDLargeResourceFields;
+virPCIVPDParseVPDLargeResourceString;
+virPCIVPDReadVPDBytes;
+virPCIVPDResourceCustomCompareIndex;
+virPCIVPDResourceCustomFree;
+virPCIVPDResourceCustomUpsertValue;
+virPCIVPDResourceFree;
+virPCIVPDResourceGetFieldValueFormat;
+virPCIVPDResourceIsValidTextValue;
+virPCIVPDResourceROFree;
+virPCIVPDResourceRONew;
+virPCIVPDResourceRWFree;
+virPCIVPDResourceRWNew;
+virPCIVPDResourceUpdateKeyword;
+
 # util/virvsock.h
 virVsockAcquireGuestCid;
 virVsockSetGuestCid;
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/virpcivpd.c b/src/util/virpcivpd.c
new file mode 100644
index 0000000000..a208224228
--- /dev/null
+++ b/src/util/virpcivpd.c
@@ -0,0 +1,754 @@
+/*
+ * 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>
+
+#ifdef __linux__
+# include <unistd.h>
+#endif
+
+#define LIBVIRT_VIRPCIVPDPRIV_H_ALLOW
+
+#include "virthread.h"
+#include "virpcivpdpriv.h"
+#include "virlog.h"
+#include "virerror.h"
+#include "virfile.h"
+
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+VIR_LOG_INIT("util.pcivpd");
+
+static bool
+virPCIVPDResourceIsUpperOrNumber(const char c)
+{
+    return g_ascii_isupper(c) || g_ascii_isdigit(c);
+}
+
+static bool
+virPCIVPDResourceIsVendorKeyword(const char *keyword)
+{
+    return g_str_has_prefix(keyword, "V") && 
virPCIVPDResourceIsUpperOrNumber(keyword[1]);
+}
+
+static bool
+virPCIVPDResourceIsSystemKeyword(const char *keyword)
+{
+    /* Special-case the system-specific keywords since they share the "Y" 
prefix with "YA". */
+    return (g_str_has_prefix(keyword, "Y") && 
virPCIVPDResourceIsUpperOrNumber(keyword[1]) &&
+            STRNEQ(keyword, "YA"));
+}
+
+static char *
+virPCIVPDResourceGetKeywordPrefix(const char *keyword)
+{
+    g_autofree char *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 (!(virPCIVPDResourceIsUpperOrNumber(keyword[0]) &&
+                 virPCIVPDResourceIsUpperOrNumber(keyword[1]))) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("The keyword is not comprised only of uppercase ASCII 
letters or digits"));
+        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);
+}
+
+static GHashTable *fieldValueFormats;
+
+static int
+virPCIVPDResourceOnceInit(void)
+{
+    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. NOTE: string literals are copied as 
g_hash_table_insert
+         * requires pointers to non-const data. */
+        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));
+    } else {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("Field value formats must only be initialized 
once."));
+        return -1;
+    }
+    return 0;
+}
+
+VIR_ONCE_GLOBAL_INIT(virPCIVPDResource);
+
+/**
+ * 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 char *keyword)
+{
+    g_autofree char *key = NULL;
+    gpointer keyVal = 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 (virPCIVPDResourceInitialize() < 0)
+        return VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST;
+
+    /* The system and vendor-specific keywords have a variable part - lookup
+     * the prefix significant for determining the value format. */
+    key = virPCIVPDResourceGetKeywordPrefix(keyword);
+    if (key) {
+        keyVal = g_hash_table_lookup(fieldValueFormats, key);
+        if (keyVal) {
+            format = GPOINTER_TO_INT(keyVal);
+        }
+    }
+    return format;
+}
+
+#define ACCEPTED_CHARS 
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_,.:;="
+
+/**
+ * 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).
+ */
+bool
+virPCIVPDResourceIsValidTextValue(const char *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 (value == NULL) {
+        return false;
+    }
+    /* An empty string is a valid value. */
+    if (STREQ(value, "")) {
+        return true;
+    }
+
+    if (strspn(value, ACCEPTED_CHARS) != strlen(value)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("The provided value contains invalid characters: 
%s"), value);
+        return false;
+    }
+    return true;
+}
+
+void
+virPCIVPDResourceFree(virPCIVPDResource *res)
+{
+    if (!res) {
+        return;
+    }
+    g_free(res->name);
+    virPCIVPDResourceROFree(res->ro);
+    virPCIVPDResourceRWFree(res->rw);
+    g_free(res);
+}
+
+virPCIVPDResourceRO *
+virPCIVPDResourceRONew(void)
+{
+    g_autoptr(virPCIVPDResourceRO) ro = g_new0(virPCIVPDResourceRO, 1);
+    ro->vendor_specific = g_ptr_array_new_full(0, 
(GDestroyNotify)virPCIVPDResourceCustomFree);
+    return g_steal_pointer(&ro);
+}
+
+void
+virPCIVPDResourceROFree(virPCIVPDResourceRO *ro)
+{
+    if (!ro) {
+        return;
+    }
+    g_free(ro->change_level);
+    g_free(ro->manufacture_id);
+    g_free(ro->part_number);
+    g_free(ro->serial_number);
+    g_ptr_array_unref(ro->vendor_specific);
+    g_free(ro);
+}
+
+virPCIVPDResourceRW *
+virPCIVPDResourceRWNew(void)
+{
+    g_autoptr(virPCIVPDResourceRW) rw = g_new0(virPCIVPDResourceRW, 1);
+    rw->vendor_specific = g_ptr_array_new_full(0, 
(GDestroyNotify)virPCIVPDResourceCustomFree);
+    rw->system_specific = g_ptr_array_new_full(0, 
(GDestroyNotify)virPCIVPDResourceCustomFree);
+    return g_steal_pointer(&rw);
+}
+
+void
+virPCIVPDResourceRWFree(virPCIVPDResourceRW *rw)
+{
+    if (!rw) {
+        return;
+    }
+    g_free(rw->asset_tag);
+    g_ptr_array_unref(rw->vendor_specific);
+    g_ptr_array_unref(rw->system_specific);
+    g_free(rw);
+}
+
+void
+virPCIVPDResourceCustomFree(virPCIVPDResourceCustom *custom)
+{
+    g_free(custom->value);
+    g_free(custom);
+}
+
+bool
+virPCIVPDResourceCustomCompareIndex(virPCIVPDResourceCustom *a, 
virPCIVPDResourceCustom *b)
+{
+    if (a == b) {
+        return true;
+    } else if (a == NULL || b == NULL) {
+        return false;
+    } else {
+        return a->idx == b->idx;
+    }
+    return true;
+}
+
+/**
+ * virPCIVPDResourceCustomUpsertValue:
+ * @arr: A GPtrArray with virPCIVPDResourceCustom entries to update.
+ * @index: An index character for the keyword.
+ * @value: A pointer to the value to be inserted at a given index.
+ *
+ * Returns: true if a value has been updated successfully, false otherwise.
+ */
+bool
+virPCIVPDResourceCustomUpsertValue(GPtrArray *arr, char index, const char 
*const value)
+{
+    g_autoptr(virPCIVPDResourceCustom) custom = NULL;
+    virPCIVPDResourceCustom *existing = NULL;
+    guint pos = 0;
+    bool found = false;
+
+    if (arr == NULL || value == NULL) {
+        return false;
+    }
+
+    custom = g_new0(virPCIVPDResourceCustom, 1);
+    custom->idx = index;
+    custom->value = g_strdup(value);
+    found = g_ptr_array_find_with_equal_func(arr, custom,
+                                             
(GEqualFunc)virPCIVPDResourceCustomCompareIndex,
+                                             &pos);
+    if (found) {
+        existing = g_ptr_array_index(arr, pos);
+        g_free(existing->value);
+        existing->value = g_steal_pointer(&custom->value);
+    } else {
+        g_ptr_array_add(arr, g_steal_pointer(&custom));
+    }
+    return true;
+}
+
+/**
+ * virPCIVPDResourceUpdateKeyword:
+ * @res: A non-NULL pointer to a virPCIVPDResource where a keyword will be 
updated.
+ * @readOnly: A bool specifying which section to update (in-memory): read-only 
or read-write.
+ * @keyword: A non-NULL pointer to a name of the keyword that will be updated.
+ * @value: A pointer to the keyword value or NULL. The value is copied on 
successful update.
+ *
+ * The caller is responsible for initializing the relevant RO or RW sections 
of the resource,
+ * otherwise, false will be returned.
+ *
+ * Keyword names are either 2-byte keywords from the spec or their 
human-readable alternatives
+ * used in XML elements. For vendor-specific and system-specific keywords only 
V%s and Y%s
+ * (except "YA" which is an asset tag) formatted values are accepted.
+ *
+ * Returns: true if a keyword has been updated successfully, false otherwise.
+ */
+bool
+virPCIVPDResourceUpdateKeyword(virPCIVPDResource *res, const bool readOnly,
+                               const char *const keyword, const char *const 
value)
+{
+    if (!res) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("Cannot update the resource: a NULL resource pointer 
has been provided."));
+        return false;
+    } else if (!keyword) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("Cannot update the resource: a NULL keyword pointer 
has been provided."));
+        return false;
+    }
+
+    if (readOnly) {
+        if (!res->ro) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _("Cannot update the read-only keyword: RO section 
not initialized."));
+            return false;
+        }
+
+        if (STREQ("EC", keyword) || STREQ("change_level", keyword)) {
+            g_free(res->ro->change_level);
+            res->ro->change_level = g_strdup(value);
+            return true;
+        } else if (STREQ("MN", keyword) || STREQ("manufacture_id", keyword)) {
+            g_free(res->ro->manufacture_id);
+            res->ro->manufacture_id = g_strdup(value);
+            return true;
+        } else if (STREQ("PN", keyword) || STREQ("part_number", keyword)) {
+            g_free(res->ro->part_number);
+            res->ro->part_number = g_strdup(value);
+            return true;
+        } else if (STREQ("SN", keyword) || STREQ("serial_number", keyword)) {
+            g_free(res->ro->serial_number);
+            res->ro->serial_number = g_strdup(value);
+            return true;
+        } else if (virPCIVPDResourceIsVendorKeyword(keyword)) {
+            if (!virPCIVPDResourceCustomUpsertValue(res->ro->vendor_specific, 
keyword[1], value)) {
+                return false;
+            }
+            return true;
+        } else if (STREQ("FG", keyword) || STREQ("LC", keyword) || STREQ("PG", 
keyword)) {
+            /* Legacy PICMIG keywords are skipped on purpose. */
+            return true;
+        } else if (STREQ("CP", keyword)) {
+            /* The CP keyword is currently not supported and is skipped. */
+            return true;
+        }
+
+    } else {
+        if (!res->rw) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _
+                           ("Cannot update the read-write keyword: read-write 
section not initialized."));
+            return false;
+        }
+
+        if (STREQ("YA", keyword) || STREQ("asset_tag", keyword)) {
+            g_free(res->rw->asset_tag);
+            res->rw->asset_tag = g_strdup(value);
+            return true;
+        } else if (virPCIVPDResourceIsVendorKeyword(keyword)) {
+            if (!virPCIVPDResourceCustomUpsertValue(res->rw->vendor_specific, 
keyword[1], value)) {
+                return false;
+            }
+            return true;
+        } else if (virPCIVPDResourceIsSystemKeyword(keyword)) {
+            if (!virPCIVPDResourceCustomUpsertValue(res->rw->system_specific, 
keyword[1], value)) {
+                return false;
+            }
+            return true;
+        }
+    }
+    VIR_WARN("Tried to update an unsupported keyword %s: skipping.", keyword);
+    return true;
+}
+
+#ifdef __linux__
+
+/**
+ * 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 numRead = pread(vpdFileFd, buf, count, offset);
+
+    if (numRead == -1) {
+        VIR_DEBUG("Unable to read %zu bytes at offset %ld from fd: %d", count, 
offset, vpdFileFd);
+    } else if (numRead) {
+        /*
+         * 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 numRead;
+}
+
+/**
+ * virPCIVPDParseVPDLargeResourceFields:
+ * @vpdFileFd: A file descriptor associated with a file containing PCI device 
VPD.
+ * @resPos: A position where the resource data bytes begin in a file 
descriptor.
+ * @resDataLen: A length of the data portion of a resource.
+ * @readOnly: A boolean showing whether the resource type is VPD-R or VPD-W.
+ * @csum: A pointer to a 1-byte checksum.
+ * @res: A pointer to virPCIVPDResource.
+ *
+ * Returns: a pointer to a VPDResource which needs to be freed by the caller or
+ * NULL if getting it failed for some reason.
+ */
+bool
+virPCIVPDParseVPDLargeResourceFields(int vpdFileFd, uint16_t resPos, uint16_t 
resDataLen,
+                                     bool readOnly, uint8_t *csum, 
virPCIVPDResource *res)
+{
+    g_autofree char *fieldKeyword = NULL;
+    g_autofree char *fieldValue = NULL;
+    virPCIVPDResourceFieldValueFormat fieldFormat = 
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 fieldDataLen = 0, bytesToRead = 0;
+    uint16_t fieldPos = resPos;
+
+    bool hasChecksum = false;
+    bool hasRW = false;
+
+    while (fieldPos + 3 < resPos + resDataLen) {
+        /* Keyword resources consist of keywords (2 ASCII bytes per the spec) 
and 1-byte length. */
+        if (virPCIVPDReadVPDBytes(vpdFileFd, buf, 3, fieldPos, 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 false;
+        }
+        fieldDataLen = buf[2];
+        /* Change the position to the field's data portion skipping the 
keyword and length bytes. */
+        fieldPos += 3;
+        fieldKeyword = g_strndup((char *)buf, 2);
+        fieldFormat = virPCIVPDResourceGetFieldValueFormat(fieldKeyword);
+
+        /* Handle special cases first */
+        if (!readOnly && fieldFormat == 
VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _("Unexpected RV keyword in the read-write 
section."));
+            return false;
+        } else if (readOnly && fieldFormat == 
VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _("Unexpected RW keyword in the read-only 
section."));
+            return false;
+        }
+
+        /* Determine how many bytes to read per field value type. */
+        switch (fieldFormat) {
+            case VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT:
+            case VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR:
+            case VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_BINARY:
+                bytesToRead = fieldDataLen;
+                break;
+            case VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RESVD:
+                /* Only need one byte to be read and accounted towards
+                 * the checksum calculation. */
+                bytesToRead = 1;
+                break;
+            case VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST:
+                /* The VPD format could be extended in future versions with new
+                 * keywords - attempt to skip them by reading past them since
+                 * their data length would still be specified. */
+                VIR_DEBUG("Could not determine a field value format for 
keyword: %s", fieldKeyword);
+                bytesToRead = fieldDataLen;
+                break;
+            default:
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                               _("Unexpected field value format 
encountered."));
+                return false;
+        }
+
+        if (virPCIVPDReadVPDBytes(vpdFileFd, buf, bytesToRead, fieldPos, csum) 
!= bytesToRead) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _("Could not parse a resource field data - VPD has 
invalid format"));
+            return false;
+        }
+        /* Advance the position to the first byte of the next field. */
+        fieldPos += fieldDataLen;
+
+        if (fieldFormat == 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.
+             */
+            fieldValue = g_strstrip(g_strndup((char *)buf, fieldDataLen));
+            if (!virPCIVPDResourceIsValidTextValue(fieldValue)) {
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                               _("Field value contains invalid characters"));
+                return false;
+            }
+        } else if (fieldFormat == 
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 false;
+            }
+            hasChecksum = true;
+            g_free(g_steal_pointer(&fieldKeyword));
+            g_free(g_steal_pointer(&fieldValue));
+            continue;
+        } else if (fieldFormat == 
VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_RDWR) {
+            /* Skip the read-write space since it is used for indication only. 
*/
+            hasRW = true;
+            g_free(g_steal_pointer(&fieldKeyword));
+            g_free(g_steal_pointer(&fieldValue));
+        } else if (fieldFormat == 
VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST) {
+            /* Skip unknown fields */
+            g_free(g_steal_pointer(&fieldKeyword));
+            g_free(g_steal_pointer(&fieldValue));
+            continue;
+        } else {
+            fieldValue = g_malloc(fieldDataLen);
+            memcpy(fieldValue, buf, fieldDataLen);
+        }
+
+        if (readOnly) {
+            if (!res->ro) {
+                res->ro = virPCIVPDResourceRONew();
+            }
+        } else {
+            if (!res->rw) {
+                res->rw = virPCIVPDResourceRWNew();
+            }
+        }
+        /* The field format, keyword and value are determined. Attempt to 
update the resource. */
+        if (!virPCIVPDResourceUpdateKeyword(res, readOnly, fieldKeyword, 
fieldValue)) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           _("Could not update the VPD resource keyword: %s"), 
fieldKeyword);
+            return false;
+        }
+        /* No longer need those since copies were made during the keyword 
update. */
+        g_free(g_steal_pointer(&fieldKeyword));
+        g_free(g_steal_pointer(&fieldValue));
+    }
+    if (readOnly && !hasChecksum) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("VPD-R does not contain the mandatory RV field"));
+        return false;
+    } else if (!readOnly && !hasRW) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("VPD-W does not contain the mandatory RW field"));
+        return false;
+    }
+
+    return true;
+}
+
+/**
+ * virPCIVPDParseVPDLargeResourceString:
+ * @vpdFileFd: A file descriptor associated with a file containing PCI device 
VPD.
+ * @resPos: A position where the resource data bytes begin in a file 
descriptor.
+ * @resDataLen: 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.
+ */
+bool
+virPCIVPDParseVPDLargeResourceString(int vpdFileFd, uint16_t resPos,
+                                     uint16_t resDataLen, uint8_t *csum, 
virPCIVPDResource *res)
+{
+    g_autofree char *resValue = NULL;
+
+    /* The resource value is not NULL-terminated so add one more byte. */
+    g_autofree char *buf = g_malloc0(resDataLen + 1);
+
+    if (virPCIVPDReadVPDBytes(vpdFileFd, (uint8_t *)buf, resDataLen, resPos, 
csum) != resDataLen) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("Could not read a part of a resource - VPD has 
invalid format"));
+        return false;
+    }
+    resValue = g_strdup(g_strstrip(buf));
+    if (!virPCIVPDResourceIsValidTextValue(resValue)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       _("The string resource has invalid characters in its 
value"));
+        return false;
+    }
+    res->name = g_steal_pointer(&resValue);
+    return true;
+}
+
+/**
+ * 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.
+ */
+virPCIVPDResource *
+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 headerBuf[2];
+
+    bool isWellFormed = false;
+    uint16_t resPos = 0, resDataLen;
+    uint8_t tag = 0;
+    bool endResReached = false, hasReadOnly = false;
+
+    g_autoptr(virPCIVPDResource) res = g_new0(virPCIVPDResource, 1);
+
+    while (resPos <= PCI_VPD_ADDR_MASK) {
+        /* Read the resource data type tag. */
+        if (virPCIVPDReadVPDBytes(vpdFileFd, &tag, 1, resPos, &csum) != 1) {
+            break;
+        }
+        /* 0x80 == 0b10000000 - the large resource data type flag. */
+        if (tag & PCI_VPD_LARGE_RESOURCE_FLAG) {
+            if (resPos > 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, headerBuf, 2, resPos + 1, 
&csum) != 2) {
+                break;
+            }
+            resDataLen = headerBuf[0] + (headerBuf[1] << 8);
+            /* Change the position to the byte following the tag and length 
bytes. */
+            resPos += 3;
+        } else {
+            /* Handle a small resource record.
+             * 0xxxxyyy & 00000111, where xxxx - resource data type bits, yyy 
- length bits. */
+            resDataLen = tag & 7;
+            /* 0xxxxyyy >> 3 == 0000xxxx */
+            tag >>= 3;
+            /* Change the position to the byte past the byte containing tag 
and length bits. */
+            resPos += 1;
+        }
+        if (tag == PCI_VPD_RESOURCE_END_TAG) {
+            /* Stop VPD traversal since the end tag was encountered. */
+            endResReached = true;
+            break;
+        }
+        if (resDataLen > PCI_VPD_ADDR_MASK + 1 - resPos) {
+            /* 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:
+                isWellFormed = virPCIVPDParseVPDLargeResourceString(vpdFileFd, 
resPos, resDataLen,
+                                                                    &csum, 
res);
+                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:
+                isWellFormed = virPCIVPDParseVPDLargeResourceFields(vpdFileFd, 
resPos,
+                                                                    
resDataLen, true, &csum, res);
+                /* Encountered the VPD-R tag. The resource record parsing also 
validates
+                 * the presence of the required checksum in the RV field. */
+                hasReadOnly = 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:
+                isWellFormed = virPCIVPDParseVPDLargeResourceFields(vpdFileFd, 
resPos, resDataLen,
+                                                                    false, 
&csum, res);
+                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);
+                resPos += resDataLen;
+                continue;
+        }
+
+        if (!isWellFormed) {
+            VIR_DEBUG("Encountered an invalid VPD");
+            return NULL;
+        }
+
+        /* Continue processing other resource records. */
+        resPos += resDataLen;
+    }
+    if (!hasReadOnly) {
+        VIR_DEBUG("Encountered an invalid VPD: does not have a VPD-R record");
+        return NULL;
+    } else if (!endResReached) {
+        /* Does not have an end tag. */
+        VIR_DEBUG("Encountered an invalid VPD");
+        return NULL;
+    }
+    return g_steal_pointer(&res);
+}
+
+#endif /* __linux__ */
diff --git a/src/util/virpcivpd.h b/src/util/virpcivpd.h
new file mode 100644
index 0000000000..9bfec43e03
--- /dev/null
+++ b/src/util/virpcivpd.h
@@ -0,0 +1,76 @@
+/*
+ * 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"
+
+typedef struct virPCIVPDResourceCustom virPCIVPDResourceCustom;
+struct virPCIVPDResourceCustom {
+    char idx;
+    char *value;
+};
+
+typedef struct virPCIVPDResourceRO virPCIVPDResourceRO;
+struct virPCIVPDResourceRO {
+    char *part_number;
+    char *change_level;
+    char *manufacture_id;
+    char *serial_number;
+    GPtrArray *vendor_specific;
+};
+
+typedef struct virPCIVPDResourceRW virPCIVPDResourceRW;
+struct virPCIVPDResourceRW {
+    char *asset_tag;
+    GPtrArray *vendor_specific;
+    GPtrArray *system_specific;
+};
+
+typedef struct virPCIVPDResource virPCIVPDResource;
+struct virPCIVPDResource {
+    char *name;
+    virPCIVPDResourceRO *ro;
+    virPCIVPDResourceRW *rw;
+};
+
+
+virPCIVPDResource *virPCIVPDParse(int vpdFileFd);
+void virPCIVPDResourceFree(virPCIVPDResource *res);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(virPCIVPDResource, virPCIVPDResourceFree);
+
+virPCIVPDResourceRO *virPCIVPDResourceRONew(void);
+void virPCIVPDResourceROFree(virPCIVPDResourceRO *ro);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(virPCIVPDResourceRO, virPCIVPDResourceROFree);
+
+virPCIVPDResourceRW *virPCIVPDResourceRWNew(void);
+void virPCIVPDResourceRWFree(virPCIVPDResourceRW *rw);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(virPCIVPDResourceRW, virPCIVPDResourceRWFree);
+
+bool
+virPCIVPDResourceUpdateKeyword(virPCIVPDResource *res, const bool readOnly,
+                               const char *const keyword, const char *const 
value);
+
+void virPCIVPDResourceCustomFree(virPCIVPDResourceCustom *custom);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(virPCIVPDResourceCustom, 
virPCIVPDResourceCustomFree);
diff --git a/src/util/virpcivpdpriv.h b/src/util/virpcivpdpriv.h
new file mode 100644
index 0000000000..c122e16018
--- /dev/null
+++ b/src/util/virpcivpdpriv.h
@@ -0,0 +1,83 @@
+/*
+ * 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"
+
+/*
+ * 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_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 
char *value);
+
+bool virPCIVPDResourceIsValidTextValue(const char *value);
+
+bool
+virPCIVPDResourceCustomCompareIndex(virPCIVPDResourceCustom *a, 
virPCIVPDResourceCustom *b);
+
+bool
+virPCIVPDResourceCustomUpsertValue(GPtrArray *arr, char index, const char 
*const value);
+
+#ifdef __linux__
+
+size_t
+virPCIVPDReadVPDBytes(int vpdFileFd, uint8_t *buf, size_t count, off_t offset, 
uint8_t *csum);
+
+bool virPCIVPDParseVPDLargeResourceFields(int vpdFileFd, uint16_t resPos, 
uint16_t resDataLen,
+                                          bool readOnly, uint8_t *csum, 
virPCIVPDResource *res);
+
+bool virPCIVPDParseVPDLargeResourceString(int vpdFileFd, uint16_t resPos, 
uint16_t resDataLen,
+                                          uint8_t *csum, virPCIVPDResource 
*res);
+
+#endif /* __linux__ */
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/testutils.c b/tests/testutils.c
index d071abd6d7..3bc4274e97 100644
--- a/tests/testutils.c
+++ b/tests/testutils.c
@@ -1143,3 +1143,38 @@ virTestStablePath(const char *path)
 
     return g_strdup(path);
 }
+
+#ifdef __linux__
+/**
+ * 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.
+ *
+ */
+int
+virCreateAnonymousFile(const uint8_t *data, size_t len)
+{
+    int fd = -1;
+    char path[] = abs_builddir "testutils-memfd-XXXXXX";
+    /* A temp file is used since not all supported distributions support 
memfd. */
+    if ((fd = g_mkstemp_full(path, O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR)) < 
0) {
+        return fd;
+    }
+    g_unlink(path);
+
+    if (safewrite(fd, data, len) != len) {
+        VIR_TEST_DEBUG("%s: %s", "failed to write to an anonymous file",
+                g_strerror(errno));
+        goto cleanup;
+    }
+    return fd;
+ cleanup:
+    if (VIR_CLOSE(fd) < 0) {
+        VIR_TEST_DEBUG("%s: %s", "failed to close an anonymous file",
+                g_strerror(errno));
+    }
+    return -1;
+}
+#endif
diff --git a/tests/testutils.h b/tests/testutils.h
index 27d135fc02..13a154a5af 100644
--- a/tests/testutils.h
+++ b/tests/testutils.h
@@ -173,3 +173,7 @@ int testCompareDomXML2XMLFiles(virCaps *caps,
 
 char *
 virTestStablePath(const char *path);
+
+#ifdef __linux__
+int virCreateAnonymousFile(const uint8_t *data, size_t len);
+#endif
diff --git a/tests/virpcivpdtest.c b/tests/virpcivpdtest.c
new file mode 100644
index 0000000000..4660d32aaa
--- /dev/null
+++ b/tests/virpcivpdtest.c
@@ -0,0 +1,809 @@
+/*
+ * 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 "internal.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
+
+#ifdef __linux__
+
+VIR_LOG_INIT("tests.vpdtest");
+
+
+typedef struct _TestPCIVPDKeywordValue {
+    const char *keyword;
+    const char *value;
+    char **actual;
+} TestPCIVPDKeywordValue;
+
+static int
+testPCIVPDResourceBasic(const void *data G_GNUC_UNUSED)
+{
+    size_t i = 0;
+    g_autoptr(virPCIVPDResourceRO) ro = virPCIVPDResourceRONew();
+    g_autoptr(virPCIVPDResourceRW) rw = virPCIVPDResourceRWNew();
+    /* Note: when the same keyword is updated multiple times the
+     * virPCIVPDResourceUpdateKeyword function is expected to free the
+     * previous value whether it is a fixed keyword or a custom one.
+     * */
+    const TestPCIVPDKeywordValue readOnlyCases[] = {
+        {.keyword = "EC", .value = "level1", .actual = &ro->change_level},
+        {.keyword = "EC", .value = "level2", .actual = &ro->change_level},
+        {.keyword = "change_level", .value = "level3", .actual = 
&ro->change_level},
+        {.keyword = "PN", .value = "number1", .actual = &ro->part_number},
+        {.keyword = "PN", .value = "number2", .actual = &ro->part_number},
+        {.keyword = "part_number", .value = "number3", .actual = 
&ro->part_number},
+        {.keyword = "MN", .value = "id1", .actual = &ro->manufacture_id},
+        {.keyword = "MN", .value = "id2", .actual = &ro->manufacture_id},
+        {.keyword = "manufacture_id", .value = "id3", &ro->manufacture_id},
+        {.keyword = "SN", .value = "serial1", .actual = &ro->serial_number},
+        {.keyword = "SN", .value = "serial2", .actual = &ro->serial_number},
+        {.keyword = "serial_number", .value = "serial3", .actual = 
&ro->serial_number},
+    };
+    const TestPCIVPDKeywordValue readWriteCases[] = {
+        {.keyword = "YA", .value = "tag1", .actual = &ro->change_level},
+        {.keyword = "YA", .value = "tag2", .actual = &ro->change_level},
+        {.keyword = "asset_tag", .value = "tag3", .actual = &ro->change_level},
+    };
+    const TestPCIVPDKeywordValue unsupportedFieldCases[] = {
+        {.keyword = "FG", .value = "42", .actual = NULL},
+        {.keyword = "LC", .value = "42", .actual = NULL},
+        {.keyword = "PG", .value = "42", .actual = NULL},
+        {.keyword = "CP", .value = "42", .actual = NULL},
+        {.keyword = "EX", .value = "42", .actual = NULL},
+    };
+    size_t numROCases = sizeof(readOnlyCases) / sizeof(TestPCIVPDKeywordValue);
+    size_t numRWCases = sizeof(readWriteCases) / 
sizeof(TestPCIVPDKeywordValue);
+    size_t numUnsupportedCases = sizeof(unsupportedFieldCases) / 
sizeof(TestPCIVPDKeywordValue);
+    g_autoptr(virPCIVPDResource) res = g_new0(virPCIVPDResource, 1);
+    virPCIVPDResourceCustom *custom = NULL;
+
+    g_autofree char *val = g_strdup("testval");
+    res->name = g_steal_pointer(&val);
+
+    /* RO has not been initialized - make sure updates fail. */
+    for (i = 0; i < numROCases; ++i) {
+        if (virPCIVPDResourceUpdateKeyword(res, true,
+                                           readOnlyCases[i].keyword,
+                                           readOnlyCases[i].value)) {
+            return -1;
+        }
+    }
+    /* RW has not been initialized - make sure updates fail. */
+    for (i = 0; i < numRWCases; ++i) {
+        if (virPCIVPDResourceUpdateKeyword(res, false,
+                                           readWriteCases[i].keyword,
+                                           readWriteCases[i].value)) {
+            return -1;
+        }
+    }
+    /* Initialize RO */
+    res->ro = g_steal_pointer(&ro);
+
+    /* Update keywords one by one and compare actual values with the expected 
ones. */
+    for (i = 0; i < numROCases; ++i) {
+        if (!virPCIVPDResourceUpdateKeyword(res, true,
+                                            readOnlyCases[i].keyword,
+                                            readOnlyCases[i].value)) {
+            return -1;
+        }
+        if (STRNEQ(readOnlyCases[i].value, *readOnlyCases[i].actual)) {
+            return -1;
+        }
+    }
+
+    /* Do a basic vendor field check. */
+    if (!virPCIVPDResourceUpdateKeyword(res, true, "V0", "vendor0")) {
+        return -1;
+    }
+    if (res->ro->vendor_specific->len != 1) {
+        return -1;
+    }
+    custom = g_ptr_array_index(res->ro->vendor_specific, 0);
+    if (custom->idx != '0' || STRNEQ(custom->value, "vendor0")) {
+        return -1;
+    }
+
+    /* Make sure unsupported RO keyword updates are not fatal. */
+    for (i = 0; i < numUnsupportedCases; ++i) {
+        if (!virPCIVPDResourceUpdateKeyword(res, true,
+                                            unsupportedFieldCases[i].keyword,
+                                            unsupportedFieldCases[i].value)) {
+            return -1;
+        }
+    }
+
+    /* Check that RW updates fail if RW has not been initialized. */
+    if (virPCIVPDResourceUpdateKeyword(res, false, "YA", "tag1")) {
+        return -1;
+    }
+    if (virPCIVPDResourceUpdateKeyword(res, false, "asset_tag", "tag1")) {
+        return -1;
+    }
+
+    /* Initialize RW */
+    res->rw = g_steal_pointer(&rw);
+    if (!virPCIVPDResourceUpdateKeyword(res, false, "YA", "tag1")
+        || STRNEQ(res->rw->asset_tag, "tag1")) {
+        return -1;
+    }
+    if (!virPCIVPDResourceUpdateKeyword(res, false, "asset_tag", "tag2")
+        || STRNEQ(res->rw->asset_tag, "tag2")) {
+        return -1;
+    }
+
+    /* Do a basic system field check. */
+    if (!virPCIVPDResourceUpdateKeyword(res, false, "Y0", "system0")) {
+        return -1;
+    }
+    if (res->rw->system_specific->len != 1) {
+        return -1;
+    }
+    custom = g_ptr_array_index(res->rw->system_specific, 0);
+    if (custom->idx != '0' || STRNEQ(custom->value, "system0")) {
+        return -1;
+    }
+
+    /* Make sure unsupported RW keyword updates are not fatal. */
+    for (i = 0; i < numUnsupportedCases; ++i) {
+        if (!virPCIVPDResourceUpdateKeyword(res, false,
+                                            unsupportedFieldCases[i].keyword,
+                                            unsupportedFieldCases[i].value)) {
+            return -1;
+        }
+    }
+
+
+    /* Just make sure the name has not been changed during keyword updates. */
+    if (!STREQ_NULLABLE(res->name, "testval")) {
+        return -1;
+    }
+    return 0;
+}
+
+static int
+testPCIVPDResourceCustomCompareIndex(const void *data G_GNUC_UNUSED)
+{
+    g_autoptr(virPCIVPDResourceCustom) a = NULL, b = NULL;
+
+    /* Both are NULL */
+    if (!virPCIVPDResourceCustomCompareIndex(a, b)) {
+        return -1;
+    }
+
+    /* a is not NULL */
+    a = g_new0(virPCIVPDResourceCustom, 1);
+    if (virPCIVPDResourceCustomCompareIndex(a, b)) {
+        return -1;
+    }
+
+    /* Reverse */
+    if (virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+
+    /* Same index, different strings */
+    b = g_new0(virPCIVPDResourceCustom, 1);
+    a->idx = 'z';
+    a->value = g_strdup("42");
+    b->idx = 'z';
+    b->value = g_strdup("24");
+    if (!virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+    /* Different index, different strings */
+    a->idx = 'a';
+    if (virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+
+    virPCIVPDResourceCustomFree(a);
+    virPCIVPDResourceCustomFree(b);
+    a = g_new0(virPCIVPDResourceCustom, 1);
+    b = g_new0(virPCIVPDResourceCustom, 1);
+
+    /* Same index, same strings */
+    a->idx = 'z';
+    a->value = g_strdup("42");
+    b->idx = 'z';
+    b->value = g_strdup("42");
+    if (!virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+    /* Different index, same strings */
+    a->idx = 'a';
+    if (virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+    /* Different index, same value pointers */
+    g_free(b->value);
+    b->value = a->value;
+    if (virPCIVPDResourceCustomCompareIndex(b, a)) {
+        return -1;
+    }
+    b->value = NULL;
+
+    return 0;
+}
+
+static int
+testPCIVPDResourceCustomUpsertValue(const void *data G_GNUC_UNUSED)
+{
+    g_autoptr(GPtrArray) arr = g_ptr_array_new_full(0, 
(GDestroyNotify)virPCIVPDResourceCustomFree);
+    virPCIVPDResourceCustom *custom = NULL;
+    if (!virPCIVPDResourceCustomUpsertValue(arr, 'A', "testval")) {
+        return -1;
+    }
+    if (arr->len != 1) {
+        return -1;
+    }
+    custom = g_ptr_array_index(arr, 0);
+    if (custom == NULL || custom->idx != 'A' || STRNEQ_NULLABLE(custom->value, 
"testval")) {
+        return -1;
+    }
+
+    /* Idempotency */
+    if (!virPCIVPDResourceCustomUpsertValue(arr, 'A', "testval")) {
+        return -1;
+    }
+    if (arr->len != 1) {
+        return -1;
+    }
+    custom = g_ptr_array_index(arr, 0);
+    if (custom == NULL || custom->idx != 'A' || STRNEQ_NULLABLE(custom->value, 
"testval")) {
+        return -1;
+    }
+
+    /* Existing value updates. */
+    if (!virPCIVPDResourceCustomUpsertValue(arr, 'A', "testvalnew")) {
+        return -1;
+    }
+    if (arr->len != 1) {
+        return -1;
+    }
+    custom = g_ptr_array_index(arr, 0);
+    if (custom == NULL || custom->idx != 'A' || STRNEQ_NULLABLE(custom->value, 
"testvalnew")) {
+        return -1;
+    }
+
+    /* Inserting multiple values */
+    if (!virPCIVPDResourceCustomUpsertValue(arr, '1', "42")) {
+        return -1;
+    }
+    if (arr->len != 2) {
+        return -1;
+    }
+    custom = g_ptr_array_index(arr, 1);
+    if (custom == NULL || custom->idx != '1' || STRNEQ_NULLABLE(custom->value, 
"42")) {
+        return -1;
+    }
+
+    return 0;
+}
+
+
+typedef struct _TestPCIVPDExpectedString {
+    const char *keyword;
+    bool expected;
+} TestPCIVPDExpectedString;
+
+/*
+ * 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 textValueCases[] = {
+        /* 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},
+        {";", true},
+        {"\\42", false},
+        {"/42", false},
+    };
+    for (i = 0; i < sizeof(textValueCases) / sizeof(textValueCases[0]); ++i) {
+        if (virPCIVPDResourceIsValidTextValue(textValueCases[i].keyword) !=
+            textValueCases[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 valueFormatCases[] = {
+        {"SN", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"EC", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"MN", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_TEXT},
+        {"PN", 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},
+        {"sn", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"ec", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"mn", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"pn", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"4", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"42", 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},
+        {"v", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"vA", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"va", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"ya", VIR_PCI_VPD_RESOURCE_FIELD_VALUE_FORMAT_LAST},
+        {"Ya", 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(valueFormatCases) / sizeof(valueFormatCases[0]); 
++i) {
+        if (virPCIVPDResourceGetFieldValueFormat(valueFormatCases[i].keyword) 
!=
+            valueFormatCases[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 readBytes = 0;
+    size_t dataLen = 0;
+
+    /* An example of a valid VPD record with one VPD-R resource and 2 fields. 
*/
+    uint8_t fullVPDExample[] = {
+        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
+    };
+    dataLen = sizeof(fullVPDExample) / sizeof(uint8_t) - 2;
+    buf = g_malloc0(dataLen);
+
+    fd = virCreateAnonymousFile(fullVPDExample, dataLen);
+
+    readBytes = virPCIVPDReadVPDBytes(fd, buf, dataLen, 0, &csum);
+
+    if (readBytes != dataLen) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       "The number of bytes read %zu is lower than expected 
%zu ",
+                       readBytes, dataLen);
+        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 dataLen = 0;
+    bool result = false;
+
+    g_autoptr(virPCIVPDResource) res = g_new0(virPCIVPDResource, 1);
+    const char *expectedValue = "testname";
+
+    const uint8_t stringResExample[] = {
+        VPD_STRING_RESOURCE_EXAMPLE_DATA
+    };
+
+    dataLen = sizeof(stringResExample) / sizeof(uint8_t);
+    fd = virCreateAnonymousFile(stringResExample, dataLen);
+    result = virPCIVPDParseVPDLargeResourceString(fd, 0, dataLen, &csum, res);
+    VIR_FORCE_CLOSE(fd);
+
+    if (!result) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       "Could not parse the example resource.");
+        return -1;
+    }
+
+    if (STRNEQ(expectedValue, res->name)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       "Unexpected string resource value: %s, expected: %s",
+                       res->name, expectedValue);
+        return -1;
+    }
+    return 0;
+}
+
+static int
+testVirPCIVPDValidateExampleReadOnlyFields(virPCIVPDResource *res)
+{
+    const char *expectedName = "testname";
+    virPCIVPDResourceCustom *custom = NULL;
+    if (STRNEQ(res->name, expectedName)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                "Unexpected string resource value: %s, expected: %s",
+                res->name, expectedName);
+        return -1;
+    }
+
+    if (!res->ro) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                "Read-only keywords are missing from the VPD resource.");
+        return -1;
+    }
+
+    if (STRNEQ_NULLABLE(res->ro->part_number, "42")) {
+        return -1;
+    } else if (STRNEQ_NULLABLE(res->ro->change_level, "4242")) {
+        return -1;
+    }
+    if (!res->ro->vendor_specific) {
+        return -1;
+    }
+
+    custom = g_ptr_array_index(res->ro->vendor_specific, 0);
+    if (custom->idx != 'A' || STRNEQ_NULLABLE(custom->value, "EX")) {
+        return -1;
+    }
+    return 0;
+}
+
+static int
+testVirPCIVPDParseFullVPD(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    size_t dataLen = 0;
+    int ret = 0;
+
+    g_autoptr(virPCIVPDResource) res = NULL;
+    /* Note: Custom fields are supposed to be freed by the resource cleanup 
code. */
+    virPCIVPDResourceCustom *custom = NULL;
+
+    const uint8_t fullVPDExample[] = {
+        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
+    };
+
+    dataLen = sizeof(fullVPDExample) / sizeof(uint8_t);
+    fd = virCreateAnonymousFile(fullVPDExample, dataLen);
+    res = virPCIVPDParse(fd);
+    VIR_FORCE_CLOSE(fd);
+
+    if (!res) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       "The resource pointer is NULL after parsing which is 
unexpected");
+        return ret;
+    }
+
+    if (!res->ro) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                "Read-only keywords are missing from the VPD resource.");
+        return -1;
+    } else if (!res->rw) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                "Read-write keywords are missing from the VPD resource.");
+        return -1;
+    }
+
+    if (testVirPCIVPDValidateExampleReadOnlyFields(res)) {
+        return -1;
+    }
+
+    if (STRNEQ_NULLABLE(res->rw->asset_tag, "ID42")) {
+        return -1;
+    }
+
+    if (!res->rw->vendor_specific) {
+        return -1;
+    }
+    custom = g_ptr_array_index(res->rw->vendor_specific, 0);
+    if (custom->idx != 'Z' || STRNEQ_NULLABLE(custom->value, "42")) {
+        return -1;
+    }
+
+    if (!res->rw->system_specific) {
+        return -1;
+    }
+
+    custom = g_ptr_array_index(res->rw->system_specific, 0);
+    if (custom->idx != 'F' || STRNEQ_NULLABLE(custom->value, "EX")) {
+        return -1;
+    }
+
+    custom = g_ptr_array_index(res->rw->system_specific, 1);
+    if (custom->idx != 'E' || STRNEQ_NULLABLE(custom->value, "")) {
+        return -1;
+    }
+
+    custom = NULL;
+    return ret;
+}
+
+static int
+testVirPCIVPDParseFullVPDSkipInvalidKeywords(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    size_t dataLen = 0;
+
+    g_autoptr(virPCIVPDResource) res = NULL;
+
+    const uint8_t fullVPDExample[] = {
+        VPD_STRING_RESOURCE_EXAMPLE_HEADER,
+        VPD_STRING_RESOURCE_EXAMPLE_DATA,
+        PCI_VPD_LARGE_RESOURCE_FLAG | PCI_VPD_READ_ONLY_LARGE_RESOURCE_FLAG, 
0x25, 0x00,
+        VPD_R_EXAMPLE_FIELDS,
+        /* The keywords below (except for "RV") are invalid but will be 
skipped by the parser */
+        0x07, 'A', 0x02, 0x00, 0x00,
+        'V', 0x07, 0x02, 0x00, 0x00,
+        'e', 'x', 0x02, 0x00, 0x00,
+        'R', 'V', 0x02, 0x9A, 0x00,
+        PCI_VPD_RESOURCE_END_VAL
+    };
+
+    dataLen = sizeof(fullVPDExample) / sizeof(uint8_t);
+    fd = virCreateAnonymousFile(fullVPDExample, dataLen);
+    res = virPCIVPDParse(fd);
+    VIR_FORCE_CLOSE(fd);
+
+    if (!res) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       "The resource pointer is NULL after parsing which is 
unexpected.");
+        return -1;
+    }
+    if (!res->ro) {
+        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                       "The RO portion of the VPD resource is NULL.");
+        return -1;
+    }
+
+    if (testVirPCIVPDValidateExampleReadOnlyFields(res)) {
+        return -1;
+    }
+    return 0;
+}
+
+static int
+testVirPCIVPDParseFullVPDInvalid(const void *opaque G_GNUC_UNUSED)
+{
+    int fd = -1;
+    size_t dataLen = 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_RW_IN_VPD_R_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, 0x81, 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(invalidVPD) \
+    do { \
+        g_autoptr(virPCIVPDResource) res = NULL; \
+        const uint8_t testCase[] = { invalidVPD }; \
+        dataLen = sizeof(testCase) / sizeof(uint8_t); \
+        fd = virCreateAnonymousFile(testCase, dataLen); \
+        if ((res = 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_RW_IN_VPD_R_KEY);
+    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("Basic functionality of virPCIVPDResource ", 
testPCIVPDResourceBasic, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Custom field index comparison",
+                   testPCIVPDResourceCustomCompareIndex, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Custom field value insertion and updates ",
+                   testPCIVPDResourceCustomUpsertValue, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Valid text values ", testPCIVPDIsValidTextValue, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Determining a field value format by a key ",
+                   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 VPD resource with an invalid keyword ",
+                   testVirPCIVPDParseFullVPDSkipInvalidKeywords, NULL) < 0)
+        ret = -1;
+    if (virTestRun("Parsing VPD resources from a full VPD ", 
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)
+#endif
-- 
2.32.0


Reply via email to