aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/json/json_grammar.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/json/json_grammar.rs')
-rw-r--r--crates/shirabe/src/json/json_grammar.rs96
1 files changed, 96 insertions, 0 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());
+ }
}