#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct GraphqlStats {
pub max_depth: u32,
pub aliases: u32,
pub fields: u32,
pub directives: u32,
pub has_introspection: bool,
}
pub fn graphql_lex(s: &str) -> GraphqlStats {
let b = s.as_bytes();
let n = b.len();
let mut i = 0usize;
let mut depth: u32 = 0;
let mut paren: u32 = 0;
let mut st = GraphqlStats::default();
while i < n {
let c = b[i];
match c {
b'#' => {
while i < n && b[i] != b'\n' {
i += 1;
}
}
b'"' if i + 2 < n && b[i + 1] == b'"' && b[i + 2] == b'"' => {
i += 3;
while i + 2 < n && !(b[i] == b'"' && b[i + 1] == b'"' && b[i + 2] == b'"') {
if b[i] == b'\\' {
i += 1; }
i += 1;
}
i = (i + 3).min(n); }
b'"' => {
i += 1;
while i < n && b[i] != b'"' {
if b[i] == b'\\' {
i += 1;
}
i += 1;
}
i += 1; }
b'(' => {
paren += 1;
i += 1;
}
b')' => {
paren = paren.saturating_sub(1);
i += 1;
}
b'{' => {
if paren == 0 {
depth += 1;
if depth > st.max_depth {
st.max_depth = depth;
}
}
i += 1;
}
b'}' => {
if paren == 0 {
depth = depth.saturating_sub(1);
}
i += 1;
}
b':' if paren == 0 => {
st.aliases += 1;
i += 1;
}
b'@' => {
st.directives += 1;
i += 1;
}
_ if c == b'_' || c.is_ascii_alphabetic() => {
let start = i;
while i < n && (b[i] == b'_' || b[i].is_ascii_alphanumeric()) {
i += 1;
}
if paren == 0 && depth > 0 {
st.fields += 1;
let name = &b[start..i];
if name == b"__schema" || name == b"__type" {
st.has_introspection = true;
}
}
}
_ => {
i += 1;
}
}
}
st
}
pub fn unwrap_query_envelope(s: &str) -> Option<Vec<String>> {
let trimmed = s.trim_start();
if !(trimmed.starts_with('{') || trimmed.starts_with('[')) {
return None;
}
let value: serde_json::Value = serde_json::from_str(trimmed).ok()?;
let mut out = Vec::new();
match &value {
serde_json::Value::Object(_) => push_query_leaf(&value, &mut out),
serde_json::Value::Array(arr) => arr.iter().for_each(|el| push_query_leaf(el, &mut out)),
_ => {}
}
(!out.is_empty()).then_some(out)
}
fn push_query_leaf(v: &serde_json::Value, out: &mut Vec<String>) {
if let serde_json::Value::Object(map) = v {
if let Some(serde_json::Value::String(q)) = map.get("query") {
out.push(q.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unwrap_envelope_single_operation() {
let ops = unwrap_query_envelope(r#"{"query":"query{__schema{name}}"}"#).unwrap();
assert_eq!(ops, vec!["query{__schema{name}}".to_string()]);
assert!(graphql_lex(&ops[0]).has_introspection);
}
#[test]
fn unwrap_envelope_batch_array() {
let ops = unwrap_query_envelope(r#"[{"query":"{a}"},{"query":"{b}"}]"#).unwrap();
assert_eq!(ops, vec!["{a}".to_string(), "{b}".to_string()]);
}
#[test]
fn unwrap_envelope_rejects_bare_document_and_non_envelope() {
assert!(unwrap_query_envelope("query{__schema{name}}").is_none());
assert!(unwrap_query_envelope("{__typename}").is_none()); assert!(unwrap_query_envelope(r#"{"a":1}"#).is_none());
assert!(unwrap_query_envelope(r#"{"variables":{"query":"x"}}"#).is_none());
}
#[test]
fn simple_query_depth_and_fields() {
let st = graphql_lex("query { user { id name } }");
assert_eq!(st.max_depth, 2); assert_eq!(st.fields, 3); assert_eq!(st.aliases, 0);
assert!(!st.has_introspection);
}
#[test]
fn paren_aware_trap_flat_selection_deep_input_object() {
let st = graphql_lex("mutation{c(input:{a:{b:{c:{d:1}}}}){id}}");
assert_eq!(st.max_depth, 2);
}
#[test]
fn deep_nesting() {
let mut q = String::from("query");
for _ in 0..20 {
q.push_str("{a");
}
q.push_str("{id}");
for _ in 0..21 {
q.push('}');
}
assert_eq!(graphql_lex(&q).max_depth, 21);
}
#[test]
fn alias_bomb() {
let st = graphql_lex("query{a:f b:f c:f d:f}");
assert_eq!(st.aliases, 4);
}
#[test]
fn introspection_detected() {
assert!(graphql_lex("query{__schema{types{name}}}").has_introspection);
assert!(graphql_lex("{__type(name:\"X\"){fields{name}}}").has_introspection);
}
#[test]
fn typename_is_not_introspection() {
assert!(!graphql_lex("query{user{__typename id}}").has_introspection);
}
#[test]
fn directives_counted() {
let st = graphql_lex("query{field @include(if:true) other @skip(if:false)}");
assert_eq!(st.directives, 2);
}
#[test]
fn braces_in_string_do_not_count() {
let st = graphql_lex(r#"{a(x:"}}}}}}")b}"#);
assert_eq!(st.max_depth, 1);
}
#[test]
fn braces_in_comment_do_not_count() {
let st = graphql_lex("{a # }}}} not real\n b}");
assert_eq!(st.max_depth, 1);
}
#[test]
fn braces_in_block_string_do_not_count() {
let st = graphql_lex(r#"{a(x:"""}}} still a string """)b}"#);
assert_eq!(st.max_depth, 1);
}
#[test]
fn unterminated_string_and_braces_terminate() {
let _ = graphql_lex("query{a(x:\"unterminated");
let _ = graphql_lex("{{{{{{{{");
let _ = graphql_lex("\"\"\"unterminated block");
let _ = graphql_lex("");
}
}