diff options
| -rw-r--r-- | crates/shirabe/src/json/json_grammar.rs | 96 | ||||
| -rw-r--r-- | crates/shirabe/src/json/json_manipulator.rs | 189 |
2 files changed, 181 insertions, 104 deletions
diff --git a/crates/shirabe/src/json/json_grammar.rs b/crates/shirabe/src/json/json_grammar.rs index 8eb4237..ce7ce86 100644 --- a/crates/shirabe/src/json/json_grammar.rs +++ b/crates/shirabe/src/json/json_grammar.rs @@ -176,6 +176,87 @@ pub fn scan_object(b: &[u8], p: usize) -> Option<usize> { } } +/// The kind of value a top-level key must hold for a match to be accepted, mirroring whether the +/// PHP pattern captures `(?&json)`, `(?&object)` or `(?&array)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueKind { + Json, + Object, + Array, +} + +/// A located top-level key/value within a JSON object's text. All fields are byte offsets into the +/// scanned contents: +/// - `key_pos`: where the `"key"` token begins (after `\s* { \s*` and any preceding pairs) +/// - `value_pos`: where the value begins (after `"key" \s* : \s*`) +/// - `value_end`: just past the value +#[derive(Debug, Clone, Copy)] +pub struct KeyMatch { + pub key_pos: usize, + pub value_pos: usize, + pub value_end: usize, +} + +/// Locates a top-level key whose token text is exactly `encoded_key` (e.g. `"config"`), reproducing +/// the JsonManipulator patterns of the form +/// `\s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*? KEY \s* : \s* (value)`. +/// +/// Returns `None` when the object cannot be parsed up to the key, or the target value is not of the +/// requested `kind` (matching how the PCRE grammar would fail and the caller would abort). +pub fn find_top_level_key( + contents: &[u8], + encoded_key: &[u8], + kind: ValueKind, +) -> Option<KeyMatch> { + let mut i = skip_ws(contents, 0); + if contents.get(i) != Some(&b'{') { + return None; + } + i = skip_ws(contents, i + 1); + loop { + // Each top-level entry must start with a string key. + if contents.get(i) != Some(&b'"') { + return None; + } + let key_pos = i; + let key_end = scan_string(contents, i)?; + let is_target = &contents[key_pos..key_end] == encoded_key; + + let colon = skip_ws(contents, key_end); + if contents.get(colon) != Some(&b':') { + return None; + } + let value_pos = skip_ws(contents, colon + 1); + + if is_target { + let value_end = match kind { + ValueKind::Json => scan_value(contents, value_pos), + ValueKind::Object if contents.get(value_pos) == Some(&b'{') => { + scan_object(contents, value_pos) + } + ValueKind::Array if contents.get(value_pos) == Some(&b'[') => { + scan_array(contents, value_pos) + } + _ => None, + }; + // Keys are unique, so a wrong-kind target cannot match later either. + return value_end.map(|value_end| KeyMatch { + key_pos, + value_pos, + value_end, + }); + } + + // A non-target entry must be a complete `key : value ,` pair to keep scanning. + let value_end = scan_value(contents, value_pos)?; + let comma = skip_ws(contents, value_end); + if contents.get(comma) != Some(&b',') { + return None; + } + i = skip_ws(contents, comma + 1); + } +} + #[cfg(test)] mod tests { use super::*; @@ -222,4 +303,19 @@ mod tests { // scan_value reports the end of the value, leaving trailing content. assert_eq!(scan_value(br#"{"a": 1}, "rest""#, 0), Some(8)); } + + #[test] + fn finds_top_level_key() { + let c = "{\n \"foo\": \"bar\",\n \"require\": {\n \"a\": \"1\"\n }\n}"; + let m = find_top_level_key(c.as_bytes(), br#""require""#, ValueKind::Object).unwrap(); + assert_eq!(&c[m.key_pos..m.value_pos], "\"require\": "); + assert_eq!(&c[m.value_pos..m.value_end], "{\n \"a\": \"1\"\n }"); + // first key + let m2 = find_top_level_key(c.as_bytes(), br#""foo""#, ValueKind::Json).unwrap(); + assert_eq!(&c[m2.value_pos..m2.value_end], "\"bar\""); + // missing key + assert!(find_top_level_key(c.as_bytes(), br#""nope""#, ValueKind::Json).is_none()); + // wrong kind + assert!(find_top_level_key(c.as_bytes(), br#""foo""#, ValueKind::Array).is_none()); + } } diff --git a/crates/shirabe/src/json/json_manipulator.rs b/crates/shirabe/src/json/json_manipulator.rs index 30ceedb..73b70b9 100644 --- a/crates/shirabe/src/json/json_manipulator.rs +++ b/crates/shirabe/src/json/json_manipulator.rs @@ -11,6 +11,7 @@ use shirabe_php_shim::{ }; use crate::json::JsonFile; +use crate::json::json_grammar::{self, ValueKind}; use crate::repository::PlatformRepository; #[derive(Debug)] @@ -659,18 +660,17 @@ impl JsonManipulator { } // main node content not match-able - let node_regex = format!( - "{{{}^(?P<start> \\s* \\{{ \\s* (?: (?&string) \\s* : (?&json) \\s* , \\s* )*?{}\\s*:\\s*)(?P<content>(?&object))(?P<end>.*)}}sx", - Self::DEFINES, - preg_quote(&JsonFile::encode(main_node), None) - ); - - let mut match_map: IndexMap<String, String> = IndexMap::new(); - if !Preg::is_match_named(&node_regex, &self.contents, &mut match_map) { - return Ok(false); - } - - let mut children = match_map.get("content").cloned().unwrap_or_default(); + let node = match json_grammar::find_top_level_key( + self.contents.as_bytes(), + JsonFile::encode(main_node).as_bytes(), + ValueKind::Object, + ) { + Some(node) => node, + None => return Ok(false), + }; + let node_start = self.contents[..node.value_pos].to_string(); + let node_end = self.contents[node.value_end..].to_string(); + let mut children = self.contents[node.value_pos..node.value_end].to_string(); // invalid match due to un-regexable content, abort if json_decode(&children, false)?.is_null() || json_decode(&children, false)?.as_bool() == Some(false) @@ -678,47 +678,34 @@ impl JsonManipulator { return Ok(false); } - // child exists - let child_regex = format!( - "{{{}(?P<start>\"{}\"\\s*:\\s*)(?P<content>(?&json))(?P<end>,?)}}x", - Self::DEFINES, - preg_quote(&name_owned, None) + // child exists. The child pattern looks for the raw `"name"` key token within the children + // object and rewrites its value in place; the surrounding `"name"\s*:\s*` and optional comma + // are preserved as-is. + let child_key = format!("\"{}\"", name_owned); + let child = json_grammar::find_top_level_key( + children.as_bytes(), + child_key.as_bytes(), + ValueKind::Json, ); - let mut child_match_map: IndexMap<String, String> = IndexMap::new(); - if Preg::is_match_named(&child_regex, &children, &mut child_match_map) { - let value_capture = value.clone(); - let sub_name_capture = sub_name.clone(); - let formatter = ManipulatorFormatter { - newline: self.newline.clone(), - indent: self.indent.clone(), - }; - children = Preg::replace_callback( - &child_regex, - move |matches: &IndexMap<CaptureKey, String>| -> String { - let content_key = CaptureKey::ByName("content".to_string()); - let start_key = CaptureKey::ByName("start".to_string()); - let end_key = CaptureKey::ByName("end".to_string()); - let mut value_local = value_capture.clone(); - if sub_name_capture.is_some() && matches.get(&content_key).is_some() { - let mut cur_val = json_decode(matches.get(&content_key).unwrap(), true) - .unwrap_or(PhpMixed::Null); - if !is_array(&cur_val) { - cur_val = PhpMixed::Array(IndexMap::new()); - } - if let Some(arr) = cur_val.as_array_mut() { - arr.insert(sub_name_capture.clone().unwrap(), value_local.clone()); - } - value_local = cur_val; - } - - format!( - "{}{}{}", - matches.get(&start_key).cloned().unwrap_or_default(), - formatter.format(&value_local, 1, false).unwrap_or_default(), - matches.get(&end_key).cloned().unwrap_or_default() - ) - }, - &children, + if let Some(cm) = child { + let content_str = children[cm.value_pos..cm.value_end].to_string(); + let mut value_local = value.clone(); + if sub_name.is_some() { + let mut cur_val = json_decode(&content_str, true).unwrap_or(PhpMixed::Null); + if !is_array(&cur_val) { + cur_val = PhpMixed::Array(IndexMap::new()); + } + if let Some(arr) = cur_val.as_array_mut() { + arr.insert(sub_name.clone().unwrap(), value_local.clone()); + } + value_local = cur_val; + } + let formatted = self.format(&value_local, 1, false)?; + children = format!( + "{}{}{}", + &children[..cm.value_pos], + formatted, + &children[cm.value_end..] ); } else { let mut leading_match: IndexMap<String, String> = IndexMap::new(); @@ -809,23 +796,7 @@ impl JsonManipulator { } } - let children_owned = children; - self.contents = Preg::replace_callback( - &node_regex, - move |m: &IndexMap<CaptureKey, String>| -> String { - format!( - "{}{}{}", - m.get(&CaptureKey::ByName("start".to_string())) - .cloned() - .unwrap_or_default(), - children_owned, - m.get(&CaptureKey::ByName("end".to_string())) - .cloned() - .unwrap_or_default() - ) - }, - &self.contents, - ); + self.contents = format!("{}{}{}", node_start, children, node_end); Ok(true) } @@ -1395,27 +1366,29 @@ impl JsonManipulator { let content = self.format(&content, 0, false)?; // key exists already - let regex = format!( - "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?)(?P<key>{}\\s*:\\s*(?&json))(?P<end>.*)}}sx", - Self::DEFINES, - preg_quote(&JsonFile::encode(key), None) - ); - let mut matches: IndexMap<String, String> = IndexMap::new(); - if decoded.as_array().and_then(|a| a.get(key)).is_some() - && Preg::is_match_named(®ex, &self.contents, &mut matches) - { + let encoded_key = JsonFile::encode(key); + let key_match = if decoded.as_array().and_then(|a| a.get(key)).is_some() { + json_grammar::find_top_level_key( + self.contents.as_bytes(), + encoded_key.as_bytes(), + ValueKind::Json, + ) + } else { + None + }; + if let Some(m) = key_match { // invalid match due to un-regexable content, abort - let key_match = matches.get("key").cloned().unwrap_or_default(); - if json_decode(&format!("{{{}}}", key_match), false)?.is_null() { + let key_capture = &self.contents[m.key_pos..m.value_end]; + if json_decode(&format!("{{{}}}", key_capture), false)?.is_null() { return Ok(false); } self.contents = format!( "{}{}: {}{}", - matches.get("start").cloned().unwrap_or_default(), - JsonFile::encode(key), + &self.contents[..m.key_pos], + encoded_key, content, - matches.get("end").cloned().unwrap_or_default() + &self.contents[m.value_end..] ); return Ok(true); @@ -1475,22 +1448,30 @@ impl JsonManipulator { } // key exists already - let regex = format!( - "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?)(?P<removal>{}\\s*:\\s*(?&json))\\s*,?\\s*(?P<end>.*)}}sx", - Self::DEFINES, - preg_quote(&JsonFile::encode(key), None) + let encoded_key = JsonFile::encode(key); + let key_match = json_grammar::find_top_level_key( + self.contents.as_bytes(), + encoded_key.as_bytes(), + ValueKind::Json, ); - let mut matches: IndexMap<String, String> = IndexMap::new(); - if Preg::is_match_named(®ex, &self.contents, &mut matches) { + if let Some(m) = key_match { // invalid match due to un-regexable content, abort - let removal = matches.get("removal").cloned().unwrap_or_default(); + let removal = &self.contents[m.key_pos..m.value_end]; if json_decode(&format!("{{{}}}", removal), false)?.is_null() { return Ok(false); } + // The pattern consumes `\s*,?\s*` between the removed key and the rest (`end`). + let cb = self.contents.as_bytes(); + let mut e = json_grammar::skip_ws(cb, m.value_end); + if cb.get(e) == Some(&b',') { + e += 1; + } + e = json_grammar::skip_ws(cb, e); + // check that we are not leaving a dangling comma on the previous line if the last line was removed - let mut start = matches.get("start").cloned().unwrap_or_default(); - let end = matches.get("end").cloned().unwrap_or_default(); + let mut start = self.contents[..m.key_pos].to_string(); + let end = self.contents[e..].to_string(); if Preg::is_match3("#,\\s*$#", &start, None) && Preg::is_match3("#^\\}$#", &end, None) { start = rtrim( &Preg::replace("#,(\\s*)$#", "$1", &start), @@ -1517,24 +1498,24 @@ impl JsonManipulator { return Ok(true); } - let regex = format!( - "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?{}\\s*:\\s*)(?P<removal>\\{{(?P<removal_space>\\s*+)\\}})(?P<end>\\s*,?\\s*.*)}}sx", - Self::DEFINES, - preg_quote(&JsonFile::encode(key), None) - ); - let mut matches: IndexMap<String, String> = IndexMap::new(); - if Preg::is_match_named(®ex, &self.contents, &mut matches) { + // Match the key only when its value is an empty object `{ <space> }`. + let encoded_key = JsonFile::encode(key); + let cb = self.contents.as_bytes(); + let key_match = json_grammar::find_top_level_key(cb, encoded_key.as_bytes(), ValueKind::Object) + .filter(|m| json_grammar::skip_ws(cb, m.value_pos + 1) == m.value_end - 1); + if let Some(m) = key_match { // invalid match due to un-regexable content, abort - let removal = matches.get("removal").cloned().unwrap_or_default(); - if json_decode(&removal, false)?.as_bool() == Some(false) { + let removal = &self.contents[m.value_pos..m.value_end]; + if json_decode(removal, false)?.as_bool() == Some(false) { return Ok(false); } + let removal_space = &self.contents[m.value_pos + 1..m.value_end - 1]; self.contents = format!( "{}[{}]{}", - matches.get("start").cloned().unwrap_or_default(), - matches.get("removal_space").cloned().unwrap_or_default(), - matches.get("end").cloned().unwrap_or_default() + &self.contents[..m.value_pos], + removal_space, + &self.contents[m.value_end..] ); return Ok(true); |
