#[derive(Debug, Clone, PartialEq, serde::Serialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum V8Value {
Undefined,
Null,
Hole,
Bool(bool),
Int(i64),
Double(f64),
BigInt(String),
String(String),
Date(f64),
RegExp {
source: String,
flags: u32,
},
Array(Vec<V8Value>),
Object(Vec<(String, V8Value)>),
Map(Vec<(V8Value, V8Value)>),
Set(Vec<V8Value>),
ArrayBuffer(Vec<u8>),
NumberObject(f64),
StringObject(String),
BooleanObject(bool),
BigIntObject(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum V8Error {
#[error("truncated at offset {offset}: needed {needed} more byte(s), {available} available")]
Truncated {
offset: usize,
needed: usize,
available: usize,
},
#[error("missing V8 version header at offset {offset}: expected 0xFF, found 0x{found:02x}")]
BadVersion { offset: usize, found: u8 },
#[error("unsupported V8 serialization tag 0x{tag:02x} ({tag:?} as char) at offset {offset}")]
UnsupportedTag { offset: usize, tag: u8 },
#[error(
"unsupported host/embedder object at offset {offset} (kHostObject, Blink tag 0x{blink_tag:02x})"
)]
HostObject { offset: usize, blink_tag: u8 },
#[error("dangling object reference to id {id} at offset {offset}")]
BadReference { offset: usize, id: u64 },
#[error("malformed varint at offset {offset}")]
BadVarint { offset: usize },
#[error("odd-length UTF-16 string ({len} bytes) at offset {offset}")]
OddUtf16 { offset: usize, len: usize },
#[error("unsupported property key type at offset {offset}")]
BadKey { offset: usize },
#[error("recursion depth cap ({cap}) exceeded at offset {offset}")]
DepthCap { offset: usize, cap: usize },
#[error("value/node cap ({cap}) exceeded")]
NodeCap { cap: usize },
#[error("declared length {len} exceeds cap {cap} at offset {offset}")]
LengthCap { offset: usize, len: u64, cap: usize },
}
#[derive(Debug, Clone, Copy)]
pub struct V8Limits {
pub max_depth: usize,
pub max_nodes: usize,
}
impl Default for V8Limits {
fn default() -> Self {
Self {
max_depth: 256,
max_nodes: 4_000_000,
}
}
}
const TAG_VERSION: u8 = 0xFF;
const TAG_THE_HOLE: u8 = b'-';
const TAG_UNDEFINED: u8 = b'_';
const TAG_NULL: u8 = b'0';
const TAG_TRUE: u8 = b'T';
const TAG_FALSE: u8 = b'F';
const TAG_INT32: u8 = b'I';
const TAG_UINT32: u8 = b'U';
const TAG_DOUBLE: u8 = b'N';
const TAG_BIGINT: u8 = b'Z';
const TAG_UTF8_STRING: u8 = b'S';
const TAG_ONE_BYTE_STRING: u8 = b'"';
const TAG_TWO_BYTE_STRING: u8 = b'c';
const TAG_OBJECT_REFERENCE: u8 = b'^';
const TAG_BEGIN_OBJECT: u8 = b'o';
const TAG_END_OBJECT: u8 = b'{';
const TAG_BEGIN_SPARSE_ARRAY: u8 = b'a';
const TAG_END_SPARSE_ARRAY: u8 = b'@';
const TAG_BEGIN_DENSE_ARRAY: u8 = b'A';
const TAG_END_DENSE_ARRAY: u8 = b'$';
const TAG_DATE: u8 = b'D';
const TAG_TRUE_OBJECT: u8 = b'y';
const TAG_FALSE_OBJECT: u8 = b'x';
const TAG_NUMBER_OBJECT: u8 = b'n';
const TAG_BIGINT_OBJECT: u8 = b'z';
const TAG_STRING_OBJECT: u8 = b's';
const TAG_REGEXP: u8 = b'R';
const TAG_BEGIN_MAP: u8 = b';';
const TAG_END_MAP: u8 = b':';
const TAG_BEGIN_SET: u8 = b'\'';
const TAG_END_SET: u8 = b',';
const TAG_ARRAY_BUFFER: u8 = b'B';
const TAG_HOST_OBJECT: u8 = b'\\';
const BLINK_TRAILER_OFFSET: u8 = 0xFE;
pub fn deserialize(bytes: &[u8]) -> Result<V8Value, V8Error> {
deserialize_with_limits(bytes, V8Limits::default())
}
pub fn deserialize_with_limits(bytes: &[u8], limits: V8Limits) -> Result<V8Value, V8Error> {
let mut r = Reader::new(bytes, limits);
r.read_version_header()?;
r.read_value(0)
}
pub fn deserialize_blink(bytes: &[u8]) -> Result<V8Value, V8Error> {
deserialize_blink_with_limits(bytes, V8Limits::default())
}
pub fn deserialize_blink_with_limits(bytes: &[u8], limits: V8Limits) -> Result<V8Value, V8Error> {
let mut r = Reader::new(bytes, limits);
if r.peek() == Some(TAG_VERSION) {
r.pos += 1;
let _blink_version = r.read_varint()?;
while r.peek() == Some(BLINK_TRAILER_OFFSET) {
r.pos += 1;
r.take(12)?;
}
}
r.read_version_header()?;
r.read_value(0)
}
#[must_use]
pub fn is_value_tag(tag: u8) -> bool {
matches!(
tag,
TAG_THE_HOLE
| TAG_UNDEFINED
| TAG_NULL
| TAG_TRUE
| TAG_FALSE
| TAG_INT32
| TAG_UINT32
| TAG_DOUBLE
| TAG_BIGINT
| TAG_UTF8_STRING
| TAG_ONE_BYTE_STRING
| TAG_TWO_BYTE_STRING
| TAG_OBJECT_REFERENCE
| TAG_BEGIN_OBJECT
| TAG_BEGIN_SPARSE_ARRAY
| TAG_BEGIN_DENSE_ARRAY
| TAG_DATE
| TAG_TRUE_OBJECT
| TAG_FALSE_OBJECT
| TAG_NUMBER_OBJECT
| TAG_BIGINT_OBJECT
| TAG_STRING_OBJECT
| TAG_REGEXP
| TAG_BEGIN_MAP
| TAG_BEGIN_SET
| TAG_ARRAY_BUFFER
| TAG_HOST_OBJECT
)
}
impl V8Value {
#[must_use]
pub fn summary(&self) -> String {
match self {
Self::Undefined => "undefined".to_owned(),
Self::Null => "null".to_owned(),
Self::Hole => "hole".to_owned(),
Self::Bool(b) => format!("boolean {b}"),
Self::Int(i) => format!("integer {i}"),
Self::Double(d) => format!("number {d}"),
Self::BigInt(s) => format!("bigint {s}n"),
Self::String(s) => format!("string {:?}", ellipsize(s)),
Self::Date(ms) => format!("date ({ms} ms)"),
Self::RegExp { source, flags } => {
format!("regexp /{}/ (flags {flags})", ellipsize(source))
}
Self::Array(v) => format!("array ({} element{})", v.len(), plural(v.len())),
Self::Object(kv) => format!("object ({} key{})", kv.len(), plural(kv.len())),
Self::Map(kv) => format!(
"map ({} entr{})",
kv.len(),
if kv.len() == 1 { "y" } else { "ies" }
),
Self::Set(v) => format!("set ({} member{})", v.len(), plural(v.len())),
Self::ArrayBuffer(b) => format!("arraybuffer ({} byte{})", b.len(), plural(b.len())),
Self::NumberObject(d) => format!("Number({d})"),
Self::StringObject(s) => format!("String({:?})", ellipsize(s)),
Self::BooleanObject(b) => format!("Boolean({b})"),
Self::BigIntObject(s) => format!("BigInt({s})"),
}
}
}
fn plural(n: usize) -> &'static str {
if n == 1 {
""
} else {
"s"
}
}
fn ellipsize(s: &str) -> String {
const MAX: usize = 32;
if s.chars().count() <= MAX {
s.to_owned()
} else {
let head: String = s.chars().take(MAX).collect();
format!("{head}…")
}
}
struct Reader<'a> {
data: &'a [u8],
pos: usize,
limits: V8Limits,
id_map: Vec<Option<V8Value>>,
budget: usize,
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8], limits: V8Limits) -> Self {
let budget = limits.max_nodes;
Self {
data,
pos: 0,
limits,
id_map: Vec::new(),
budget,
}
}
fn peek(&self) -> Option<u8> {
self.data.get(self.pos).copied()
}
fn read_u8(&mut self) -> Result<u8, V8Error> {
let b = self.data.get(self.pos).copied().ok_or(V8Error::Truncated {
offset: self.pos,
needed: 1,
available: 0,
})?;
self.pos += 1;
Ok(b)
}
fn take(&mut self, n: usize) -> Result<&'a [u8], V8Error> {
let end = self.pos.checked_add(n).ok_or(V8Error::Truncated {
offset: self.pos,
needed: n,
available: self.data.len().saturating_sub(self.pos),
})?;
let slice = self.data.get(self.pos..end).ok_or(V8Error::Truncated {
offset: self.pos,
needed: n,
available: self.data.len().saturating_sub(self.pos),
})?;
self.pos = end;
Ok(slice)
}
fn read_varint(&mut self) -> Result<u64, V8Error> {
let start = self.pos;
let mut result: u64 = 0;
let mut shift: u32 = 0;
loop {
if shift >= 64 {
return Err(V8Error::BadVarint { offset: start });
}
let byte = self.read_u8()?;
result |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Ok(result);
}
shift += 7;
}
}
fn read_zigzag(&mut self) -> Result<i64, V8Error> {
let u = self.read_varint()?;
Ok(((u >> 1) as i64) ^ -((u & 1) as i64))
}
fn read_f64_le(&mut self) -> Result<f64, V8Error> {
let b = self.take(8)?;
let mut arr = [0u8; 8];
arr.copy_from_slice(b);
Ok(f64::from_le_bytes(arr))
}
fn read_version_header(&mut self) -> Result<(), V8Error> {
let offset = self.pos;
let tag = self.read_u8()?;
if tag != TAG_VERSION {
return Err(V8Error::BadVersion { offset, found: tag });
}
let _version = self.read_varint()?;
Ok(())
}
fn charge(&mut self, n: usize) -> Result<(), V8Error> {
if n > self.budget {
return Err(V8Error::NodeCap {
cap: self.limits.max_nodes,
});
}
self.budget -= n;
Ok(())
}
fn reserve_id(&mut self) -> usize {
let id = self.id_map.len();
self.id_map.push(None);
id
}
fn fill_id(&mut self, id: usize, value: &V8Value) {
if let Some(slot) = self.id_map.get_mut(id) {
*slot = Some(value.clone());
}
}
fn read_value(&mut self, depth: usize) -> Result<V8Value, V8Error> {
if depth > self.limits.max_depth {
return Err(V8Error::DepthCap {
offset: self.pos,
cap: self.limits.max_depth,
});
}
self.charge(1)?;
let offset = self.pos;
let tag = self.read_u8()?;
match tag {
TAG_UNDEFINED => Ok(V8Value::Undefined),
TAG_NULL => Ok(V8Value::Null),
TAG_THE_HOLE => Ok(V8Value::Hole),
TAG_TRUE => Ok(V8Value::Bool(true)),
TAG_FALSE => Ok(V8Value::Bool(false)),
TAG_INT32 => Ok(V8Value::Int(self.read_zigzag()?)),
TAG_UINT32 => Ok(V8Value::Int(self.read_varint()? as i64)),
TAG_DOUBLE => Ok(V8Value::Double(self.read_f64_le()?)),
TAG_BIGINT => Ok(V8Value::BigInt(self.read_bigint()?)),
TAG_UTF8_STRING => Ok(V8Value::String(self.read_utf8_string()?)),
TAG_ONE_BYTE_STRING => Ok(V8Value::String(self.read_one_byte_string()?)),
TAG_TWO_BYTE_STRING => Ok(V8Value::String(self.read_two_byte_string()?)),
TAG_DATE => {
let id = self.reserve_id();
let v = V8Value::Date(self.read_f64_le()?);
self.fill_id(id, &v);
Ok(v)
}
TAG_BEGIN_OBJECT => self.read_js_object(depth),
TAG_BEGIN_DENSE_ARRAY => self.read_dense_array(depth),
TAG_BEGIN_SPARSE_ARRAY => self.read_sparse_array(depth),
TAG_BEGIN_MAP => self.read_map(depth),
TAG_BEGIN_SET => self.read_set(depth),
TAG_ARRAY_BUFFER => self.read_array_buffer(),
TAG_REGEXP => self.read_regexp(depth),
TAG_NUMBER_OBJECT => {
let id = self.reserve_id();
let v = V8Value::NumberObject(self.read_f64_le()?);
self.fill_id(id, &v);
Ok(v)
}
TAG_TRUE_OBJECT => {
let id = self.reserve_id();
let v = V8Value::BooleanObject(true);
self.fill_id(id, &v);
Ok(v)
}
TAG_FALSE_OBJECT => {
let id = self.reserve_id();
let v = V8Value::BooleanObject(false);
self.fill_id(id, &v);
Ok(v)
}
TAG_STRING_OBJECT => {
let id = self.reserve_id();
let inner = self.read_value(depth + 1)?;
let V8Value::String(s) = inner else {
return Err(V8Error::UnsupportedTag { offset, tag });
};
let v = V8Value::StringObject(s);
self.fill_id(id, &v);
Ok(v)
}
TAG_BIGINT_OBJECT => {
let id = self.reserve_id();
let v = V8Value::BigIntObject(self.read_bigint()?);
self.fill_id(id, &v);
Ok(v)
}
TAG_OBJECT_REFERENCE => self.read_reference(),
TAG_HOST_OBJECT => {
let blink_tag = self.read_u8().unwrap_or(0);
Err(V8Error::HostObject { offset, blink_tag })
}
_ => Err(V8Error::UnsupportedTag { offset, tag }),
}
}
fn read_utf8_string(&mut self) -> Result<String, V8Error> {
let raw = self.read_varint()?;
let len = self.checked_len(raw)?;
let bytes = self.take(len)?;
Ok(String::from_utf8_lossy(bytes).into_owned())
}
fn read_one_byte_string(&mut self) -> Result<String, V8Error> {
let raw = self.read_varint()?;
let len = self.checked_len(raw)?;
let bytes = self.take(len)?;
Ok(bytes.iter().map(|&b| b as char).collect())
}
fn read_two_byte_string(&mut self) -> Result<String, V8Error> {
let offset = self.pos;
let raw = self.read_varint()?;
let len = self.checked_len(raw)?;
if len % 2 != 0 {
return Err(V8Error::OddUtf16 { offset, len });
}
let bytes = self.take(len)?;
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Ok(String::from_utf16_lossy(&units))
}
fn read_bigint(&mut self) -> Result<String, V8Error> {
let offset = self.pos;
let bitfield = self.read_varint()?;
let negative = bitfield & 1 == 1;
let byte_len = self.checked_len(bitfield >> 1)?;
let digits = self.take(byte_len)?;
let magnitude = le_bytes_to_decimal(digits);
if magnitude == "0" {
return Ok(magnitude);
}
if negative {
Ok(format!("-{magnitude}"))
} else {
let _ = offset;
Ok(magnitude)
}
}
fn read_array_buffer(&mut self) -> Result<V8Value, V8Error> {
let id = self.reserve_id();
let raw = self.read_varint()?;
let len = self.checked_len(raw)?;
let bytes = self.take(len)?.to_vec();
let v = V8Value::ArrayBuffer(bytes);
self.fill_id(id, &v);
Ok(v)
}
fn read_regexp(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let offset = self.pos;
let id = self.reserve_id();
let V8Value::String(source) = self.read_value(depth + 1)? else {
return Err(V8Error::BadKey { offset });
};
let flags = u32::try_from(self.read_varint()?).unwrap_or(u32::MAX);
let v = V8Value::RegExp { source, flags };
self.fill_id(id, &v);
Ok(v)
}
fn read_js_object(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let id = self.reserve_id();
let mut props = Vec::new();
loop {
if self.peek() == Some(TAG_END_OBJECT) {
self.pos += 1;
break;
}
let key = self.read_property_key(depth + 1)?;
let value = self.read_value(depth + 1)?;
props.push((key, value));
}
let _count = self.read_varint()?;
let v = V8Value::Object(props);
self.fill_id(id, &v);
Ok(v)
}
fn read_dense_array(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let raw = self.read_varint()?;
let length = self.checked_len(raw)?;
let id = self.reserve_id();
let mut elems = Vec::new();
for _ in 0..length {
elems.push(self.read_value(depth + 1)?);
}
let offset = self.pos;
let end = self.read_u8()?;
if end != TAG_END_DENSE_ARRAY {
return Err(V8Error::UnsupportedTag { offset, tag: end });
}
let num_props = self.read_varint()?;
for _ in 0..num_props {
let _k = self.read_property_key(depth + 1)?;
let _v = self.read_value(depth + 1)?;
}
let _length_again = self.read_varint()?;
let v = V8Value::Array(elems);
self.fill_id(id, &v);
Ok(v)
}
fn read_sparse_array(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let raw = self.read_varint()?;
let length = self.checked_len(raw)?;
let id = self.reserve_id();
self.charge(length)?;
let mut elems = vec![V8Value::Hole; length];
loop {
if self.peek() == Some(TAG_END_SPARSE_ARRAY) {
self.pos += 1;
break;
}
let key = self.read_value(depth + 1)?;
let value = self.read_value(depth + 1)?;
if let V8Value::Int(i) = key {
if let Ok(idx) = usize::try_from(i) {
if idx < elems.len() {
elems[idx] = value;
}
}
}
}
let _num_props = self.read_varint()?;
let _length_again = self.read_varint()?;
let v = V8Value::Array(elems);
self.fill_id(id, &v);
Ok(v)
}
fn read_map(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let id = self.reserve_id();
let mut entries = Vec::new();
loop {
if self.peek() == Some(TAG_END_MAP) {
self.pos += 1;
break;
}
let key = self.read_value(depth + 1)?;
let value = self.read_value(depth + 1)?;
entries.push((key, value));
}
let _count = self.read_varint()?;
let v = V8Value::Map(entries);
self.fill_id(id, &v);
Ok(v)
}
fn read_set(&mut self, depth: usize) -> Result<V8Value, V8Error> {
let id = self.reserve_id();
let mut members = Vec::new();
loop {
if self.peek() == Some(TAG_END_SET) {
self.pos += 1;
break;
}
members.push(self.read_value(depth + 1)?);
}
let _count = self.read_varint()?;
let v = V8Value::Set(members);
self.fill_id(id, &v);
Ok(v)
}
fn read_property_key(&mut self, depth: usize) -> Result<String, V8Error> {
let offset = self.pos;
match self.read_value(depth)? {
V8Value::String(s) => Ok(s),
V8Value::Int(i) => Ok(i.to_string()),
V8Value::Double(d) => Ok(format!("{d}")),
_ => Err(V8Error::BadKey { offset }),
}
}
fn read_reference(&mut self) -> Result<V8Value, V8Error> {
let offset = self.pos;
let id = self.read_varint()?;
let idx = usize::try_from(id).map_err(|_| V8Error::BadReference { offset, id })?;
let value = self
.id_map
.get(idx)
.cloned()
.flatten()
.ok_or(V8Error::BadReference { offset, id })?;
self.charge(count_nodes(&value))?;
Ok(value)
}
fn checked_len(&self, len: u64) -> Result<usize, V8Error> {
let cap = self.limits.max_nodes as u64;
if len > cap {
return Err(V8Error::LengthCap {
offset: self.pos,
len,
cap: self.limits.max_nodes,
});
}
usize::try_from(len).map_err(|_| V8Error::LengthCap {
offset: self.pos,
len,
cap: self.limits.max_nodes,
})
}
}
fn count_nodes(v: &V8Value) -> usize {
match v {
V8Value::Array(items) | V8Value::Set(items) => {
1 + items.iter().map(count_nodes).sum::<usize>()
}
V8Value::Object(kv) => 1 + kv.iter().map(|(_, val)| count_nodes(val)).sum::<usize>(),
V8Value::Map(kv) => {
1 + kv
.iter()
.map(|(k, val)| count_nodes(k) + count_nodes(val))
.sum::<usize>()
}
_ => 1,
}
}
fn le_bytes_to_decimal(bytes: &[u8]) -> String {
let mut decimal: Vec<u8> = vec![0]; for &byte in bytes.iter().rev() {
let mut carry = u32::from(byte);
for d in &mut decimal {
let v = u32::from(*d) * 256 + carry;
*d = (v % 10) as u8;
carry = v / 10;
}
while carry > 0 {
decimal.push((carry % 10) as u8);
carry /= 10;
}
}
while decimal.len() > 1 && *decimal.last().unwrap_or(&0) == 0 {
decimal.pop();
}
decimal.iter().rev().map(|d| (b'0' + d) as char).collect()
}