use crate::{Result, SkimError};
use serde_json::Value;
const MAX_JSON_DEPTH: usize = 500;
const MAX_JSON_KEYS: usize = 10_000;
pub(crate) fn transform_json(source: &str) -> Result<String> {
let value: Value = serde_json::from_str(source)
.map_err(|e| SkimError::ParseError(format!("Invalid JSON: {}", e)))?;
let mut key_count = 0;
let structure = extract_structure(&value, 0, &mut key_count)?;
Ok(structure)
}
fn extract_structure(value: &Value, depth: usize, key_count: &mut usize) -> Result<String> {
if depth > MAX_JSON_DEPTH {
return Err(SkimError::ParseError(format!(
"JSON nesting depth exceeded: {} (max: {}). Possible malicious input.",
depth, MAX_JSON_DEPTH
)));
}
match value {
Value::Object(map) => extract_object_structure(map, depth, key_count),
Value::Array(arr) => extract_array_structure(arr, depth, key_count),
_ => Ok(String::new()), }
}
fn extract_object_structure(
map: &serde_json::Map<String, Value>,
depth: usize,
key_count: &mut usize,
) -> Result<String> {
if map.is_empty() {
return Ok("{}".to_string());
}
*key_count += map.len();
if *key_count > MAX_JSON_KEYS {
return Err(SkimError::ParseError(format!(
"JSON key count exceeded: {} (max: {}). Possible malicious input.",
key_count, MAX_JSON_KEYS
)));
}
let indent = " ".repeat(depth);
let next_indent = " ".repeat(depth + 1);
let estimated_capacity = map.len() * 30 + 10;
let mut result = String::with_capacity(estimated_capacity);
result.push_str("{\n");
for (i, (key, val)) in map.iter().enumerate() {
result.push_str(&next_indent);
result.push_str(key);
let value_str = format_value(val, depth + 1, key_count)?;
result.push_str(&value_str);
if i < map.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&indent);
result.push('}');
Ok(result)
}
fn format_value(val: &Value, depth: usize, key_count: &mut usize) -> Result<String> {
match val {
Value::Object(_) => {
let structure = extract_structure(val, depth, key_count)?;
Ok(format!(": {}", structure))
}
Value::Array(arr) => format_array_value(arr, depth, key_count),
_ => Ok(String::new()), }
}
fn format_array_value(arr: &[Value], depth: usize, key_count: &mut usize) -> Result<String> {
let Some(first) = arr.first() else {
return Ok(String::new()); };
if first.is_object() {
let structure = extract_structure(first, depth, key_count)?;
Ok(format!(": {}", structure))
} else {
Ok(String::new()) }
}
fn extract_array_structure(arr: &[Value], depth: usize, key_count: &mut usize) -> Result<String> {
let Some(first) = arr.first() else {
return Ok("[]".to_string());
};
if first.is_object() {
extract_structure(first, depth, key_count)
} else {
Ok("[]".to_string())
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn test_simple_object() {
let input = r#"{"name": "John", "age": 30}"#;
let result = transform_json(input).expect("test JSON should parse successfully");
assert!(result.contains("name"));
assert!(result.contains("age"));
assert!(!result.contains("John"));
assert!(!result.contains("30"));
}
#[test]
fn test_nested_object() {
let input = r#"{
"user": {
"name": "John",
"age": 30
}
}"#;
let result = transform_json(input).expect("nested JSON should parse successfully");
assert!(result.contains("user"));
assert!(result.contains("name"));
assert!(result.contains("age"));
assert!(!result.contains("John"));
}
#[test]
fn test_array_of_primitives() {
let input = r#"{"tags": ["admin", "user", "moderator"]}"#;
let result = transform_json(input).expect("array of primitives should parse successfully");
assert!(result.contains("tags"));
assert!(!result.contains("admin"));
assert!(!result.contains("user"));
assert!(!result.contains("moderator"));
}
#[test]
fn test_array_of_objects() {
let input = r#"{
"items": [
{"id": 1, "price": 100},
{"id": 2, "price": 200}
]
}"#;
let result = transform_json(input).expect("array of objects should parse successfully");
assert!(result.contains("items"));
assert!(result.contains("id"));
assert!(result.contains("price"));
assert!(!result.contains("100"));
assert!(!result.contains("200"));
}
#[test]
fn test_empty_object() {
let input = r#"{"empty": {}}"#;
let result = transform_json(input).expect("empty object should parse successfully");
assert!(result.contains("empty"));
}
#[test]
fn test_empty_array() {
let input = r#"{"items": []}"#;
let result = transform_json(input).expect("empty array should parse successfully");
assert!(result.contains("items"));
}
#[test]
fn test_mixed_array() {
let input = r#"{"mixed": [1, "string", {"id": 1}]}"#;
let result = transform_json(input).expect("mixed array should parse successfully");
assert!(result.contains("mixed"));
assert!(!result.contains("id"));
}
#[test]
fn test_invalid_json() {
let input = r#"{"invalid": "#;
let result = transform_json(input);
assert!(result.is_err());
}
}