use crate::tables::VersionTable;
use serde::Serialize;
use std::borrow::Cow;
pub type ObjId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Ref {
Object(ObjId),
Root(usize),
RoRef(u32, u32),
Attached(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Raw {
pub off: usize,
pub len: usize,
pub advances: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Ty {
Pending,
Unknown,
Root(u16),
MapObj(u16),
Ro(u32, u32),
Str(StrKind),
Structural(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum StrKind {
OneByte,
TwoByte,
Unknown,
}
impl Ty {
pub fn name<'t>(&self, table: &'t VersionTable) -> Cow<'t, str> {
match self {
Ty::Pending => Cow::Borrowed("?pending"),
Ty::Unknown => Cow::Borrowed("?unknown"),
Ty::Root(i) => match table.root_name(*i as usize) {
Some(n) => Cow::Borrowed(n),
None => Cow::Owned(format!("root#{i}")),
},
Ty::MapObj(it) => Cow::Owned(format!("Map(instance_type={it})")),
Ty::Ro(c, o) => Cow::Owned(format!("?ro:{c}/{o}")),
Ty::Str(k) => Cow::Borrowed(match k {
StrKind::OneByte => "OneByteString",
StrKind::TwoByte => "TwoByteString",
StrKind::Unknown => "String",
}),
Ty::Structural(s) => Cow::Borrowed(s),
}
}
pub fn is(&self, table: &VersionTable, want: &str) -> bool {
let n = self.name(table);
let base = n
.strip_prefix("String:")
.or_else(|| n.strip_prefix("Symbol:"))
.unwrap_or(&n);
let untrusted = base.strip_prefix("Trusted").unwrap_or(base);
let stem = |s: &str| s.strip_suffix("Map").unwrap_or(s).to_string();
base == want
|| stem(base) == want
|| untrusted == want
|| stem(untrusted) == want
}
pub fn is_string(&self, table: &VersionTable) -> bool {
match self {
Ty::Str(_) => true,
Ty::Root(i) => table
.roots
.get(*i as usize)
.map(|n| n.starts_with("String:"))
.unwrap_or(false),
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Object {
pub ty: Ty,
pub byte_size: usize,
pub start_offset: usize,
pub slots: Vec<Slot>,
pub raw_len: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct Slot {
pub index: usize,
pub value: SlotValue,
}
#[derive(Debug, Clone, Serialize)]
pub enum SlotValue {
Ref(Ref),
Repeat(usize, Box<SlotValue>),
RepeatPrev(usize),
Skip(usize),
StreamBytes(usize),
Raw(Raw),
ClearedWeak,
WeakRef(Box<SlotValue>),
PendingRef(u32),
}
impl SlotValue {
pub fn as_ref(&self) -> Option<Ref> {
match self {
SlotValue::Ref(r) => Some(*r),
SlotValue::WeakRef(inner) => inner.as_ref().as_ref(),
_ => None,
}
}
pub fn as_raw(&self) -> Option<Raw> {
match self {
SlotValue::Raw(r) => Some(*r),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Elem<'a> {
Ref(Ref),
Smi(i64),
Bytes(&'a [u8]),
}
impl<'a> Elem<'a> {
pub fn as_ref(&self) -> Option<Ref> {
match self {
Elem::Ref(r) => Some(*r),
_ => None,
}
}
pub fn as_smi(&self) -> Option<i64> {
match self {
Elem::Smi(v) => Some(*v),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
enum HotEntry {
Object(ObjId),
Root(usize),
Ro(u32, u32),
}
#[derive(Debug, Clone, Default)]
struct HotRing {
slots: [Option<HotEntry>; 8],
index: usize,
}
impl HotRing {
#[inline]
fn add(&mut self, e: HotEntry) {
self.slots[self.index] = Some(e);
self.index = (self.index + 1) & 7;
}
#[inline]
fn get(&self, i: usize) -> Option<HotEntry> {
self.slots.get(i).copied().flatten()
}
}
pub struct CodeCache<'a> {
payload: &'a [u8],
tagged_size: usize,
pub objects: Vec<Object>,
pub top_sfi: ObjId,
}
struct Walker<'a> {
data: &'a [u8],
pos: usize,
tagged_size: usize,
tags: &'a std::collections::HashMap<String, u8>,
legacy_consts: &'a std::collections::HashMap<String, u32>,
legacy: Option<LegacyAlloc>,
objects: Vec<Object>,
hot: HotRing,
pending: std::collections::HashMap<u32, Vec<(ObjId, usize)>>,
extra_skip: usize,
table: &'a VersionTable,
}
type R<T> = Result<T, String>;
impl<'a> Walker<'a> {
#[inline]
fn tag(&self, name: &str) -> R<u8> {
self.tags
.get(name)
.copied()
.ok_or_else(|| format!("tag {name} missing from table"))
}
#[inline]
fn opt_tag(&self, name: &str) -> Option<u8> {
self.tags.get(name).copied()
}
#[inline]
fn tag_any(&self, names: &[&str]) -> Option<u8> {
names.iter().find_map(|n| self.opt_tag(n))
}
#[inline]
fn byte(&mut self) -> R<u8> {
let b = *self.data.get(self.pos).ok_or("unexpected end of payload")?;
self.pos += 1;
Ok(b)
}
#[inline]
fn peek(&self) -> Option<u8> {
self.data.get(self.pos).copied()
}
#[inline]
fn putint(&mut self) -> R<u32> {
let b0 = self.byte()?;
let n = (b0 & 3) as usize + 1;
let mut raw = b0 as u32;
for i in 1..n {
raw |= (self.byte()? as u32) << (8 * i);
}
Ok(raw >> 2)
}
#[inline]
fn raw(&mut self, n: usize) -> R<Raw> {
if self.pos + n > self.data.len() {
return Err(format!("raw read {n} bytes overruns payload"));
}
let r = Raw {
off: self.pos,
len: n,
advances: true,
};
self.pos += n;
Ok(r)
}
fn obj_map_name(&self, id: ObjId) -> String {
match self.objects.get(id).and_then(|o| o.slots.first()) {
Some(Slot { value: SlotValue::Ref(Ref::Root(k)), .. }) => {
self.table.root_name(*k).unwrap_or("?").to_string()
}
Some(Slot { value: SlotValue::Ref(Ref::Object(m)), .. }) => format!("map#{m}"),
_ => "?".into(),
}
}
#[inline]
fn lnum(&self, name: &str) -> Option<u32> {
self.legacy_consts.get(name).copied()
}
#[inline]
fn is_six_x(&self) -> bool {
self.legacy_consts.contains_key("kBackrefWithSkip")
}
#[inline]
fn n_spaces(&self) -> u8 {
self.lnum("kNumberOfSpaces").map(|v| v as u8).unwrap_or(6)
}
#[inline]
fn map_space(&self) -> u8 {
self.lnum("kMapSpace").map(|v| v as u8).unwrap_or(4)
}
#[inline]
fn lo_space(&self) -> u8 {
self.lnum("kLoSpace").map(|v| v as u8).unwrap_or(5)
}
fn hot_tag_index(&self, b: u8) -> Option<(bool, usize)> {
let plain = self.opt_tag("kHotObject")?;
if (plain..plain + 8).contains(&b) {
return Some((false, (b & 7) as usize));
}
if let Some(hs) = self.opt_tag("kHotObjectWithSkip") {
if (hs..hs + 8).contains(&b) {
return Some((true, (b & 7) as usize));
}
}
None
}
fn root_const_tag_index(&self, b: u8) -> Option<(bool, usize)> {
let mask = self.lnum("kRootArrayConstantsMask").unwrap_or(0x1f) as u8;
let plain = self.opt_tag("kRootArrayConstants")?;
if (plain..plain + mask + 1).contains(&b) {
return Some((false, (b & mask) as usize));
}
if let Some(hs) = self.opt_tag("kRootArrayConstantsWithSkip") {
if (hs..hs + mask + 1).contains(&b) {
return Some((true, (b & mask) as usize));
}
}
None
}
#[inline]
fn push_slot(&mut self, id: ObjId, index: usize, value: SlotValue) {
if let Ok(want) = std::env::var("JSCD_DBG_OBJSLOTS") {
if want == "*" || want.parse::<usize>() == Ok(id) {
eprintln!("[slot] obj={id} idx={index} value={value:?}");
}
}
self.objects[id].slots.push(Slot { index, value });
}
fn parse_ref(&mut self, depth: usize) -> R<SlotValue> {
if depth > 64 {
return Err("ref recursion too deep".into());
}
if std::env::var("JSCD_DBG_REF").is_err() {
return self.parse_ref_inner(depth);
}
let ref_pos = self.pos;
let b0 = self.data.get(ref_pos).copied().unwrap_or(0);
let t_new = self.opt_tag("kNewObject").unwrap_or(0);
let new_spaces = if self.legacy.is_some() {
self.n_spaces()
} else {
0x08
};
let is_new = (t_new..t_new + new_spaces).contains(&b0);
if is_new {
let a0 = self.data.get(ref_pos + 1).copied().unwrap_or(0) as u32;
let a1 = self.data.get(ref_pos + 2).copied().unwrap_or(0) as u32;
let a2 = self.data.get(ref_pos + 3).copied().unwrap_or(0) as u32;
let a3 = self.data.get(ref_pos + 4).copied().unwrap_or(0) as u32;
let raw = a0 | (a1 << 8) | (a2 << 16) | (a3 << 24);
let bytes = (raw & 3) + 1;
let mask = 0xffff_ffffu32 >> (32 - (bytes << 3));
let words = (raw & mask) >> 2;
eprintln!("[ref] new pos={ref_pos} space={} words={words}", b0 - t_new);
}
let r = self.parse_ref_inner(depth);
match &r {
Err(e) => {
eprintln!("[ref] pos={ref_pos} ERR {e}");
return r;
}
Ok(_) if is_new => return r,
Ok(_) => {}
}
let t_backref = self.opt_tag("kBackref").unwrap_or(8);
let _t_hot = self.opt_tag("kHotObject").unwrap_or(0x90);
let t_root = self.opt_tag("kRootArray");
let _t_rootc = self.opt_tag("kRootArrayConstants").unwrap_or(0x40);
let desc = match r.as_ref().unwrap() {
SlotValue::Ref(Ref::Object(id)) => format!("obj={id} ty={}", self.obj_map_name(*id)),
SlotValue::Ref(Ref::Root(ix)) => format!("root={ix}"),
SlotValue::Ref(Ref::RoRef(c, o)) => format!("ro=({c},{o})"),
SlotValue::Ref(Ref::Attached(i)) => format!("attached={i}"),
SlotValue::Raw(_) => "raw".into(),
SlotValue::Repeat(n, _) => format!("repeat={n}"),
SlotValue::RepeatPrev(n) => format!("repeatprev={n}"),
SlotValue::Skip(n) => format!("skip={n}"),
SlotValue::StreamBytes(n) => format!("streambytes={n}"),
SlotValue::WeakRef(_) => "weak".into(),
SlotValue::ClearedWeak => "cleared".into(),
SlotValue::PendingRef(_) => "pending".into(),
};
let n_sp_dbg = self.n_spaces();
if (t_backref..t_backref + n_sp_dbg).contains(&b0) {
eprintln!("[ref] backref pos={ref_pos} space={} {desc}", b0 - t_backref);
} else if let Some((ws, i)) = self.hot_tag_index(b0) {
eprintln!("[ref] hot pos={ref_pos} i={i} skip={ws} {desc}");
} else if Some(b0) == t_root {
eprintln!("[ref] root pos={ref_pos} {desc}");
} else if let Some((ws, i)) = self.root_const_tag_index(b0) {
eprintln!("[ref] rootconst pos={ref_pos} i={i} skip={ws} {desc}");
} else {
eprintln!("[ref] other pos={ref_pos} b={b0:#04x} {desc}");
}
r
}
fn parse_ref_inner(&mut self, depth: usize) -> R<SlotValue> {
let b_raw = self.byte()?;
let b = b_raw;
if self.legacy.is_some() {
if std::env::var("JSCD_DBG_LEGACY").is_ok() {
eprintln!("[legacy-ref] pos={} b={b_raw:#04x}", self.pos - 1);
}
let t_new = self.tag("kNewObject")?;
let t_backref = self.tag("kBackref")?;
let n_sp = self.n_spaces();
if (t_new..t_new + n_sp).contains(&b_raw) {
let space = b_raw - t_new;
let id = self.parse_new_object_space(depth, space)?;
return Ok(SlotValue::Ref(Ref::Object(id)));
}
if self.is_six_x() {
if let Some(t_bs) = self.opt_tag("kBackrefWithSkip") {
if (t_bs..t_bs + n_sp).contains(&b_raw) {
let skip = self.putint()? as usize;
self.extra_skip += skip;
return self.legacy_backref(b_raw - t_bs);
}
}
}
if (t_backref..t_backref + n_sp).contains(&b_raw) {
return self.legacy_backref(b_raw - t_backref);
}
if Some(b_raw) == self.opt_tag("kNextChunk") {
let space = self.byte()?;
if let Some(l) = self.legacy.as_mut() {
l.next_chunk(space);
}
return self.parse_ref(depth + 1);
}
if let Some(align) = self.opt_tag("kAlignmentPrefix") {
if (align..align + 3).contains(&b_raw) {
let a = (b_raw - align + 1) as u32;
if let Some(l) = self.legacy.as_mut() {
l.align = a;
}
return self.parse_ref(depth + 1);
}
}
}
let t_new = self.tag("kNewObject")?;
let t_backref = self.tag("kBackref")?;
let _t_hot = self.tag("kHotObject")?;
let _t_root_const = self.tag("kRootArrayConstants")?;
let t_fixed_raw = self.tag("kFixedRawData")?;
let t_fixed_repeat = self.tag_any(&["kFixedRepeat", "kFixedRepeatRoot"]);
let t_fixed_repeat_root_only =
self.opt_tag("kFixedRepeat").is_none() && self.opt_tag("kFixedRepeatRoot").is_some();
let n_new = if self.legacy.is_some() {
self.n_spaces()
} else {
t_backref.saturating_sub(t_new)
};
if (t_new..t_new.saturating_add(n_new)).contains(&b) {
let id = self.parse_new_object(depth)?;
Ok(SlotValue::Ref(Ref::Object(id)))
} else if b == t_backref {
let idx = self.putint()? as usize;
if idx >= self.objects.len() {
return Err(format!("backref {idx} out of range ({})", self.objects.len()));
}
self.hot.add(HotEntry::Object(idx)); Ok(SlotValue::Ref(Ref::Object(idx)))
} else if let Some((with_skip, i)) = self.hot_tag_index(b_raw) {
if with_skip {
let skip = self.putint()? as usize;
self.extra_skip += skip;
}
match self.hot.get(i) {
Some(HotEntry::Object(id)) => Ok(SlotValue::Ref(Ref::Object(id))),
Some(HotEntry::Root(r)) => Ok(SlotValue::Ref(Ref::Root(r))),
Some(HotEntry::Ro(c, o)) => Ok(SlotValue::Ref(Ref::RoRef(c, o))),
None => Err(format!("hot object index {i} out of ring")),
}
} else if b == self.tag("kRootArray")? {
let idx = self.putint()? as usize;
self.hot.add(HotEntry::Root(idx));
Ok(SlotValue::Ref(Ref::Root(idx)))
} else if let Some((with_skip, i)) = self.root_const_tag_index(b_raw) {
if with_skip {
let skip = self.putint()? as usize;
self.extra_skip += skip;
}
Ok(SlotValue::Ref(Ref::Root(i)))
} else if Some(b) == self.opt_tag("kReadOnlyHeapRef") {
let c = self.putint()?;
let o = self.putint()?;
Ok(SlotValue::Ref(Ref::RoRef(c, o)))
} else if b == self.tag("kAttachedReference")? {
Ok(SlotValue::Ref(Ref::Attached(self.putint()? as usize)))
} else if self.is_six_x() && Some(b) == self.opt_tag("kBuiltin") {
let i = self.putint()?;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX - 1, i)))
} else if self.is_six_x() && Some(b) == self.opt_tag("kExternalReference") {
let i = self.putint()?;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX - 2, i)))
} else if self.is_six_x() && Some(b) == self.opt_tag("kApiReference") {
let skip = self.putint()? as usize;
self.extra_skip += skip;
let i = self.putint()?;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX - 3, i)))
} else if self.is_six_x()
&& (Some(b) == self.opt_tag("kInternalReference")
|| Some(b) == self.opt_tag("kInternalReferenceEncoded"))
{
let _ = self.putint()?;
let _ = self.putint()?;
Ok(SlotValue::Skip(0))
} else if self.is_six_x()
&& (Some(b) == self.opt_tag("kDeoptimizerEntryPlain")
|| Some(b) == self.opt_tag("kDeoptimizerEntryFromCode"))
{
let _ = self.putint()?;
Ok(SlotValue::Skip(0))
} else if self.is_six_x() && Some(b) == self.opt_tag("kOffHeapBackingStore") {
let len = self.putint()? as usize;
Ok(SlotValue::StreamBytes(len))
} else if self.is_six_x() && Some(b) == self.opt_tag("kVariableRawCode") {
let len = self.putint()? as usize;
Ok(SlotValue::StreamBytes(len))
} else if self.is_six_x() && Some(b) == self.opt_tag("kEmbedderFieldsData") {
let _ = self.putint()?;
let len = self.putint()? as usize;
Ok(SlotValue::StreamBytes(len))
} else if self.is_six_x() && Some(b) == self.opt_tag("kOffHeapTarget") {
let skip = self.putint()? as usize;
self.extra_skip += skip;
let _ = self.putint()?;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX - 4, 0)))
} else if Some(b) == self.opt_tag("kPartialSnapshotCache") {
let i = self.putint()? as usize;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX, i as u32)))
} else if Some(b) == self.opt_tag("kStartupObjectCache")
|| Some(b) == self.opt_tag("kReadOnlyObjectCache")
|| Some(b) == self.opt_tag("kSharedHeapObjectCache")
{
let i = self.putint()? as usize;
Ok(SlotValue::Ref(Ref::RoRef(u32::MAX, i as u32)))
} else if Some(b) == self.opt_tag("kRegisterPendingForwardRef") {
Ok(SlotValue::PendingRef(self.putint()?))
} else if Some(b) == self.opt_tag("kClearedWeakReference") {
Ok(SlotValue::ClearedWeak)
} else if Some(b) == self.opt_tag("kWeakPrefix") {
let inner = self.parse_ref(depth + 1)?;
Ok(SlotValue::WeakRef(Box::new(inner)))
} else if Some(b) == self.opt_tag("kIndirectPointerPrefix")
|| Some(b) == self.opt_tag("kProtectedPointerPrefix")
{
let inner = self.parse_ref(depth + 1)?;
Ok(inner)
} else if Some(b) == self.opt_tag("kInitializeSelfIndirectPointer") {
Ok(SlotValue::ClearedWeak)
} else if Some(b) == self.opt_tag("kAllocateJSDispatchEntry")
|| Some(b) == self.opt_tag("kJSDispatchEntry")
{
let _ = self.putint()?;
Ok(SlotValue::ClearedWeak)
} else if (t_fixed_raw..t_fixed_raw + 32).contains(&b) {
let n = (b - t_fixed_raw + 1) as usize;
let len = n * self.tagged_size;
Ok(SlotValue::Raw(Raw { advances: true, ..self.raw(len)? }))
} else if Some(b) == self.opt_tag("kVariableRawData") {
let n = self.putint()? as usize;
let bytes = if self.legacy.is_some() { n } else { n * self.tagged_size };
let advances = self.table.var_raw_advances;
Ok(SlotValue::Raw(Raw {
advances,
..self.raw(bytes.max(1))?
}))
} else if Some(b) == self.opt_tag("kSkip") {
let bytes = self.putint()? as usize;
Ok(SlotValue::Skip(bytes))
} else if t_fixed_repeat.is_some_and(|t| (t..t + 16).contains(&b)) {
let base = t_fixed_repeat.unwrap();
if self.is_six_x() {
return Ok(SlotValue::RepeatPrev((b - base + 1) as usize));
}
let n = (b - base + 2) as usize;
if t_fixed_repeat_root_only {
let root = self.byte()? as usize;
Ok(SlotValue::Repeat(n, Box::new(SlotValue::Ref(Ref::Root(root)))))
} else {
let inner = self.parse_ref(depth + 1)?;
Ok(SlotValue::Repeat(n, Box::new(inner)))
}
} else if Some(b) == self.tag_any(&["kVariableRepeat"]) {
if self.is_six_x() {
let n = self.putint()? as usize;
return Ok(SlotValue::RepeatPrev(n));
}
let n = self.putint()? as usize + 18;
let inner = self.parse_ref(depth + 1)?;
Ok(SlotValue::Repeat(n, Box::new(inner)))
} else if Some(b) == self.opt_tag("kVariableRepeatRoot") {
let n = self.putint()? as usize + 18;
let root = self.byte()? as usize;
Ok(SlotValue::Repeat(n, Box::new(SlotValue::Ref(Ref::Root(root)))))
} else if Some(b) == self.opt_tag("kNewMetaMap")
|| Some(b) == self.opt_tag("kNewContextlessMetaMap")
|| Some(b) == self.opt_tag("kNewContextfulMetaMap")
{
Err(format!(
"meta map tag 0x{b:02x} at {} not supported (rare in code caches)",
self.pos - 1
))
} else {
Err(format!(
"unknown serialization tag 0x{b:02x} at {}",
self.pos - 1
))
}
}
fn legacy_backref(&mut self, space: u8) -> R<SlotValue> {
let map_space = self.map_space();
let lo_space = self.lo_space();
if self.is_six_x() {
let bf = self.putint()?;
let space_tag = self.lnum("kSpaceTagSize").unwrap_or(3);
let value_bits = 32 - space_tag;
let index = bf & ((1u32 << value_bits) - 1);
if space == map_space {
return match self.legacy.as_ref().and_then(|l| l.resolve_ordinal(4, index as usize)) {
Some(id) => {
self.hot.add(HotEntry::Object(id));
Ok(SlotValue::Ref(Ref::Object(id)))
}
None => Err(format!("legacy map backref index {index} out of range")),
};
}
if space == lo_space {
return match self.legacy.as_ref().and_then(|l| l.resolve_ordinal(5, index as usize)) {
Some(id) => {
self.hot.add(HotEntry::Object(id));
Ok(SlotValue::Ref(Ref::Object(id)))
}
None => Err(format!("legacy large-object backref index {index} out of range")),
};
}
let align_bits = (self.tagged_size as u32).max(1).trailing_zeros();
let off_bits = self
.lnum("kPageSizeBits")
.unwrap_or(19)
.saturating_sub(align_bits);
let chunk_offset = (index & ((1u32 << off_bits) - 1)) << align_bits;
let chunk_index = index >> off_bits;
return match self
.legacy
.as_ref()
.and_then(|l| l.resolve(space, chunk_index, chunk_offset))
{
Some(id) => {
self.hot.add(HotEntry::Object(id));
Ok(SlotValue::Ref(Ref::Object(id)))
}
None => {
self.hot.add(HotEntry::Ro(chunk_index, chunk_offset));
Ok(SlotValue::Ref(Ref::RoRef(chunk_index, chunk_offset)))
}
};
}
if space == map_space || space == lo_space {
let index = self.putint()? as usize;
let canonical = if space == map_space { 4 } else { 5 };
return match self.legacy.as_ref().and_then(|l| l.resolve_ordinal(canonical, index)) {
Some(id) => {
self.hot.add(HotEntry::Object(id));
Ok(SlotValue::Ref(Ref::Object(id)))
}
None => Err(format!(
"legacy {} backref index {index} out of range (have {})",
if space == map_space { "map" } else { "large-object" },
if space == map_space {
self.legacy.as_ref().map_or(0, |l| l.maps.len())
} else {
self.legacy.as_ref().map_or(0, |l| l.large.len())
}
)),
};
}
let chunk = self.putint()?;
let offset = self.putint()?;
match self.legacy.as_ref().and_then(|l| l.resolve(space, chunk, offset)) {
Some(id) => {
self.hot.add(HotEntry::Object(id));
Ok(SlotValue::Ref(Ref::Object(id)))
}
None => {
if std::env::var("JSCD_DBG_LEGACY").is_ok() {
eprintln!("[ro-ref] ({space},{chunk},{offset}) → 按只读堆引用处理");
}
self.hot.add(HotEntry::Ro(chunk, offset));
Ok(SlotValue::Ref(Ref::RoRef(chunk, offset)))
}
}
}
fn parse_new_object(&mut self, depth: usize) -> R<ObjId> {
self.parse_new_object_space(depth, 0)
}
fn parse_new_object_space(&mut self, depth: usize, space: u8) -> R<ObjId> {
let tag_offset = self.pos.saturating_sub(1);
let size_words = self.putint()? as usize;
if self.legacy.is_some() && self.is_six_x() && space == self.lo_space() {
let _ = self.byte()?;
}
let unit = self.tagged_size.max(4);
let byte_size = size_words * unit;
if let Ok(v) = std::env::var("JSCD_DBG_OBJ") {
let want = v == "1" || space == v.parse::<u8>().unwrap_or(255);
if want {
let ctx = self
.data
.get(tag_offset..(tag_offset + 12).min(self.data.len()))
.unwrap_or(&[]);
eprintln!(
"[objtag] ts={} pos={tag_offset} space={space} size_words={size_words} unit={unit} bytes={byte_size} ctx={ctx:02x?}",
self.tagged_size
);
}
}
let id = self.objects.len();
self.objects.push(Object {
ty: Ty::Pending,
byte_size,
start_offset: tag_offset,
slots: Vec::with_capacity(size_words.min(64)),
raw_len: false,
});
if let Some(l) = self.legacy.as_mut() {
l.allocate(space, byte_size as u32, id);
if std::env::var("JSCD_DBG_LEGACY").is_ok() && (id >= 18) {
let ctx = self.data.get(tag_offset..(tag_offset + 12).min(self.data.len())).unwrap_or(&[]);
eprintln!(
"[obj] id={id} space={space} words={size_words} tag_pos={tag_offset} bytes={:02x?}",
ctx
);
}
}
let map = self.parse_ref(depth + 1)?;
if let SlotValue::Ref(Ref::Root(ri)) = &map {
if let Some(n) = self.table.root_name(*ri) {
if (n.contains("Trusted") || n.contains("Protected")) && n.contains("Array") {
self.objects[id].raw_len = true;
}
}
}
self.push_slot(id, 0, map);
if let Some(t_resolve) = self.opt_tag("kResolvePendingForwardRef") {
loop {
match self.peek() {
Some(b) if b == t_resolve => {
self.byte()?;
let pid = self.putint()?;
if let Some(list) = self.pending.get_mut(&pid) {
for (oid, vec_pos) in list.drain(..) {
self.objects[oid].slots[vec_pos].value =
SlotValue::Ref(Ref::Object(id));
}
}
self.pending.remove(&pid);
}
_ => break,
}
}
}
let ts = self.tagged_size;
let size_bytes = size_words * unit;
let mut consumed_bytes = ts; let t_deferred = self.opt_tag("kDeferred");
let dbg_slots = std::env::var("JSCD_DBG_SLOTS").ok();
while consumed_bytes < size_bytes {
let slot_pos = self.pos;
if t_deferred.is_some() && self.peek() == t_deferred {
self.byte()?;
if let Some(l) = self.legacy.as_mut() {
l.deferred.push(id);
}
return Ok(id);
}
let v = self.parse_ref(depth + 1)?;
let extra_bytes = std::mem::take(&mut self.extra_skip);
let n_bytes = match &v {
SlotValue::Raw(r) => if r.advances { r.len } else { 0 },
SlotValue::Repeat(n, _) => *n * ts,
SlotValue::RepeatPrev(n) => *n * ts,
SlotValue::Skip(bytes) => *bytes,
SlotValue::StreamBytes(_) => 0,
_ => ts, };
if let Some(w) = &dbg_slots {
if w == "1" || *w == id.to_string() {
let tag = self.data.get(slot_pos).copied().unwrap_or(0);
eprintln!(
"[slot] ts={ts} id={id} pos={slot_pos} tag=0x{tag:02x} n_bytes={n_bytes} consumed={consumed_bytes} budget={size_bytes}"
);
}
}
if let SlotValue::StreamBytes(_) = v {
continue;
}
if let SlotValue::Skip(_) = v {
consumed_bytes += n_bytes;
continue;
}
let v = match v {
SlotValue::RepeatPrev(n) => {
let prev = self.objects[id]
.slots
.last()
.map(|s| s.value.clone())
.unwrap_or(SlotValue::ClearedWeak);
SlotValue::Repeat(n, Box::new(prev))
}
other => other,
};
if let SlotValue::PendingRef(pid) = &v {
let vec_pos = self.objects[id].slots.len();
self.pending.entry(*pid).or_default().push((id, vec_pos));
}
let at = consumed_bytes + extra_bytes;
self.push_slot(id, at / ts, v);
consumed_bytes = at + n_bytes;
}
if std::env::var("JSCD_DBG_LEGACY").is_ok() {
eprintln!(
"[objend] id={id} pos={tag_offset} bytes={consumed_bytes} budget={size_bytes} (words {size_words})"
);
}
if consumed_bytes != size_bytes {
return Err(format!(
"object {id} (at payload +{tag_offset}) size mismatch: consumed {consumed_bytes} of {size_bytes} bytes"
));
}
Ok(id)
}
}
#[derive(Default)]
struct LegacyAlloc {
chunks: Vec<Vec<u32>>,
cur: Vec<usize>,
used: Vec<u32>,
by_addr: std::collections::HashMap<(u8, u32, u32), ObjId>,
maps: Vec<ObjId>,
large: Vec<ObjId>,
align: u32,
ts: u32,
deferred: Vec<ObjId>,
trace: Vec<(u8, u32, u32, u32, ObjId)>,
}
impl LegacyAlloc {
fn from_reservations(res: &[u32]) -> Self {
let mut me = LegacyAlloc::default();
let mut space = 0usize;
me.chunks.push(Vec::new());
me.cur.push(0);
me.used.push(0);
for r in res {
let size = r & 0x7fff_ffff;
me.chunks[space].push(size);
if r & 0x8000_0000 != 0 {
space += 1;
me.chunks.push(Vec::new());
me.cur.push(0);
me.used.push(0);
}
}
if std::env::var("JSCD_DBG_ALLOC").is_ok() {
for (s, cs) in me.chunks.iter().enumerate() {
eprintln!("[res] space={s} chunks={cs:?}");
}
}
me
}
fn allocate(&mut self, space: u8, size: u32, id: ObjId) -> Option<(u32, u32)> {
if space == 4 {
self.maps.push(id);
} else if space == 5 {
self.large.push(id);
}
let s = space as usize;
if s >= self.cur.len() {
return None;
}
let idx = self.cur[s] as u32;
let mut off = self.used[s];
let mut advance = size;
if self.align != 0 {
let pre = fill_to_align(self.used[s], self.align, self.ts);
off = self.used[s] + pre;
advance = size + max_fill_to_align(self.ts);
self.align = 0;
}
if std::env::var("JSCD_DBG_ALLOC").is_ok() {
eprintln!(
"[alloc] space={space} size={size} id={id} chunk={idx} off={off} \
pre={} advance={advance} align={}",
off - (self.used[s]),
self.align
);
}
self.by_addr.insert((space, idx, off), id);
self.trace.push((space, idx, off, size, id));
self.used[s] += advance;
Some((idx, off))
}
fn next_chunk(&mut self, space: u8) {
let s = space as usize;
if s < self.cur.len() {
self.cur[s] += 1;
self.used[s] = 0;
}
}
fn resolve_ordinal(&self, space: u8, index: usize) -> Option<ObjId> {
match space {
4 => self.maps.get(index).copied(),
5 => self.large.get(index).copied(),
_ => None,
}
}
fn resolve(&self, space: u8, chunk: u32, offset: u32) -> Option<ObjId> {
if let Some(id) = self.by_addr.get(&(space, chunk, offset)) {
return Some(*id);
}
if self.align != 0 {
let pad = fill_to_align(offset, self.align, self.ts);
if pad != 0 {
if let Some(id) = self.by_addr.get(&(space, chunk, offset + pad)) {
return Some(*id);
}
}
}
None
}
}
fn max_fill_to_align(ts: u32) -> u32 {
8u32.saturating_sub(ts)
}
fn fill_to_align(addr: u32, alignment: u32, ts: u32) -> u32 {
if alignment == 1 && addr & 7 != 0 {
ts
} else if alignment == 2 && addr & 7 == 0 {
8u32.saturating_sub(ts)
} else {
0
}
}
pub fn parse<'a>(payload: &'a [u8], table: &VersionTable) -> R<CodeCache<'a>> {
parse_with(payload, table, &[])
}
pub fn parse_with<'a>(
payload: &'a [u8],
table: &VersionTable,
reservations: &[u32],
) -> R<CodeCache<'a>> {
let mut merged_map: std::collections::HashMap<String, u8> = table.serialization.tags.clone();
for (k, v) in &table.serialization.legacy {
if let Ok(v8) = u8::try_from(*v) {
merged_map.insert(k.clone(), v8);
}
}
let merged_tags: &'static std::collections::HashMap<String, u8> =
Box::leak(Box::new(merged_map));
for n in [
"kNewObject",
"kBackref",
"kRootArray",
"kAttachedReference",
"kSynchronize",
"kNop",
"kHotObject",
"kRootArrayConstants",
"kFixedRawData",
] {
if !merged_tags.contains_key(n) {
return Err(format!("table missing serialization tag {n}"));
}
}
let ts = table.tagged_size as usize;
let legacy_ctx = if table.serialization.legacy.contains_key("kSpaceMask") {
let mut l = LegacyAlloc::from_reservations(reservations);
l.ts = ts as u32;
Some(l)
} else {
None
};
let mut w = Walker {
data: payload,
pos: 0,
tagged_size: ts,
tags: merged_tags,
legacy_consts: &table.serialization.legacy,
legacy: legacy_ctx,
objects: Vec::new(),
hot: HotRing::default(),
pending: std::collections::HashMap::new(),
extra_skip: 0,
table,
};
if std::env::var("JSCD_DBG_LEGACY").is_ok() {
eprintln!(
"[table] v8={} six_x={} spaces={} map={} lo={} ts={}",
table.v8,
w.is_six_x(),
w.n_spaces(),
w.map_space(),
w.lo_space(),
ts
);
}
let b = w.byte()?;
let t_new = w.tag("kNewObject")?;
if !(t_new..w.tag("kBackref")?).contains(&b) {
return Err(format!("payload does not start with kNewObject (got 0x{b:02x})"));
}
let top_space = if w.legacy.is_some() { b - t_new } else { 0 };
let top = w.parse_new_object_space(0, top_space)?;
let t_sync = w.tag("kSynchronize")?;
if std::env::var("JSCD_DBG_LEGACY").is_ok() {
let tail = w.data.get(w.pos..(w.pos + 24).min(w.data.len())).unwrap_or(&[]);
eprintln!("[legacy] 主段结束 pos={} 后续字节={:02x?}", w.pos, tail);
}
let legacy_tolerant = w.legacy.is_some();
loop {
match w.peek() {
None => {
if legacy_tolerant {
break;
}
return Err("payload ended before kSynchronize".into());
}
Some(b) if b == t_sync => {
w.byte()?;
break;
}
Some(_) => {
let before = w.pos;
let outcome: R<()> = (|| {
let Some(b) = w.peek() else { return Ok(()) };
if let Some(align) = w.opt_tag("kAlignmentPrefix") {
if (align..align + 3).contains(&b) {
let a = (b - align + 1) as u32;
if let Some(l) = w.legacy.as_mut() {
l.align = a;
}
w.byte()?;
return Ok(());
}
}
let t_new = w.tag("kNewObject")?;
let n_sp = w.n_spaces();
if legacy_tolerant && (t_new..t_new + n_sp).contains(&b) {
let space = b - t_new;
w.byte()?;
let id = match w.legacy_backref(space)? {
SlotValue::Ref(Ref::Object(id)) => id,
other => {
return Err(format!("deferred backref 不是对象引用: {other:?}"))
}
};
let size_words = w.putint()? as usize;
let mut slot_index = w.objects[id].slots.len().max(1);
let mut consumed = 1usize;
while consumed < size_words {
let v = w.parse_ref(1)?;
let ts = w.tagged_size;
let n_bytes = match &v {
SlotValue::Raw(r) => if r.advances { r.len } else { 0 },
SlotValue::Repeat(n, _) => *n * ts,
SlotValue::RepeatPrev(n) => *n * ts,
SlotValue::Skip(bytes) => *bytes,
SlotValue::StreamBytes(_) => 0,
_ => ts,
};
if let SlotValue::Skip(_) = v {
consumed += n_bytes;
continue;
}
let n_slots = n_bytes / ts.max(1);
for k in 0..n_slots.max(1) {
let val = match &v {
SlotValue::Repeat(_, inner) => (**inner).clone(),
other => other.clone(),
};
w.push_slot(id, slot_index + k, val);
}
slot_index += n_slots.max(1);
consumed += n_bytes.max(ts);
}
return Ok(());
}
w.parse_ref(0)?;
Ok(())
})();
match outcome {
Ok(()) => {}
Err(e) if legacy_tolerant => {
eprintln!("jscd: 老族 deferred 段停在 offset {}: {e}", w.pos);
break;
}
Err(e) => return Err(format!("deferred section: {e}")),
}
if w.pos == before {
if legacy_tolerant {
break;
}
return Err("deferred section made no progress".into());
}
}
}
}
let t_nop = w.tag("kNop")?;
while let Some(b) = w.peek() {
if b != t_nop {
return Err(format!(
"unexpected byte 0x{b:02x} after kSynchronize at {}",
w.pos
));
}
w.byte()?;
}
classify(&mut w.objects, table, ts);
if let Ok(want) = std::env::var("JSCD_DBG_EMPTY") {
let range = want.parse::<usize>().ok();
for (i, o) in w.objects.iter().enumerate() {
let show = match range {
None => o.slots.is_empty(),
Some(base) => i >= base && i < base + 12,
};
if !show {
continue;
}
let mut s = Vec::new();
for sl in o.slots.iter() {
s.push(match &sl.value {
SlotValue::Ref(r) => format!("{}:ref{:?}", sl.index, r),
SlotValue::Raw(r) => format!("{}:raw{}", sl.index, r.len),
SlotValue::Repeat(n, _) => format!("{}:rep{}", sl.index, n),
SlotValue::PendingRef(p) => format!("{}:pend{}", sl.index, p),
other => format!("{}:{other:?}", sl.index),
});
}
eprintln!(
"[empty] id={i} ty={:?} bytes={} tag_pos={} slots={s:?}",
o.ty, o.byte_size, o.start_offset
);
}
}
Ok(CodeCache {
payload,
tagged_size: ts,
objects: w.objects,
top_sfi: top,
})
}
fn classify(objects: &mut [Object], table: &VersionTable, _ts: usize) {
let n = objects.len();
let inst_types: Vec<u16> = objects.iter().map(instance_type_from_slots).collect();
let mut tys: Vec<Ty> = Vec::with_capacity(n);
for obj in objects.iter() {
let mut ty = match obj.slots.first().and_then(|s| s.value.as_ref()) {
Some(Ref::Root(i)) => {
let name = table.root_name(i).unwrap_or("");
if name.ends_with("Map") {
Ty::Root(i as u16)
} else if name.starts_with("String:") {
Ty::Str(StrKind::OneByte)
} else {
Ty::Unknown
}
}
Some(Ref::Object(m)) if m < n => Ty::MapObj(inst_types[m]),
Some(Ref::RoRef(c, o)) => Ty::Ro(c, o),
_ => Ty::Unknown,
};
if let Ty::Root(i) = ty {
let name = table.root_name(i as usize).unwrap_or("");
if name.contains("String") {
ty = Ty::Str(if name.contains("OneByte") {
StrKind::OneByte
} else {
StrKind::TwoByte
});
}
}
if matches!(ty, Ty::Unknown | Ty::Root(_)) {
if let Some(t) = structural_type(obj) {
ty = Ty::Structural(t);
}
}
tys.push(ty);
}
for (obj, ty) in objects.iter_mut().zip(tys) {
obj.ty = ty;
}
}
fn instance_type_from_slots(map: &Object) -> u16 {
for s in &map.slots {
if let SlotValue::Raw(r) = &s.value {
let start = s.index * 8;
if start <= 12 && start + r.len >= 14 {
return 1; }
}
}
0
}
fn structural_type(o: &Object) -> Option<&'static str> {
if o.byte_size >= 96
&& matches!(
o.slots.get(1),
Some(Slot {
value: SlotValue::Ref(Ref::Attached(0)),
..
})
)
{
return Some("Script");
}
if o.slots.len() == 3 {
let s1_ref = matches!(o.slots.get(1), Some(Slot { value: SlotValue::Ref(_), .. }));
let s2_raw = matches!(
o.slots.get(2),
Some(Slot { value: SlotValue::Raw(r), .. }) if r.len == 8
);
if s1_ref && s2_raw && o.byte_size == 32 {
return Some("UncompiledData");
}
}
None
}
impl<'a> CodeCache<'a> {
pub fn map_instance_type(&self, map_id: ObjId) -> Option<u16> {
let ts = self.tagged_size;
let d = self.raw_at(map_id, 12, 2)?;
let _ = ts;
Some(u16::from_le_bytes(d.try_into().ok()?))
}
#[inline]
pub fn obj(&self, id: ObjId) -> &Object {
&self.objects[id]
}
#[inline]
pub fn payload(&self) -> &'a [u8] {
self.payload
}
#[inline]
pub fn tagged_size(&self) -> usize {
self.tagged_size
}
#[inline]
pub fn raw_bytes(&self, r: Raw) -> &'a [u8] {
&self.payload[r.off..r.off + r.len]
}
#[inline]
pub fn ref_object(&self, r: Ref) -> Option<ObjId> {
match r {
Ref::Object(id) => Some(id),
_ => None,
}
}
#[inline]
pub fn slot_at(&self, id: ObjId, index: usize) -> Option<&SlotValue> {
let slots = &self.objects[id].slots;
let i = slots.partition_point(|s| s.index < index);
slots.get(i).filter(|s| s.index == index).map(|s| &s.value)
}
pub fn unit_at_ts(&self, id: ObjId, byte_off: usize, ts: usize) -> Option<&SlotValue> {
let want = byte_off / ts;
let slots = &self.objects[id].slots;
let mut i = slots.partition_point(|s| s.index <= want);
while i > 0 {
i -= 1;
let s = &slots[i];
let end = match &s.value {
SlotValue::Raw(r) => s.index + r.len / ts,
SlotValue::Repeat(n, _) => s.index + n,
_ => s.index + 1,
};
if want >= s.index && want < end {
return Some(&s.value);
}
if s.index + 4 < want {
break;
}
}
None
}
#[inline]
pub fn unit_at(&self, id: ObjId, byte_off: usize) -> Option<&SlotValue> {
self.unit_at_ts(id, byte_off, self.tagged_size)
}
pub fn raw_at_ts(&self, id: ObjId, byte_off: usize, len: usize, ts: usize) -> Option<&'a [u8]> {
let want = byte_off / ts;
let slots = &self.objects[id].slots;
let mut i = slots.partition_point(|s| s.index <= want);
while i > 0 {
i -= 1;
let s = &slots[i];
if let SlotValue::Raw(r) = &s.value {
let start = s.index * ts;
if byte_off >= start && byte_off + len <= start + r.len {
let rel = byte_off - start;
return Some(&self.payload[r.off + rel..r.off + rel + len]);
}
}
if s.index + 4 < want {
break;
}
}
None
}
#[inline]
pub fn bca_rt_off(
&self,
table: &crate::tables::VersionTable,
ts: usize,
id: ObjId,
capture_off: usize,
) -> usize {
let cap = table.slot_unit();
if ts == cap {
return capture_off;
}
let tail = self
.objects[id]
.slots
.iter()
.rev()
.find_map(|s| matches!(s.value, SlotValue::Raw(_)).then_some(s.index));
match tail {
Some(tail) => {
let tail_cap = tail * cap;
if capture_off >= tail_cap {
tail * ts + (capture_off - tail_cap)
} else {
capture_off / cap * ts
}
}
None => capture_off / cap * ts,
}
}
pub fn raw_at(&self, id: ObjId, byte_off: usize, len: usize) -> Option<&'a [u8]> {
self.raw_at_ts(id, byte_off, len, self.tagged_size)
}
pub fn array_len(&self, id: ObjId) -> usize {
let ts = self.tagged_size;
let size = self.objects[id].byte_size;
let raw = self
.raw_at(id, ts, 4)
.map(|d| u32::from_le_bytes(d.try_into().unwrap()) as usize)
.unwrap_or(0);
let by_ts = size.saturating_sub(2 * ts) / ts.max(1);
let by_8 = size.saturating_sub(2 * ts) / 8;
let max_len = self.payload.len() / ts.max(1) + 2;
let clamp = |n: usize| n.min(max_len);
let smi = self
.raw_at(id, ts, ts)
.and_then(decode_smi_bytes)
.map(|v| v as usize);
if let Some(n) = smi {
if n == by_ts || n == by_8 {
return clamp(n);
}
}
if raw == by_ts || raw == by_8 {
return clamp(raw);
}
if self.objects[id].raw_len {
return clamp(raw);
}
clamp(smi.unwrap_or(0))
}
pub fn array_elem(&self, id: ObjId, index: usize) -> Option<Elem<'a>> {
let ts = self.tagged_size;
let off = (2 + index) * ts;
let want = off / ts;
let slots = &self.objects[id].slots;
let mut i = slots.partition_point(|s| s.index <= want);
while i > 0 {
i -= 1;
let s = &slots[i];
match &s.value {
SlotValue::Ref(r) => {
if s.index == want {
return Some(Elem::Ref(*r));
}
}
SlotValue::Raw(r) => {
let start = s.index * ts;
if off >= start && off + ts <= start + r.len {
let rel = off - start;
let d = &self.payload[r.off + rel..r.off + rel + ts];
return Some(match decode_smi_bytes(d) {
Some(v) => Elem::Smi(v),
None => Elem::Bytes(d),
});
}
}
SlotValue::Repeat(n, inner) => {
let start = s.index * ts;
if off >= start && off < start + n * ts {
return match inner.as_ref() {
SlotValue::Ref(r) => Some(Elem::Ref(*r)),
SlotValue::Raw(r) => {
let d = &self.payload[r.off..r.off + r.len.min(ts)];
Some(decode_smi_bytes(d).map(Elem::Smi).unwrap_or(Elem::Bytes(d)))
}
_ => None,
};
}
}
_ => {}
}
if s.index + 4 < want {
break;
}
}
None
}
}
#[inline]
pub fn decode_smi_bytes(d: &[u8]) -> Option<i64> {
match d.len() {
8 => {
let raw = u64::from_le_bytes(d.try_into().ok()?);
if raw & 0xFFFF_FFFF == 0 {
Some(((raw >> 32) as u32 as i32) as i64)
} else {
None
}
}
4 => {
let raw = u32::from_le_bytes(d.try_into().ok()?);
if raw & 1 != 0 {
None
} else {
Some(((raw as i32) >> 1) as i64)
}
}
_ => None,
}
}