Coverage Report

Created: 2023-11-12 09:30

/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
37.5k
void blockFormat(YAML::Node node) {
31
37.5k
  node.SetStyle(YAML::EmitterStyle::Block);
32
33
37.5k
  if (node.Type() == YAML::NodeType::Sequence) {
34
1.73k
    for (const auto& it : node) {
35
1.73k
      blockFormat(it);
36
1.73k
    }
37
1.46k
  }
38
37.5k
  if (node.Type() == YAML::NodeType::Map) {
39
35.0k
    for (const auto& it : node) {
40
35.0k
      blockFormat(it.second);
41
35.0k
    }
42
2.17k
  }
43
37.5k
}
44
45
354k
ProtobufWkt::Value parseYamlNode(const YAML::Node& node) {
46
354k
  ProtobufWkt::Value value;
47
354k
  switch (node.Type()) {
48
78.6k
  case YAML::NodeType::Null:
49
78.6k
    value.set_null_value(ProtobufWkt::NULL_VALUE);
50
78.6k
    break;
51
143k
  case YAML::NodeType::Scalar: {
52
143k
    if (node.Tag() == "!") {
53
21.0k
      value.set_string_value(node.as<std::string>());
54
21.0k
      break;
55
21.0k
    }
56
122k
    bool bool_value;
57
122k
    if (YAML::convert<bool>::decode(node, bool_value)) {
58
50.9k
      value.set_bool_value(bool_value);
59
50.9k
      break;
60
50.9k
    }
61
71.8k
    int64_t int_value;
62
71.8k
    if (YAML::convert<int64_t>::decode(node, int_value)) {
63
11.4k
      if (std::numeric_limits<int32_t>::min() <= int_value &&
64
11.4k
          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
11.4k
        value.set_number_value(int_value);
68
11.4k
      } 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
11.4k
      break;
74
11.4k
    }
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
60.3k
    value.set_string_value(node.as<std::string>());
78
60.3k
    break;
79
71.8k
  }
80
21.0k
  case YAML::NodeType::Sequence: {
81
21.0k
    auto& list_values = *value.mutable_list_value()->mutable_values();
82
21.8k
    for (const auto& it : node) {
83
21.8k
      *list_values.Add() = parseYamlNode(it);
84
21.8k
    }
85
21.0k
    break;
86
71.8k
  }
87
111k
  case YAML::NodeType::Map: {
88
111k
    auto& struct_fields = *value.mutable_struct_value()->mutable_fields();
89
278k
    for (const auto& it : node) {
90
278k
      if (it.first.Tag() != "!ignore") {
91
278k
        struct_fields[it.first.as<std::string>()] = parseYamlNode(it.second);
92
278k
      }
93
278k
    }
94
111k
    break;
95
71.8k
  }
96
0
  case YAML::NodeType::Undefined:
97
0
    throw EnvoyException("Undefined YAML value");
98
354k
  }
99
354k
  return value;
100
354k
}
101
102
void jsonConvertInternal(const Protobuf::Message& source,
103
                         ProtobufMessage::ValidationVisitor& validation_visitor,
104
4.49k
                         Protobuf::Message& dest) {
105
4.49k
  Protobuf::util::JsonPrintOptions json_options;
106
4.49k
  json_options.preserve_proto_field_names = true;
107
4.49k
  std::string json;
108
4.49k
  const auto status = Protobuf::util::MessageToJsonString(source, &json, json_options);
109
4.49k
  if (!status.ok()) {
110
1
    throw EnvoyException(fmt::format("Unable to convert protobuf message to JSON string: {} {}",
111
1
                                     status.ToString(), source.DebugString()));
112
1
  }
113
4.48k
  MessageUtil::loadFromJson(json, dest, validation_visitor);
114
4.48k
}
115
116
} // namespace
117
118
void MessageUtil::loadFromJson(const std::string& json, Protobuf::Message& message,
119
8.04k
                               ProtobufMessage::ValidationVisitor& validation_visitor) {
120
8.04k
  bool has_unknown_field;
121
8.04k
  auto status = loadFromJsonNoThrow(json, message, has_unknown_field);
122
8.04k
  if (status.ok()) {
123
7.89k
    return;
124
7.89k
  }
125
159
  if (has_unknown_field) {
126
    // If the parsing failure is caused by the unknown fields.
127
54
    validation_visitor.onUnknownField("type " + message.GetTypeName() + " reason " +
128
54
                                      status.ToString());
129
105
  } else {
130
    // If the error has nothing to do with unknown field.
131
105
    throw EnvoyException("Unable to parse JSON as proto (" + status.ToString() + "): " + json);
132
105
  }
133
159
}
134
135
absl::Status MessageUtil::loadFromJsonNoThrow(const std::string& json, Protobuf::Message& message,
136
8.05k
                                              bool& has_unknown_fileld) {
137
8.05k
  has_unknown_fileld = false;
138
8.05k
  Protobuf::util::JsonParseOptions options;
139
8.05k
  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
8.05k
  options.ignore_unknown_fields = false;
143
  // Clear existing values (if any) from the destination message before serialization.
144
8.05k
  message.Clear();
145
8.05k
  const auto strict_status = Protobuf::util::JsonStringToMessage(json, &message, options);
146
8.05k
  if (strict_status.ok()) {
147
    // Success, no need to do any extra work.
148
7.89k
    return strict_status;
149
7.89k
  }
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
167
  options.ignore_unknown_fields = true;
156
167
  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
167
  if (relaxed_status.ok()) {
160
54
    has_unknown_fileld = true;
161
54
    return strict_status;
162
54
  }
163
113
  return relaxed_status;
164
167
}
165
166
875
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
875
  loadFromJson(json, message, ProtobufMessage::getNullValidationVisitor());
170
875
}
171
172
void MessageUtil::loadFromYaml(const std::string& yaml, Protobuf::Message& message,
173
4.13k
                               ProtobufMessage::ValidationVisitor& validation_visitor) {
174
4.13k
  ProtobufWkt::Value value = ValueUtil::loadFromYaml(yaml);
175
4.13k
  if (value.kind_case() == ProtobufWkt::Value::kStructValue ||
176
4.13k
      value.kind_case() == ProtobufWkt::Value::kListValue) {
177
4.08k
    jsonConvertInternal(value, validation_visitor, message);
178
4.08k
    return;
179
4.08k
  }
180
43
  throw EnvoyException("Unable to convert YAML as JSON: " + yaml);
181
4.13k
}
182
183
void MessageUtil::loadFromFile(const std::string& path, Protobuf::Message& message,
184
                               ProtobufMessage::ValidationVisitor& validation_visitor,
185
14.8k
                               Api::Api& api) {
186
14.8k
  auto file_or_error = api.fileSystem().fileReadToEnd(path);
187
14.8k
  THROW_IF_STATUS_NOT_OK(file_or_error, throw);
188
14.8k
  const std::string contents = file_or_error.value();
189
  // If the filename ends with .pb, attempt to parse it as a binary proto.
190
14.8k
  if (absl::EndsWithIgnoreCase(path, FileExtensions::get().ProtoBinary)) {
191
    // Attempt to parse the binary format.
192
2.64k
    if (message.ParseFromString(contents)) {
193
2.64k
      MessageUtil::checkForUnexpectedFields(message, validation_visitor);
194
2.64k
    }
195
2.64k
    return;
196
2.64k
  }
197
198
  // If the filename ends with .pb_text, attempt to parse it as a text proto.
199
12.2k
  if (absl::EndsWithIgnoreCase(path, FileExtensions::get().ProtoText)) {
200
9.52k
    if (Protobuf::TextFormat::ParseFromString(contents, &message)) {
201
9.49k
      return;
202
9.49k
    }
203
37
    throw EnvoyException("Unable to parse file \"" + path + "\" as a text protobuf (type " +
204
37
                         message.GetTypeName() + ")");
205
9.52k
  }
206
2.68k
  if (absl::EndsWithIgnoreCase(path, FileExtensions::get().Yaml) ||
207
2.68k
      absl::EndsWithIgnoreCase(path, FileExtensions::get().Yml)) {
208
0
    loadFromYaml(contents, message, validation_visitor);
209
2.68k
  } else {
210
2.68k
    loadFromJson(contents, message, validation_visitor);
211
2.68k
  }
212
2.68k
}
213
214
std::string MessageUtil::getYamlStringFromMessage(const Protobuf::Message& message,
215
                                                  const bool block_print,
216
766
                                                  const bool always_print_primitive_fields) {
217
218
766
  auto json_or_error = getJsonStringFromMessage(message, false, always_print_primitive_fields);
219
766
  if (!json_or_error.ok()) {
220
0
    throw EnvoyException(json_or_error.status().ToString());
221
0
  }
222
766
  YAML::Node node;
223
766
  TRY_ASSERT_MAIN_THREAD { node = YAML::Load(json_or_error.value()); }
224
766
  END_TRY
225
766
  catch (YAML::ParserException& e) {
226
11
    throw EnvoyException(e.what());
227
11
  }
228
766
  catch (YAML::BadConversion& e) {
229
0
    throw EnvoyException(e.what());
230
0
  }
231
766
  catch (std::exception& e) {
232
    // Catch unknown YAML parsing exceptions.
233
0
    throw EnvoyException(fmt::format("Unexpected YAML exception: {}", +e.what()));
234
0
  }
235
755
  if (block_print) {
236
755
    blockFormat(node);
237
755
  }
238
755
  YAML::Emitter out;
239
755
  out << node;
240
755
  return out.c_str();
241
766
}
242
243
absl::StatusOr<std::string>
244
MessageUtil::getJsonStringFromMessage(const Protobuf::Message& message, const bool pretty_print,
245
505k
                                      const bool always_print_primitive_fields) {
246
505k
  Protobuf::util::JsonPrintOptions json_options;
247
  // By default, proto field names are converted to camelCase when the message is converted to JSON.
248
  // Setting this option makes debugging easier because it keeps field names consistent in JSON
249
  // printouts.
250
505k
  json_options.preserve_proto_field_names = true;
251
505k
  if (pretty_print) {
252
6.06k
    json_options.add_whitespace = true;
253
6.06k
  }
254
  // Primitive types such as int32s and enums will not be serialized if they have the default value.
255
  // This flag disables that behavior.
256
505k
  if (always_print_primitive_fields) {
257
493k
    json_options.always_print_primitive_fields = true;
258
493k
  }
259
505k
  std::string json;
260
505k
  if (auto status = Protobuf::util::MessageToJsonString(message, &json, json_options);
261
505k
      !status.ok()) {
262
478
    return status;
263
478
  }
264
504k
  return json;
265
505k
}
266
267
std::string MessageUtil::getJsonStringFromMessageOrError(const Protobuf::Message& message,
268
                                                         bool pretty_print,
269
492k
                                                         bool always_print_primitive_fields) {
270
492k
  auto json_or_error =
271
492k
      getJsonStringFromMessage(message, pretty_print, always_print_primitive_fields);
272
492k
  return json_or_error.ok() ? std::move(json_or_error).value()
273
492k
                            : fmt::format("Failed to convert protobuf message to JSON string: {}",
274
2
                                          json_or_error.status().ToString());
275
492k
}
276
277
0
void MessageUtil::jsonConvert(const Protobuf::Message& source, Protobuf::Message& dest) {
278
0
  jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest);
279
0
}
280
281
0
void MessageUtil::jsonConvert(const Protobuf::Message& source, ProtobufWkt::Struct& dest) {
282
  // Any proto3 message can be transformed to Struct, so there is no need to check for unknown
283
  // fields. There is one catch; Duration/Timestamp etc. which have non-object canonical JSON
284
  // representations don't work.
285
0
  jsonConvertInternal(source, ProtobufMessage::getNullValidationVisitor(), dest);
286
0
}
287
288
void MessageUtil::jsonConvert(const ProtobufWkt::Struct& source,
289
                              ProtobufMessage::ValidationVisitor& validation_visitor,
290
403
                              Protobuf::Message& dest) {
291
403
  jsonConvertInternal(source, validation_visitor, dest);
292
403
}
293
294
0
bool MessageUtil::jsonConvertValue(const Protobuf::Message& source, ProtobufWkt::Value& dest) {
295
0
  Protobuf::util::JsonPrintOptions json_options;
296
0
  json_options.preserve_proto_field_names = true;
297
0
  std::string json;
298
0
  auto status = Protobuf::util::MessageToJsonString(source, &json, json_options);
299
0
  if (!status.ok()) {
300
0
    return false;
301
0
  }
302
0
  bool has_unknow_field;
303
0
  status = MessageUtil::loadFromJsonNoThrow(json, dest, has_unknow_field);
304
0
  if (status.ok() || has_unknow_field) {
305
0
    return true;
306
0
  }
307
0
  return false;
308
0
}
309
310
54.8k
ProtobufWkt::Value ValueUtil::loadFromYaml(const std::string& yaml) {
311
54.8k
  TRY_ASSERT_MAIN_THREAD { return parseYamlNode(YAML::Load(yaml)); }
312
54.8k
  END_TRY
313
54.8k
  catch (YAML::ParserException& e) {
314
45
    throw EnvoyException(e.what());
315
45
  }
316
54.8k
  catch (YAML::BadConversion& e) {
317
0
    throw EnvoyException(e.what());
318
0
  }
319
54.8k
  catch (std::exception& e) {
320
    // There is a potentially wide space of exceptions thrown by the YAML parser,
321
    // and enumerating them all may be difficult. Envoy doesn't work well with
322
    // unhandled exceptions, so we capture them and record the exception name in
323
    // the Envoy Exception text.
324
0
    throw EnvoyException(fmt::format("Unexpected YAML exception: {}", +e.what()));
325
0
  }
326
54.8k
}
327
328
namespace {
329
330
// This is a modified copy of the UTF8CoerceToStructurallyValid from the protobuf code.
331
// A copy was needed after if was removed from the protobuf.
332
1.38k
std::string utf8CoerceToStructurallyValid(absl::string_view str, const char replace_char) {
333
1.38k
  std::string result(str);
334
1.38k
  auto replace_pos = result.begin();
335
1.38k
  while (!str.empty()) {
336
1.26k
    size_t n_valid_bytes = utf8_range::SpanStructurallyValid(str);
337
1.26k
    if (n_valid_bytes == str.size()) {
338
1.26k
      break;
339
1.26k
    }
340
0
    replace_pos += n_valid_bytes;
341
0
    *replace_pos++ = replace_char; // replace one bad byte
342
0
    str.remove_prefix(n_valid_bytes + 1);
343
0
  }
344
1.38k
  return result;
345
1.38k
}
346
347
} // namespace
348
349
1.38k
std::string MessageUtil::sanitizeUtf8String(absl::string_view input) {
350
  // The choice of '!' is somewhat arbitrary, but we wanted to avoid any character that has
351
  // special semantic meaning in URLs or similar.
352
1.38k
  return utf8CoerceToStructurallyValid(input, '!');
353
1.38k
}
354
355
} // namespace Envoy