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 `&#123;` and `&#125;`.
552///   - If `false`, curly braces are left unchanged.
553///
554/// # Escaped Characters
555///
556/// The following characters are always escaped:
557/// - `&` → `&amp;`
558/// - `<` → `&lt;`
559/// - `>` → `&gt;`
560/// - `"` → `&quot;`
561/// - `'` → `&#x27;`
562/// - `/` → `&#x2F;`
563///
564/// If `escape_braces` is `true`, the following characters are also escaped:
565/// - `{` → `&#123;`
566/// - `}` → `&#125;`
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, &lt;world&gt; &amp; &quot;friends&quot;! {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, &lt;world&gt; &amp; &quot;friends&quot;! &#123;example&#125;"#);
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("&amp;"),
590                '<' => result.push_str("&lt;"),
591                '>' => result.push_str("&gt;"),
592                '"' => result.push_str("&quot;"),
593                '\'' => result.push_str("&#x27;"),
594                '/' => result.push_str("&#x2F;"),
595                '{' if escape_braces => result.push_str("&#123;"),
596                '}' if escape_braces => result.push_str("&#125;"),
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/// - `&amp;` → `&`
612/// - `&lt;` → `<`
613/// - `&gt;` → `>`
614/// - `&quot;` → `"`
615/// - `&#x27;` → `'`
616/// - `&#x2F;` → `/`
617///
618/// If `escape_braces` is `true`, it also replaces:
619/// - `&#123;` → `{`
620/// - `&#125;` → `}`
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`, `&#123;` and `&#125;` are unescaped to `{` and `}`.
629///   - If `false`, `&#123;` and `&#125;` are left unchanged.
630///
631/// # Examples
632///
633/// Basic usage:
634/// ```text
635/// let input = "&lt;script&gt;alert(&quot;Hello &amp; &#x27;World&#x27;&quot;);&lt;/script&gt;";
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 = "&#123;example&#125;";
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}