[bugreport] Bug Report Client

This is the other side of bugreport that runs within the client.
Parses the json and generates targets that are meant for easy
outputting.

The outputting of these targets is left for another CL.

DX-1550 # Comment

TEST=Unittests

Change-Id: Id666b2dcb0c3f3b1dd808cd962e57c6205894a26
diff --git a/src/developer/BUILD.gn b/src/developer/BUILD.gn
index 83ce21f..0bb2201 100644
--- a/src/developer/BUILD.gn
+++ b/src/developer/BUILD.gn
@@ -20,6 +20,7 @@
 
   data_deps = [
     "//src/developer/bugreport/tests:bugreport_tests",
+    "//src/developer/bugreport/tests:bugreport_client_tests($host_toolchain)",
     "//src/developer/crashpad_agent/tests:crashpad_agent_tests",
     "//src/developer/feedback_agent/tests:feedback_agent_tests",
     "//src/developer/tracing:tests",
diff --git a/src/developer/bugreport/BUILD.gn b/src/developer/bugreport/BUILD.gn
index 7d4aeae..604f39d 100644
--- a/src/developer/bugreport/BUILD.gn
+++ b/src/developer/bugreport/BUILD.gn
@@ -61,3 +61,19 @@
     "bug_report_schema.h",
   ]
 }
+
+# Host side client -------------------------------------------------------------
+
+if (current_toolchain == host_toolchain) {
+  source_set("bug_report_client_lib") {
+    sources = [
+      "bug_report_client.cc",
+      "bug_report_client.h",
+    ]
+
+    deps = [
+      "//src/lib/fxl",
+      "//third_party/rapidjson",
+    ]
+  }
+}
diff --git a/src/developer/bugreport/bug_report_client.cc b/src/developer/bugreport/bug_report_client.cc
new file mode 100644
index 0000000..9e703ed
--- /dev/null
+++ b/src/developer/bugreport/bug_report_client.cc
@@ -0,0 +1,165 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/bugreport/bug_report_client.h"
+
+#include <rapidjson/error/en.h>
+#include <rapidjson/prettywriter.h>
+#include <rapidjson/schema.h>
+
+#include "src/developer/bugreport/bug_report_schema.h"
+#include "src/lib/fxl/logging.h"
+#include "src/lib/fxl/strings/join_strings.h"
+#include "src/lib/fxl/strings/string_printf.h"
+
+namespace bugreport {
+
+namespace {
+
+template <typename JsonNode>
+std::optional<std::string> PrettyPrintJson(const JsonNode& json_node) {
+  if (json_node.IsString())
+    return json_node.GetString();
+
+  if (json_node.IsObject()) {
+    rapidjson::StringBuffer buf;
+    rapidjson::PrettyWriter json_writer(buf);
+    json_node.Accept(json_writer);
+
+    return buf.GetString();
+  }
+
+  FXL_LOG(ERROR) << "Json node is not a printable type.";
+  return std::nullopt;
+}
+
+// Annotations are meant to be joined into a single target.
+template <typename JsonNode>
+std::optional<Target> ParseAnnotations(const JsonNode& annotations) {
+  if (!annotations.IsObject()) {
+    FXL_LOG(ERROR) << "Annotations are not an object.";
+    return std::nullopt;
+  }
+
+  auto contents = PrettyPrintJson(annotations);
+  if (!contents)
+    return std::nullopt;
+
+  Target target;
+  target.name = "annotations.json";
+  target.contents = std::move(*contents);
+
+  return target;
+}
+
+// Each attachment is big enough to warrant its own target.
+template <typename JsonNode>
+std::optional<std::vector<Target>> ParseAttachments(
+    const JsonNode& attachments) {
+  if (!attachments.IsObject()) {
+    FXL_LOG(ERROR) << "Attachments are not an object.";
+    return std::nullopt;
+  }
+
+  std::vector<Target> targets;
+  for (const auto& [key_obj, attachment] : attachments.GetObject()) {
+    auto key = key_obj.GetString();
+    if (!attachment.IsString()) {
+      FXL_LOG(ERROR) << "Attachment " << key << " is not a string.";
+      continue;
+    }
+
+    Target target;
+
+    // If the document conforms to json, we can output the name as such.
+    rapidjson::Document target_doc;
+    target_doc.Parse(attachment.GetString());
+
+    if (target_doc.HasParseError()) {
+      // Simple string.
+      target.name = fxl::StringPrintf("%s.txt", key);
+      target.contents = attachment.GetString();
+    } else {
+      // It's a valid json object.
+      target.name = fxl::StringPrintf("%s.json", key);
+      auto content = PrettyPrintJson(target_doc);
+      // If pretty printing failed, we add the incoming string.
+      if (!content) {
+        target.contents = attachment.GetString();
+      } else {
+        target.contents = *content;
+      }
+    }
+
+    targets.push_back(std::move(target));
+  }
+
+  return targets;
+}
+
+std::optional<rapidjson::Document> ParseDocument(const std::string& input) {
+  rapidjson::Document document;
+  rapidjson::ParseResult result = document.Parse(input);
+  if (!result) {
+    FXL_LOG(ERROR) << "Error parsing json: "
+                   << rapidjson::GetParseError_En(result.Code()) << "("
+                   << result.Offset() << ").";
+    return std::nullopt;
+  }
+
+  return document;
+}
+
+bool Validate(const rapidjson::Document& document,
+              const std::string& schema_str) {
+  auto input_document = ParseDocument(schema_str);
+  if (!input_document)
+    return false;
+
+  rapidjson::SchemaDocument schema_document(*input_document);
+  rapidjson::SchemaValidator validator(schema_document);
+  if (!document.Accept(validator)) {
+    rapidjson::StringBuffer buf;
+    validator.GetInvalidSchemaPointer().StringifyUriFragment(buf);
+    FXL_LOG(ERROR) << "Document does not conform to schema. Rule: "
+                   << validator.GetInvalidSchemaKeyword();
+
+    return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+std::optional<std::vector<Target>> HandleBugReport(const std::string& input) {
+  auto opt_document = ParseDocument(input);
+  if (!opt_document)
+    return std::nullopt;
+
+  auto& document = *opt_document;
+  if (!Validate(document, fuchsia::bugreport::kBugReportJsonSchema))
+    return std::nullopt;
+
+  std::vector<Target> targets;
+
+  // Annotations.
+  if (document.HasMember("annotations")) {
+    auto annotations = ParseAnnotations(document["annotations"]);
+    if (annotations)
+      targets.push_back(std::move(*annotations));
+  }
+
+  // Attachments.
+  if (document.HasMember("attachments")) {
+    auto attachments = ParseAttachments(document["attachments"]);
+    if (attachments)
+      targets.insert(targets.end(), attachments->begin(), attachments->end());
+  }
+
+  if (targets.empty())
+    FXL_LOG(WARNING) << "No annotations or attachments are present.";
+  return targets;
+}
+
+}  // namespace bugreport
diff --git a/src/developer/bugreport/bug_report_client.h b/src/developer/bugreport/bug_report_client.h
new file mode 100644
index 0000000..c3c76de
--- /dev/null
+++ b/src/developer/bugreport/bug_report_client.h
@@ -0,0 +1,27 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_DEVELOPER_BUGREPORT_BUG_REPORT_CLIENT_H_
+#define SRC_DEVELOPER_BUGREPORT_BUG_REPORT_CLIENT_H_
+
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace bugreport {
+
+// Meant to represent a single unit of data gathered from the input json
+// document. Each one of these should normally be outputted to its own file.
+struct Target {
+  std::string name;
+  std::string contents;
+};
+
+// Complete stage of processing: parsing, validating and separating.
+std::optional<std::vector<Target>> HandleBugReport(
+    const std::string& json_input);
+
+}  // namespace bugreport
+
+#endif  // SRC_DEVELOPER_BUGREPORT_BUG_REPORT_CLIENT_H_
diff --git a/src/developer/bugreport/tests/BUILD.gn b/src/developer/bugreport/tests/BUILD.gn
index 7bc4de4..7926b7c8 100644
--- a/src/developer/bugreport/tests/BUILD.gn
+++ b/src/developer/bugreport/tests/BUILD.gn
@@ -63,3 +63,31 @@
     "//third_party/rapidjson",
   ]
 }
+
+if (current_toolchain == host_toolchain) {
+  test_package("bugreport_client_tests") {
+    tests = [
+      {
+        name = "bugreport_client_unittests"
+      },
+    ]
+
+    deps = [
+      ":bugreport_client_unittests",
+    ]
+  }
+
+  executable("bugreport_client_unittests") {
+    testonly = true
+
+    sources = [
+      "bug_report_client_unittest.cc",
+    ]
+
+    deps = [
+      "//src/developer/bugreport:bug_report_client_lib",
+      "//src/lib/fxl/test:gtest_main",
+      "//third_party/googletest:gtest",
+    ]
+  }
+}
diff --git a/src/developer/bugreport/tests/bug_report_client_unittest.cc b/src/developer/bugreport/tests/bug_report_client_unittest.cc
new file mode 100644
index 0000000..8b7a8fe
--- /dev/null
+++ b/src/developer/bugreport/tests/bug_report_client_unittest.cc
@@ -0,0 +1,125 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/bugreport/bug_report_client.h"
+
+#include <gtest/gtest.h>
+
+namespace bugreport {
+
+namespace {
+
+constexpr char kValidDocument[] = R"(
+  {
+    "annotations":
+    {
+      "annotation.1.key": "annotation.1.value",
+      "annotation.2.key": "annotation.2.value"
+    },
+    "attachments":
+    {
+      "attachment.1.key": "{\"embedded\": [\"array\"], \"another\": \"key\"}",
+      "attachment.2.key": "attachment.2.value"
+    }
+  }
+)";
+
+constexpr char kEmpty[] = R"(
+  {
+    "annotations": { },
+    "attachments": { }
+  }
+)";
+
+constexpr char kMissingAnnotations[] = R"(
+  {
+    "attachments":
+    {
+      "attachment.1.key": "{\"embedded\": [\"json\", \"array\"]}",
+      "attachment.2.key": "attachment.2.value"
+    }
+  }
+)";
+
+constexpr char kMissingAttachments[] = R"(
+  {
+    "annotations":
+    {
+      "annotation.1.key": "annotation.1.value",
+      "annotation.2.key": "annotation.2.value"
+    }
+  }
+)";
+
+constexpr char kWrongAnnotationType[] = R"(
+  {
+    "annotations":
+    {
+      "annotation.1.key": {"not": "string"},
+      "annotation.2.key": "annotation.2.value"
+    },
+    "attachments":
+    {
+      "attachment.1.key": "{\"embedded\": \"json\"}",
+      "attachment.2.key": "attachment.2.value"
+    }
+  }
+)";
+
+constexpr char kWrongAttachmentType[] = R"(
+  {
+    "annotations":
+    {
+      "annotation.1.key": "annotation.1.value",
+      "annotation.2.key": "annotation.2.value"
+    },
+    "attachments":
+    {
+      "attachment.1.key": {"not": "string"},
+      "attachment.2.key": "attachment.2.value"
+    }
+  }
+)";
+
+}  // namespace
+
+TEST(BugReportClient, ValidDocument) {
+  auto targets = HandleBugReport(kValidDocument);
+  ASSERT_TRUE(targets);
+
+  ASSERT_EQ(targets->size(), 3u);
+
+  auto& annotation = targets->at(0);
+  EXPECT_EQ(annotation.name, "annotations.json");
+  EXPECT_EQ(annotation.contents,
+            R"({
+    "annotation.1.key": "annotation.1.value",
+    "annotation.2.key": "annotation.2.value"
+})");
+
+  auto attachment1 = targets->at(1);
+  EXPECT_EQ(attachment1.name, "attachment.1.key.json");
+  EXPECT_EQ(attachment1.contents,
+            R"({
+    "embedded": [
+        "array"
+    ],
+    "another": "key"
+})");
+
+  auto attachment2 = targets->at(2);
+  EXPECT_EQ(attachment2.name, "attachment.2.key.txt");
+  EXPECT_EQ(attachment2.contents, "attachment.2.value");
+}
+
+TEST(BugReportClient, EdgeCases) {
+  EXPECT_TRUE(HandleBugReport(kEmpty));
+  EXPECT_FALSE(HandleBugReport("{{{{"));
+  EXPECT_FALSE(HandleBugReport(kMissingAnnotations));
+  EXPECT_FALSE(HandleBugReport(kMissingAttachments));
+  EXPECT_FALSE(HandleBugReport(kWrongAnnotationType));
+  EXPECT_FALSE(HandleBugReport(kWrongAttachmentType));
+}
+
+}  // namespace bugreport