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();) {