storagecommands: Split validation logic

The Fru validation logic is useful in isolation, split it into a
separate library that can be included.

Tested:
[0/1] Running all tests.
 1/12 phosphor-objmgr / well_known                          OK              0.24s
 2/12 phosphor-objmgr / need_to_introspect                  OK              0.22s
 3/12 phosphor-objmgr / name_change                         OK              0.16s
 4/12 phosphor-objmgr / interfaces_added                    OK              0.13s
 5/12 phosphor-objmgr / handler                             OK              0.09s
 6/12 phosphor-objmgr / mapper                              OK              0.07s
 7/12 phosphor-host-ipmid / entitymap_json                  OK              0.06s
 8/12 phosphor-host-ipmid / message                         OK              0.05s
 9/12 phosphor-host-ipmid / session/closesession            OK              0.03s
10/12 phosphor-objmgr / associations                        OK              0.20s
11/12 phosphor-host-ipmid / dbus-sdr/sensorcommands         OK              0.02s
12/12 intel-ipmi-oem / message                              OK              0.02s

Ok:                 12
Expected Fail:      0
Fail:               0
Unexpected Pass:    0
Skipped:            0
Timeout:            0

Change-Id: I9130eb81703b0cda7c3229f16cd689dd2c96c55c
Signed-off-by: Peter Foley <pefoley@google.com>
diff --git a/include/fruutils.hpp b/include/fruutils.hpp
new file mode 100644
index 0000000..f830b71
--- /dev/null
+++ b/include/fruutils.hpp
@@ -0,0 +1,24 @@
+/*
+// Copyright (c) 2022-2023 Intel Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+*/
+
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+// Validate the vector holds a complete FRU's contents.
+bool validateBasicFruContent(const std::vector<uint8_t>& fru,
+                             size_t lastWriteAddr);
diff --git a/include/storagecommands.hpp b/include/storagecommands.hpp
index 865427f..c359702 100644
--- a/include/storagecommands.hpp
+++ b/include/storagecommands.hpp
@@ -252,20 +252,6 @@
 };
 
 #pragma pack(push, 1)
-struct FRUHeader
-{
-    uint8_t commonHeaderFormat;
-    uint8_t internalOffset;
-    uint8_t chassisOffset;
-    uint8_t boardOffset;
-    uint8_t productOffset;
-    uint8_t multiRecordOffset;
-    uint8_t pad;
-    uint8_t checksum;
-};
-#pragma pack(pop)
-
-#pragma pack(push, 1)
 struct Type12Record
 {
     get_sdr::SensorDataRecordHeader header;
diff --git a/meson.build b/meson.build
index 2f01792..7383ce8 100644
--- a/meson.build
+++ b/meson.build
@@ -111,6 +111,7 @@
   'src/me_to_redfish_hooks.cpp',
   'src/chassiscommands.cpp',
   'src/allowlist-filter.cpp',
+  'src/fruutils.cpp',
   ipmiallowlist,
 ]
 
diff --git a/src/fruutils.cpp b/src/fruutils.cpp
new file mode 100644
index 0000000..625ca91
--- /dev/null
+++ b/src/fruutils.cpp
@@ -0,0 +1,91 @@
+/*
+// Copyright (c) 2022-2023 Intel Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+*/
+
+#include "fruutils.hpp"
+
+#include <algorithm>
+#include <vector>
+
+#pragma pack(push, 1)
+struct FRUHeader
+{
+    uint8_t commonHeaderFormat;
+    uint8_t internalOffset;
+    uint8_t chassisOffset;
+    uint8_t boardOffset;
+    uint8_t productOffset;
+    uint8_t multiRecordOffset;
+    uint8_t pad;
+    uint8_t checksum;
+};
+#pragma pack(pop)
+
+bool validateBasicFruContent(const std::vector<uint8_t>& fru,
+                             size_t lastWriteAddr)
+{
+    bool atEnd = false;
+
+    if (fru.size() >= sizeof(FRUHeader))
+    {
+        const FRUHeader* header =
+            reinterpret_cast<const FRUHeader*>(fru.data());
+
+        int areaLength = 0;
+        size_t lastRecordStart = std::max(
+            {header->internalOffset, header->chassisOffset, header->boardOffset,
+             header->productOffset, header->multiRecordOffset});
+        lastRecordStart *= 8; // header starts in are multiples of 8 bytes
+
+        if (header->multiRecordOffset)
+        {
+            // This FRU has a MultiRecord Area
+            uint8_t endOfList = 0;
+            // Walk the MultiRecord headers until the last record
+            while (!endOfList)
+            {
+                // The MSB in the second byte of the MultiRecord header signals
+                // "End of list"
+                endOfList = fru[lastRecordStart + 1] & 0x80;
+                // Third byte in the MultiRecord header is the length
+                areaLength = fru[lastRecordStart + 2];
+                // This length is in bytes (not 8 bytes like other headers)
+                areaLength += 5; // The length omits the 5 byte header
+                if (!endOfList)
+                {
+                    // Next MultiRecord header
+                    lastRecordStart += areaLength;
+                }
+            }
+        }
+        else
+        {
+            // This FRU does not have a MultiRecord Area
+            // Get the length of the area in multiples of 8 bytes
+            if (lastWriteAddr > (lastRecordStart + 1))
+            {
+                // second byte in record area is the length
+                areaLength = fru[lastRecordStart + 1];
+                areaLength *= 8; // it is in multiples of 8 bytes
+            }
+        }
+        if (lastWriteAddr >= (areaLength + lastRecordStart))
+        {
+            atEnd = true;
+        }
+    }
+
+    return atEnd;
+}
diff --git a/src/storagecommands.cpp b/src/storagecommands.cpp
index 8c9b49a..5f1a8f8 100644
--- a/src/storagecommands.cpp
+++ b/src/storagecommands.cpp
@@ -17,6 +17,7 @@
 #include "storagecommands.hpp"
 
 #include "commandutils.hpp"
+#include "fruutils.hpp"
 #include "ipmi_to_redfish_hooks.hpp"
 #include "sdrutils.hpp"
 #include "types.hpp"
@@ -523,55 +524,7 @@
     std::copy(dataToWrite.begin(), dataToWrite.begin() + writeLen,
               fruCache.begin() + fruInventoryOffset);
 
-    bool atEnd = false;
-
-    if (fruCache.size() >= sizeof(FRUHeader))
-    {
-        FRUHeader* header = reinterpret_cast<FRUHeader*>(fruCache.data());
-
-        int areaLength = 0;
-        size_t lastRecordStart = std::max(
-            {header->internalOffset, header->chassisOffset, header->boardOffset,
-             header->productOffset, header->multiRecordOffset});
-        lastRecordStart *= 8; // header starts in are multiples of 8 bytes
-
-        if (header->multiRecordOffset)
-        {
-            // This FRU has a MultiRecord Area
-            uint8_t endOfList = 0;
-            // Walk the MultiRecord headers until the last record
-            while (!endOfList)
-            {
-                // The MSB in the second byte of the MultiRecord header signals
-                // "End of list"
-                endOfList = fruCache[lastRecordStart + 1] & 0x80;
-                // Third byte in the MultiRecord header is the length
-                areaLength = fruCache[lastRecordStart + 2];
-                // This length is in bytes (not 8 bytes like other headers)
-                areaLength += 5; // The length omits the 5 byte header
-                if (!endOfList)
-                {
-                    // Next MultiRecord header
-                    lastRecordStart += areaLength;
-                }
-            }
-        }
-        else
-        {
-            // This FRU does not have a MultiRecord Area
-            // Get the length of the area in multiples of 8 bytes
-            if (lastWriteAddr > (lastRecordStart + 1))
-            {
-                // second byte in record area is the length
-                areaLength = fruCache[lastRecordStart + 1];
-                areaLength *= 8; // it is in multiples of 8 bytes
-            }
-        }
-        if (lastWriteAddr >= (areaLength + lastRecordStart))
-        {
-            atEnd = true;
-        }
-    }
+    bool atEnd = validateBasicFruContent(fruCache, lastWriteAddr);
     uint8_t countWritten = 0;
 
     writeBus = cacheBus;