pw_hex_dump: Fix bug with unaligned trailing bytes

When bytes at the end of a dump were not aligned with the "group_every"
option, half of the last byte would be truncated.

No-Docs-Update-Reason: Bugfix
Change-Id: Ibe349c9f34f41281122583a6e7dff84d4f991333
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/49305
Pigweed-Auto-Submit: Armando Montanez <amontanez@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/pw_hex_dump/hex_dump.cc b/pw_hex_dump/hex_dump.cc
index 67f0f3c..77c15cf 100644
--- a/pw_hex_dump/hex_dump.cc
+++ b/pw_hex_dump/hex_dump.cc
@@ -44,6 +44,26 @@
   return std::to_integer<char>(b);
 }
 
+void AddGroupingByte(size_t byte_index,
+                     FormattedHexDumper::Flags& flags,
+                     StringBuilder& builder) {
+  // Never add grouping when it is disabled.
+  if (flags.group_every == 0) {
+    return;
+  }
+  // If this byte isn't at the end of a group, don't add a space.
+  if ((byte_index + 1) % flags.group_every != 0) {
+    return;
+  }
+  // If this byte is the last byte in a line, don't add a grouping byte
+  // (prevents trailing spaces).
+  if (byte_index + 1 == flags.bytes_per_line) {
+    return;
+  }
+
+  builder << ' ';
+}
+
 }  // namespace
 
 Status DumpAddr(std::span<char> dest, uintptr_t addr) {
@@ -100,17 +120,7 @@
     } else {
       builder.append(2, ' ');
     }
-    if (flags.group_every != 0 && (i + 1) % flags.group_every == 0) {
-      builder << ' ';
-    }
-  }
-
-  // Removes extraneous space from end when bytes_per_line is divisible by
-  // group_every. kSectionSeparator includes a space, so it's unnecessary.
-  // Ommitting the space from the section separator actually makes for more
-  // workarounds and code duplication, so this is better.
-  if (flags.group_every != 0 && flags.bytes_per_line % flags.group_every == 0) {
-    builder.pop_back();
+    AddGroupingByte(i, flags, builder);
   }
 
   if (flags.show_ascii) {
@@ -173,9 +183,7 @@
     // width bytes? (`04` instead of `4`, for example)
     builder << std::byte(c >> 4);
     builder << std::byte(c & 0xF);
-    if (flags.group_every != 0 && (i + 1) % flags.group_every == 0) {
-      builder << ' ';
-    }
+    AddGroupingByte(i, flags, builder);
   }
   // Add padding spaces to ensure lines are aligned.
   if (flags.show_ascii) {
@@ -183,20 +191,10 @@
          i < static_cast<size_t>(flags.bytes_per_line);
          ++i) {
       builder.append(2, ' ');
-      if (flags.group_every != 0 && (i + 1) % flags.group_every == 0) {
-        builder << ' ';
-      }
+      AddGroupingByte(i, flags, builder);
     }
   }
 
-  // Removes extraneous space from end when bytes_per_line is divisible by
-  // group_every. kSectionSeparator includes a space, so it's unnecessary.
-  // Ommitting the space from the section separator actually makes for more
-  // workarounds and code duplication, so this is better.
-  if (flags.group_every != 0 && flags.bytes_per_line % flags.group_every == 0) {
-    builder.pop_back();
-  }
-
   // Interpret bytes as characters.
   if (flags.show_ascii) {
     builder << kSectionSeparator;
diff --git a/pw_hex_dump/hex_dump_test.cc b/pw_hex_dump/hex_dump_test.cc
index f374431..d283c18 100644
--- a/pw_hex_dump/hex_dump_test.cc
+++ b/pw_hex_dump/hex_dump_test.cc
@@ -223,6 +223,27 @@
   EXPECT_STREQ(expected2, dest_.data());
 }
 
+TEST_F(HexDump, FormattedHexDump_LastLineCheck) {
+  constexpr const char* expected1 = "a4cc32629b46381a 231a2a7abce240a0";
+  constexpr const char* expected2 = "ff33e52b9e9f6b3c be9b893c7e4a7a48";
+  constexpr const char* expected3 = "18";
+
+  default_flags_.bytes_per_line = 16;
+  default_flags_.group_every = 8;
+  dumper_ = FormattedHexDumper(dest_, default_flags_);
+
+  EXPECT_TRUE(dumper_.BeginDump(source_data).ok());
+  // Dump first line.
+  EXPECT_TRUE(dumper_.DumpLine().ok());
+  EXPECT_STREQ(expected1, dest_.data());
+  // Dump second line.
+  EXPECT_TRUE(dumper_.DumpLine().ok());
+  EXPECT_STREQ(expected2, dest_.data());
+  // Dump third line.
+  EXPECT_TRUE(dumper_.DumpLine().ok());
+  EXPECT_STREQ(expected3, dest_.data());
+}
+
 TEST_F(HexDump, FormattedHexDump_Ascii) {
   constexpr const char* expected1 = "6d 79 20 74 65 73 74 20  my test ";
   constexpr const char* expected2 = "73 74 72 69 6e 67 0a     string.";