use crate::cache::{get_cached_regex, parse_pattern, ParsedPattern};
use crate::config::ProcessingConfig;
use crate::convert::try_convert_string_to_json_bytes;
use crate::error::JsonToolsError;
use crate::flatten::{
apply_value_replacement_cow, escape_json_string, scan_and_fixup, skip_tape_value,
tape_content_str, tape_is_empty_array, tape_is_empty_object, tape_quoted_str,
tape_scalar_bytes, trim_ascii, unescape_json_string, write_json_escaped_key, EntryKind,
TapeEntry,
};
use crate::fxhash::FxHashMap;
use memchr::memmem;
use serde_json::Value;
use smallvec::SmallVec;
use std::borrow::Cow;
#[inline(always)]
pub(crate) fn apply_key_replacement_patterns(
key: &str,
patterns: &[(String, String)],
) -> Result<Option<String>, JsonToolsError> {
let mut new_key = Cow::Borrowed(key);
let mut changed = false;
for (pattern, replacement) in patterns {
match parse_pattern(pattern) {
ParsedPattern::Regex(inner) => {
if let Ok(regex) = get_cached_regex(inner) {
if let Cow::Owned(s) = regex.replace_all(&new_key, replacement.as_str()) {
new_key = Cow::Owned(s);
changed = true;
}
}
}
ParsedPattern::Literal(lit) => {
if memmem::find(new_key.as_bytes(), lit.as_bytes()).is_some() {
new_key = Cow::Owned(new_key.replace(lit, replacement));
changed = true;
}
}
}
}
if changed {
Ok(Some(new_key.into_owned()))
} else {
Ok(None)
}
}
#[inline]
pub(crate) fn apply_value_replacement_patterns(
value: &mut Value,
patterns: &[(String, String)],
) -> Result<(), JsonToolsError> {
if let Value::String(s) = value {
if let Some(replaced) = apply_key_replacement_patterns(s, patterns)? {
*s = replaced;
}
}
Ok(())
}
#[inline]
pub(crate) fn process_single_json_normal(
json: &str,
config: &ProcessingConfig,
) -> Result<String, JsonToolsError> {
let input = json.as_bytes();
if input.len() > u32::MAX as usize {
return Err(JsonToolsError::input_validation_error(
"Input exceeds 4 GiB limit",
));
}
let trimmed = trim_ascii(input);
if trimmed.is_empty() {
return Ok(json.to_string());
}
match trimmed[0] {
b'{' | b'[' => {}
_ => return handle_root_primitive(trimmed, config),
}
let tape = scan_and_fixup(input)?;
if tape.is_empty() {
return Ok(json.to_string());
}
let needs_slow = config.lowercase_keys
|| config.replacements.has_key_replacements()
|| config.collision.has_collision_handling();
if needs_slow {
normal_slow_walk(input, &tape, config)
} else {
Ok(normal_fast_walk(input, &tape, config))
}
}
fn handle_root_primitive(
trimmed: &[u8],
config: &ProcessingConfig,
) -> Result<String, JsonToolsError> {
let s = unsafe { std::str::from_utf8_unchecked(trimmed) };
if trimmed.len() >= 2 && trimmed[0] == b'"' && trimmed[trimmed.len() - 1] == b'"' {
let content = &s[1..s.len() - 1];
if config.replacements.has_value_replacements() {
let unescaped = if memchr::memchr(b'\\', content.as_bytes()).is_some() {
unescape_json_string(content)
} else {
Cow::Borrowed(content)
};
if let Some(replaced) = apply_value_replacement_cow(
unescaped.as_ref(),
&config.replacements.value_replacements,
) {
let escaped = escape_json_string(&replaced);
return Ok(format!("\"{}\"", escaped));
}
}
if config.auto_convert_types {
let unescaped = if memchr::memchr(b'\\', content.as_bytes()).is_some() {
unescape_json_string(content)
} else {
Cow::Borrowed(content)
};
if let Some(converted) = try_convert_string_to_json_bytes(unescaped.as_ref()) {
return Ok(converted.into_owned());
}
}
}
if config.filtering.remove_nulls && trimmed == b"null" {
return Ok(s.to_string());
}
Ok(s.to_string())
}
#[inline(always)]
fn emit_string_value_shared(
output: &mut String,
input: &[u8],
entry: TapeEntry,
config: &ProcessingConfig,
) -> (bool, bool) {
let content_str = tape_content_str(input, entry);
let has_replacements = config.replacements.has_value_replacements();
let auto_convert = config.auto_convert_types;
if !has_replacements && !auto_convert {
return (false, false);
}
let unescaped = if entry.string_has_escapes() {
unescape_json_string(content_str)
} else {
Cow::Borrowed(content_str)
};
if has_replacements {
if let Some(replaced) =
apply_value_replacement_cow(unescaped.as_ref(), &config.replacements.value_replacements)
{
if config.filtering.remove_empty_strings && replaced.is_empty() {
return (true, true);
}
if auto_convert {
if let Some(converted) = try_convert_string_to_json_bytes(&replaced) {
if config.filtering.remove_nulls && converted == "null" {
return (true, true);
}
output.push_str(&converted);
return (false, true);
}
}
let escaped = escape_json_string(&replaced);
output.push('"');
output.push_str(escaped.as_ref());
output.push('"');
return (false, true);
}
}
if auto_convert {
if let Some(converted) = try_convert_string_to_json_bytes(unescaped.as_ref()) {
if config.filtering.remove_nulls && converted == "null" {
return (true, true);
}
output.push_str(&converted);
return (false, true);
}
}
(false, false)
}
struct NormalFastWalker<'a> {
input: &'a [u8],
tape: &'a [TapeEntry],
config: &'a ProcessingConfig,
output: String,
}
fn normal_fast_walk(input: &[u8], tape: &[TapeEntry], config: &ProcessingConfig) -> String {
let mut walker = NormalFastWalker {
input,
tape,
config,
output: String::with_capacity(input.len()),
};
walker.walk_value(0);
walker.output
}
impl<'a> NormalFastWalker<'a> {
#[inline(always)]
fn walk_value(&mut self, idx: usize) -> (usize, bool) {
if idx >= self.tape.len() {
return (idx, false);
}
let entry = self.tape[idx];
match entry.kind() {
EntryKind::ObjectStart => self.walk_object(idx),
EntryKind::ArrayStart => self.walk_array(idx),
EntryKind::StringStart => self.emit_string_value(idx),
EntryKind::ScalarStart => self.emit_scalar_value(idx),
_ => (idx + 1, false),
}
}
fn walk_object(&mut self, start_idx: usize) -> (usize, bool) {
let end_idx = self.tape[start_idx].aux() as usize;
if tape_is_empty_object(self.tape, start_idx) {
if self.config.filtering.remove_empty_objects {
return (end_idx + 1, true);
}
self.output.push_str("{}");
return (end_idx + 1, false);
}
let obj_start = self.output.len();
self.output.push('{');
let mut first = true;
let mut cursor = start_idx + 1;
while cursor < end_idx {
let entry = self.tape[cursor];
if entry.kind() == EntryKind::StringStart {
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
let key_with_quotes = tape_quoted_str(self.input, entry);
self.output.push_str(key_with_quotes);
cursor += 1;
if cursor < end_idx && self.tape[cursor].kind() == EntryKind::Colon {
self.output.push(':');
cursor += 1;
}
let (next, filtered) = self.walk_value(cursor);
cursor = next;
if filtered {
self.output.truncate(checkpoint);
} else {
first = false;
}
} else if entry.kind() == EntryKind::Comma {
cursor += 1;
} else {
cursor += 1;
}
}
if self.output.len() == obj_start + 1 && self.config.filtering.remove_empty_objects {
self.output.truncate(obj_start);
return (end_idx + 1, true);
}
self.output.push('}');
(end_idx + 1, false)
}
fn walk_array(&mut self, start_idx: usize) -> (usize, bool) {
let end_idx = self.tape[start_idx].aux() as usize;
if tape_is_empty_array(self.tape, start_idx) {
if self.config.filtering.remove_empty_arrays {
return (end_idx + 1, true);
}
self.output.push_str("[]");
return (end_idx + 1, false);
}
let arr_start = self.output.len();
self.output.push('[');
let mut first = true;
let mut cursor = start_idx + 1;
while cursor < end_idx {
let entry = self.tape[cursor];
if entry.kind() == EntryKind::Comma {
cursor += 1;
continue;
}
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
let (next, filtered) = self.walk_value(cursor);
cursor = next;
if filtered {
self.output.truncate(checkpoint);
} else {
first = false;
}
}
if self.output.len() == arr_start + 1 && self.config.filtering.remove_empty_arrays {
self.output.truncate(arr_start);
return (end_idx + 1, true);
}
self.output.push(']');
(end_idx + 1, false)
}
#[inline(always)]
fn emit_string_value(&mut self, idx: usize) -> (usize, bool) {
let entry = self.tape[idx];
if self.config.filtering.remove_empty_strings && entry.string_content_len() == 0 {
return (idx + 1, true);
}
let (was_filtered, was_written) =
emit_string_value_shared(&mut self.output, self.input, entry, self.config);
if was_written {
return (idx + 1, was_filtered);
}
self.output.push_str(tape_quoted_str(self.input, entry));
(idx + 1, false)
}
#[inline(always)]
fn emit_scalar_value(&mut self, idx: usize) -> (usize, bool) {
let entry = self.tape[idx];
let trimmed = trim_ascii(tape_scalar_bytes(self.input, entry));
if self.config.filtering.remove_nulls && trimmed == b"null" {
return (idx + 1, true);
}
let s = unsafe { std::str::from_utf8_unchecked(trimmed) };
self.output.push_str(s);
(idx + 1, false)
}
}
struct SlowObjectEntry {
key: String,
val_start: usize,
}
#[inline]
fn lowercase_if_needed(s: String) -> String {
if s.chars().any(char::is_uppercase) {
s.to_lowercase()
} else {
s
}
}
fn normal_slow_walk(
input: &[u8],
tape: &[TapeEntry],
config: &ProcessingConfig,
) -> Result<String, JsonToolsError> {
let mut walker = NormalSlowWalker {
input,
tape,
config,
output: String::with_capacity(input.len()),
};
walker.walk_value(0)?;
Ok(walker.output)
}
struct NormalSlowWalker<'a> {
input: &'a [u8],
tape: &'a [TapeEntry],
config: &'a ProcessingConfig,
output: String,
}
impl<'a> NormalSlowWalker<'a> {
fn walk_value(&mut self, idx: usize) -> Result<(usize, bool), JsonToolsError> {
if idx >= self.tape.len() {
return Ok((idx, false));
}
let entry = self.tape[idx];
match entry.kind() {
EntryKind::ObjectStart => self.walk_object(idx),
EntryKind::ArrayStart => self.walk_array(idx),
EntryKind::StringStart => Ok(self.emit_string_value(idx)),
EntryKind::ScalarStart => Ok(self.emit_scalar_value(idx)),
_ => Ok((idx + 1, false)),
}
}
fn walk_object(&mut self, start_idx: usize) -> Result<(usize, bool), JsonToolsError> {
let end_idx = self.tape[start_idx].aux() as usize;
if tape_is_empty_object(self.tape, start_idx) {
if self.config.filtering.remove_empty_objects {
return Ok((end_idx + 1, true));
}
self.output.push_str("{}");
return Ok((end_idx + 1, false));
}
let mut entries: SmallVec<[SlowObjectEntry; 16]> = SmallVec::new();
let mut cursor = start_idx + 1;
while cursor < end_idx {
let entry = self.tape[cursor];
if entry.kind() == EntryKind::StringStart {
let key_str = tape_content_str(self.input, entry);
let key_unescaped = if entry.string_has_escapes() {
unescape_json_string(key_str)
} else {
Cow::Borrowed(key_str)
};
let mut transformed_key = if self.config.replacements.has_key_replacements() {
apply_key_replacement_patterns(
key_unescaped.as_ref(),
&self.config.replacements.key_replacements,
)?
.unwrap_or_else(|| key_unescaped.into_owned())
} else {
key_unescaped.into_owned()
};
if self.config.lowercase_keys {
transformed_key = lowercase_if_needed(transformed_key);
}
cursor += 1;
if cursor < end_idx && self.tape[cursor].kind() == EntryKind::Colon {
cursor += 1;
}
let val_start = cursor;
cursor = skip_tape_value(self.tape, cursor);
entries.push(SlowObjectEntry {
key: transformed_key,
val_start,
});
} else {
cursor += 1;
}
}
let has_collisions = if self.config.collision.has_collision_handling() {
let mut seen = FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
entries.iter().any(|e| seen.insert(&e.key, ()).is_some())
} else {
false
};
let obj_start = self.output.len();
self.output.push('{');
if has_collisions {
self.serialize_with_collisions(&entries)?;
} else {
self.serialize_no_collisions(&entries)?;
}
if self.output.len() == obj_start + 1 && self.config.filtering.remove_empty_objects {
self.output.truncate(obj_start);
return Ok((end_idx + 1, true));
}
self.output.push('}');
Ok((end_idx + 1, false))
}
fn serialize_no_collisions(
&mut self,
entries: &[SlowObjectEntry],
) -> Result<(), JsonToolsError> {
let mut first = true;
for entry in entries {
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
self.output.push('"');
write_json_escaped_key(&mut self.output, &entry.key);
self.output.push_str("\":");
let (_, filtered) = self.walk_value(entry.val_start)?;
if filtered {
self.output.truncate(checkpoint);
} else {
first = false;
}
}
Ok(())
}
fn serialize_with_collisions(
&mut self,
entries: &[SlowObjectEntry],
) -> Result<(), JsonToolsError> {
let mut key_order: Vec<&str> = Vec::new();
let mut key_indices: FxHashMap<&str, SmallVec<[usize; 2]>> =
FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
for (i, entry) in entries.iter().enumerate() {
key_indices
.entry(entry.key.as_str())
.and_modify(|v| v.push(i))
.or_insert_with(|| {
key_order.push(entry.key.as_str());
SmallVec::from_elem(i, 1)
});
}
let mut first = true;
for key in &key_order {
let indices = &key_indices[key];
if indices.len() == 1 {
let entry = &entries[indices[0]];
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
self.output.push('"');
write_json_escaped_key(&mut self.output, &entry.key);
self.output.push_str("\":");
let (_, filtered) = self.walk_value(entry.val_start)?;
if filtered {
self.output.truncate(checkpoint);
} else {
first = false;
}
} else {
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
self.output.push('"');
write_json_escaped_key(&mut self.output, key);
self.output.push_str("\":[");
let arr_start = self.output.len();
let mut arr_first = true;
for &idx in indices {
let entry = &entries[idx];
let val_checkpoint = self.output.len();
if !arr_first {
self.output.push(',');
}
let (_, filtered) = self.walk_value(entry.val_start)?;
if filtered {
self.output.truncate(val_checkpoint);
} else {
arr_first = false;
}
}
if self.output.len() == arr_start {
self.output.truncate(checkpoint);
} else {
self.output.push(']');
first = false;
}
}
}
Ok(())
}
fn walk_array(&mut self, start_idx: usize) -> Result<(usize, bool), JsonToolsError> {
let end_idx = self.tape[start_idx].aux() as usize;
if tape_is_empty_array(self.tape, start_idx) {
if self.config.filtering.remove_empty_arrays {
return Ok((end_idx + 1, true));
}
self.output.push_str("[]");
return Ok((end_idx + 1, false));
}
let arr_start = self.output.len();
self.output.push('[');
let mut first = true;
let mut cursor = start_idx + 1;
while cursor < end_idx {
let entry = self.tape[cursor];
if entry.kind() == EntryKind::Comma {
cursor += 1;
continue;
}
let checkpoint = self.output.len();
if !first {
self.output.push(',');
}
let (next, filtered) = self.walk_value(cursor)?;
cursor = next;
if filtered {
self.output.truncate(checkpoint);
} else {
first = false;
}
}
if self.output.len() == arr_start + 1 && self.config.filtering.remove_empty_arrays {
self.output.truncate(arr_start);
return Ok((end_idx + 1, true));
}
self.output.push(']');
Ok((end_idx + 1, false))
}
#[inline(always)]
fn emit_string_value(&mut self, idx: usize) -> (usize, bool) {
let entry = self.tape[idx];
if self.config.filtering.remove_empty_strings && entry.string_content_len() == 0 {
return (idx + 1, true);
}
let (was_filtered, was_written) =
emit_string_value_shared(&mut self.output, self.input, entry, self.config);
if was_written {
return (idx + 1, was_filtered);
}
self.output.push_str(tape_quoted_str(self.input, entry));
(idx + 1, false)
}
#[inline(always)]
fn emit_scalar_value(&mut self, idx: usize) -> (usize, bool) {
let entry = self.tape[idx];
let trimmed = trim_ascii(tape_scalar_bytes(self.input, entry));
if self.config.filtering.remove_nulls && trimmed == b"null" {
return (idx + 1, true);
}
let s = unsafe { std::str::from_utf8_unchecked(trimmed) };
self.output.push_str(s);
(idx + 1, false)
}
}