Add JSON schema validation (using valijson)

This patch adds a dependency on the valijson library, and new JSON
validation methods in util that use it.

Bug=b/174759086
Change-Id: I3607a95dcefb3efff103cc3b5c7c14e8ee2b9739
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2527747
Reviewed-by: mark a. foltz <mfoltz@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
diff --git a/.gitignore b/.gitignore
index 59e8ec0..d91c5c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@
 *.profraw
 *.profdata
 generated_root_cast_receiver*
-yajsv
+yajsv
\ No newline at end of file
diff --git a/DEPS b/DEPS
index cca362a..1c0dc97 100644
--- a/DEPS
+++ b/DEPS
@@ -122,6 +122,12 @@
       '@' + 'debe7d2d1982e540fbd6bd78604bf001753f9e74',
     'condition': 'not build_with_chromium',
   },
+
+  'third_party/valijson/src': {
+    'url': Var('github') + '/tristanpenman/valijson.git' +
+      '@' + 'cf648930313655b19dc07ebae2f9c3fc37966a33', # Tip-of-tree
+    'condition': 'not build_with_chromium',
+  }
 }
 
 hooks = [
@@ -173,6 +179,7 @@
 
 recursedeps = [
   'third_party/chromium_quic/src',
+  'cast',
   'buildtools',
 ]
 
diff --git a/build/config/data_headers_template.gni b/build/config/data_headers_template.gni
new file mode 100644
index 0000000..b50c499
--- /dev/null
+++ b/build/config/data_headers_template.gni
@@ -0,0 +1,25 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This template takes an input list of files, and copies their contents
+# into C++ header files as constexpr char[] raw strings with variable names
+# taken directly from the original file name.
+
+template("data_headers") {
+  action_foreach(target_name) {
+    forward_variables_from(invoker,
+                           [
+                             "namespace",
+                             "sources",
+                             "testonly",
+                           ])
+    script = "../../tools/convert_to_data_file.py"
+    outputs = [ "{{source_gen_dir}}/{{source_name_part}}_data.h" ]
+    args = [
+      namespace,
+      "{{source}}",
+      "{{source_gen_dir}}/{{source_name_part}}_data.h",
+    ]
+  }
+}
diff --git a/cast/.gitignore b/cast/.gitignore
index 9e970fb..e2af0f2 100644
--- a/cast/.gitignore
+++ b/cast/.gitignore
@@ -1 +1,2 @@
 /internal/
+third_party/*/src/
diff --git a/cast/DEPS b/cast/DEPS
index 4e73fd0..fe325fc 100644
--- a/cast/DEPS
+++ b/cast/DEPS
@@ -12,4 +12,6 @@
 
   # All libcast code can use cast/third_party.
   '+cast/third_party',
+  '+valijson',
+  '+json',
 ]
diff --git a/cast/protocol/BUILD.gn b/cast/protocol/BUILD.gn
new file mode 100644
index 0000000..590caed
--- /dev/null
+++ b/cast/protocol/BUILD.gn
@@ -0,0 +1,73 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/data_headers_template.gni")
+import("//build_overrides/build.gni")
+assert(!build_with_chromium)
+
+data_headers("castv2_schema_headers") {
+  namespace = "cast"
+  sources = [
+    "castv2/receiver_schema.json",
+    "castv2/streaming_schema.json",
+  ]
+}
+
+source_set("castv2") {
+  sources = [
+    "castv2/validation.cc",
+    "castv2/validation.h",
+  ]
+
+  public_deps = [ "../../third_party/jsoncpp" ]
+
+  deps = [
+    ":castv2_schema_headers",
+    "../../util",
+    "//third_party/valijson",
+  ]
+
+  public_configs = [ "../../build:openscreen_include_dirs" ]
+}
+
+data_headers("streaming_examples") {
+  testonly = true
+  namespace = "cast"
+  sources = [
+    "castv2/streaming_examples/answer.json",
+    "castv2/streaming_examples/capabilities_response.json",
+    "castv2/streaming_examples/get_capabilities.json",
+    "castv2/streaming_examples/get_status.json",
+    "castv2/streaming_examples/offer.json",
+    "castv2/streaming_examples/rpc.json",
+    "castv2/streaming_examples/status_response.json",
+  ]
+}
+
+data_headers("receiver_examples") {
+  testonly = true
+  namespace = "cast"
+  sources = [
+    "castv2/receiver_examples/get_app_availability.json",
+    "castv2/receiver_examples/get_app_availability_response.json",
+    "castv2/receiver_examples/launch.json",
+    "castv2/receiver_examples/stop.json",
+  ]
+}
+
+source_set("unittests") {
+  testonly = true
+
+  sources = [ "castv2/validation_unittest.cc" ]
+
+  deps = [
+    ":castv2",
+    ":receiver_examples",
+    ":streaming_examples",
+    "../../third_party/abseil",
+    "../../third_party/googletest:gmock",
+    "../../third_party/googletest:gtest",
+    "//third_party/valijson",
+  ]
+}
diff --git a/cast/protocol/castv2/README.md b/cast/protocol/castv2/README.md
index f86c7e2..be6dde6 100644
--- a/cast/protocol/castv2/README.md
+++ b/cast/protocol/castv2/README.md
@@ -56,7 +56,7 @@
 Since `clang-format` doesn't support JSON files (currently only Python, C++,
 and JavaScript), the JSON files here are instead formatted using
 (json-stringify-pretty-compact)[https://github.com/lydell/json-stringify-pretty-compact]
-with a max line length of 80 and a 2-character indent. Many IDEs have an extension
-for this, such as VSCode's
+with a max line length of 80 and a 2-character indent. Many IDEs have an
+extension for this, such as VSCode's
 (json-compact-prettifier)[https://marketplace.visualstudio.com/items?itemName=inadarei.json-compact-prettifier].
 
diff --git a/cast/protocol/castv2/streaming_schema.json b/cast/protocol/castv2/streaming_schema.json
index 832b612..d35da42 100644
--- a/cast/protocol/castv2/streaming_schema.json
+++ b/cast/protocol/castv2/streaming_schema.json
@@ -49,7 +49,6 @@
         "codecName",
         "rtpPayloadType",
         "ssrc",
-        "targetDelay",
         "aesKey",
         "aesIvMask",
         "timeBase"
@@ -175,7 +174,7 @@
         "receiverGetStatus": {"type": "boolean"},
         "rtpExtensions": {"$ref": "#/definitions/rtp_extensions"}
       },
-      "required": ["udpPort", "constraints", "sendIndexes", "ssrcs"]
+      "required": ["udpPort", "sendIndexes", "ssrcs"]
     },
     "status_response": {
       "properties": {
diff --git a/cast/protocol/castv2/validation.cc b/cast/protocol/castv2/validation.cc
new file mode 100644
index 0000000..67a9b35
--- /dev/null
+++ b/cast/protocol/castv2/validation.cc
@@ -0,0 +1,86 @@
+// Copyright 2020 The Chromium 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 "cast/protocol/castv2/validation.h"
+
+#include <mutex>  // NOLINT
+#include <string>
+
+#include "cast/protocol/castv2/receiver_schema_data.h"
+#include "cast/protocol/castv2/streaming_schema_data.h"
+#include "third_party/valijson/src/include/valijson/adapters/jsoncpp_adapter.hpp"
+#include "third_party/valijson/src/include/valijson/schema.hpp"
+#include "third_party/valijson/src/include/valijson/schema_parser.hpp"
+#include "third_party/valijson/src/include/valijson/utils/jsoncpp_utils.hpp"
+#include "third_party/valijson/src/include/valijson/validator.hpp"
+#include "util/json/json_serialization.h"
+#include "util/osp_logging.h"
+#include "util/std_util.h"
+#include "util/stringprintf.h"
+
+namespace openscreen {
+namespace cast {
+
+namespace {
+
+std::vector<Error> MapErrors(const valijson::ValidationResults& results) {
+  std::vector<Error> errors;
+  errors.reserve(results.numErrors());
+  for (const auto& result : results) {
+    const std::string context = Join(result.context, ", ");
+    errors.emplace_back(Error::Code::kJsonParseError,
+                        StringPrintf("Node: %s, Message: %s", context.c_str(),
+                                     result.description.c_str()));
+
+    OSP_DVLOG << "JsonCpp validation error: "
+              << errors.at(errors.size() - 1).message();
+  }
+  return errors;
+}
+
+void LoadSchema(const char* schema_json, valijson::Schema* schema) {
+  Json::Value root = json::Parse(schema_json).value();
+  valijson::adapters::JsonCppAdapter adapter(root);
+  valijson::SchemaParser parser;
+  parser.populateSchema(adapter, *schema);
+}
+
+std::vector<Error> Validate(const Json::Value& document,
+                            const valijson::Schema& schema) {
+  valijson::Validator validator;
+  valijson::adapters::JsonCppAdapter document_adapter(document);
+  valijson::ValidationResults results;
+  if (validator.validate(schema, document_adapter, &results)) {
+    return {};
+  }
+  return MapErrors(results);
+}
+
+}  // anonymous namespace
+std::vector<Error> Validate(const Json::Value& document,
+                            const Json::Value& schema_root) {
+  valijson::adapters::JsonCppAdapter adapter(schema_root);
+  valijson::Schema schema;
+  valijson::SchemaParser parser;
+  parser.populateSchema(adapter, schema);
+
+  return Validate(document, schema);
+}
+
+std::vector<Error> ValidateStreamingMessage(const Json::Value& message) {
+  static valijson::Schema schema;
+  static std::once_flag flag;
+  std::call_once(flag, [] { LoadSchema(kStreamingSchema, &schema); });
+  return Validate(message, schema);
+}
+
+std::vector<Error> ValidateReceiverMessage(const Json::Value& message) {
+  static valijson::Schema schema;
+  static std::once_flag flag;
+  std::call_once(flag, [] { LoadSchema(kReceiverSchema, &schema); });
+  return Validate(message, schema);
+}
+
+}  // namespace cast
+}  // namespace openscreen
diff --git a/cast/protocol/castv2/validation.h b/cast/protocol/castv2/validation.h
new file mode 100644
index 0000000..79410df
--- /dev/null
+++ b/cast/protocol/castv2/validation.h
@@ -0,0 +1,29 @@
+// Copyright 2020 The Chromium 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 CAST_PROTOCOL_CASTV2_VALIDATION_H_
+#define CAST_PROTOCOL_CASTV2_VALIDATION_H_
+
+#include <vector>
+
+#include "json/value.h"
+#include "platform/base/error.h"
+
+namespace openscreen {
+namespace cast {
+
+// Used to validate a JSON message against a JSON schema.
+std::vector<Error> Validate(const Json::Value& document,
+                            const Json::Value& schema_root);
+
+// Used to validate streaming messages, such as OFFER or ANSWER.
+std::vector<Error> ValidateStreamingMessage(const Json::Value& message);
+
+// Used to validate receiver messages, such as LAUNCH or STOP.
+std::vector<Error> ValidateReceiverMessage(const Json::Value& message);
+
+}  // namespace cast
+}  // namespace openscreen
+
+#endif  // CAST_PROTOCOL_CASTV2_VALIDATION_H_
diff --git a/cast/protocol/castv2/validation_unittest.cc b/cast/protocol/castv2/validation_unittest.cc
new file mode 100644
index 0000000..46ded40
--- /dev/null
+++ b/cast/protocol/castv2/validation_unittest.cc
@@ -0,0 +1,161 @@
+// Copyright 2020 The Chromium 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 "cast/protocol/castv2/validation.h"
+
+#include <numeric>
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "cast/protocol/castv2/receiver_examples/get_app_availability_data.h"
+#include "cast/protocol/castv2/receiver_examples/get_app_availability_response_data.h"
+#include "cast/protocol/castv2/receiver_examples/launch_data.h"
+#include "cast/protocol/castv2/receiver_examples/stop_data.h"
+#include "cast/protocol/castv2/receiver_schema_data.h"
+#include "cast/protocol/castv2/streaming_examples/answer_data.h"
+#include "cast/protocol/castv2/streaming_examples/capabilities_response_data.h"
+#include "cast/protocol/castv2/streaming_examples/get_capabilities_data.h"
+#include "cast/protocol/castv2/streaming_examples/get_status_data.h"
+#include "cast/protocol/castv2/streaming_examples/offer_data.h"
+#include "cast/protocol/castv2/streaming_examples/rpc_data.h"
+#include "cast/protocol/castv2/streaming_examples/status_response_data.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "json/value.h"
+#include "platform/base/error.h"
+#include "util/json/json_serialization.h"
+#include "util/osp_logging.h"
+#include "util/std_util.h"
+#include "util/stringprintf.h"
+
+namespace openscreen {
+namespace cast {
+
+namespace {
+
+constexpr char kEmptyJson[] = "{}";
+
+// Schema format string, that allows for specifying definitions,
+// properties, and required fields.
+constexpr char kSchemaFormat[] = R"({
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://something/app_schema_data.h",
+  "definitions": {
+    %s
+  },
+  "type": "object",
+  "properties": {
+    %s
+  },
+  "required": [%s]
+})";
+
+// Fields used for an appId containing schema
+constexpr char kAppIdDefinition[] = R"("app_id": {
+    "type": "string",
+    "enum": ["0F5096E8", "85CDB22F"]
+  })";
+constexpr char kAppIdName[] = "\"appId\"";
+constexpr char kAppIdProperty[] =
+    R"(  "appId": {"$ref": "#/definitions/app_id"})";
+
+// Teest documents containing an appId.
+constexpr char kValidAppIdDocument[] = R"({ "appId": "0F5096E8" })";
+constexpr char kInvalidAppIdDocument[] = R"({ "appId": "FooBar" })";
+
+std::string BuildSchema(const char* definitions,
+                        const char* properties,
+                        const char* required) {
+  return StringPrintf(kSchemaFormat, definitions, properties, required);
+}
+
+bool TestValidate(absl::string_view document, absl::string_view schema) {
+  OSP_DVLOG << "Validating document: \"" << document << "\" against schema: \""
+            << schema << "\"";
+  ErrorOr<Json::Value> document_root = json::Parse(document);
+  EXPECT_TRUE(document_root.is_value());
+  ErrorOr<Json::Value> schema_root = json::Parse(schema);
+  EXPECT_TRUE(schema_root.is_value());
+
+  std::vector<Error> errors =
+      Validate(document_root.value(), schema_root.value());
+  return errors.empty();
+}
+
+const std::string& GetEmptySchema() {
+  static const std::string kEmptySchema = BuildSchema("", "", "");
+  return kEmptySchema;
+}
+
+const std::string& GetAppSchema() {
+  static const std::string kAppIdSchema =
+      BuildSchema(kAppIdDefinition, kAppIdProperty, kAppIdName);
+  return kAppIdSchema;
+}
+
+class StreamingValidationTest : public testing::TestWithParam<const char*> {};
+class ReceiverValidationTest : public testing::TestWithParam<const char*> {};
+
+}  // namespace
+
+TEST(ValidationTest, EmptyPassesEmpty) {
+  EXPECT_TRUE(TestValidate(kEmptyJson, kEmptyJson));
+}
+
+TEST(ValidationTest, EmptyPassesBasicSchema) {
+  EXPECT_TRUE(TestValidate(kEmptyJson, GetEmptySchema()));
+}
+
+TEST(ValidationTest, EmptyFailsAppIdSchema) {
+  EXPECT_FALSE(TestValidate(kEmptyJson, GetAppSchema()));
+}
+
+TEST(ValidationTest, InvalidAppIdFailsAppIdSchema) {
+  EXPECT_FALSE(TestValidate(kInvalidAppIdDocument, GetAppSchema()));
+}
+
+TEST(ValidationTest, ValidAppIdPassesAppIdSchema) {
+  EXPECT_TRUE(TestValidate(kValidAppIdDocument, GetAppSchema()));
+}
+
+TEST(ValidationTest, InvalidAppIdPassesEmptySchema) {
+  EXPECT_TRUE(TestValidate(kInvalidAppIdDocument, GetEmptySchema()));
+}
+
+TEST(ValidationTest, ValidAppIdPassesEmptySchema) {
+  EXPECT_TRUE(TestValidate(kValidAppIdDocument, GetEmptySchema()));
+}
+
+INSTANTIATE_TEST_SUITE_P(StreamingValidations,
+                         StreamingValidationTest,
+                         testing::Values(kAnswer,
+                                         kCapabilitiesResponse,
+                                         kGetCapabilities,
+                                         kGetStatus,
+                                         kOffer,
+                                         kRpc,
+                                         kStatusResponse));
+
+TEST_P(StreamingValidationTest, ExampleStreamingMessages) {
+  ErrorOr<Json::Value> message_root = json::Parse(GetParam());
+  EXPECT_TRUE(message_root.is_value());
+  EXPECT_TRUE(ValidateStreamingMessage(message_root.value()).empty());
+}
+
+void ExpectReceiverMessageValid(const char* message) {}
+
+INSTANTIATE_TEST_SUITE_P(ReceiverValidations,
+                         ReceiverValidationTest,
+                         testing::Values(kGetAppAvailability,
+                                         kGetAppAvailabilityResponse,
+                                         kLaunch,
+                                         kStop));
+
+TEST_P(ReceiverValidationTest, ExampleReceiverMessages) {
+  ErrorOr<Json::Value> message_root = json::Parse(GetParam());
+  EXPECT_TRUE(message_root.is_value());
+  EXPECT_TRUE(ValidateReceiverMessage(message_root.value()).empty());
+}
+}  // namespace cast
+}  // namespace openscreen
diff --git a/cast/streaming/BUILD.gn b/cast/streaming/BUILD.gn
index a35eaa1..de4600d 100644
--- a/cast/streaming/BUILD.gn
+++ b/cast/streaming/BUILD.gn
@@ -68,6 +68,10 @@
     "../../platform",
     "../../util",
   ]
+
+  if (!build_with_chromium) {
+    deps += [ "../protocol:castv2" ]
+  }
 }
 
 source_set("receiver") {
diff --git a/cast/streaming/receiver_session_unittest.cc b/cast/streaming/receiver_session_unittest.cc
index f6a6e89..a5af361 100644
--- a/cast/streaming/receiver_session_unittest.cc
+++ b/cast/streaming/receiver_session_unittest.cc
@@ -393,12 +393,11 @@
 
 TEST_F(ReceiverSessionTest, CanNegotiateWithCustomConstraints) {
   auto constraints = std::make_unique<Constraints>(Constraints{
-      AudioConstraints{1, 2, 3, 4},
-
+      AudioConstraints{48001, 2, 32001, 32002, milliseconds(3001)},
       VideoConstraints{3.14159,
                        absl::optional<Dimensions>(
                            Dimensions{320, 240, SimpleFraction{24, 1}}),
-                       Dimensions{1920, 1080, SimpleFraction{144, 1}}, 3000,
+                       Dimensions{1920, 1080, SimpleFraction{144, 1}}, 300000,
                        90000000, milliseconds(1000)}});
 
   auto display = std::make_unique<DisplayDescription>(DisplayDescription{
@@ -444,11 +443,11 @@
 
   const Json::Value& audio = constraints_json["audio"];
   ASSERT_TRUE(audio.isObject());
-  EXPECT_EQ(4, audio["maxBitRate"].asInt());
+  EXPECT_EQ(32002, audio["maxBitRate"].asInt());
   EXPECT_EQ(2, audio["maxChannels"].asInt());
-  EXPECT_EQ(0, audio["maxDelay"].asInt());
-  EXPECT_EQ(1, audio["maxSampleRate"].asInt());
-  EXPECT_EQ(3, audio["minBitRate"].asInt());
+  EXPECT_EQ(3001, audio["maxDelay"].asInt());
+  EXPECT_EQ(48001, audio["maxSampleRate"].asInt());
+  EXPECT_EQ(32001, audio["minBitRate"].asInt());
 
   const Json::Value& video = constraints_json["video"];
   ASSERT_TRUE(video.isObject());
@@ -458,7 +457,7 @@
   EXPECT_EQ(1920, video["maxDimensions"]["width"].asInt());
   EXPECT_EQ(1080, video["maxDimensions"]["height"].asInt());
   EXPECT_DOUBLE_EQ(3.14159, video["maxPixelsPerSecond"].asDouble());
-  EXPECT_EQ(3000, video["minBitRate"].asInt());
+  EXPECT_EQ(300000, video["minBitRate"].asInt());
   EXPECT_EQ("24", video["minDimensions"]["frameRate"].asString());
   EXPECT_EQ(320, video["minDimensions"]["width"].asInt());
   EXPECT_EQ(240, video["minDimensions"]["height"].asInt());
diff --git a/cast/test/BUILD.gn b/cast/test/BUILD.gn
index dedf376..d37e6af 100644
--- a/cast/test/BUILD.gn
+++ b/cast/test/BUILD.gn
@@ -20,6 +20,10 @@
     "../receiver:test_helpers",
     "../sender:channel",
   ]
+
+  if (!build_with_chromium) {
+    deps += [ "../protocol:unittests" ]
+  }
 }
 
 if (is_posix && !build_with_chromium) {
diff --git a/third_party/valijson/BUILD.gn b/third_party/valijson/BUILD.gn
new file mode 100644
index 0000000..8df8459
--- /dev/null
+++ b/third_party/valijson/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build_overrides/build.gni")
+
+if (!build_with_chromium) {
+  config("valijson_config") {
+    cflags_cc = [ "-Wno-extra-semi" ]
+
+    # NOTE: while this allows files to use #include "valijson/<foo>.hpp", Open
+    # Screen files should use the fully qualified include and this should be
+    # reserved for valijson files to include each other.
+    include_dirs = [ "//third_party/valijson/src/include" ]
+  }
+
+  source_set("valijson") {
+    sources = [
+      "src/include/valijson/adapters/adapter.hpp",
+      "src/include/valijson/adapters/basic_adapter.hpp",
+      "src/include/valijson/adapters/frozen_value.hpp",
+
+      # We only need the adapter for JsonCpp.
+      "src/include/valijson/adapters/jsoncpp_adapter.hpp",
+      "src/include/valijson/constraints_builder.hpp",
+      "src/include/valijson/internal/custom_allocator.hpp",
+      "src/include/valijson/internal/debug.hpp",
+      "src/include/valijson/internal/json_pointer.hpp",
+      "src/include/valijson/internal/json_reference.hpp",
+      "src/include/valijson/internal/optional.hpp",
+      "src/include/valijson/internal/uri.hpp",
+      "src/include/valijson/schema.hpp",
+      "src/include/valijson/schema_parser.hpp",
+      "src/include/valijson/subschema.hpp",
+      "src/include/valijson/utils/jsoncpp_utils.hpp",
+      "src/include/valijson/validation_results.hpp",
+      "src/include/valijson/validation_visitor.hpp",
+      "src/include/valijson/validator.hpp",
+    ]
+
+    defines = [ "VALIJSON_USE_EXCEPTIONS=0" ]
+    public_configs = [ ":valijson_config" ]
+  }
+}
diff --git a/third_party/valijson/README.chromium b/third_party/valijson/README.chromium
new file mode 100644
index 0000000..28eb306
--- /dev/null
+++ b/third_party/valijson/README.chromium
@@ -0,0 +1,14 @@
+Name: Valijson
+Short Name: valijson
+URL: https://github.com/tristanpenman/valijson
+Security Critical: yes
+License: 2-Clause BSD
+License File: src/LICENSE
+
+Description:
+Valijson is a header-only JSON Schema Validation library for C++11.
+
+Valijson provides a simple validation API that allows you to load JSON Schemas,
+and validate documents loaded by one of several supported parser libraries. In
+Open Screen, Valijson is used exclusively with JsonCpp.
+
diff --git a/tools/convert_to_data_file.py b/tools/convert_to_data_file.py
new file mode 100755
index 0000000..05456f0
--- /dev/null
+++ b/tools/convert_to_data_file.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""
+Converts a data file, e.g. a JSON file, into a C++ raw string that
+can be #included.
+"""
+
+import argparse
+import os
+import sys
+
+FORMAT_STRING = """#pragma once
+
+namespace openscreen {{
+namespace {0} {{
+
+constexpr char {1}[] = R"(
+        {2}
+)";
+
+}} // namspace {0}
+}} // namespace openscreen
+"""
+
+
+def ToCamelCase(snake_case):
+    """Converts snake_case to TitleCamelCase."""
+    return ''.join(x.title() for x in snake_case.split('_'))
+
+
+def GetVariableName(path):
+    """Converts a snake case file name into a kCamelCase variable name."""
+    file_name = os.path.splitext(os.path.split(path)[1])[0]
+    return 'k' + ToCamelCase(file_name)
+
+
+def Convert(namespace, input_path, output_path):
+    """Takes an input file, such as a JSON file, and converts it into a C++
+       data file, in the form of a character array constant in a header."""
+    if not os.path.exists(input_path):
+        print('\tERROR: failed to generate, invalid path supplied: ' +
+              input_path)
+        return 1
+
+    content = False
+    with open(input_path, 'r') as f:
+        content = f.read()
+
+    with open(output_path, 'w') as f:
+        f.write(
+            FORMAT_STRING.format(namespace, GetVariableName(input_path),
+                                 content))
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Convert a file to a C++ data file')
+    parser.add_argument(
+        'namespace',
+        help='Namespace to scope data variable (nested under openscreen)')
+    parser.add_argument('input_path', help='Path to file to convert')
+    parser.add_argument('output_path', help='Output path of converted file')
+    args = parser.parse_args()
+
+    input_path = os.path.abspath(args.input_path)
+    output_path = os.path.abspath(args.output_path)
+    Convert(args.namespace, input_path, output_path)
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/util/BUILD.gn b/util/BUILD.gn
index 2e395b1..f1da81e 100644
--- a/util/BUILD.gn
+++ b/util/BUILD.gn
@@ -55,6 +55,7 @@
     "saturate_cast.h",
     "simple_fraction.cc",
     "simple_fraction.h",
+    "std_util.cc",
     "std_util.h",
     "stringprintf.cc",
     "stringprintf.h",
@@ -69,10 +70,12 @@
     "yet_another_bit_vector.h",
   ]
 
-  public_deps = [ "../third_party/jsoncpp" ]
+  public_deps = [
+    "../third_party/abseil",
+    "../third_party/jsoncpp",
+  ]
 
   deps = [
-    "../third_party/abseil",
     "../third_party/boringssl",
     "../third_party/mozilla",
   ]
diff --git a/util/std_util.cc b/util/std_util.cc
new file mode 100644
index 0000000..a7d82d8
--- /dev/null
+++ b/util/std_util.cc
@@ -0,0 +1,26 @@
+// Copyright 2020 The Chromium 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 "util/std_util.h"
+
+namespace openscreen {
+
+std::string Join(const std::vector<std::string>& strings,
+                 const char* delimiter) {
+  size_t size_to_reserve = 0;
+  for (const auto& piece : strings) {
+    size_to_reserve += piece.length();
+  }
+  std::string out;
+  out.reserve(size_to_reserve);
+  auto it = strings.begin();
+  out += *it;
+  for (++it; it != strings.end(); ++it) {
+    out += delimiter + *it;
+  }
+
+  return out;
+}
+
+}  // namespace openscreen
diff --git a/util/std_util.h b/util/std_util.h
index 8772695..ca65483 100644
--- a/util/std_util.h
+++ b/util/std_util.h
@@ -12,6 +12,7 @@
 #include <vector>
 
 #include "absl/algorithm/container.h"
+#include "util/stringprintf.h"
 
 namespace openscreen {
 
@@ -32,6 +33,9 @@
   return std::addressof(str[0]);
 }
 
+std::string Join(const std::vector<std::string>& strings,
+                 const char* delimiter);
+
 template <typename Key, typename Value>
 void RemoveValueFromMap(std::map<Key, Value*>* map, Value* value) {
   for (auto it = map->begin(); it != map->end();) {