mod builder;
mod lazy;
#[cfg(test)]
mod tests;
mod to_json;
mod value;
use crate::constants::{MAX_JSON_DEPTH, get_max_json_size};
use miniserde::json::{Number, Value};
pub use builder::{arr, bool, float, int, null, obj, str};
pub use to_json::ToJson;
pub use value::JsonValue;
pub use miniserde::json::Value as RawValue;
pub(crate) fn json_depth_exceeds_limit(data: &[u8]) -> bool {
let mut depth: usize = 0;
let mut in_string = false;
let mut escape = false;
for &byte in data {
if escape {
escape = false;
continue;
}
match byte {
b'\\' if in_string => escape = true,
b'"' => in_string = !in_string,
b'[' | b'{' if !in_string => {
depth += 1;
if depth > MAX_JSON_DEPTH {
return true;
}
},
b']' | b'}' if !in_string => {
depth = depth.saturating_sub(1);
},
_ => {},
}
}
false
}
#[must_use]
pub fn try_parse(data: &[u8]) -> Option<JsonValue> {
if data.len() > get_max_json_size() {
return None;
}
if json_depth_exceeds_limit(data) {
return None;
}
std::str::from_utf8(data).ok()?;
let value_end = find_json_value_end(data)?;
if has_trailing_content(data, value_end) {
return None;
}
Some(JsonValue::from_bytes(data))
}
#[must_use]
pub fn try_parse_full(data: &[u8]) -> Option<JsonValue> {
if data.len() > get_max_json_size() {
return None;
}
if json_depth_exceeds_limit(data) {
return None;
}
let s = std::str::from_utf8(data).ok()?;
let value_end = find_json_value_end(s.as_bytes())?;
if has_trailing_content(s.as_bytes(), value_end) {
return None;
}
let parsed: Value = miniserde::json::from_str(s).ok()?;
Some(JsonValue::new(parsed))
}
#[inline]
fn has_trailing_content(bytes: &[u8], pos: usize) -> bool {
bytes[pos..]
.iter()
.any(|&b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
}
fn find_json_value_end(bytes: &[u8]) -> Option<usize> {
let pos = skip_ws(bytes, 0);
let b = *bytes.get(pos)?;
match b {
b'"' => find_string_end_pos(bytes, pos + 1).map(|end| end + 1),
b'{' => find_balanced_end_pos(bytes, pos, b'{', b'}'),
b'[' => find_balanced_end_pos(bytes, pos, b'[', b']'),
b't' => bytes
.get(pos..pos + 4)
.filter(|s| *s == b"true")
.map(|_| pos + 4),
b'f' => bytes
.get(pos..pos + 5)
.filter(|s| *s == b"false")
.map(|_| pos + 5),
b'n' => bytes
.get(pos..pos + 4)
.filter(|s| *s == b"null")
.map(|_| pos + 4),
b'-' | b'0'..=b'9' => {
let mut end = pos;
while end < bytes.len()
&& matches!(bytes[end], b'0'..=b'9' | b'-' | b'+' | b'.' | b'e' | b'E')
{
end += 1;
}
Some(end)
},
_ => None,
}
}
#[inline]
fn skip_ws(bytes: &[u8], mut pos: usize) -> usize {
while pos < bytes.len() && matches!(bytes[pos], b' ' | b'\t' | b'\n' | b'\r') {
pos += 1;
}
pos
}
fn find_string_end_pos(bytes: &[u8], mut pos: usize) -> Option<usize> {
while pos < bytes.len() {
match bytes[pos] {
b'"' => return Some(pos),
b'\\' => pos += 2,
_ => pos += 1,
}
}
None
}
fn find_balanced_end_pos(bytes: &[u8], mut pos: usize, open: u8, close: u8) -> Option<usize> {
let mut depth = 0;
let mut in_string = false;
let mut escape = false;
while pos < bytes.len() {
let b = bytes[pos];
if escape {
escape = false;
pos += 1;
continue;
}
match b {
b'\\' if in_string => escape = true,
b'"' => in_string = !in_string,
_ if in_string => {},
_ if b == open => depth += 1,
_ if b == close => {
depth -= 1;
if depth == 0 {
return Some(pos + 1);
}
},
_ => {},
}
pos += 1;
}
None
}
#[inline]
#[must_use]
pub fn raw_str(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
_ => None,
}
}
#[inline]
#[must_use]
pub fn raw_int(v: &Value) -> Option<i64> {
match v {
Value::Number(n) => match n {
Number::I64(i) => Some(*i),
Number::U64(u) => (*u).try_into().ok(),
Number::F64(f) => {
const MAX_SAFE_INT: f64 = 9007199254740992.0;
if f.is_finite() && f.abs() <= MAX_SAFE_INT {
Some(*f as i64)
} else {
None
}
},
},
_ => None,
}
}
#[inline]
#[must_use]
#[allow(clippy::cast_precision_loss)] pub const fn raw_float(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => match n {
Number::F64(f) if f.is_finite() => Some(*f),
Number::I64(i) => Some(*i as f64),
Number::U64(u) => Some(*u as f64),
Number::F64(_) => None, },
_ => None,
}
}
#[inline]
#[must_use]
pub const fn raw_bool(v: &Value) -> Option<bool> {
match v {
Value::Bool(b) => Some(*b),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn raw_is_null(v: &Value) -> bool {
matches!(v, Value::Null)
}