Commit bb18bc24 authored by Andrey Kosyakov's avatar Andrey Kosyakov Committed by V8 LUCI CQ

Roll third_party/inspector_protocol to 817313aa48ebb9a53cba1bd88bbe6a1c5048060c

This includes conversion of python scripts to python3.

Change-Id: I5c05b3ab2aa00711a0dc26f1885a73f0ef4dbd85
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3530115Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80702}
parent 37569a5a
......@@ -2,7 +2,7 @@ Name: inspector protocol
Short Name: inspector_protocol
URL: https://chromium.googlesource.com/deps/inspector_protocol/
Version: 0
Revision: 5221cbfa7f940d56ae8b79bf34c446a56781dd56
Revision: 817313aa48ebb9a53cba1bd88bbe6a1c5048060c
License: BSD
License File: LICENSE
Security Critical: no
......
#!/usr/bin/env python
#!/usr/bin/env python3
# Copyright 2016 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.
......@@ -95,27 +95,28 @@ def read_config():
config_base)
config_json_file.close()
defaults = {
".use_snake_file_names": False,
".use_title_case_methods": False,
".imported": False,
".imported.export_macro": "",
".imported.export_header": False,
".imported.header": False,
".imported.package": False,
".imported.options": False,
".protocol.export_macro": "",
".protocol.export_header": False,
".protocol.options": False,
".protocol.file_name_prefix": "",
".exported": False,
".exported.export_macro": "",
".exported.export_header": False,
".lib": False,
".lib.export_macro": "",
".lib.export_header": False,
".crdtp": False,
".crdtp.dir": os.path.join(inspector_protocol_dir, "crdtp"),
".crdtp.namespace": "crdtp",
".use_snake_file_names": False,
".use_title_case_methods": False,
".use_embedder_types": False,
".imported": False,
".imported.export_macro": "",
".imported.export_header": False,
".imported.header": False,
".imported.package": False,
".imported.options": False,
".protocol.export_macro": "",
".protocol.export_header": False,
".protocol.options": False,
".protocol.file_name_prefix": "",
".exported": False,
".exported.export_macro": "",
".exported.export_header": False,
".lib": False,
".lib.export_macro": "",
".lib.export_header": False,
".crdtp": False,
".crdtp.dir": os.path.join(inspector_protocol_dir, "crdtp"),
".crdtp.namespace": "crdtp",
}
for key_value in config_values:
parts = key_value.split("=")
......@@ -638,31 +639,32 @@ def main():
lib_templates_dir = os.path.join(module_path, "lib")
# Note these should be sorted in the right order.
# TODO(dgozman): sort them programmatically based on commented includes.
protocol_h_templates = [
"Values_h.template",
"Object_h.template",
"ValueConversions_h.template",
]
protocol_cpp_templates = [
"Protocol_cpp.template",
"Values_cpp.template",
"Object_cpp.template",
"ValueConversions_cpp.template",
]
# TODO(dgozman): sort them programmatically based on commented includes.
forward_h_templates = [
"Forward_h.template",
]
base_string_adapter_h_templates = [
"base_string_adapter_h.template",
]
base_string_adapter_cc_templates = [
"base_string_adapter_cc.template",
]
protocol_h_templates = []
protocol_cpp_templates = []
if not config.use_embedder_types:
protocol_h_templates += [
"Values_h.template",
"Object_h.template",
"ValueConversions_h.template",
]
protocol_cpp_templates += [
"Protocol_cpp.template",
"Values_cpp.template",
"Object_cpp.template",
"ValueConversions_cpp.template",
]
else:
protocol_h_templates += [
"Forward_h.template",
]
def generate_lib_file(file_name, template_files):
parts = []
......@@ -676,12 +678,11 @@ def main():
config, "Forward.h")), forward_h_templates)
generate_lib_file(os.path.join(config.lib.output, to_file_name(
config, "Protocol.h")), protocol_h_templates)
generate_lib_file(os.path.join(config.lib.output, to_file_name(
config, "Protocol.cpp")), protocol_cpp_templates)
generate_lib_file(os.path.join(config.lib.output, to_file_name(
config, "base_string_adapter.h")), base_string_adapter_h_templates)
generate_lib_file(os.path.join(config.lib.output, to_file_name(
config, "base_string_adapter.cc")), base_string_adapter_cc_templates)
if not config.use_embedder_types:
generate_lib_file(
os.path.join(config.lib.output, to_file_name(config, "Protocol.cpp")),
protocol_cpp_templates)
# Make gyp / make generatos happy, otherwise make rebuilds world.
inputs_ts = max(map(os.path.getmtime, inputs))
......
#!/usr/bin/env python
#!/usr/bin/env python3
# Copyright 2016 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.
......
#!/usr/bin/env python
#!/usr/bin/env python3
# Copyright 2017 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.
......@@ -11,31 +11,34 @@ import sys
import pdl
def open_to_write(path):
if sys.version_info >= (3,0):
return open(path, 'w', encoding='utf-8')
else:
return open(path, 'wb')
if sys.version_info >= (3, 0):
return open(path, 'w', encoding='utf-8')
else:
return open(path, 'wb')
def main(argv):
parser = argparse.ArgumentParser(description=(
"Converts from .pdl to .json by invoking the pdl Python module."))
parser.add_argument('--map_binary_to_string', type=bool,
help=('If set, binary in the .pdl is mapped to a '
'string in .json. Client code will have to '
'base64 decode the string to get the payload.'))
parser.add_argument("pdl_file", help="The .pdl input file to parse.")
parser.add_argument("json_file", help="The .json output file write.")
args = parser.parse_args(argv)
file_name = os.path.normpath(args.pdl_file)
input_file = open(file_name, "r")
pdl_string = input_file.read()
protocol = pdl.loads(pdl_string, file_name, args.map_binary_to_string)
input_file.close()
output_file = open_to_write(os.path.normpath(args.json_file))
json.dump(protocol, output_file, indent=4, separators=(',', ': '))
output_file.close()
parser = argparse.ArgumentParser(
description=(
"Converts from .pdl to .json by invoking the pdl Python module."))
parser.add_argument(
'--map_binary_to_string',
type=bool,
help=('If set, binary in the .pdl is mapped to a '
'string in .json. Client code will have to '
'base64 decode the string to get the payload.'))
parser.add_argument("pdl_file", help="The .pdl input file to parse.")
parser.add_argument("json_file", help="The .json output file write.")
args = parser.parse_args(argv)
file_name = os.path.normpath(args.pdl_file)
input_file = open(file_name, "r")
pdl_string = input_file.read()
protocol = pdl.loads(pdl_string, file_name, args.map_binary_to_string)
input_file.close()
output_file = open_to_write(os.path.normpath(args.json_file))
json.dump(protocol, output_file, indent=4, separators=(',', ': '))
output_file.close()
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
sys.exit(main(sys.argv[1:]))
......@@ -595,7 +595,7 @@ span<uint8_t> CBORTokenizer::GetEnvelopeContents() const {
// and then checking whether the sum went past it.
//
// See also
// https://chromium.googlesource.com/chromium/src/+/master/docs/security/integer-semantics.md
// https://chromium.googlesource.com/chromium/src/+/main/docs/security/integer-semantics.md
static const uint64_t kMaxValidLength =
std::min<uint64_t>(std::numeric_limits<uint64_t>::max() >> 2,
std::numeric_limits<size_t>::max());
......
......@@ -310,15 +310,10 @@ class ProtocolError : public Serializable {
std::unique_ptr<Serializable> CreateErrorResponse(
int call_id,
DispatchResponse dispatch_response,
const ErrorSupport* errors) {
DispatchResponse dispatch_response) {
auto protocol_error =
std::make_unique<ProtocolError>(std::move(dispatch_response));
protocol_error->SetCallId(call_id);
if (errors && !errors->Errors().empty()) {
protocol_error->SetData(
std::string(errors->Errors().begin(), errors->Errors().end()));
}
return protocol_error;
}
......@@ -473,26 +468,9 @@ void DomainDispatcher::sendResponse(int call_id,
frontend_channel_->SendProtocolResponse(call_id, std::move(serializable));
}
bool DomainDispatcher::MaybeReportInvalidParams(
const Dispatchable& dispatchable,
const ErrorSupport& errors) {
if (errors.Errors().empty())
return false;
if (frontend_channel_) {
frontend_channel_->SendProtocolResponse(
dispatchable.CallId(),
CreateErrorResponse(
dispatchable.CallId(),
DispatchResponse::InvalidParams("Invalid parameters"), &errors));
}
return true;
}
bool DomainDispatcher::MaybeReportInvalidParams(
const Dispatchable& dispatchable,
const DeserializerState& state) {
if (state.status().ok())
return false;
void DomainDispatcher::ReportInvalidParams(const Dispatchable& dispatchable,
const DeserializerState& state) {
assert(!state.status().ok());
if (frontend_channel_) {
frontend_channel_->SendProtocolResponse(
dispatchable.CallId(),
......@@ -500,7 +478,6 @@ bool DomainDispatcher::MaybeReportInvalidParams(
dispatchable.CallId(),
DispatchResponse::InvalidParams("Invalid parameters"), state));
}
return true;
}
void DomainDispatcher::clearFrontend() {
......
......@@ -31,7 +31,7 @@ enum class DispatchCode {
FALL_THROUGH = 2,
// For historical reasons, these error codes correspond to commonly used
// XMLRPC codes (e.g. see METHOD_NOT_FOUND in
// https://github.com/python/cpython/blob/master/Lib/xmlrpc/client.py).
// https://github.com/python/cpython/blob/main/Lib/xmlrpc/client.py).
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
......@@ -150,8 +150,7 @@ class Dispatchable {
std::unique_ptr<Serializable> CreateErrorResponse(
int callId,
DispatchResponse dispatch_response,
const ErrorSupport* errors = nullptr);
DispatchResponse dispatch_response);
std::unique_ptr<Serializable> CreateErrorNotification(
DispatchResponse dispatch_response);
......@@ -230,13 +229,8 @@ class DomainDispatcher {
const DispatchResponse&,
std::unique_ptr<Serializable> result = nullptr);
// Returns true if |errors| contains errors *and* reports these errors
// as a response on the frontend channel. Called from generated code,
// optimized for code size of the callee.
bool MaybeReportInvalidParams(const Dispatchable& dispatchable,
const ErrorSupport& errors);
bool MaybeReportInvalidParams(const Dispatchable& dispatchable,
const DeserializerState& state);
void ReportInvalidParams(const Dispatchable& dispatchable,
const DeserializerState& state);
FrontendChannel* channel() { return frontend_channel_; }
......
......@@ -269,16 +269,8 @@ TEST(DispatchableTest, FaultyCBORTrailingJunk) {
// Helpers for creating protocol cresponses and notifications.
// =============================================================================
TEST(CreateErrorResponseTest, SmokeTest) {
ErrorSupport errors;
errors.Push();
errors.SetName("foo");
errors.Push();
errors.SetName("bar");
errors.AddError("expected a string");
errors.SetName("baz");
errors.AddError("expected a surprise");
auto serializable = CreateErrorResponse(
42, DispatchResponse::InvalidParams("invalid params message"), &errors);
42, DispatchResponse::InvalidParams("invalid params message"));
std::string json;
auto status =
json::ConvertCBORToJSON(SpanFrom(serializable->Serialize()), &json);
......@@ -286,9 +278,7 @@ TEST(CreateErrorResponseTest, SmokeTest) {
EXPECT_EQ(
"{\"id\":42,\"error\":"
"{\"code\":-32602,"
"\"message\":\"invalid params message\","
"\"data\":\"foo.bar: expected a string; "
"foo.baz: expected a surprise\"}}",
"\"message\":\"invalid params message\"}}",
json);
}
......
......@@ -267,11 +267,11 @@ class DeserializableProtocolObject {
std::unique_ptr<T> value(new T());
auto deserializer = DeferredMessage::FromSpan(span<uint8_t>(bytes, size))
->MakeDeserializer();
Deserialize(&deserializer, value.get());
std::ignore = Deserialize(&deserializer, value.get());
return value;
}
static bool Deserialize(DeserializerState* state, T* value) {
[[nodiscard]] static bool Deserialize(DeserializerState* state, T* value) {
return T::deserializer_descriptor().Deserialize(state, value);
}
......@@ -348,6 +348,16 @@ struct ProtocolTypeTraits<
}
};
template <typename T, typename F>
bool ConvertProtocolValue(const F& from, T* to) {
std::vector<uint8_t> bytes;
ProtocolTypeTraits<F>::Serialize(from, &bytes);
auto deserializer =
DeferredMessage::FromSpan(span<uint8_t>(bytes.data(), bytes.size()))
->MakeDeserializer();
return ProtocolTypeTraits<T>::Deserialize(&deserializer, to);
}
#define DECLARE_DESERIALIZATION_SUPPORT() \
friend DeserializableBase<ProtocolType>; \
static const DeserializerDescriptorType& deserializer_descriptor()
......
......@@ -115,6 +115,8 @@ std::string Status::Message() const {
return "BINDINGS: binary value expected";
case Error::BINDINGS_DICTIONARY_VALUE_EXPECTED:
return "BINDINGS: dictionary value expected";
case Error::BINDINGS_INVALID_BASE64_STRING:
return "BINDINGS: invalid base64 string";
}
// Some compilers can't figure out that we can't get here.
return "INVALID ERROR CODE";
......
......@@ -78,6 +78,7 @@ enum class Error {
BINDINGS_STRING8_VALUE_EXPECTED = 0x35,
BINDINGS_BINARY_VALUE_EXPECTED = 0x36,
BINDINGS_DICTIONARY_VALUE_EXPECTED = 0x37,
BINDINGS_INVALID_BASE64_STRING = 0x38,
};
// A status value with position that can be copied. The default status
......
......@@ -25,22 +25,14 @@ template("inspector_protocol_generate") {
assert(defined(invoker.outputs))
assert(defined(invoker.inspector_protocol_dir))
inspector_protocol_dir = invoker.inspector_protocol_dir
use_embedder_types =
defined(invoker.use_embedder_types) && invoker.use_embedder_types
action(target_name) {
script = "$inspector_protocol_dir/code_generator.py"
inputs = [
invoker.config_file,
"$inspector_protocol_dir/lib/base_string_adapter_cc.template",
"$inspector_protocol_dir/lib/base_string_adapter_h.template",
"$inspector_protocol_dir/lib/Forward_h.template",
"$inspector_protocol_dir/lib/Object_cpp.template",
"$inspector_protocol_dir/lib/Object_h.template",
"$inspector_protocol_dir/lib/Protocol_cpp.template",
"$inspector_protocol_dir/lib/ValueConversions_cpp.template",
"$inspector_protocol_dir/lib/ValueConversions_h.template",
"$inspector_protocol_dir/lib/Values_cpp.template",
"$inspector_protocol_dir/lib/Values_h.template",
"$inspector_protocol_dir/templates/Exported_h.template",
"$inspector_protocol_dir/templates/Imported_h.template",
"$inspector_protocol_dir/templates/TypeBuilder_cpp.template",
......@@ -49,12 +41,21 @@ template("inspector_protocol_generate") {
if (defined(invoker.inputs)) {
inputs += invoker.inputs
}
if (!use_embedder_types) {
inputs += [
"$inspector_protocol_dir/lib/ValueConversions_cpp.template",
"$inspector_protocol_dir/lib/ValueConversions_h.template",
"$inspector_protocol_dir/lib/Values_cpp.template",
"$inspector_protocol_dir/lib/Values_h.template",
"$inspector_protocol_dir/lib/Object_cpp.template",
"$inspector_protocol_dir/lib/Object_h.template",
]
}
args = [
"--jinja_dir",
rebase_path("//third_party/", root_build_dir), # jinja is in chromium's
# third_party
"--output_base",
rebase_path(invoker.out_dir, root_build_dir),
"--config",
......@@ -62,7 +63,12 @@ template("inspector_protocol_generate") {
"--inspector_protocol_dir",
"$inspector_protocol_dir",
]
if (use_embedder_types) {
args += [
"--config_value",
"use_embedder_types=true",
]
}
if (defined(invoker.config_values)) {
foreach(value, invoker.config_values) {
args += [
......
......@@ -10,7 +10,6 @@
{% if config.lib.export_header %}
#include {{format_include(config.lib.export_header)}}
{% endif %}
#include {{format_include(config.lib.string_header)}}
#include <memory>
#include <vector>
......@@ -20,24 +19,40 @@
#include "{{config.crdtp.dir}}/frontend_channel.h"
#include "{{config.crdtp.dir}}/protocol_core.h"
{% if config.use_embedder_types %}
#include {{format_include(config.lib.protocol_traits)}}
{% else %}
#include {{format_include(config.lib.string_header)}}
{% endif %}
{% for namespace in config.protocol.namespace %}
namespace {{namespace}} {
{% endfor %}
class DictionaryValue;
using DispatchResponse = {{config.crdtp.namespace}}::DispatchResponse;
using ErrorSupport = {{config.crdtp.namespace}}::ErrorSupport;
using Serializable = {{config.crdtp.namespace}}::Serializable;
using FrontendChannel = {{config.crdtp.namespace}}::FrontendChannel;
using DomainDispatcher = {{config.crdtp.namespace}}::DomainDispatcher;
using UberDispatcher = {{config.crdtp.namespace}}::UberDispatcher;
using Response = DispatchResponse;
{% if config.use_embedder_types %}
using DictionaryValue = crdtp::traits::DictionaryValue;
using Object = crdtp::traits::DictionaryValue;
using ListValue = crdtp::traits::ListValue;
using Value = crdtp::traits::Value;
using String = crdtp::traits::String;
using Binary = crdtp::Binary;
{% else %}
class DictionaryValue;
class FundamentalValue;
class ListValue;
class Object;
using Response = DispatchResponse;
class SerializedValue;
class StringValue;
class Value;
{% endif %}
using {{config.crdtp.namespace}}::detail::PtrMaybe;
using {{config.crdtp.namespace}}::detail::ValueMaybe;
......
// This file is generated by base_string_adapter_cc.template.
// Copyright 2019 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 {{format_include(config.protocol.package, "base_string_adapter")}}
#include {{format_include(config.protocol.package, "Protocol")}}
#include <utility>
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "{{config.crdtp.dir}}/cbor.h"
#include "{{config.crdtp.dir}}/protocol_core.h"
using namespace {{config.crdtp.namespace}};
using {{"::".join(config.protocol.namespace)}}::Binary;
using {{"::".join(config.protocol.namespace)}}::String;
using {{"::".join(config.protocol.namespace)}}::StringUtil;
{% for namespace in config.protocol.namespace %}
namespace {{namespace}} {
{% endfor %}
// In Chromium, we do not support big endian architectures, so no conversion is needed
// to interpret UTF16LE.
// static
String StringUtil::fromUTF16LE(const uint16_t* data, size_t length) {
std::string utf8;
base::UTF16ToUTF8(reinterpret_cast<const char16_t*>(data), length, &utf8);
return utf8;
}
std::unique_ptr<protocol::Value> toProtocolValue(
const base::Value& value, int depth) {
if (!depth)
return nullptr;
if (value.is_none())
return protocol::Value::null();
if (value.is_bool())
return protocol::FundamentalValue::create(value.GetBool());
if (value.is_int())
return protocol::FundamentalValue::create(value.GetInt());
if (value.is_double())
return protocol::FundamentalValue::create(value.GetDouble());
if (value.is_string())
return protocol::StringValue::create(value.GetString());
if (value.is_list()) {
auto result = protocol::ListValue::create();
for (const base::Value& item : value.GetList()) {
if (auto converted = toProtocolValue(item, depth - 1)) {
result->pushValue(std::move(converted));
}
}
return result;
}
if (value.is_dict()) {
auto result = protocol::DictionaryValue::create();
for (auto kv : value.DictItems()) {
if (auto converted = toProtocolValue(kv.second, depth - 1)) {
result->setValue(kv.first, std::move(converted));
}
}
return result;
}
return nullptr;
}
base::Value toBaseValue(Value* value, int depth) {
if (!value || !depth)
return base::Value();
if (value->type() == Value::TypeBoolean) {
bool inner;
value->asBoolean(&inner);
return base::Value(inner);
}
if (value->type() == Value::TypeInteger) {
int inner;
value->asInteger(&inner);
return base::Value(inner);
}
if (value->type() == Value::TypeDouble) {
double inner;
value->asDouble(&inner);
return base::Value(inner);
}
if (value->type() == Value::TypeString) {
std::string inner;
value->asString(&inner);
return base::Value(inner);
}
if (value->type() == Value::TypeArray) {
ListValue* list = ListValue::cast(value);
base::Value result(base::Value::Type::LIST);
for (size_t i = 0; i < list->size(); i++) {
base::Value converted = toBaseValue(list->at(i), depth - 1);
if (!converted.is_none())
result.Append(std::move(converted));
}
return result;
}
if (value->type() == Value::TypeObject) {
DictionaryValue* dict = DictionaryValue::cast(value);
base::Value result(base::Value::Type::DICTIONARY);
for (size_t i = 0; i < dict->size(); i++) {
DictionaryValue::Entry entry = dict->at(i);
base::Value converted = toBaseValue(entry.second, depth - 1);
if (!converted.is_none())
result.SetKey(entry.first, std::move(converted));
}
return result;
}
return base::Value();
}
{% for namespace in config.protocol.namespace %}
} // namespace {{namespace}} {
{% endfor %}
// This file is generated by base_string_adapter_h.template.
// Copyright 2019 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 {{"_".join(config.protocol.namespace)}}_BASE_STRING_ADAPTER_H
#define {{"_".join(config.protocol.namespace)}}_BASE_STRING_ADAPTER_H
#include "{{config.crdtp.dir}}/chromium/protocol_traits.h"
{% if config.lib.export_header %}
#include "{{config.lib.export_header}}"
{% endif %}
{% for namespace in config.protocol.namespace %}
namespace {{namespace}} {
{% endfor %}
class Value;
using String = std::string;
using Binary = crdtp::Binary;
class {{config.lib.export_macro}} StringUtil {
public:
static String fromUTF8(const uint8_t* data, size_t length) {
return std::string(reinterpret_cast<const char*>(data), length);
}
static String fromUTF16LE(const uint16_t* data, size_t length);
static const uint8_t* CharactersLatin1(const String& s) { return nullptr; }
static const uint8_t* CharactersUTF8(const String& s) {
return reinterpret_cast<const uint8_t*>(s.data());
}
static const uint16_t* CharactersUTF16(const String& s) { return nullptr; }
static size_t CharacterCount(const String& s) { return s.size(); }
};
std::unique_ptr<Value> toProtocolValue(const base::Value& value, int depth);
base::Value toBaseValue(Value* value, int depth);
{% for namespace in config.protocol.namespace %}
} // namespace {{namespace}}
{% endfor %}
#endif // !defined({{"_".join(config.protocol.namespace)}}_BASE_STRING_ADAPTER_H)
......@@ -276,10 +276,11 @@ void DomainDispatcherImpl::{{command.name}}(const {{config.crdtp.namespace}}::Di
{% if "parameters" in command %}
auto deserializer = {{config.crdtp.namespace}}::DeferredMessage::FromSpan(dispatchable.Params())->MakeDeserializer();
{{command.name}}Params params;
{{command.name}}Params::Deserialize(&deserializer, &params);
if (MaybeReportInvalidParams(dispatchable, deserializer))
if (!{{command.name}}Params::Deserialize(&deserializer, &params)) {
ReportInvalidParams(dispatchable, deserializer);
return;
{% endif %}
}
{% endif -%}
{% if "returns" in command and not protocol.is_async_command(domain.domain, command.name) %}
// Declare output parameters.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment