pw_snapshot: Add helper to read snapshot UUIDs

Adds a helper that reads the UUID of an in-memory snapshot.

Change-Id: Ie8ec8c5b5431fcab6b7bac817644fcc38b997f40
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/61781
Pigweed-Auto-Submit: Armando Montanez <amontanez@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_snapshot/BUILD.bazel b/pw_snapshot/BUILD.bazel
index 19fde86..38816e9 100644
--- a/pw_snapshot/BUILD.bazel
+++ b/pw_snapshot/BUILD.bazel
@@ -15,6 +15,7 @@
 load(
     "//pw_build:pigweed.bzl",
     "pw_cc_library",
+    "pw_cc_test",
 )
 
 package(default_visibility = ["//visibility:public"])
@@ -23,12 +24,18 @@
 
 pw_cc_library(
     name = "uuid",
+    srcs = [
+        "uuid.cc",
+    ],
     hdrs = [
         "public/pw_snapshot/uuid.h",
     ],
     includes = ["public"],
     deps = [
+        ":metadata_proto",
         "//pw_bytes",
+        "//pw_protobuf",
+        "//pw_status",
     ],
 )
 
@@ -56,3 +63,18 @@
         "cpp_compile_test.cc",
     ],
 )
+
+pw_cc_test(
+    name = "uuid_test",
+    srcs = [
+        "uuid_test.cc",
+    ],
+    deps = [
+        ":metadata_proto",
+        ":uuid",
+        "//pw_bytes",
+        "//pw_protobuf",
+        "//pw_result",
+        "//pw_status",
+    ],
+)
diff --git a/pw_snapshot/BUILD.gn b/pw_snapshot/BUILD.gn
index 3b1d920..4a6c8c1 100644
--- a/pw_snapshot/BUILD.gn
+++ b/pw_snapshot/BUILD.gn
@@ -27,7 +27,16 @@
 pw_source_set("uuid") {
   public_configs = [ ":public_include_path" ]
   public = [ "public/pw_snapshot/uuid.h" ]
-  public_deps = [ dir_pw_bytes ]
+  public_deps = [
+    dir_pw_bytes,
+    dir_pw_result,
+    dir_pw_status,
+  ]
+  deps = [
+    ":metadata_proto.pwpb",
+    dir_pw_protobuf,
+  ]
+  sources = [ "uuid.cc" ]
 }
 
 group("pw_snapshot") {
@@ -71,7 +80,10 @@
 }
 
 pw_test_group("tests") {
-  tests = [ ":cpp_compile_test" ]
+  tests = [
+    ":cpp_compile_test",
+    ":uuid_test",
+  ]
 }
 
 # An empty test to ensure the proto libraries compile correctly.
@@ -82,3 +94,15 @@
     dir_pw_protobuf,
   ]
 }
+
+pw_test("uuid_test") {
+  sources = [ "uuid_test.cc" ]
+  deps = [
+    ":metadata_proto.pwpb",
+    ":uuid",
+    dir_pw_bytes,
+    dir_pw_protobuf,
+    dir_pw_result,
+    dir_pw_status,
+  ]
+}
diff --git a/pw_snapshot/module_usage.rst b/pw_snapshot/module_usage.rst
index e50f399..be29b4d 100644
--- a/pw_snapshot/module_usage.rst
+++ b/pw_snapshot/module_usage.rst
@@ -176,3 +176,34 @@
       matcher: processor.ElfMatcher = lambda snapshot: _snapshot_elf_matcher(
           fw_bundle_dir, snapshot)
       return processor.process_snapshots(snapshot, DETOKENIZER, matcher)
+
+-------------
+C++ Utilities
+-------------
+
+UUID utilities
+==============
+Snapshot UUIDs are used to uniquely identify snapshots. Pigweed strongly
+recommends using randomly generated data as a snapshot UUID. The
+more entropy and random bits, the lower the probability that two devices will
+produce the same UUID for a snapshot. 16 bytes should be sufficient for most
+projects, so this module provides ``UuidSpan`` and ``ConstUuidSpan`` types that
+can be helpful for referring to UUID-sized byte spans.
+
+Reading a snapshot's UUID
+-------------------------
+An in-memory snapshot's UUID may be read using ``ReadUuidFromSnapshot()``.
+
+.. code-block:: cpp
+
+  void NotifyNewSnapshot(ConstByteSpan snapshot) {
+    std::array<std::byte, pw::snapshot::kUuidSizeBytes> uuid;
+    pw::Result<pw::ConstByteSpan> result =
+        pw::snapshot::ReadUuidFromSnapshot(snapshot, uuid);
+    if (!result.ok()) {
+      PW_LOG_ERROR("Failed to read UUID from new snapshot, error code %d",
+                   static_cast<int>(result.status().code()));
+      return;
+    }
+    LogNewSnapshotUuid(result.value());
+  }
diff --git a/pw_snapshot/public/pw_snapshot/uuid.h b/pw_snapshot/public/pw_snapshot/uuid.h
index 7c6ac9a..9946a36 100644
--- a/pw_snapshot/public/pw_snapshot/uuid.h
+++ b/pw_snapshot/public/pw_snapshot/uuid.h
@@ -17,6 +17,7 @@
 #include <span>
 
 #include "pw_bytes/span.h"
+#include "pw_result/result.h"
 
 namespace pw::snapshot {
 
@@ -28,4 +29,15 @@
 using UuidSpan = std::span<std::byte, kUuidSizeBytes>;
 using ConstUuidSpan = std::span<const std::byte, kUuidSizeBytes>;
 
+// Reads the snapshot UUID from an in memory snapshot, if present, and returns
+// the subspan of `output` that contains the read snapshot.
+//
+// Returns:
+//   OK - UUID found, status with size indicates number of bytes written to
+//     `output`.
+//   RESOURCE_EXHUASTED - UUID found, but `output` was too small to fit it.
+//   NOT_FOUND - No snapshot UUID found in the provided snapshot.
+Result<ConstByteSpan> ReadUuidFromSnapshot(ConstByteSpan snapshot,
+                                           UuidSpan output);
+
 }  // namespace pw::snapshot
diff --git a/pw_snapshot/uuid.cc b/pw_snapshot/uuid.cc
new file mode 100644
index 0000000..420d5e3
--- /dev/null
+++ b/pw_snapshot/uuid.cc
@@ -0,0 +1,67 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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 "pw_snapshot/uuid.h"
+
+#include <cstddef>
+#include <span>
+
+#include "pw_bytes/span.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_result/result.h"
+#include "pw_snapshot_metadata_proto/snapshot_metadata.pwpb.h"
+#include "pw_status/try.h"
+
+namespace pw::snapshot {
+
+using protobuf::Decoder;
+
+Result<ConstByteSpan> ReadUuidFromSnapshot(ConstByteSpan snapshot,
+                                           UuidSpan output) {
+  Decoder decoder(snapshot);
+  ConstByteSpan metadata;
+  while (decoder.Next().ok()) {
+    if (decoder.FieldNumber() ==
+        static_cast<uint32_t>(
+            pw::snapshot::SnapshotBasicInfo::Fields::METADATA)) {
+      PW_TRY(decoder.ReadBytes(&metadata));
+      break;
+    }
+  }
+  if (metadata.empty()) {
+    return Status::NotFound();
+  }
+
+  // Start to read from the metadata.
+  decoder.Reset(metadata);
+  ConstByteSpan snapshot_uuid;
+  while (decoder.Next().ok()) {
+    if (decoder.FieldNumber() ==
+        static_cast<uint32_t>(pw::snapshot::Metadata::Fields::SNAPSHOT_UUID)) {
+      PW_TRY(decoder.ReadBytes(&snapshot_uuid));
+      break;
+    }
+  }
+  if (snapshot_uuid.empty()) {
+    return Status::NotFound();
+  }
+  if (snapshot_uuid.size_bytes() > output.size_bytes()) {
+    return Status::ResourceExhausted();
+  }
+
+  memcpy(output.data(), snapshot_uuid.data(), snapshot_uuid.size_bytes());
+  return ConstByteSpan(output.first(snapshot_uuid.size_bytes()));
+}
+
+}  // namespace pw::snapshot
diff --git a/pw_snapshot/uuid_test.cc b/pw_snapshot/uuid_test.cc
new file mode 100644
index 0000000..795071e
--- /dev/null
+++ b/pw_snapshot/uuid_test.cc
@@ -0,0 +1,102 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+//     https://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 "pw_snapshot/uuid.h"
+
+#include <array>
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_result/result.h"
+#include "pw_snapshot_metadata_proto/snapshot_metadata.pwpb.h"
+#include "pw_status/status.h"
+
+namespace pw::snapshot {
+namespace {
+
+ConstByteSpan EncodeSnapshotWithUuid(ConstByteSpan uuid, ByteSpan dest) {
+  SnapshotBasicInfo::MemoryEncoder snapshot_encoder(dest);
+  {
+    Metadata::StreamEncoder metadata_encoder =
+        snapshot_encoder.GetMetadataEncoder();
+    EXPECT_EQ(OkStatus(), metadata_encoder.WriteSnapshotUuid(uuid));
+  }
+  EXPECT_EQ(OkStatus(), snapshot_encoder.status());
+
+  return snapshot_encoder;
+}
+
+TEST(ReadUuid, ReadUuid) {
+  const std::array<uint8_t, 8> kExpectedUuid = {
+      0x1F, 0x8F, 0xBF, 0xC4, 0x86, 0x0E, 0xED, 0xD4};
+  std::array<std::byte, 16> snapshot_buffer;
+  ConstByteSpan snapshot = EncodeSnapshotWithUuid(
+      std::as_bytes(std::span(kExpectedUuid)), snapshot_buffer);
+
+  std::array<std::byte, kUuidSizeBytes> uuid_dest;
+  Result<ConstByteSpan> result = ReadUuidFromSnapshot(snapshot, uuid_dest);
+  EXPECT_EQ(OkStatus(), result.status());
+  EXPECT_EQ(kExpectedUuid.size(), result->size());
+  EXPECT_EQ(0, memcmp(result->data(), kExpectedUuid.data(), result->size()));
+}
+
+TEST(ReadUuid, NoUuid) {
+  std::array<std::byte, 16> snapshot_buffer;
+
+  // Write some snapshot metadata, but no UUID.
+  SnapshotBasicInfo::MemoryEncoder snapshot_encoder(snapshot_buffer);
+  {
+    Metadata::StreamEncoder metadata_encoder =
+        snapshot_encoder.GetMetadataEncoder();
+    EXPECT_EQ(OkStatus(), metadata_encoder.WriteFatal(true));
+  }
+  EXPECT_EQ(OkStatus(), snapshot_encoder.status());
+
+  ConstByteSpan snapshot(snapshot_encoder);
+  std::array<std::byte, kUuidSizeBytes> uuid_dest;
+  Result<ConstByteSpan> result = ReadUuidFromSnapshot(snapshot, uuid_dest);
+  EXPECT_EQ(Status::NotFound(), result.status());
+}
+
+TEST(ReadUuid, UndersizedBuffer) {
+  const std::array<uint8_t, 17> kExpectedUuid = {0xF4,
+                                                 0x1B,
+                                                 0xE1,
+                                                 0x2D,
+                                                 0x10,
+                                                 0x9B,
+                                                 0xB2,
+                                                 0x1A,
+                                                 0x88,
+                                                 0xE0,
+                                                 0xC4,
+                                                 0x77,
+                                                 0xCA,
+                                                 0x18,
+                                                 0x83,
+                                                 0xB5,
+                                                 0xBB};
+  std::array<std::byte, 32> snapshot_buffer;
+  ConstByteSpan snapshot = EncodeSnapshotWithUuid(
+      std::as_bytes(std::span(kExpectedUuid)), snapshot_buffer);
+
+  std::array<std::byte, kUuidSizeBytes> uuid_dest;
+  Result<ConstByteSpan> result = ReadUuidFromSnapshot(snapshot, uuid_dest);
+  EXPECT_EQ(Status::ResourceExhausted(), result.status());
+}
+
+}  // namespace
+}  // namespace pw::snapshot