use crate::heap::Handle;
use crate::nanbox::{NanBox, Unpacked};
use crate::realm::Realm;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Circular;
#[must_use]
pub fn stringify(realm: &Realm, v: NanBox) -> Option<String> {
stringify_seen(realm, v, &mut Vec::new()).unwrap_or(None)
}
pub fn try_stringify(realm: &Realm, v: NanBox) -> Result<Option<String>, Circular> {
stringify_seen(realm, v, &mut Vec::new())
}
fn stringify_seen(
realm: &Realm,
v: NanBox,
seen: &mut Vec<Handle>,
) -> Result<Option<String>, Circular> {
match v.unpack() {
Unpacked::Undefined => Ok(None),
Unpacked::Null => Ok(Some(String::from("null"))),
Unpacked::Bool(b) => Ok(Some(String::from(if b { "true" } else { "false" }))),
Unpacked::Number(n) => Ok(Some(if n.is_finite() {
realm.to_display_string(v)
} else {
String::from("null")
})),
Unpacked::Handle(raw) => {
let h = Handle::from_raw(raw);
if let Some(bytes) = realm.string_bytes(h) {
return Ok(Some(quote_wtf8(&bytes)));
}
if realm.is_vm_function(h) {
return Ok(None);
}
let is_container = realm.array_elements(h).is_some() || realm.object_keys(h).is_some();
if is_container {
if seen.contains(&h) || seen.len() >= realm.limits.max_json_depth {
return Err(Circular); }
seen.push(h);
}
let result = if let Some(elems) = realm.array_elements(h).map(<[_]>::to_vec) {
let mut parts = Vec::with_capacity(elems.len());
for e in &elems {
parts.push(
stringify_seen(realm, *e, seen)?.unwrap_or_else(|| String::from("null")),
);
}
Some(alloc::format!("[{}]", parts.join(",")))
} else if let Some(keys) = realm.object_keys(h) {
let mut parts = Vec::new();
for k in keys {
let val = realm.get_property(h, &k).unwrap_or(NanBox::undefined());
if let Some(s) = stringify_seen(realm, val, seen)? {
parts.push(alloc::format!("{}:{}", quote(&k), s));
}
}
Some(alloc::format!("{{{}}}", parts.join(",")))
} else {
None };
if is_container {
seen.pop();
}
Ok(result)
}
}
}
#[must_use]
pub fn stringify_pretty(realm: &Realm, v: NanBox, indent: &str) -> Option<String> {
stringify_at(realm, v, indent, "", &mut Vec::new()).unwrap_or(None)
}
pub fn try_stringify_pretty(
realm: &Realm,
v: NanBox,
indent: &str,
) -> Result<Option<String>, Circular> {
stringify_at(realm, v, indent, "", &mut Vec::new())
}
fn stringify_at(
realm: &Realm,
v: NanBox,
indent: &str,
cur: &str,
seen: &mut Vec<Handle>,
) -> Result<Option<String>, Circular> {
match v.unpack() {
Unpacked::Handle(raw) => {
let h = Handle::from_raw(raw);
if let Some(bytes) = realm.string_bytes(h) {
return Ok(Some(quote_wtf8(&bytes)));
}
if realm.is_vm_function(h) {
return Ok(None);
}
let inner = alloc::format!("{cur}{indent}");
let is_container = realm.array_elements(h).is_some() || realm.object_keys(h).is_some();
if is_container {
if seen.contains(&h) || seen.len() >= realm.limits.max_json_depth {
return Err(Circular); }
seen.push(h);
}
let result = if let Some(elems) = realm.array_elements(h).map(<[_]>::to_vec) {
if elems.is_empty() {
Some(String::from("[]"))
} else {
let mut parts = Vec::with_capacity(elems.len());
for e in &elems {
let s = stringify_at(realm, *e, indent, &inner, seen)?
.unwrap_or_else(|| String::from("null"));
parts.push(alloc::format!("{inner}{s}"));
}
Some(alloc::format!("[\n{}\n{cur}]", parts.join(",\n")))
}
} else if let Some(keys) = realm.object_keys(h) {
let mut parts = Vec::new();
for k in keys {
let val = realm.get_property(h, &k).unwrap_or(NanBox::undefined());
if let Some(s) = stringify_at(realm, val, indent, &inner, seen)? {
parts.push(alloc::format!("{inner}{}: {}", quote(&k), s));
}
}
if parts.is_empty() {
Some(String::from("{}"))
} else {
Some(alloc::format!("{{\n{}\n{cur}}}", parts.join(",\n")))
}
} else {
None
};
if is_container {
seen.pop();
}
Ok(result)
}
_ => stringify_seen(realm, v, seen),
}
}
#[must_use]
pub fn quote(s: &str) -> String {
quote_wtf8(s.as_bytes())
}
#[must_use]
pub fn quote_wtf8(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() + 2);
out.push('"');
for cp in crate::wtf8::code_points(bytes) {
match cp {
0x22 => out.push_str("\\\""),
0x5C => out.push_str("\\\\"),
0x0A => out.push_str("\\n"),
0x0D => out.push_str("\\r"),
0x09 => out.push_str("\\t"),
cp if cp < 0x20 || crate::wtf8::is_surrogate(cp) => {
out.push_str(&alloc::format!("\\u{cp:04x}"));
}
cp => {
if let Some(c) = char::from_u32(cp) {
out.push(c);
}
}
}
}
out.push('"');
out
}
pub fn parse(realm: &mut Realm, src: &str) -> Result<NanBox, String> {
let chars: Vec<char> = src.chars().collect();
let mut pos = 0;
let v = parse_value(realm, &chars, &mut pos, 0)?;
skip_ws(&chars, &mut pos);
if pos != chars.len() {
return Err(String::from("Unexpected trailing characters in JSON"));
}
Ok(v)
}
fn parse_value(
realm: &mut Realm,
c: &[char],
pos: &mut usize,
depth: usize,
) -> Result<NanBox, String> {
skip_ws(c, pos);
let Some(&ch) = c.get(*pos) else {
return Err(String::from("Unexpected end of JSON input"));
};
if matches!(ch, '[' | '{') && depth >= realm.limits.max_json_depth {
return Err(String::from("Maximum JSON nesting depth exceeded"));
}
match ch {
'n' => lit(c, pos, "null", NanBox::null()),
't' => lit(c, pos, "true", NanBox::boolean(true)),
'f' => lit(c, pos, "false", NanBox::boolean(false)),
'"' => {
let s = parse_string(c, pos)?;
Ok(NanBox::handle(realm.new_string_wtf8(s).to_raw()))
}
'[' => {
*pos += 1;
let mut elems = Vec::new();
skip_ws(c, pos);
if c.get(*pos) == Some(&']') {
*pos += 1;
return Ok(NanBox::handle(realm.new_array(elems).to_raw()));
}
loop {
let v = parse_value(realm, c, pos, depth + 1)?;
elems.push(v);
skip_ws(c, pos);
match c.get(*pos) {
Some(',') => *pos += 1,
Some(']') => {
*pos += 1;
break;
}
_ => return Err(String::from("Expected ',' or ']' in JSON")),
}
}
Ok(NanBox::handle(realm.new_array(elems).to_raw()))
}
'{' => {
*pos += 1;
let obj = realm.new_object();
skip_ws(c, pos);
if c.get(*pos) == Some(&'}') {
*pos += 1;
return Ok(NanBox::handle(obj.to_raw()));
}
loop {
skip_ws(c, pos);
if c.get(*pos) != Some(&'"') {
return Err(String::from("Expected property name in JSON"));
}
let key = crate::wtf8::to_string_lossy(&parse_string(c, pos)?);
skip_ws(c, pos);
if c.get(*pos) != Some(&':') {
return Err(String::from("Expected ':' in JSON"));
}
*pos += 1;
let v = parse_value(realm, c, pos, depth + 1)?;
realm.set_property(obj, &key, v);
skip_ws(c, pos);
match c.get(*pos) {
Some(',') => *pos += 1,
Some('}') => {
*pos += 1;
break;
}
_ => return Err(String::from("Expected ',' or '}' in JSON")),
}
}
Ok(NanBox::handle(obj.to_raw()))
}
'-' | '0'..='9' => {
let start = *pos;
if c.get(*pos) == Some(&'-') {
*pos += 1;
}
while c
.get(*pos)
.is_some_and(|d| d.is_ascii_digit() || matches!(d, '.' | 'e' | 'E' | '+' | '-'))
{
*pos += 1;
}
let text: String = c[start..*pos].iter().collect();
text.parse::<f64>()
.map(NanBox::number)
.map_err(|_| String::from("Invalid number in JSON"))
}
_ => Err(String::from("Unexpected token in JSON")),
}
}
fn lit(c: &[char], pos: &mut usize, word: &str, value: NanBox) -> Result<NanBox, String> {
if c[*pos..].iter().take(word.len()).copied().eq(word.chars()) {
*pos += word.len();
Ok(value)
} else {
Err(String::from("Unexpected token in JSON"))
}
}
fn parse_string(c: &[char], pos: &mut usize) -> Result<Vec<u8>, String> {
*pos += 1; let mut out: Vec<u8> = Vec::new();
let push_char = |out: &mut Vec<u8>, ch: char| {
let mut buf = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
};
loop {
match c.get(*pos) {
None => return Err(String::from("Unterminated string in JSON")),
Some('"') => {
*pos += 1;
return Ok(out);
}
Some('\\') => {
*pos += 1;
match c.get(*pos) {
Some('"') => out.push(b'"'),
Some('\\') => out.push(b'\\'),
Some('/') => out.push(b'/'),
Some('n') => out.push(b'\n'),
Some('r') => out.push(b'\r'),
Some('t') => out.push(b'\t'),
Some('b') => out.push(0x08),
Some('f') => out.push(0x0C),
Some('u') => {
let hi = parse_hex4(c, *pos + 1)?;
*pos += 4;
if (0xD800..=0xDBFF).contains(&hi)
&& c.get(*pos + 1) == Some(&'\\')
&& c.get(*pos + 2) == Some(&'u')
&& let Ok(lo) = parse_hex4(c, *pos + 3)
&& (0xDC00..=0xDFFF).contains(&lo)
{
let cp = 0x1_0000
+ ((u32::from(hi) - 0xD800) << 10)
+ (u32::from(lo) - 0xDC00);
crate::wtf8::encode_code_point(cp, &mut out);
*pos += 6;
} else {
crate::wtf8::encode_utf16_unit(hi, &mut out);
}
}
_ => return Err(String::from("Invalid escape in JSON")),
}
*pos += 1;
}
Some(&ch) => {
push_char(&mut out, ch);
*pos += 1;
}
}
}
}
fn parse_hex4(c: &[char], at: usize) -> Result<u16, String> {
let hex: String = c.get(at..at + 4).unwrap_or(&[]).iter().collect();
if hex.len() == 4 {
u16::from_str_radix(&hex, 16).map_err(|_| String::from("Invalid \\u escape in JSON"))
} else {
Err(String::from("Invalid \\u escape in JSON"))
}
}
fn skip_ws(c: &[char], pos: &mut usize) {
while c
.get(*pos)
.is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n' | '\r'))
{
*pos += 1;
}
}