pw_protobuf: Add FindDecodeHandler

This change defines a protobuf DecodeHandler which searches for a single
field within a proto message and cancels the decode if it is found.

Change-Id: I2ec18a15e7f23f664fb22384a2eacb280c9dece7
diff --git a/pw_protobuf/BUILD b/pw_protobuf/BUILD
index 08b43d5..b0d0902 100644
--- a/pw_protobuf/BUILD
+++ b/pw_protobuf/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2020 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
@@ -27,11 +27,13 @@
     srcs = [
         "decoder.cc",
         "encoder.cc",
+        "find.cc",
     ],
     hdrs = [
         "public/pw_protobuf/codegen.h",
         "public/pw_protobuf/decoder.h",
         "public/pw_protobuf/encoder.h",
+        "public/pw_protobuf/find.h",
         "public/pw_protobuf/wire_format.h",
     ],
     includes = ["public"],
@@ -54,6 +56,12 @@
     deps = ["//pw_protobuf"],
 )
 
+pw_cc_test(
+    name = "find_test",
+    srcs = ["find_test.cc"],
+    deps = ["//pw_protobuf"],
+)
+
 # TODO(frolv): Figure out how to integrate pw_protobuf codegen into Bazel.
 filegroup(
     name = "codegen_test",
diff --git a/pw_protobuf/BUILD.gn b/pw_protobuf/BUILD.gn
index 2b152cc..62c28e7 100644
--- a/pw_protobuf/BUILD.gn
+++ b/pw_protobuf/BUILD.gn
@@ -32,11 +32,13 @@
     "public/pw_protobuf/codegen.h",
     "public/pw_protobuf/decoder.h",
     "public/pw_protobuf/encoder.h",
+    "public/pw_protobuf/find.h",
     "public/pw_protobuf/wire_format.h",
   ]
   sources = [
     "decoder.cc",
     "encoder.cc",
+    "find.cc",
   ]
   sources += public
 }
@@ -66,6 +68,7 @@
     ":codegen_test",
     ":decoder_test",
     ":encoder_test",
+    ":find_test",
   ]
 }
 
@@ -79,6 +82,11 @@
   sources = [ "encoder_test.cc" ]
 }
 
+pw_test("find_test") {
+  deps = [ ":pw_protobuf" ]
+  sources = [ "find_test.cc" ]
+}
+
 pw_test("codegen_test") {
   deps = [
     ":codegen_test_protos_cc",
diff --git a/pw_protobuf/decoder.cc b/pw_protobuf/decoder.cc
index 220c608..54d4675 100644
--- a/pw_protobuf/decoder.cc
+++ b/pw_protobuf/decoder.cc
@@ -43,6 +43,7 @@
     uint32_t field_number = key >> kFieldNumberShift;
     Status status = handler_->ProcessField(this, field_number);
     if (!status.ok()) {
+      state_ = status == Status::CANCELLED ? kDecodeCancelled : kDecodeFailed;
       return status;
     }
 
diff --git a/pw_protobuf/find.cc b/pw_protobuf/find.cc
new file mode 100644
index 0000000..b98c4b9
--- /dev/null
+++ b/pw_protobuf/find.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 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_protobuf/find.h"
+
+namespace pw::protobuf {
+
+Status FindDecodeHandler::ProcessField(Decoder* decoder,
+                                       uint32_t field_number) {
+  if (field_number != field_number_) {
+    // Continue to the next field.
+    return Status::OK;
+  }
+
+  found_ = true;
+  if (nested_handler_ == nullptr) {
+    return Status::CANCELLED;
+  }
+
+  span<const std::byte> submessage;
+  if (Status status = decoder->ReadBytes(field_number, &submessage);
+      !status.ok()) {
+    return status;
+  }
+
+  Decoder subdecoder;
+  subdecoder.set_handler(nested_handler_);
+  return subdecoder.Decode(submessage);
+}
+
+}  // namespace pw::protobuf
diff --git a/pw_protobuf/find_test.cc b/pw_protobuf/find_test.cc
new file mode 100644
index 0000000..860e338
--- /dev/null
+++ b/pw_protobuf/find_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2020 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_protobuf/find.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::protobuf {
+namespace {
+
+// clang-format off
+constexpr uint8_t encoded_proto[] = {
+  // type=int32, k=1, v=42
+  0x08, 0x2a,
+  // type=sint32, k=2, v=-13
+  0x10, 0x19,
+  // type=bool, k=3, v=false
+  0x18, 0x00,
+  // type=double, k=4, v=3.14159
+  0x21, 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40,
+  // type=fixed32, k=5, v=0xdeadbeef
+  0x2d, 0xef, 0xbe, 0xad, 0xde,
+  // type=string, k=6, v="Hello world"
+  0x32, 0x0b, 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd',
+
+  // type=message, k=7, len=2
+  0x3a, 0x02,
+  // (nested) type=uint32, k=1, v=3
+  0x08, 0x03
+};
+
+TEST(FindDecodeHandler, SingleLevel_FindsExistingField) {
+  Decoder decoder;
+  FindDecodeHandler finder(3);
+
+  decoder.set_handler(&finder);
+  decoder.Decode(as_bytes(span(encoded_proto)));
+
+  EXPECT_TRUE(finder.found());
+  EXPECT_TRUE(decoder.cancelled());
+}
+
+TEST(FindDecodeHandler, SingleLevel_DoesntFindNonExistingField) {
+  Decoder decoder;
+  FindDecodeHandler finder(8);
+
+  decoder.set_handler(&finder);
+  decoder.Decode(as_bytes(span(encoded_proto)));
+
+  EXPECT_FALSE(finder.found());
+  EXPECT_FALSE(decoder.cancelled());
+}
+
+TEST(FindDecodeHandler, MultiLevel_FindsExistingNestedField) {
+  Decoder decoder;
+  FindDecodeHandler nested_finder(1);
+  FindDecodeHandler finder(7, &nested_finder);
+
+  decoder.set_handler(&finder);
+  decoder.Decode(as_bytes(span(encoded_proto)));
+
+  EXPECT_TRUE(finder.found());
+  EXPECT_TRUE(nested_finder.found());
+  EXPECT_TRUE(decoder.cancelled());
+}
+
+TEST(FindDecodeHandler, MultiLevel_DoesntFindNonExistingNestedField) {
+  Decoder decoder;
+  FindDecodeHandler nested_finder(3);
+  FindDecodeHandler finder(7, &nested_finder);
+
+  decoder.set_handler(&finder);
+  decoder.Decode(as_bytes(span(encoded_proto)));
+
+  EXPECT_TRUE(finder.found());
+  EXPECT_FALSE(nested_finder.found());
+  EXPECT_FALSE(decoder.cancelled());
+}
+
+}  // namespace
+}  // namespace pw::protobuf
diff --git a/pw_protobuf/public/pw_protobuf/decoder.h b/pw_protobuf/public/pw_protobuf/decoder.h
index 7e5feed..a3aca75 100644
--- a/pw_protobuf/public/pw_protobuf/decoder.h
+++ b/pw_protobuf/public/pw_protobuf/decoder.h
@@ -208,10 +208,13 @@
     return ReadDelimited(field_number, out);
   }
 
+  bool cancelled() const { return state_ == kDecodeCancelled; };
+
  private:
   enum State {
     kReady,
     kDecodeInProgress,
+    kDecodeCancelled,
     kDecodeFailed,
   };
 
diff --git a/pw_protobuf/public/pw_protobuf/find.h b/pw_protobuf/public/pw_protobuf/find.h
new file mode 100644
index 0000000..1e2f3e8
--- /dev/null
+++ b/pw_protobuf/public/pw_protobuf/find.h
@@ -0,0 +1,46 @@
+// Copyright 2020 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.
+#pragma once
+
+#include "pw_protobuf/decoder.h"
+
+namespace pw::protobuf {
+
+// DecodeHandler that searches for a specific field in a proto message. If the
+// field is found, it cancels the decode operation. Supports searching for
+// nested fields by passing in another instance of a FindDecodeHandler for the
+// nested message.
+class FindDecodeHandler final : public DecodeHandler {
+ public:
+  constexpr FindDecodeHandler(uint32_t field_number)
+      : FindDecodeHandler(field_number, nullptr) {}
+
+  constexpr FindDecodeHandler(uint32_t field_number, FindDecodeHandler* nested)
+      : field_number_(field_number), found_(false), nested_handler_(nested) {}
+
+  Status ProcessField(Decoder* decoder, uint32_t field_number) override;
+
+  bool found() const { return found_; }
+
+  void set_nested_handler(FindDecodeHandler* nested) {
+    nested_handler_ = nested;
+  }
+
+ private:
+  uint32_t field_number_;
+  bool found_;
+  FindDecodeHandler* nested_handler_;
+};
+
+}  // namespace pw::protobuf