Coverage Report

Created: 2024-09-19 09:45

/proc/self/cwd/source/common/protobuf/yaml_utility.cc
Line
Count
Source (jump to first uncovered line)
1
#include <limits>
2
#include <numeric>
3
4
#include "envoy/annotations/deprecation.pb.h"
5
#include "envoy/protobuf/message_validator.h"
6
#include "envoy/type/v3/percent.pb.h"
7
8
#include "source/common/common/assert.h"
9
#include "source/common/common/documentation_url.h"
10
#include "source/common/common/fmt.h"
11
#include "source/common/protobuf/message_validator_impl.h"
12
#include "source/common/protobuf/protobuf.h"
13
#include "source/common/protobuf/utility.h"
14
#include "source/common/protobuf/visitor.h"
15
#include "source/common/runtime/runtime_features.h"
16
17
#include "absl/strings/match.h"
18
#include "udpa/annotations/sensitive.pb.h"
19
#include "udpa/annotations/status.pb.h"
20
#include "utf8_validity.h"
21
#include "validate/validate.h"
22
#include "xds/annotations/v3/status.pb.h"
23
#include "yaml-cpp/yaml.h"
24
25
using namespace std::chrono_literals;
26
27
namespace Envoy {
28
namespace {
29
30
41.8k
void blockFormat(YAML::Node node) {
31
41.8k
  node.SetStyle(YAML::EmitterStyle::Block);
32
33
41.8k
  if (node.Type() == YAML::NodeType::Sequence) {
34
3.60k
    for (const auto& it : node) {
35
3.60k
      blockFormat(it);
36
3.60k
    }
37
140
  }
38
41.8k
  if (node.Type() == YAML::NodeType::Map) {
39
37.5k
    for (const auto& it : node) {
40
37.5k
      blockFormat(it.second);
41
37.5k
    }
42
2.64k
  }
43
41.8k
}
44
45
451k
ProtobufWkt::Value parseYamlNode(const YAML::Node& node) {
46
451k
  ProtobufWkt::Value value;
47
451k
  switch (node.Type()) {
48
85.8k
  case YAML::NodeType::Null:
49
85.8k
    value.set_null_value(ProtobufWkt::NULL_VALUE);
50
85.8k
    break;
51
189k
  case YAML::NodeType::Scalar: {
52
189k
    if (node.Tag() == "!") {
53
50.3k
      value.set_string_value(node.as<std::string>());
54
50.3k
      break;
55
50.3k
    }
56
138k
    bool bool_value;
57
138k
    if (YAML::convert<bool>::decode(node, bool_value)) {
58
69.1k
      value.set_bool_value(bool_value);
59
69.1k
      break;
60
69.1k
    }
61
69.6k
    int64_t int_value;
62
69.6k
    if (YAML::convert<int64_t>::decode(node, int_value)) {
63
12.3k
      if (std::numeric_limits<int32_t>::min() <= int_value &&
64
12.3k
          std::numeric_limits<int32_t>::max() >= int_value) {
65
        // We could convert all integer values to string but it will break some stuff relying on
66
        // ProtobufWkt::Struct itself, only convert small numbers into number_value here.
67
12.3k
        value.set_number_value(int_value);
68
12.3k
      } else {
69
        // Proto3 JSON mapping allows use string for integer, this still has to be converted from
70
        // int_value to support hexadecimal and octal literals.
71
0
        value.set_string_value(std::to_string(int_value));
72
0
      }
73
12.3k
      break;
74
12.3k
    }
75
    // Fall back on string, including float/double case. When protobuf parse the JSON into a message
76
    // it will convert based on the type in the message definition.
77
57.3k
    value.set_string_value(node.as<std::string>());
78
57.3k
    break;
79
69.6k
  }
80
29.6k
  case YAML::NodeType::Sequence: {
81
29.6k
    auto& list_values = *value.mutable_list_value()->mutable_values();
82
47.3k
    for (const auto& it : node) {
83
47.3k
      *list_values.Add() = parseYamlNode(it);
84
47.3k
    }
85
29.6k
    break;
86
69.6k
  }
87
147k
  case YAML::NodeType::Map: {
88
147k
    auto& struct_fields = *value.mutable_struct_value()->mutable_fields();
89
352k
    for (const auto& it : node) {
90
352k
      if (it.first.Tag() != "!ignore") {
91
352k
        struct_fields[it.first.as<std::string>()] = parseYamlNode(it.second);
92
352k
      }
93
352k
    }
94
147k
    break;
95
69.6k
  }
96
0
  case YAML::NodeType::Undefined:
97
0
    throw EnvoyException("Undefined YAML value");
98
451k
  }
99
451k
  return value;
100
451k
}
101
102
void jsonConvertInternal(const Protobuf::Message& source,
103
                         ProtobufMessage::ValidationVisitor& validation_visitor,
104
8.30k
                         Protobuf::Message& dest) {
105
8.30k
  Protobuf::util::JsonPrintOptions json_options;
106
8.30k
  json_options.preserve_proto_field_names = true;
107
8.30k
  std::string json;
108
8.30k
  const auto status = Protobuf::util::MessageToJsonString(source, &json, json_options);
109
8.30k
  if (!status.ok()) {
110
4
    throw EnvoyException(fmt::format("Unable to convert protobuf message to JSON string: {} {}",
111
4
                                     status.ToString(), source.DebugString()));
112
4
  }
113
8.30k
  MessageUtil::loadFromJson(json, dest, validation_visitor);
114
8.30k
}
115
116
} // namespace
117
118
void MessageUtil::loadFromJson(const std::string& json, Protobuf::Message& message,
119
11.2k
                               ProtobufMessage::ValidationVisitor& validation_visitor) {
120
11.2k
  bool has_unknown_field;
121
11.2k
  auto load_status = loadFromJsonNoThrow(json, message, has_unknown_field);
122
11.2k
  if (load_status.ok()) {
123
11.0k
    return;
124
11.0k
  }
125
142
  if (has_unknown_field) {
126
    // If the parsing failure is caused by the unknown fields.
127
14
    THROW_IF_NOT_OK(validation_visitor.onUnknownField("type " + message.GetTypeName() + " reason " +
128
14
                                                      load_status.ToString()));
129
128
  } else {
130
    // If the error has nothing to do with unknown field.
131
128
    throw EnvoyException("Unable to parse JSON as proto (" + load_status.ToString() + "): " + json);
132
128
  }
133
142
}
134
135
absl::Status MessageUtil::loadFromJsonNoThrow(const std::string& json, Protobuf::Message& message,
136
11.2k
                                              bool& has_unknown_fileld) {
137
11.2k
  has_unknown_fileld = false;
138
11.2k
  Protobuf::util::JsonParseOptions options;
139
11.2k
  options.case_insensitive_enum_parsing = true;
140
  // Let's first try and get a clean parse when checking for unknown fields;
141
  // this should be the common case.
142
11.2k
  options.ignore_unknown_fields = false;
143
  // Clear existing values (if any) from the destination message before serialization.
144
11.2k
  message.Clear();
145
11.2k
  const auto strict_status = Protobuf::util::JsonStringToMessage(json, &message, options);
146
11.2k
  if (strict_status.ok()) {
147
    // Success, no need to do any extra work.
148
11.0k
    return strict_status;
149
11.0k
  }
150
  // If we fail, we see if we get a clean parse when allowing unknown fields.
151
  // This is essentially a workaround
152
  // for https://github.com/protocolbuffers/protobuf/issues/5967.
153
  // TODO(htuch): clean this up when protobuf supports JSON/YAML unknown field
154
  // detection directly.
155
142
  options.ignore_unknown_fields = true;
156
142
  const auto relaxed_status = Protobuf::util::JsonStringToMessage(json, &message, options);
157
  // If we still fail with relaxed unknown field checking, the error has nothing
158
  // to do with unknown fields.
159
142
  if (relaxed_status.ok()) {
160
14
    has_unknown_fileld = true;
161
14
    return strict_status;
162
14
  }
163
128
  return relaxed_status;
164
142
}
165
166
919
void MessageUtil::loadFromJson(const std::string& json, ProtobufWkt::Struct& message) {
167
  // No need to validate if converting to a Struct, since there are no unknown
168
  // fields possible.
169
919
  loadFromJson(json, message, ProtobufMessage::getNullValidationVisitor());
170
919
}
171
172
void MessageUtil::loadFromYaml(const std::string& yaml, Protobuf::Message& message,
173
6.70k
                               ProtobufMessage::ValidationVisitor& validation_visitor) {
174
6.70k
  ProtobufWkt::Value value = ValueUtil::loadFromYaml(yaml);
175
6.70k
  if (value.kind_case() == ProtobufWkt::Value::kStructValue ||
176
6.70k
      value.kind_case() == ProtobufWkt::Value::kListValue) {
177
6.64k
    jsonConvertInternal(value, validation_visitor, message);
178
6.64k
    return;
179
6.64k
  }
180
63
  throw EnvoyException("Unable to convert YAML as JSON: " + yaml);
181
6.70k
}
182
183
std::string MessageUtil::getYamlStringFromMessage(const Protobuf::Message& message,
184
                                                  const bool block_print,
185
751
                                                  const bool always_print_primitive_fields) {
186
187
751
  auto json_or_error = getJsonStringFromMessage(message, false, always_print_primitive_fields);
188
751
  if (!json_or_error.ok()) {
189
0
    throw EnvoyException(json_or_error.status().ToString());
190
0
  }
191
751
  YAML::Node node;
192
751
  TRY_ASSERT_MAIN_THREAD { node = YAML::Load(json_or_error.value()); }
193
751
  END_TRY
194
751
  catch (YAML::ParserException& e) {
195
31
    throw EnvoyException(e.what());
196
31
  }
197
751
  catch (YAML::BadConversion& e) {
198
0
    throw EnvoyException(e.what());
199
0
  }
200
751
  catch (std::exception& e) {
201
    // Catch unknown YAML parsing exceptions.
202
0
    throw EnvoyException(fmt::format("Unexpected YAML exception: {}", +e.what()));
203
0
  }
204
720
  if (block_print) {
205
720
    blockFormat(node);
206
720
  }
207
720
  YAML::Emitter out;
208
720
  out << node;
209
720
  return out.c_str();
210
751
}
211
212
absl::StatusOr<std::string>
213
MessageUtil::getJsonStringFromMessage(const Protobuf::Message& message, const bool pretty_print,
214
492k
                                      const bool always_print_primitive_fields) {
215
492k
  Protobuf::util::JsonPrintOptions json_options;
216
  // By default, proto field names are converted to camelCase when the message is converted to JSON.
217
  // Setting this option makes debugging easier because it keeps field names consistent in JSON
218
  // printouts.
219
492k
  json_options.preserve_proto_field_names = true;
220
492k
  if (pretty_print) {
221
229
    json_options.add_whitespace = true;
222
229
  }
223
  // Primitive types such as int32s and enums will not be serialized if they have the default value.
224
  // This flag disables that behavior.
225
492k
  if (always_print_primitive_fields) {
226
483k
    json_options.always_print_fields_with_no_presence = true;
227
483k
  }
228
492k
  std::string json;
229
492k
  if (auto status = Protobuf::util::MessageToJsonString(message, &json, json_options);
230
492k
      !status.ok()) {
231
0
    return status;
232
0
  }
233
492k
  return json;
234
492k
}
235
236
std::string MessageUtil::getJsonStringFromMessageOrError(const Protobuf::Message& message,
237
                                                         bool pretty_print,
238
486k
                                                         bool always_print_primitive_fields) {
239
486k
  auto json_or_error =
240
486k
      getJsonStringFromMessage(message, pretty_print, always_print_primitive_fields);
241
486k
  return json_or_error.ok() ? std::move(json_or_error).value()
242
486k
                            : fmt::format("Failed to convert protobuf message to JSON string: {}",
243
0
                                          json_or_error.status().ToString());
244
486k
}
245
246
0
void MessageUtil::jsonConvert(const Protobuf::Message& source, Protobuf::Message& dest) {
247
0
  jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest);
248
0
}
249
250
0
void MessageUtil::jsonConvert(const Protobuf::Message& source, ProtobufWkt::Struct& dest) {
251
  // Any proto3 message can be transformed to Struct, so there is no need to check for unknown
252
  // fields. There is one catch; Duration/Timestamp etc. which have non-object canonical JSON
253
  // representations don't work.
254
0
  jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest);
255
0
}
256
257
void MessageUtil::jsonConvert(const ProtobufWkt::Struct& source,
258
                              ProtobufMessage::ValidationVisitor& validation_visitor,
259
1.66k
                              Protobuf::Message& dest) {
260
1.66k
  jsonConvertInternal(source, validation_visitor, dest);
261
1.66k
}
262
263
0
bool MessageUtil::jsonConvertValue(const Protobuf::Message& source, ProtobufWkt::Value& dest) {
264
0
  Protobuf::util::JsonPrintOptions json_options;
265
0
  json_options.preserve_proto_field_names = true;
266
0
  std::string json;
267
0
  auto status = Protobuf::util::MessageToJsonString(source, &json, json_options);
268
0
  if (!status.ok()) {
269
0
    return false;
270
0
  }
271
0
  bool has_unknow_field;
272
0
  status = MessageUtil::loadFromJsonNoThrow(json, dest, has_unknow_field);
273
0
  if (status.ok() || has_unknow_field) {
274
0
    return true;
275
0
  }
276
0
  return false;
277
0
}
278
279
51.3k
ProtobufWkt::Value ValueUtil::loadFromYaml(const std::string& yaml) {
280
51.3k
  TRY_ASSERT_MAIN_THREAD { return parseYamlNode(YAML::Load(yaml)); }
281
51.3k
  END_TRY
282
51.3k
  catch (YAML::ParserException& e) {
283
94
    throw EnvoyException(e.what());
284
94
  }
285
51.3k
  catch (YAML::BadConversion& e) {
286
0
    throw EnvoyException(e.what());
287
0
  }
288
51.3k
  catch (std::exception& e) {
289
    // There is a potentially wide space of exceptions thrown by the YAML parser,
290
    // and enumerating them all may be difficult. Envoy doesn't work well with
291
    // unhandled exceptions, so we capture them and record the exception name in
292
    // the Envoy Exception text.
293
0
    throw EnvoyException(fmt::format("Unexpected YAML exception: {}", +e.what()));
294
0
  }
295
51.3k
}
296
297
namespace {
298
299
// This is a modified copy of the UTF8CoerceToStructurallyValid from the protobuf code.
300
// A copy was needed after if was removed from the protobuf.
301
74.2k
std::string utf8CoerceToStructurallyValid(absl::string_view str, const char replace_char) {
302
74.2k
  std::string result(str);
303
74.2k
  auto replace_pos = result.begin();
304
76.9k
  while (!str.empty()) {
305
51.4k
    size_t n_valid_bytes = utf8_range::SpanStructurallyValid(str);
306
51.4k
    if (n_valid_bytes == str.size()) {
307
48.7k
      break;
308
48.7k
    }
309
2.68k
    replace_pos += n_valid_bytes;
310
2.68k
    *replace_pos++ = replace_char; // replace one bad byte
311
2.68k
    str.remove_prefix(n_valid_bytes + 1);
312
2.68k
  }
313
74.2k
  return result;
314
74.2k
}
315
316
} // namespace
317
318
74.2k
std::string MessageUtil::sanitizeUtf8String(absl::string_view input) {
319
  // The choice of '!' is somewhat arbitrary, but we wanted to avoid any character that has
320
  // special semantic meaning in URLs or similar.
321
74.2k
  return utf8CoerceToStructurallyValid(input, '!');
322
74.2k
}
323
324
} // namespace Envoy