1
#include "source/common/json/json_rpc_field_extractor.h"
2

            
3
#include "source/common/common/assert.h"
4

            
5
#include "absl/strings/str_join.h"
6
#include "absl/strings/str_split.h"
7

            
8
namespace Envoy {
9
namespace Json {
10

            
11
JsonRpcFieldExtractor::JsonRpcFieldExtractor(Protobuf::Struct& metadata,
12
                                             const JsonRpcParserConfig& config)
13
56
    : root_metadata_(metadata), config_(config) {
14
  // Start with temp storage
15
56
  context_stack_.push({&temp_storage_, nullptr, ""});
16

            
17
  // Pre-calculate total fields needed for early stop optimization
18
56
  fields_needed_ = config_.getAlwaysExtract().size();
19
56
}
20

            
21
120
JsonRpcFieldExtractor* JsonRpcFieldExtractor::StartObject(absl::string_view name) {
22
120
  checkValidJsonRpc(name);
23
120
  if (array_depth_ > 0 || can_stop_parsing_) {
24
3
    return this;
25
3
  }
26

            
27
117
  depth_++;
28

            
29
117
  if (!name.empty()) {
30
57
    path_stack_.push_back(std::string(name));
31
    // Update cached path
32
57
    if (!current_path_cache_.empty()) {
33
19
      current_path_cache_ += ".";
34
19
    }
35
57
    current_path_cache_ += name;
36
57
  }
37

            
38
117
  if (context_stack_.top().is_list()) {
39
5
    auto* nested = context_stack_.top().list_ptr->add_values()->mutable_struct_value();
40
5
    context_stack_.push({nested, nullptr, ""});
41
112
  } else if (context_stack_.top().struct_ptr) {
42
112
    if (!name.empty()) {
43
57
      auto* nested = (*context_stack_.top().struct_ptr->mutable_fields())[std::string(name)]
44
57
                         .mutable_struct_value();
45
57
      context_stack_.push({nested, nullptr, std::string(name)});
46
73
    } else if (depth_ == 1) {
47
55
      context_stack_.push({&temp_storage_, nullptr, ""});
48
55
    }
49
112
  }
50
117
  return this;
51
120
}
52

            
53
111
JsonRpcFieldExtractor* JsonRpcFieldExtractor::EndObject() {
54
111
  if (array_depth_ > 0) {
55
1
    return this;
56
1
  }
57
110
  if (depth_ > 0) {
58
    // Before updating path, mark object path as collected for early-stop optimization.
59
    // This enables extraction rules targeting object paths (e.g., "params.ref") to work
60
    // with early termination, since objects themselves are not rendered as primitives.
61
108
    if (!current_path_cache_.empty()) {
62
56
      if (collected_fields_.insert(current_path_cache_).second) {
63
55
        fields_collected_count_++;
64
55
      }
65
56
    }
66

            
67
108
    depth_--;
68
108
    if (!path_stack_.empty()) {
69
      // Update cached path before removing from stack
70
56
      size_t last_dot = current_path_cache_.rfind('.');
71
56
      if (last_dot != std::string::npos) {
72
21
        current_path_cache_.resize(last_dot);
73
35
      } else {
74
35
        current_path_cache_.clear();
75
35
      }
76
56
      path_stack_.pop_back();
77
56
    }
78
108
    if (context_stack_.size() > 1) {
79
108
      context_stack_.pop();
80
108
    }
81
108
  }
82

            
83
  // When we finish the root object, do selective extraction
84
110
  if (depth_ == 0 && !can_stop_parsing_) {
85
30
    finalizeExtraction();
86
30
  }
87

            
88
110
  return this;
89
111
}
90

            
91
12
JsonRpcFieldExtractor* JsonRpcFieldExtractor::StartList(absl::string_view name) {
92
12
  if (!lists_supported()) {
93
1
    array_depth_++;
94
1
    return this;
95
1
  }
96

            
97
11
  checkValidJsonRpc(name);
98

            
99
11
  if (can_stop_parsing_) {
100
    return this;
101
  }
102

            
103
11
  if (!name.empty()) {
104
10
    path_stack_.push_back(std::string(name));
105
10
    if (!current_path_cache_.empty()) {
106
10
      current_path_cache_ += ".";
107
10
    }
108
10
    current_path_cache_ += name;
109
10
  }
110

            
111
11
  if (context_stack_.top().is_list()) {
112
1
    auto* list = context_stack_.top().list_ptr->add_values()->mutable_list_value();
113
1
    context_stack_.push({nullptr, list, ""});
114
10
  } else if (context_stack_.top().struct_ptr) {
115
10
    auto* list = (*context_stack_.top().struct_ptr->mutable_fields())[std::string(name)]
116
10
                     .mutable_list_value();
117
10
    context_stack_.push({nullptr, list, std::string(name)});
118
10
  }
119

            
120
11
  return this;
121
11
}
122

            
123
12
JsonRpcFieldExtractor* JsonRpcFieldExtractor::EndList() {
124
12
  if (!lists_supported()) {
125
1
    if (array_depth_ > 0) {
126
1
      array_depth_--;
127
1
    }
128
1
    return this;
129
1
  }
130
11
  if (!path_stack_.empty() && context_stack_.top().is_list()) {
131
10
    size_t last_dot = current_path_cache_.rfind('.');
132
10
    if (last_dot != std::string::npos) {
133
8
      current_path_cache_.resize(last_dot);
134
8
    } else {
135
2
      current_path_cache_.clear();
136
2
    }
137
10
    path_stack_.pop_back();
138
10
  }
139
11
  if (context_stack_.size() > 1) {
140
11
    context_stack_.pop();
141
11
  }
142
11
  return this;
143
12
}
144

            
145
249
std::string JsonRpcFieldExtractor::buildFullPath(absl::string_view name) const {
146
249
  std::string full_path;
147
249
  if (!name.empty()) {
148
238
    if (!current_path_cache_.empty()) {
149
102
      full_path.reserve(current_path_cache_.size() + 1 + name.size());
150
102
      full_path = current_path_cache_;
151
102
      full_path += ".";
152
102
      full_path += name;
153
156
    } else {
154
136
      full_path = std::string(name);
155
136
    }
156
238
  } else {
157
11
    full_path = current_path_cache_;
158
11
  }
159
249
  return full_path;
160
249
}
161

            
162
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderString(absl::string_view name,
163
212
                                                           absl::string_view value) {
164
212
  checkValidJsonRpc(name, value);
165
212
  if (array_depth_ > 0 || can_stop_parsing_) {
166
6
    return this;
167
6
  }
168
206
  std::string full_path = buildFullPath(name);
169
206
  ENVOY_LOG_MISC(debug, "render string name {} path {}, value {}", name, full_path, value);
170

            
171
  // Store in temp storage
172
206
  Protobuf::Value proto_value;
173
206
  proto_value.set_string_value(std::string(value));
174
206
  storeField(full_path, proto_value);
175

            
176
  // Check for early stop
177
206
  checkEarlyStop();
178
206
  return this;
179
212
}
180

            
181
8
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderBool(absl::string_view name, bool value) {
182
8
  checkValidJsonRpc(name);
183
8
  if (array_depth_ > 0) {
184
1
    return this;
185
1
  }
186
7
  if (can_stop_parsing_) {
187
1
    return this;
188
1
  }
189

            
190
6
  std::string full_path = buildFullPath(name);
191

            
192
6
  Protobuf::Value proto_value;
193
6
  proto_value.set_bool_value(value);
194
6
  storeField(full_path, proto_value);
195

            
196
6
  checkEarlyStop();
197
6
  return this;
198
7
}
199

            
200
14
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderInt32(absl::string_view name, int32_t value) {
201
14
  return RenderInt64(name, static_cast<int64_t>(value));
202
14
}
203

            
204
1
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderUint32(absl::string_view name, uint32_t value) {
205
1
  return RenderUint64(name, static_cast<uint64_t>(value));
206
1
}
207

            
208
17
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderInt64(absl::string_view name, int64_t value) {
209
17
  checkValidJsonRpc(name);
210
17
  if (array_depth_ > 0) {
211
1
    return this;
212
1
  }
213
16
  if (can_stop_parsing_) {
214
1
    return this;
215
1
  }
216

            
217
15
  std::string full_path = buildFullPath(name);
218

            
219
15
  Protobuf::Value proto_value;
220
15
  proto_value.set_number_value(static_cast<double>(value));
221
15
  storeField(full_path, proto_value);
222

            
223
15
  checkEarlyStop();
224
15
  return this;
225
16
}
226

            
227
19
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderUint64(absl::string_view name, uint64_t value) {
228
19
  checkValidJsonRpc(name);
229
19
  if (array_depth_ > 0) {
230
1
    return this;
231
1
  }
232
18
  if (can_stop_parsing_) {
233
1
    return this;
234
1
  }
235

            
236
17
  std::string full_path = buildFullPath(name);
237

            
238
17
  Protobuf::Value proto_value;
239
17
  proto_value.set_number_value(static_cast<double>(value));
240
17
  storeField(full_path, proto_value);
241

            
242
17
  checkEarlyStop();
243
17
  return this;
244
18
}
245

            
246
4
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderDouble(absl::string_view name, double value) {
247
4
  checkValidJsonRpc(name);
248

            
249
4
  if (array_depth_ > 0) {
250
1
    return this;
251
1
  }
252
3
  if (can_stop_parsing_) {
253
1
    return this;
254
1
  }
255

            
256
2
  std::string full_path = buildFullPath(name);
257

            
258
2
  Protobuf::Value proto_value;
259
2
  proto_value.set_number_value(value);
260
2
  storeField(full_path, proto_value);
261

            
262
2
  checkEarlyStop();
263
2
  return this;
264
3
}
265

            
266
1
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderFloat(absl::string_view name, float value) {
267
1
  return RenderDouble(name, static_cast<double>(value));
268
1
}
269

            
270
5
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderNull(absl::string_view name) {
271
5
  checkValidJsonRpc(name);
272
5
  if (array_depth_ > 0) {
273
1
    return this;
274
1
  }
275
4
  if (can_stop_parsing_) {
276
1
    return this;
277
1
  }
278

            
279
3
  std::string full_path = buildFullPath(name);
280

            
281
3
  Protobuf::Value proto_value;
282
3
  proto_value.set_null_value(Protobuf::NULL_VALUE);
283
3
  storeField(full_path, proto_value);
284

            
285
3
  checkEarlyStop();
286
3
  return this;
287
4
}
288

            
289
JsonRpcFieldExtractor* JsonRpcFieldExtractor::RenderBytes(absl::string_view name,
290
1
                                                          absl::string_view value) {
291
1
  return RenderString(name, value);
292
1
}
293

            
294
249
void JsonRpcFieldExtractor::storeField(const std::string& path, const Protobuf::Value& value) {
295
249
  if (context_stack_.empty()) {
296
    return;
297
  }
298
  // Store in nested structure in temp storage
299
249
  if (context_stack_.top().is_list()) {
300
11
    *context_stack_.top().list_ptr->add_values() = value;
301
238
  } else if (context_stack_.top().struct_ptr) {
302
238
    auto* current = context_stack_.top().struct_ptr;
303
238
    size_t last_dot = path.rfind('.');
304
238
    std::string field_name = (last_dot != std::string::npos) ? path.substr(last_dot + 1) : path;
305
238
    if (!field_name.empty()) {
306
238
      (*current->mutable_fields())[field_name] = value;
307
238
    }
308
238
  }
309

            
310
  // Track new fields for early stop optimization
311
249
  if (collected_fields_.insert(path).second) {
312
245
    fields_collected_count_++;
313
245
  }
314
249
}
315

            
316
249
void JsonRpcFieldExtractor::checkEarlyStop() {
317
  // Can't stop if we haven't seen the method yet
318
249
  if (!has_jsonrpc_ || !has_method_) {
319
82
    return;
320
82
  }
321

            
322
  // Update fields_needed_ now that we know the method
323
167
  if (!fields_needed_updated_) {
324
39
    const auto& required_fields = config_.getFieldsForMethod(method_);
325
39
    fields_needed_ += required_fields.size();
326
    // Notifications don't have 'id' field, so reduce the expected count
327
39
    if (is_notification_) {
328
1
      fields_needed_--;
329
1
    }
330
39
    fields_needed_updated_ = true;
331
39
  }
332

            
333
  // Fast path: check if we have collected enough fields
334
167
  if (fields_collected_count_ < fields_needed_) {
335
135
    return; // Still missing fields
336
135
  }
337

            
338
  // Verify we actually have all required fields (not just the count)
339
  // Notifications don't have an 'id' field per JSON-RPC spec, so skip it for them
340
94
  for (const auto& field : config_.getAlwaysExtract()) {
341
94
    if (is_notification_ && field == "id") {
342
1
      continue;
343
1
    }
344
93
    if (collected_fields_.count(field) == 0) {
345
2
      return;
346
2
    }
347
93
  }
348

            
349
30
  const auto& required_fields = config_.getFieldsForMethod(method_);
350
95
  for (const auto& field : required_fields) {
351
93
    if (collected_fields_.count(field.path) == 0) {
352
13
      return;
353
13
    }
354
93
  }
355

            
356
17
  can_stop_parsing_ = true;
357
17
  ENVOY_LOG(debug, "early stop: Have all fields for method {}", method_);
358
17
}
359

            
360
90
void JsonRpcFieldExtractor::finalizeExtraction() {
361
  // Valid JSON-RPC message must have jsonrpc and either:
362
  // - method (for requests)
363
  // - result or error (for responses)
364
90
  if (!has_jsonrpc_ || (!has_method_ && !has_result_ && !has_error_)) {
365
17
    ENVOY_LOG(debug, "not a valid {} message", protocolName());
366
17
    is_valid_jsonrpc_ = false;
367
17
    return;
368
17
  }
369
73
  is_valid_jsonrpc_ = true;
370

            
371
73
  if (has_method_) {
372
    // Copy selected fields from temp to final
373
65
    copySelectedFields();
374
    // Validate required fields
375
65
    validateRequiredFields();
376
65
  } else if (has_result_ || has_error_) {
377
    // response: copy jsonrpc, id, result and/or error
378
8
    copyFieldByPath("jsonrpc");
379
8
    copyFieldByPath("id");
380
8
    if (has_result_) {
381
6
      copyFieldByPath("result");
382
6
    }
383
8
    if (has_error_) {
384
4
      copyFieldByPath("error");
385
4
    }
386
8
  }
387
73
}
388

            
389
65
void JsonRpcFieldExtractor::copySelectedFields() {
390
195
  for (const auto& field : config_.getAlwaysExtract()) {
391
195
    copyFieldByPath(field);
392
195
  }
393

            
394
  // Copy method-specific fields
395
65
  const auto& fields = config_.getFieldsForMethod(method_);
396
334
  for (const auto& field : fields) {
397
334
    copyFieldByPath(field.path);
398
334
  }
399
65
}
400

            
401
555
void JsonRpcFieldExtractor::copyFieldByPath(const std::string& path) {
402
555
  std::vector<std::string> segments = absl::StrSplit(path, '.');
403

            
404
  // Navigate source to find value
405
555
  const Protobuf::Struct* current_source = &temp_storage_;
406
555
  const Protobuf::Value* value = nullptr;
407

            
408
1319
  for (size_t i = 0; i < segments.size(); ++i) {
409
948
    auto it = current_source->fields().find(segments[i]);
410
948
    if (it == current_source->fields().end()) {
411
184
      return; // Field doesn't exist
412
184
    }
413

            
414
764
    if (i == segments.size() - 1) {
415
371
      value = &it->second;
416
440
    } else {
417
393
      if (it->second.has_list_value()) {
418
        // if path segment is list but we have more segments, not supported for copy.
419
        // TODO(tyxia): support list indexing in path.
420
        return;
421
393
      } else if (!it->second.has_struct_value()) {
422
        return;
423
      }
424
393
      current_source = &it->second.struct_value();
425
393
    }
426
764
  }
427

            
428
371
  if (!value) {
429
    return;
430
  }
431

            
432
  // Navigate dest and create nested structure
433
371
  Protobuf::Struct* current_dest = &root_metadata_;
434

            
435
580
  for (size_t i = 0; i < segments.size() - 1; ++i) {
436
209
    auto& fields = *current_dest->mutable_fields();
437
209
    auto it = fields.find(segments[i]);
438

            
439
209
    if (it == fields.end() || !it->second.has_struct_value()) {
440
33
      current_dest = fields[segments[i]].mutable_struct_value();
441
177
    } else {
442
176
      current_dest = it->second.mutable_struct_value();
443
176
    }
444
209
  }
445

            
446
  // Copy the final value
447
371
  (*current_dest->mutable_fields())[segments.back()] = *value;
448
371
  extracted_fields_.insert(path);
449
371
}
450

            
451
65
void JsonRpcFieldExtractor::validateRequiredFields() {
452
65
  const auto& fields = config_.getFieldsForMethod(method_);
453
334
  for (const auto& field : fields) {
454
334
    if (extracted_fields_.count(field.path) == 0) {
455
174
      missing_required_fields_.push_back(field.path);
456
174
      ENVOY_LOG(debug, "missing required field for {}: {}", method_, field.path);
457
174
    }
458
334
  }
459
65
}
460

            
461
void JsonRpcFieldExtractor::checkValidJsonRpc(absl::string_view name,
462
396
                                              absl::optional<absl::string_view> value) {
463
396
  if (depth_ == 1) {
464
180
    if (name == jsonRpcField()) {
465
48
      if (value.has_value() && value.value() == jsonRpcVersion()) {
466
48
        has_jsonrpc_ = true;
467
48
      } else {
468
        // Early stop if it is not a valid JSON-RPC version.
469
        can_stop_parsing_ = true;
470
      }
471
132
    } else if (name == methodField()) {
472
43
      if (value.has_value()) {
473
43
        has_method_ = true;
474
43
        method_ = std::string(value.value());
475
43
        is_notification_ = isNotification(method_);
476
43
      } else {
477
        // Early stop if method value is not a valid JSON-RPC method.
478
        can_stop_parsing_ = true;
479
      }
480
      // JSON-RPC 2.0 response.
481
90
    } else if (name == "result") {
482
3
      has_result_ = true;
483
86
    } else if (name == "error") {
484
2
      has_error_ = true;
485
2
    }
486

            
487
180
    if (has_jsonrpc_ && (has_method_ || has_result_ || has_error_)) {
488
114
      is_valid_jsonrpc_ = true;
489
114
    }
490
180
  }
491
396
}
492

            
493
} // namespace Json
494
} // namespace Envoy