fram_utils: Improve parsing to handle truncated HSM FRAM log reads

The Cavium HSM FRU parser can fail or produce incorrect data if the
FRAM log read is truncated (i.e., ends mid-line without a newline).
If a truncated line contains partial key information like
"Board Information:EB", this could overwrite a complete value like
"Board Information:EBB6100..." read from an earlier line.

This change updates ParseFramData to prevent partial data from
overwriting complete data. Since a line not terminated by a newline is likely incomplete, it is safer and simpler to discard it entirely rather than attempting to parse potentially partial data. This commit refactors `ParseFramData` to
ignore the final line segment if the input data does not end with a
newline character.This resolves issues where incorrect data was parsed due to
truncation.

In `fru_scanner_fram_test.cc`:
*   Updated tests with new coverage
*   Updated test suite comments to explain test behavior

In `fru_scanner_fram_test.cc`:
*   `SuccessfulPartialRead` previously read 160 bytes, which ended in a
    truncated line. Because this line is now ignored, downstream
    validation failed. The test is updated to read 166 bytes, ensuring
    the partial data ends in a newline, allowing validation to succeed.

Tested: Unit test pass
PiperOrigin-RevId: 822751274
Change-Id: Id79a339e87dc152929c939414ba194b02dac0817
diff --git a/tlbmc/utils/fram_utils.cc b/tlbmc/utils/fram_utils.cc
index 6570b35..166f80f 100644
--- a/tlbmc/utils/fram_utils.cc
+++ b/tlbmc/utils/fram_utils.cc
@@ -1,5 +1,6 @@
 #include "tlbmc/utils/fram_utils.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <string>
@@ -58,11 +59,20 @@
     }
   }
 
+  const bool last_line_is_complete =
+      !content.empty() && content.back() == '\n';
+
   // Split the content into lines.
   std::vector<absl::string_view> lines = absl::StrSplit(content, '\n');
 
+  // If the last line is not complete, it's likely truncated, so discard it.
+  if (!last_line_is_complete && !lines.empty()) {
+    lines.pop_back();
+  }
+
   // Iterate through each line to find relevant information.
-  for (const auto& line : lines) {
+  for (size_t i = 0; i < lines.size(); ++i) {
+    absl::string_view line = lines[i];
     absl::string_view trimmed_line = absl::StripAsciiWhitespace(line);
     // We only care about lines that start with "INFO:".
     if (!absl::StartsWith(trimmed_line, "INFO:")) {
@@ -83,9 +93,12 @@
     if (module == "Linux") {
       // Handle Linux:Board Serial Number: <value>
       if (absl::StartsWith(message, kBoardSerialNumberKey)) {
-        parsed_data.board_serial_number =
+        const std::string board_serial_number =
             std::string(absl::StripAsciiWhitespace(
                 message.substr(kBoardSerialNumberKey.length())));
+        if (!board_serial_number.empty()) {
+          parsed_data.board_serial_number = board_serial_number;
+        }
       }
       continue;
     }
@@ -93,8 +106,12 @@
     if (module == "Bootloader") {
       // Handle Bootloader:Board Information: <value>
       if (absl::StartsWith(message, kBoardInfoKey)) {
-        parsed_data.board_info = std::string(
+        const std::string board_info_str = std::string(
             absl::StripAsciiWhitespace(message.substr(kBoardInfoKey.length())));
+
+        if (!board_info_str.empty()) {
+          parsed_data.board_info = board_info_str;
+        }
         // Attempt to extract the product name from the Board Information.
         // This assumes the product name is the first word in the Board
         // Information string. This is based on the observed format:
@@ -104,8 +121,8 @@
         // Example: "EBB6100 board revision major:1, minor:0, serial #: unknown"
         // Here, "EBB6100" is the product name.
         std::vector<std::string> board_parts =
-            absl::StrSplit(parsed_data.board_info, ' ');
-        if (!board_parts.empty()) {
+            absl::StrSplit(parsed_data.board_info, ' ', absl::SkipWhitespace());
+        if (!board_parts.empty() && !board_parts[0].empty()) {
           parsed_data.board_product_name = board_parts[0];
         }
       }