pub fn otp_allowed_statement(query: &str) -> Result<String, String> {
let query = query.trim();
let first_word = query.split_whitespace().next().unwrap_or_default();
if !first_word.eq_ignore_ascii_case("select") {
return Err("it must be a single SELECT".to_string());
}
let mut statement = String::with_capacity(query.len());
let mut refers_to_email = false;
let mut quote = None;
let mut rest = query;
while let Some(c) = rest.chars().next() {
if let Some(open) = quote {
if c == open {
quote = None;
}
} else if c == '\'' || c == '"' {
quote = Some(c);
} else if c == ';' {
return Err("it must be a single statement, with no ';'".to_string());
} else if rest.starts_with("--") || rest.starts_with("/*") {
return Err("it must not contain a comment".to_string());
} else if c == '$' {
let after = rest.strip_prefix("$email").filter(|after| {
!after.chars().next().is_some_and(|next| next.is_alphanumeric() || next == '_')
});
let Some(after) = after else {
return Err(
"$email is the only parameter it may use, and it may not use dollar quoting"
.to_string(),
);
};
statement.push_str("$1");
refers_to_email = true;
rest = after;
continue;
}
statement.push(c);
rest = &rest[c.len_utf8()..];
}
if quote.is_some() {
return Err("it has an unterminated quote".to_string());
}
if !refers_to_email {
return Err("it must refer to the address as $email".to_string());
}
Ok(statement)
}
#[cfg(test)]
#[path = "otp_allowed_query_tests.rs"]
mod tests;