use pdk_core::log::debug;
use crate::validator::JsonConstraintValidator;
use crate::ValidationError;
#[derive(Clone, Debug)]
pub(crate) struct ParserContext {
parser: Vec<JsonParser>,
}
impl ParserContext {
pub(crate) fn new() -> Self {
Self {
parser: vec![JsonParser::Any],
}
}
pub(crate) fn recursion(&self) -> usize {
self.parser.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.parser.is_empty()
}
pub(crate) fn current(&self) -> Option<JsonParser> {
self.parser.last().copied()
}
pub(crate) fn enclosing_context(&self) -> Option<JsonParser> {
let previous_parser_idx = self.parser.len().checked_sub(2)?;
self.parser.get(previous_parser_idx).copied()
}
pub(crate) fn push_parser(&mut self, parser: JsonParser) {
self.parser.push(parser)
}
pub(crate) fn swap_current(&mut self, parser: JsonParser) {
self.pop_parser();
self.push_parser(parser);
}
pub(crate) fn pop_parser(&mut self) {
self.parser.pop();
}
}
#[derive(Default, Copy, Clone)]
struct Index {
idx: usize,
offset: usize,
}
impl Index {
fn increment(&mut self, i: usize) {
self.idx += i;
}
fn idx(&self) -> usize {
self.chunk_idx() + self.offset
}
fn chunk_idx(&self) -> usize {
self.idx
}
}
#[derive(Clone)]
pub(crate) struct DataStream<'a> {
metadata: Index,
chars: &'a [u8],
}
impl<'a> DataStream<'a> {
pub(crate) fn new(chars: &'a [u8], offset: usize) -> Self {
Self {
metadata: Index { idx: 0, offset },
chars,
}
}
pub(crate) fn idx(&self) -> usize {
self.metadata.idx()
}
pub(crate) fn peek(&self) -> Option<char> {
if self.is_empty() {
return None;
}
Some(self.chars[self.metadata.chunk_idx()] as char)
}
pub(crate) fn peek_range(&self, end: i64) -> Option<&str> {
let idx = self.metadata.chunk_idx();
if end < 0 {
let start = idx.saturating_sub(end.unsigned_abs() as usize);
std::str::from_utf8(&self.chars[start..idx]).ok()
} else {
let end = end as usize;
if self.chars.len() < idx + end {
return None;
}
std::str::from_utf8(&self.chars[idx..idx + end]).ok()
}
}
pub(crate) fn consume(&mut self) -> Option<char> {
if self.is_empty() {
return None;
}
let char = self.chars[self.metadata.chunk_idx()] as char;
self.metadata.increment(1);
Some(char)
}
pub(crate) fn drop(&mut self, amount: usize) {
self.metadata.increment(amount);
}
pub(crate) fn is_empty(&self) -> bool {
self.chars.len() <= self.metadata.chunk_idx()
}
}
pub(crate) fn is_whitespace(current: char) -> bool {
matches!(current, '\n' | '\t' | ' ' | '\r')
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum JsonParser {
String { opened: usize },
Number { expected: NumberStateMachine },
Boolean,
Null,
Array { element_count: usize },
Object(ObjectValue),
Any,
}
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub(crate) struct ObjectValue {
processing_key: bool,
element_count: usize,
}
impl ObjectValue {
pub(crate) fn new(processing_key: bool, element_count: usize) -> Self {
Self {
processing_key,
element_count,
}
}
}
impl JsonParser {
pub(crate) fn is_processing_key(&self) -> bool {
matches!(
self,
&JsonParser::Object(ObjectValue {
processing_key: true,
..
})
)
}
pub(crate) fn element_count(&self) -> Option<usize> {
match self {
JsonParser::Array { element_count } => Some(*element_count),
JsonParser::Object(ObjectValue { element_count, .. }) => Some(*element_count),
_ => None,
}
}
pub(crate) fn parse(
mut self,
stream: &mut DataStream<'_>,
parser_context: &mut ParserContext,
constraint_validator: &JsonConstraintValidator,
) -> Result<(), ValidationError> {
match self {
JsonParser::Number { expected } => self.parse_number(stream, expected, parser_context),
JsonParser::Boolean => self.parse_boolean(stream, parser_context),
JsonParser::String { opened } => {
self.parse_string(stream, opened, parser_context, constraint_validator)
}
JsonParser::Null => self.parse_null(stream, parser_context),
JsonParser::Any => self.choose_parser(stream, parser_context, constraint_validator),
JsonParser::Object(object) => self.parse_object(
stream,
object.element_count,
parser_context,
constraint_validator,
),
JsonParser::Array { element_count } => {
self.parse_array(stream, element_count, parser_context, constraint_validator)
}
}
}
fn parse_array(
&mut self,
chars: &mut DataStream<'_>,
array_element_count: usize,
parser_context: &mut ParserContext,
constraint_validator: &JsonConstraintValidator,
) -> Result<(), ValidationError> {
let c = chars.peek().unwrap();
match c {
'[' => {
if constraint_validator.max_depth_restriction_violated(parser_context) {
return Err(ValidationError::MaxDepthExceeded);
}
parser_context.push_parser(JsonParser::Any)
}
']' => parser_context.pop_parser(),
',' => {
parser_context.swap_current(JsonParser::Array {
element_count: array_element_count + 1,
});
if constraint_validator.array_max_length_restriction_violated(parser_context) {
return Err(ValidationError::ArrayMaxLengthExceeded);
} else {
parser_context.push_parser(JsonParser::Any)
}
}
c if chars.idx() != 0 && is_whitespace(c) => {}
_ => {
debug!("Unexpected char: {c} while parsing array");
return Err(ValidationError::UnableToProcessPayload);
}
}
chars.consume();
Ok(())
}
fn parse_object(
&mut self,
chars: &mut DataStream<'_>,
current_element_count: usize,
parser_context: &mut ParserContext,
constraint_validator: &JsonConstraintValidator,
) -> Result<(), ValidationError> {
let c = chars.peek().unwrap();
match c {
'{' => {
if constraint_validator.max_depth_restriction_violated(parser_context) {
return Err(ValidationError::MaxDepthExceeded);
}
parser_context.push_parser(JsonParser::Any);
}
'}' => parser_context.pop_parser(),
':' => {
parser_context.swap_current(JsonParser::Object(ObjectValue::new(
false,
current_element_count,
)));
parser_context.push_parser(JsonParser::Any)
}
',' => {
if constraint_validator
.object_max_element_count_restriction_violated(parser_context)
{
return Err(ValidationError::ObjectMaxLengthExceeded);
}
parser_context.swap_current(JsonParser::Object(ObjectValue::new(
true,
current_element_count + 1,
)));
parser_context.push_parser(JsonParser::Any)
}
c if chars.idx() != 0 && is_whitespace(c) => {}
_ => {
debug!("Unexpected char: {c} while parsing object");
return Err(ValidationError::UnableToProcessPayload);
}
}
chars.consume();
Ok(())
}
fn choose_parser(
&mut self,
chars: &mut DataStream<'_>,
parser_context: &mut ParserContext,
_constraint_validator: &JsonConstraintValidator,
) -> Result<(), ValidationError> {
let c = chars.peek().unwrap();
match c {
c if is_whitespace(c) => chars.drop(1),
't' => parser_context.swap_current(JsonParser::Boolean),
'f' => parser_context.swap_current(JsonParser::Boolean),
'n' => parser_context.swap_current(JsonParser::Null),
'"' => parser_context.swap_current(JsonParser::String {
opened: chars.idx(),
}),
'-' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' => {
parser_context.swap_current(JsonParser::Number {
expected: NumberStateMachine::new(),
});
}
'[' => parser_context.swap_current(JsonParser::Array { element_count: 0 }),
']' => parser_context.pop_parser(),
'{' => parser_context.swap_current(JsonParser::Object(ObjectValue::new(true, 0))),
'}' if chars.idx() != 0 => parser_context.pop_parser(),
_ => {
debug!("Unexpected char: {c} while choosing parser");
return Err(ValidationError::UnableToProcessPayload);
}
};
Ok(())
}
fn parse_string(
&mut self,
chars: &mut DataStream<'_>,
opened: usize,
parser_context: &mut ParserContext,
constraint_validator: &JsonConstraintValidator,
) -> Result<(), ValidationError> {
if constraint_validator.current_string_above_max_length(opened, chars.idx(), parser_context)
{
return Err(ValidationError::StringMaxLengthExceeded);
}
let c = chars.consume().unwrap();
match c {
'\\' => {
let escaped = chars.peek().unwrap_or('\0');
match escaped {
'n' | '"' | '\'' | '\\' | 'r' | 't' | 'b' | 'f' | 'v' => {
chars.drop(1);
}
'u' => chars.drop(5),
_ => {
debug!("Unexpected char: \\{escaped} while parsing string");
return Err(ValidationError::UnableToProcessPayload);
}
}
}
'"' if opened != chars.idx() - 1 => parser_context.pop_parser(),
'\0' => {
debug!("Unexpected char: \\0 while parsing string");
return Err(ValidationError::UnableToProcessPayload);
}
_ => {}
};
Ok(())
}
fn parse_number(
&mut self,
chars: &mut DataStream<'_>,
expected: NumberStateMachine,
parser_context: &mut ParserContext,
) -> Result<(), ValidationError> {
let c = chars.peek().unwrap();
if [':', ',', '}', ']'].contains(&c) || is_whitespace(c) {
if expected.is_terminal() {
parser_context.pop_parser();
return Ok(());
}
parser_context.pop_parser();
debug!("Incomplete number (state {expected:?}) terminated by '{c}'");
return Err(ValidationError::UnableToProcessPayload);
}
match expected.process(c) {
Ok(next) => parser_context.swap_current(JsonParser::Number { expected: next }),
Err(_) => {
parser_context.pop_parser();
debug!("Unexpected char: {c} while parsing number");
return Err(ValidationError::UnableToProcessPayload);
}
}
chars.drop(1);
Ok(())
}
fn parse_boolean(
&mut self,
chars: &mut DataStream<'_>,
parser_context: &mut ParserContext,
) -> Result<(), ValidationError> {
match chars.peek().unwrap() {
't' => Self::consume_word("true", chars)?,
'f' => Self::consume_word("false", chars)?,
_ => return Err(ValidationError::UnableToProcessPayload),
}
parser_context.pop_parser();
Ok(())
}
fn parse_null(
&mut self,
chars: &mut DataStream<'_>,
parser_context: &mut ParserContext,
) -> Result<(), ValidationError> {
parser_context.pop_parser();
Self::consume_word("null", chars)
}
fn consume_word(word: &str, chars: &mut DataStream<'_>) -> Result<(), ValidationError> {
if Some(word) != chars.peek_range(word.len() as i64) {
return Err(ValidationError::InvalidToken(word.to_owned()));
}
chars.drop(word.len());
Ok(())
}
}
#[derive(Eq, PartialEq, Copy, Clone, Debug)]
pub(crate) enum NumberStateMachine {
DigitOrSign,
Digit,
DigitPeriodOrExponent,
Sign,
}
impl NumberStateMachine {
pub(crate) fn new() -> Self {
Self::DigitOrSign
}
pub(crate) fn is_terminal(&self) -> bool {
matches!(self, Self::DigitPeriodOrExponent)
}
pub(crate) fn process(&self, c: char) -> Result<Self, ()> {
match (self, c) {
(Self::DigitOrSign, c) if c.is_ascii_digit() => Ok(Self::DigitPeriodOrExponent),
(Self::DigitOrSign, '+' | '-') => Ok(Self::Digit),
(Self::Digit, c) if c.is_ascii_digit() => Ok(Self::DigitPeriodOrExponent),
(Self::DigitPeriodOrExponent, c) if c.is_ascii_digit() => {
Ok(Self::DigitPeriodOrExponent)
}
(Self::DigitPeriodOrExponent, '.') => Ok(Self::Digit),
(Self::DigitPeriodOrExponent, 'e' | 'E') => Ok(Self::Sign),
(Self::Sign, '+' | '-') => Ok(Self::Digit),
(Self::Sign, c) if c.is_ascii_digit() => Ok(Self::DigitPeriodOrExponent),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parser_context_starts_with_any() {
let parser = ParserContext::new();
assert_eq!(parser.current(), Some(JsonParser::Any));
assert_eq!(parser.recursion(), 1);
}
#[test]
fn parser_context_recursion_increases_on_push() {
let mut parser = ParserContext::new();
parser.push_parser(JsonParser::Array { element_count: 0 });
assert_eq!(parser.recursion(), 2);
}
#[test]
fn data_stream_peek_returns_next_char() {
let bytes = b"hello";
let stream = DataStream::new(bytes, 0);
assert_eq!(stream.peek(), Some('h'));
}
#[test]
fn data_stream_consume_advances_position() {
let bytes = b"hello";
let mut stream = DataStream::new(bytes, 0);
assert_eq!(stream.consume(), Some('h'));
assert_eq!(stream.peek(), Some('e'));
}
#[test]
fn number_state_machine_processes_valid_digit() {
let machine = NumberStateMachine::new();
let result = machine.process('5').unwrap();
assert!(result.is_terminal());
}
#[test]
fn number_state_machine_sign_is_not_terminal() {
let result = NumberStateMachine::new().process('-').unwrap();
assert!(!result.is_terminal());
}
#[test]
fn number_state_machine_after_decimal_is_not_terminal() {
let result = NumberStateMachine::new()
.process('1')
.and_then(|m| m.process('.'))
.unwrap();
assert!(!result.is_terminal());
}
#[test]
fn number_state_machine_after_exponent_char_is_not_terminal() {
let result = NumberStateMachine::new()
.process('1')
.and_then(|m| m.process('e'))
.unwrap();
assert!(!result.is_terminal());
}
#[test]
fn number_state_machine_digit_after_sign_cannot_be_followed_by_sign() {
let result = NumberStateMachine::new()
.process('1')
.and_then(|m| m.process('-'));
assert!(result.is_err());
}
#[test]
fn number_state_machine_rejects_invalid_char() {
let machine = NumberStateMachine::new();
let result = machine.process('-').unwrap();
assert!(result.process('x').is_err());
}
#[test]
fn json_parser_element_count_returns_correct_value() {
let parser = JsonParser::Array { element_count: 5 };
assert_eq!(parser.element_count(), Some(5));
}
#[test]
fn json_parser_is_processing_key_returns_true_for_key() {
let parser = JsonParser::Object(ObjectValue::new(true, 0));
assert!(parser.is_processing_key());
}
#[test]
fn number_state_machine_plain_integer_is_valid() {
let result = NumberStateMachine::new()
.process('1')
.and_then(|m| m.process('2'))
.and_then(|m| m.process('3'));
assert!(result.is_ok());
assert!(result.unwrap().is_terminal());
}
#[test]
fn number_state_machine_negative_integer_is_valid() {
let result = NumberStateMachine::new()
.process('-')
.and_then(|m| m.process('4'))
.and_then(|m| m.process('2'));
assert!(result.is_ok());
assert!(result.unwrap().is_terminal());
}
#[test]
fn number_state_machine_decimal_is_valid() {
let result = NumberStateMachine::new()
.process('3')
.and_then(|m| m.process('.'))
.and_then(|m| m.process('1'))
.and_then(|m| m.process('4'));
assert!(result.is_ok());
assert!(result.unwrap().is_terminal());
}
#[test]
fn number_state_machine_exponential_without_sign_is_valid() {
let result = NumberStateMachine::new()
.process('1')
.and_then(|m| m.process('e'))
.and_then(|m| m.process('1'))
.and_then(|m| m.process('0'));
assert!(result.is_ok());
}
#[test]
fn number_state_machine_exponential_uppercase_without_sign_is_valid() {
let result = NumberStateMachine::new()
.process('8')
.and_then(|m| m.process('.'))
.and_then(|m| m.process('8'))
.and_then(|m| m.process('E'))
.and_then(|m| m.process('9'));
assert!(result.is_ok());
}
#[test]
fn number_state_machine_exponential_with_negative_sign_is_valid() {
let result = NumberStateMachine::new()
.process('2')
.and_then(|m| m.process('.'))
.and_then(|m| m.process('5'))
.and_then(|m| m.process('e'))
.and_then(|m| m.process('-'))
.and_then(|m| m.process('3'));
assert!(result.is_ok());
}
#[test]
fn number_state_machine_exponential_with_positive_sign_is_valid() {
let result = NumberStateMachine::new()
.process('-')
.and_then(|m| m.process('1'))
.and_then(|m| m.process('4'))
.and_then(|m| m.process('.'))
.and_then(|m| m.process('1'))
.and_then(|m| m.process('5'))
.and_then(|m| m.process('e'))
.and_then(|m| m.process('+'))
.and_then(|m| m.process('1'))
.and_then(|m| m.process('3'));
assert!(result.is_ok());
}
}