neutralts/utils.rs
1
2use serde_json::Value;
3use crate::constants::*;
4
5/// Merges two JSON schemas represented as `serde_json::Value`.
6///
7/// This function performs a recursive merge between two JSON objects.
8/// If an object has common keys, the values are merged recursively.
9/// If the value is not an object, it is directly overwritten.
10///
11/// # Arguments
12///
13/// * `a` - A mutable reference to the first JSON object (`serde_json::Value::Object`).
14/// * `b` - A reference to the second JSON object (`serde_json::Value::Object`) that will be merged with the first.
15///
16/// # Example
17///
18/// ```text
19/// use serde_json::{json, Value};
20///
21/// let mut schema1 = json!({
22/// "name": "John",
23/// "age": 30,
24/// });
25///
26/// let schema2 = json!({
27/// "age": 31,
28/// "city": "New York"
29/// });
30///
31/// merge_schema(&mut schema1, &schema2);
32/// assert_eq!(schema1, json!({
33/// "name": "John",
34/// "age": 31,
35/// "city": "New York"
36/// }));
37/// ```
38pub fn merge_schema(a: &mut Value, b: &Value) {
39 match (a, b) {
40 (Value::Object(a_map), Value::Object(b_map)) => {
41 for (k, v) in b_map {
42 if let Some(va) = a_map.get_mut(k) {
43 merge_schema(va, v);
44 } else {
45 a_map.insert(k.clone(), v.clone());
46 }
47 }
48 }
49 (a, b) => *a = b.clone(),
50 }
51}
52
53/// Merge schema and update some keys
54///
55/// This is a thin wrapper around `merge_schema` that additionally:
56/// 1. Copies the value of the header key `requested-with-ajax` (all lower-case) into the
57/// variants `Requested-With-Ajax` (Pascal-Case) and `REQUESTED-WITH-AJAX` (upper-case),
58/// or vice-versa, depending on which variant is present in the incoming schema.
59/// 2. Overwrites the top-level `version` field with the compile-time constant `VERSION`.
60///
61/// The three header variants are created so that downstream code can read the header
62/// regardless of the casing rules enforced by the environment (HTTP servers, proxies, etc.).
63///
64/// # Arguments
65/// * `a` – the target `Value` (must be an `Object`) that will receive the merge result.
66/// * `b` – the source `Value` (must be an `Object`) whose contents are merged into `a`.
67///
68pub fn update_schema(a: &mut Value, b: &Value) {
69 merge_schema(a, b);
70
71 // Different environments may ignore or add capitalization in headers
72 let headers = &b["data"]["CONTEXT"]["HEADERS"];
73 if headers.get("requested-with-ajax").is_some() {
74 a["data"]["CONTEXT"]["HEADERS"]["Requested-With-Ajax"] = b["data"]["CONTEXT"]["HEADERS"]["requested-with-ajax"].clone();
75 a["data"]["CONTEXT"]["HEADERS"]["REQUESTED-WITH-AJAX"] = b["data"]["CONTEXT"]["HEADERS"]["requested-with-ajax"].clone();
76 } else if headers.get("Requested-With-Ajax").is_some() {
77 a["data"]["CONTEXT"]["HEADERS"]["requested-with-ajax"] = b["data"]["CONTEXT"]["HEADERS"]["Requested-With-Ajax"].clone();
78 a["data"]["CONTEXT"]["HEADERS"]["REQUESTED-WITH-AJAX"] = b["data"]["CONTEXT"]["HEADERS"]["Requested-With-Ajax"].clone();
79 } else if headers.get("REQUESTED-WITH-AJAX").is_some() {
80 a["data"]["CONTEXT"]["HEADERS"]["requested-with-ajax"] = b["data"]["CONTEXT"]["HEADERS"]["REQUESTED-WITH-AJAX"].clone();
81 a["data"]["CONTEXT"]["HEADERS"]["Requested-With-Ajax"] = b["data"]["CONTEXT"]["HEADERS"]["REQUESTED-WITH-AJAX"].clone();
82 }
83
84 // Update version
85 a["version"] = VERSION.to_string().to_string().into();
86}
87
88/// Extract same level blocks positions.
89///
90/// ```text
91///
92/// .-----> .-----> {:code:
93/// | | {:code: ... :}
94/// | | {:code: ... :}
95/// | | {:code: ... :}
96/// Level block --> | ·-----> :}
97/// | -----> {:code: ... :}
98/// | .-----> {:code:
99/// | | {:code: ... :}
100/// ·-----> ·-----> :}
101///
102/// # Arguments
103///
104/// * `raw_source` - A string slice containing the template source text.
105///
106/// # Returns
107///
108/// * `Ok(Vec<(usize, usize)>)`: A vector of tuples representing the start and end positions of each extracted block.
109/// * `Err(usize)`: An error position if there are unmatched closing tags or other issues
110/// ```
111pub fn extract_blocks(raw_source: &str) -> Result<Vec<(usize, usize)>, usize> {
112 let mut blocks = Vec::new();
113 let bytes = raw_source.as_bytes();
114 let mut curr_pos: usize = 0;
115 let mut open_pos: usize;
116 let mut nested = 0;
117 let mut nested_comment = 0;
118 let len_open = BIF_OPEN_B.len();
119 let len_close = BIF_CLOSE_B.len();
120 let len_src = bytes.len();
121
122 while let Some(pos) = find_bytes(bytes, BIF_OPEN_B, curr_pos) {
123 curr_pos = pos + len_open;
124 open_pos = pos;
125
126 // It is important to extract the comments first because they may have bif commented,
127 // we avoid that they are detected as valid and other errors.
128 if bytes[curr_pos] == BIF_COMMENT_B {
129 while let Some(pos) = find_bytes(bytes, BIF_DELIM_B, curr_pos) {
130 curr_pos = pos;
131
132 if curr_pos >= len_src {
133 break;
134 }
135
136 if bytes[curr_pos - 1] == BIF_OPEN0 && bytes[curr_pos + 1] == BIF_COMMENT_B {
137 nested_comment += 1;
138 curr_pos += 1;
139 continue;
140 }
141 if nested_comment > 0 && bytes[curr_pos + 1] == BIF_CLOSE1 && bytes[curr_pos - 1] == BIF_COMMENT_B {
142 nested_comment -= 1;
143 curr_pos += 1;
144 continue;
145 }
146 if bytes[curr_pos + 1] == BIF_CLOSE1 && bytes[curr_pos - 1] == BIF_COMMENT_B {
147 curr_pos += len_close;
148 blocks.push((open_pos, curr_pos));
149 break;
150 } else {
151 curr_pos += 1;
152 }
153 }
154
155 continue;
156 }
157
158 while let Some(pos) = find_bytes(bytes, BIF_DELIM_B, curr_pos) {
159 curr_pos = pos;
160
161 if curr_pos >= len_src {
162 break;
163 }
164
165 if bytes[curr_pos - 1] == BIF_OPEN0 {
166 nested += 1;
167 curr_pos += 1;
168 continue;
169 }
170 if nested > 0 && bytes[curr_pos + 1] == BIF_CLOSE1 {
171 nested -= 1;
172 curr_pos += 1;
173 continue;
174 }
175 if bytes[curr_pos + 1] == BIF_CLOSE1 {
176 curr_pos += len_close;
177 blocks.push((open_pos, curr_pos));
178 break;
179 } else {
180 curr_pos += 1;
181 }
182 }
183 }
184
185 // Search BIF_CLOSE in the blocks that are not bif, given that we start looking
186 // for BIF_OPEN all these keys are found, if anything is left is BIF_CLOSE
187 let mut prev_end = 0;
188 for (start, end) in &blocks {
189 if let Some(error_pos) = find_bytes(&bytes[prev_end..*start], BIF_CLOSE_B, 0) {
190 return Err(error_pos + prev_end);
191 }
192 prev_end = *end;
193 }
194
195 let rest = if curr_pos == 0 { 0 } else { curr_pos - 1 };
196 if let Some(error_pos) = find_bytes(bytes, BIF_CLOSE_B, rest) {
197 return Err(error_pos);
198 }
199
200 Ok(blocks)
201}
202
203fn find_bytes(bytes: &[u8], substring: &[u8], start_pos: usize) -> Option<usize> {
204 let bytes_len = bytes.len();
205 let subs_len = substring.len();
206
207 if start_pos >= bytes_len || substring.is_empty() || start_pos + subs_len > bytes_len {
208 return None;
209 }
210
211 (start_pos..=bytes_len.saturating_sub(subs_len)).find(|&i| &bytes[i..i + subs_len] == substring)
212}
213
214/// Removes a prefix and suffix from a string slice.
215///
216/// # Arguments
217///
218/// * `str`: The input string slice.
219/// * `prefix`: The prefix to remove.
220/// * `suffix`: The suffix to remove.
221///
222/// # Returns
223///
224/// * A new string slice with the prefix and suffix removed, or the original string if not found.
225pub fn strip_prefix_suffix<'a>(str: &'a str, prefix: &'a str, suffix: &'a str) -> &'a str {
226 let start = match str.strip_prefix(prefix) {
227 Some(striped) => striped,
228 None => return str,
229 };
230 let end = match start.strip_suffix(suffix) {
231 Some(striped) => striped,
232 None => return str,
233 };
234
235 end
236}
237
238/// Retrieves a value from a JSON schema using a specified key.
239///
240/// # Arguments
241///
242/// * `schema`: A reference to the JSON schema as a `Value`.
243/// * `key`: The key used to retrieve the value from the schema.
244///
245/// # Returns
246///
247/// * A `String` containing the retrieved value, or an empty string if the key is not found.
248pub fn get_from_key(schema: &Value, key: &str) -> String {
249 let tmp: String = format!("{}{}", "/", key);
250 let k = tmp.replace(BIF_ARRAY, "/");
251 let mut result = "";
252 let num: String;
253
254 if let Some(v) = schema.pointer(&k) {
255 match v {
256 Value::Null => result = "",
257 Value::Bool(_b) => result = "",
258 Value::Number(n) => {
259 num = n.to_string();
260 result = num.as_str();
261 }
262 Value::String(s) => result = s,
263 _ => result = "",
264 }
265 }
266
267 result.to_string()
268}
269
270/// Checks if the value associated with a key in the schema is considered empty.
271///
272/// # Arguments
273///
274/// * `schema`: A reference to the JSON schema as a `Value`.
275/// * `key`: The key used to check the value in the schema.
276///
277/// # Returns
278///
279/// * `true` if the value is considered empty, otherwise `false`.
280pub fn is_empty_key(schema: &Value, key: &str) -> bool {
281 let tmp: String = format!("{}{}", "/", key);
282 let k = tmp.replace(BIF_ARRAY, "/");
283
284 if let Some(value) = schema.pointer(&k) {
285 match value {
286 Value::Object(map) => map.is_empty(),
287 Value::Array(arr) => arr.is_empty(),
288 Value::String(s) => s.is_empty(),
289 Value::Null => true,
290 Value::Number(_) => false,
291 Value::Bool(_) => false,
292 }
293 } else {
294 true
295 }
296}
297
298/// Checks if the value associated with a key in the schema is considered a boolean true.
299///
300/// # Arguments
301///
302/// * `schema`: A reference to the JSON schema as a `Value`.
303/// * `key`: The key used to check the value in the schema.
304///
305/// # Returns
306///
307/// * `true` if the value is considered a boolean true, otherwise `false`.
308pub fn is_bool_key(schema: &Value, key: &str) -> bool {
309 let tmp: String = format!("{}{}", "/", key);
310 let k = tmp.replace(BIF_ARRAY, "/");
311
312 if let Some(value) = schema.pointer(&k) {
313 match value {
314 Value::Object(obj) => !obj.is_empty(),
315 Value::Array(arr) => !arr.is_empty(),
316 Value::String(s) if s.is_empty() || s == "false" => false,
317 Value::String(s) => s.parse::<f64>().ok().map_or(true, |n| n > 0.0),
318 Value::Null => false,
319 Value::Number(n) => n.as_f64().map_or(false, |f| f > 0.0),
320 Value::Bool(b) => *b,
321 }
322 } else {
323 false
324 }
325}
326
327/// Checks if the value associated with a key in the schema is considered an array.
328///
329/// # Arguments
330///
331/// * `schema`: A reference to the JSON schema as a `Value`.
332/// * `key`: The key used to check the value in the schema.
333///
334/// # Returns
335///
336/// * `true` if the value is an array, otherwise `false`.
337pub fn is_array_key(schema: &Value, key: &str) -> bool {
338 let tmp: String = format!("{}{}", "/", key);
339 let k = tmp.replace(BIF_ARRAY, "/");
340
341 if let Some(value) = schema.pointer(&k) {
342 match value {
343 Value::Object(_) => true,
344 Value::Array(_) => true,
345 _ => false,
346 }
347 } else {
348 false
349 }
350}
351
352/// Checks if the value associated with a key in the schema is considered defined.
353///
354/// # Arguments
355///
356/// * `schema`: A reference to the JSON schema as a `Value`.
357/// * `key`: The key used to check the value in the schema.
358///
359/// # Returns
360///
361/// * `true` if the value is defined and not null, otherwise `false`.
362pub fn is_defined_key(schema: &Value, key: &str) -> bool {
363 let tmp: String = format!("{}{}", "/", key);
364 let k = tmp.replace(BIF_ARRAY, "/");
365
366 match schema.pointer(&k) {
367 Some(value) => !value.is_null(),
368 None => false,
369 }
370}
371
372/// Finds the position of the first occurrence of BIF_CODE_B in the source string,
373/// but only when it is not inside any nested brackets.
374///
375/// ```text
376/// .------------------------------> params
377/// | .----------------------> this
378/// | |
379/// | | .----> code
380/// | | |
381/// v v v
382/// ------------ -- ------------------------------
383/// {:!snippet; snippet_name >> <div>... {:* ... *:} ...</div> :}
384pub fn get_code_position(src: &str) -> Option<usize> {
385 let mut level = 0;
386 src.as_bytes()
387 .windows(2)
388 .enumerate()
389 .find(|&(_, window)| match window {
390 x if x == BIF_OPEN_B => {
391 level += 1;
392 false
393 }
394 x if x == BIF_CLOSE_B => {
395 level -= 1;
396 false
397 }
398 x if x == BIF_CODE_B && level == 0 => true,
399 _ => false,
400 })
401 .map(|(i, _)| i)
402}
403
404/// Removes comments from the template source.
405pub fn remove_comments(raw_source: &str) -> String {
406 let mut result = String::new();
407 let mut blocks = Vec::new();
408 let bytes = raw_source.as_bytes();
409 let mut curr_pos: usize = 0;
410 let mut open_pos: usize;
411 let mut nested_comment = 0;
412 let len_open = BIF_OPEN_B.len();
413 let len_close = BIF_CLOSE_B.len();
414 let len_src = bytes.len();
415
416 while let Some(pos) = find_bytes(bytes, BIF_COMMENT_OPEN_B, curr_pos) {
417 curr_pos = pos + len_open;
418 open_pos = pos;
419
420 while let Some(pos) = find_bytes(bytes, BIF_DELIM_B, curr_pos) {
421 curr_pos = pos;
422
423 if curr_pos >= len_src {
424 break;
425 }
426
427 if bytes[curr_pos - 1] == BIF_OPEN0 && bytes[curr_pos + 1] == BIF_COMMENT_B {
428 nested_comment += 1;
429 curr_pos += 1;
430 continue;
431 }
432 if nested_comment > 0 && bytes[curr_pos + 1] == BIF_CLOSE1 && bytes[curr_pos - 1] == BIF_COMMENT_B {
433 nested_comment -= 1;
434 curr_pos += 1;
435 continue;
436 }
437 if bytes[curr_pos + 1] == BIF_CLOSE1 && bytes[curr_pos - 1] == BIF_COMMENT_B {
438 curr_pos += len_close;
439 blocks.push((open_pos, curr_pos));
440 break;
441 } else {
442 curr_pos += 1;
443 }
444 }
445
446 }
447
448 let mut prev_end = 0;
449 for (start, end) in &blocks {
450 result.push_str(&raw_source[prev_end..*start]);
451 prev_end = *end;
452 }
453 result.push_str(&raw_source[curr_pos..]);
454
455 result
456}
457
458/// Performs a wildcard matching between a text and a pattern.
459///
460/// Used in bif "allow" and "declare"
461///
462/// # Arguments
463///
464/// * `text`: The text to match against the pattern.
465/// * `pattern`: The pattern containing wildcards ('.', '?', '*', '~').
466///
467/// # Returns
468///
469/// * `true` if the text matches the pattern, otherwise `false`.
470pub fn wildcard_match(text: &str, pattern: &str) -> bool {
471 let text_chars: Vec<char> = text.chars().collect();
472 let pattern_chars: Vec<char> = pattern.chars().collect();
473
474 fn match_recursive(text: &[char], pattern: &[char]) -> bool {
475 if pattern.is_empty() {
476 return text.is_empty();
477 }
478
479 let first_char = *pattern.first().unwrap();
480 let rest_pattern = &pattern[1..];
481
482 match first_char {
483 '\\' => {
484 if rest_pattern.is_empty() || text.is_empty() {
485 return false;
486 }
487 let escaped_char = rest_pattern.first().unwrap();
488 match_recursive(&text[1..], &rest_pattern[1..]) && *text.first().unwrap() == *escaped_char
489 }
490 '.' => {
491 match_recursive(text, rest_pattern) || (!text.is_empty() && match_recursive(&text[1..], rest_pattern))
492 }
493 '?' => {
494 !text.is_empty() && match_recursive(&text[1..], rest_pattern)
495 }
496 '*' => {
497 match_recursive(text, rest_pattern) || (!text.is_empty() && match_recursive(&text[1..], pattern))
498 }
499 '~' => {
500 text.is_empty()
501 },
502 _ => {
503 if text.is_empty() || first_char != *text.first().unwrap() {
504 false
505 } else {
506 match_recursive(&text[1..], rest_pattern)
507 }
508 }
509 }
510 }
511
512 match_recursive(&text_chars, &pattern_chars)
513}
514
515
516/// Finds the position of a tag in the text.
517///
518/// It is used in the bif "moveto".
519///
520/// # Arguments
521///
522/// * `text`: The text to search for the tag.
523/// * `tag`: The tag to find.
524///
525/// # Returns
526///
527/// * `Some(usize)`: The position of the end of the tag, or None if the tag is not found.
528pub fn find_tag_position(text: &str, tag: &str) -> Option<usize> {
529 if let Some(start_pos) = text.find(tag) {
530 if !tag.starts_with("</") {
531 if let Some(end_tag_pos) = text[start_pos..].find('>') {
532 return Some(start_pos + end_tag_pos + 1);
533 }
534 } else {
535 return Some(start_pos);
536 }
537 }
538
539 None
540}
541
542/// Escapes special characters in a given input string.
543///
544/// This function replaces specific ASCII characters with their corresponding HTML entities.
545/// It is designed to handle both general HTML escaping and optional escaping of curly braces (`{` and `}`).
546///
547/// # Arguments
548///
549/// * `input` - The input string to escape.
550/// * `escape_braces` - A boolean flag indicating whether to escape curly braces (`{` and `}`).
551/// - If `true`, curly braces are escaped as `{` and `}`.
552/// - If `false`, curly braces are left unchanged.
553///
554/// # Escaped Characters
555///
556/// The following characters are always escaped:
557/// - `&` → `&`
558/// - `<` → `<`
559/// - `>` → `>`
560/// - `"` → `"`
561/// - `'` → `'`
562/// - `/` → `/`
563///
564/// If `escape_braces` is `true`, the following characters are also escaped:
565/// - `{` → `{`
566/// - `}` → `}`
567///
568/// # Examples
569///
570/// Basic usage without escaping curly braces:
571/// ```text
572/// let input = r#"Hello, <world> & "friends"! {example}"#;
573/// let escaped = escape_chars(input, false);
574/// assert_eq!(escaped, r#"Hello, <world> & "friends"! {example}"#);
575/// ```
576///
577/// Escaping curly braces:
578/// ```text
579/// let input = r#"Hello, <world> & "friends"! {example}"#;
580/// let escaped = escape_chars(input, true);
581/// assert_eq!(escaped, r#"Hello, <world> & "friends"! {example}"#);
582/// ```
583pub fn escape_chars(input: &str, escape_braces: bool) -> String {
584 let mut result = String::with_capacity(input.len() * 2);
585
586 for c in input.chars() {
587 if c.is_ascii() {
588 match c {
589 '&' => result.push_str("&"),
590 '<' => result.push_str("<"),
591 '>' => result.push_str(">"),
592 '"' => result.push_str("""),
593 '\'' => result.push_str("'"),
594 '/' => result.push_str("/"),
595 '{' if escape_braces => result.push_str("{"),
596 '}' if escape_braces => result.push_str("}"),
597 _ => result.push(c),
598 }
599 } else {
600 result.push(c);
601 }
602 }
603 result
604}
605
606/// Unescapes HTML entities in a given input string.
607///
608/// This function is designed specifically to reverse the escaping performed by `escape_chars`.
609/// It is not intended to be a general-purpose HTML decoder. It replaces the following HTML
610/// entities with their corresponding characters:
611/// - `&` → `&`
612/// - `<` → `<`
613/// - `>` → `>`
614/// - `"` → `"`
615/// - `'` → `'`
616/// - `/` → `/`
617///
618/// If `escape_braces` is `true`, it also replaces:
619/// - `{` → `{`
620/// - `}` → `}`
621///
622/// If an unrecognized entity is encountered, it is left unchanged in the output.
623///
624/// # Arguments
625///
626/// * `input` - The input string containing HTML entities to unescape.
627/// * `escape_braces` - A boolean flag indicating whether to unescape curly braces (`{` and `}`).
628/// - If `true`, `{` and `}` are unescaped to `{` and `}`.
629/// - If `false`, `{` and `}` are left unchanged.
630///
631/// # Examples
632///
633/// Basic usage:
634/// ```text
635/// let input = "<script>alert("Hello & 'World'");</script>";
636/// let unescaped = unescape_chars(input, false);
637/// assert_eq!(unescaped, r#"<script>alert("Hello & 'World'");</script>"#);
638/// ```
639///
640/// Unescaping curly braces:
641/// ```text
642/// let input = "{example}";
643/// let unescaped = unescape_chars(input, true);
644/// assert_eq!(unescaped, "{example}");
645/// ```
646///
647/// Unrecognized entities are preserved:
648/// ```text
649/// let input = "This is an &unknown; entity.";
650/// let unescaped = unescape_chars(input, false);
651/// assert_eq!(unescaped, "This is an &unknown; entity.");
652/// ```
653pub fn unescape_chars(input: &str, escape_braces: bool) -> String {
654 if !input.contains('&') {
655 return input.to_string();
656 }
657 let mut result = String::with_capacity(input.len());
658 let mut chars = input.chars().peekable();
659 while let Some(c) = chars.next() {
660 if c == '&' {
661 let mut entity = String::new();
662 let mut has_semicolon = false;
663 while let Some(&next_char) = chars.peek() {
664 if next_char == ';' {
665 chars.next();
666 has_semicolon = true;
667 break;
668 }
669 entity.push(chars.next().unwrap());
670 }
671 match (entity.as_str(), has_semicolon) {
672 ("amp", true) => result.push('&'),
673 ("lt", true) => result.push('<'),
674 ("gt", true) => result.push('>'),
675 ("quot", true) => result.push('"'),
676 ("#x27", true) => result.push('\''),
677 ("#x2F", true) => result.push('/'),
678 ("#123", true) if escape_braces => result.push('{'),
679 ("#125", true) if escape_braces => result.push('}'),
680 _ => {
681 result.push('&');
682 result.push_str(&entity);
683 if has_semicolon {
684 result.push(';');
685 }
686 }
687 }
688 } else {
689 result.push(c);
690 }
691 }
692 result
693}
694
695/// Recursively filter a Value with the function escape_chars
696///
697/// # Arguments
698/// * `value` - A mutable reference to a JSON `Value`. It can be a string (`String`),
699/// an object (`Object`), or an array (`Array`).
700///
701pub fn filter_value(value: &mut Value) {
702 match value {
703 Value::String(s) => *s = escape_chars(&unescape_chars(&s, true), true),
704 Value::Object(obj) => for v in obj.values_mut() {
705 filter_value(v) ;
706 },
707 Value::Array(arr) => for item in arr.iter_mut() {
708 filter_value(item);
709 },
710 _ => {}
711 }
712}
713
714/// Recursively filters the keys (names) of a Value with the function escape_chars
715///
716/// # Arguments
717/// * `value` - A mutable reference to a JSON `Value`. It can be a string (`String`),
718/// an object (`Object`), or an array (`Array`).
719///
720pub fn filter_value_keys(value: &mut Value) {
721 match value {
722 Value::Object(obj) => {
723 let mut new_obj = serde_json::Map::new();
724
725 for (key, val) in obj.iter_mut() {
726 let new_key = escape_chars(&unescape_chars(key, true), true);
727 filter_value_keys(val);
728 new_obj.insert(new_key, val.clone());
729 }
730
731 *obj = new_obj;
732 }
733 Value::Array(arr) => {
734 for item in arr.iter_mut() {
735 filter_value_keys(item);
736 }
737 }
738 _ => {}
739 }
740}