use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
mod embed {
include!("tables_embed.rs");
}
#[derive(Debug, Deserialize)]
pub struct Manifest {
pub schema_version: u32,
pub generated_at: String,
pub versions: Vec<VersionEntry>,
}
#[derive(Debug, Deserialize)]
pub struct VersionEntry {
pub v8: String,
pub hash: u32,
pub node: String,
pub table: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VersionTable {
pub v8: String,
pub source_tag: String,
pub header: crate::header::HeaderLayout,
pub hash: HashCfg,
pub tagged_size: u8,
pub bytecodes: Vec<BytecodeDef>,
pub operand_types: HashMap<String, OperandTypeInfo>,
#[serde(default)]
pub roots: Vec<String>,
#[serde(default)]
pub roots_shift: u32,
#[serde(default)]
pub runtime_names: Vec<String>,
#[serde(default)]
pub intrinsic_names: Vec<String>,
#[serde(default)]
pub bytecode_array: Option<BytecodeArrayLayout>,
#[serde(default)]
pub frame: Option<FrameLayout>,
#[serde(default)]
pub shared_function_info: Option<StructLayout>,
#[serde(default)]
pub scope_info: Option<ScopeInfoLayout>,
#[serde(default)]
pub parameter_count: Option<ParameterCountCfg>,
#[serde(default = "default_min_context_slots")]
pub min_context_slots: usize,
#[serde(default)]
pub string_length_smi: bool,
#[serde(default)]
pub string_length_offset: Option<usize>,
#[serde(default = "default_true")]
pub var_raw_advances: bool,
#[serde(default)]
pub string_chars_offset: Option<usize>,
#[serde(default)]
pub serialization: SerializationCfg,
}
fn default_min_context_slots() -> usize {
2
}
fn default_true() -> bool {
true
}
impl VersionTable {
pub fn v8_tags(&self) -> String {
self.source_tag.clone()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct HashCfg {
pub algorithm: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct BytecodeDef {
pub name: String,
#[serde(default)]
pub operands: Vec<String>,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub acc: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct OperandTypeInfo {
pub size: u32,
pub scalable: bool,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct SerializationCfg {
#[serde(default)]
pub tags: HashMap<String, u8>,
#[serde(default)]
pub legacy: HashMap<String, u32>,
#[serde(default)]
pub code_items: HashMap<String, u8>,
#[serde(default)]
pub switches: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StructLayout {
pub header_size: usize,
#[serde(default)]
pub fields: HashMap<String, usize>,
}
impl StructLayout {
pub fn off(&self, name: &str) -> Option<usize> {
self.fields.get(name).copied()
}
pub fn slot(&self, tagged_size: usize, name: &str) -> Option<usize> {
self.off(name).map(|o| o / tagged_size)
}
pub fn slot_any(&self, tagged_size: usize, names: &[&str]) -> Option<usize> {
names.iter().find_map(|n| self.slot(tagged_size, n))
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct ParameterCountCfg {
pub direct: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScopeInfoLayout {
#[serde(default)]
pub context_local_count_slot: Option<usize>,
#[serde(default)]
pub variable_part: Option<usize>,
#[serde(default)]
pub parameter_names_first: bool,
pub flags_smi: bool,
pub position_info_early: bool,
pub max_inlined_names: usize,
pub saved_class_bit: u32,
#[serde(default)]
pub has_outer_bit: Option<u32>,
pub function_variable_bits: [u32; 2],
pub receiver_bits: [u32; 2],
pub has_inferred_bit: u32,
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct FrameLayout {
pub reg_file_start: i32,
pub context_index: i32,
pub closure_index: i32,
#[serde(default)]
pub first_param: Option<i32>,
#[serde(default)]
pub last_param: Option<i32>,
#[serde(default)]
pub param_base: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BytecodeArrayLayout {
pub header_size: usize,
pub fields: HashMap<String, usize>,
}
impl BytecodeArrayLayout {
pub fn fallback(tagged_size: usize) -> Self {
let mut fields = HashMap::new();
fields.insert("constant_pool".to_string(), 2 * tagged_size);
fields.insert("handler_table".to_string(), 3 * tagged_size);
fields.insert("source_position_table".to_string(), 4 * tagged_size);
fields.insert("frame_size".to_string(), 5 * tagged_size);
fields.insert("parameter_size".to_string(), 5 * tagged_size + 4);
fields.insert("bytecode_age".to_string(), 5 * tagged_size + 13);
BytecodeArrayLayout {
header_size: 5 * tagged_size + 14,
fields,
}
}
pub fn off(&self, name: &str) -> Option<usize> {
self.fields.get(name).copied()
}
}
#[derive(Debug, Clone)]
pub struct Identified {
pub node: &'static str,
pub v8: &'static str,
pub confidence: &'static str,
}
impl Identified {
pub fn supported(&self) -> bool {
table_for(self.v8).is_some()
}
pub fn render_text(&self) -> String {
use std::fmt::Write as _;
let mut s = String::new();
let _ = writeln!(s, "node: {}", self.node);
let _ = writeln!(s, "v8: {} ({})", self.v8, self.confidence);
let _ = writeln!(
s,
"supported: {}",
if self.supported() {
"yes"
} else {
crate::bi!(
"no (tables cover Node 8.0.0-26.10.0 / V8 5.8-14.6)",
"否(当前表覆盖 Node 8.0.0-26.10.0 / V8 5.8-14.6)"
)
}
);
s
}
}
fn manifest() -> &'static Option<Manifest> {
static M: OnceLock<Option<Manifest>> = OnceLock::new();
M.get_or_init(|| serde_json::from_str(embed::MANIFEST_JSON).ok())
}
fn table_map() -> &'static HashMap<&'static str, &'static str> {
static T: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
T.get_or_init(|| EMBEDDED_TABLE_FILES.iter().copied().collect())
}
impl VersionTable {
pub fn root_name(&self, i: usize) -> Option<&str> {
let shift = self.roots_shift as usize;
if shift > 0 && i >= shift {
if let Some(n) = self.roots.get(i - shift) {
if n.starts_with("String:") {
return Some(n);
}
}
}
self.roots.get(i).map(|s| s.as_str())
}
}
pub fn all_tables() -> Vec<VersionTable> {
let mut out: Vec<VersionTable> = Vec::new();
if let Some(m) = manifest().as_ref() {
for e in &m.versions {
if let Some(t) = table_for(&e.v8) {
if !out.iter().any(|x| x.v8 == t.v8) {
out.push(t);
}
}
}
}
out.sort_by_key(|t| {
let maj: u32 = t.v8.split('.').next().and_then(|s| s.parse().ok()).unwrap_or(0);
maj
});
out
}
pub fn identify(version_hash: u32) -> Identified {
if let Some(m) = manifest() {
if let Some(v) = m.versions.iter().find(|v| v.hash == version_hash) {
return Identified {
node: leaked(v.node.as_str()),
v8: leaked(v.v8.as_str()),
confidence: "exact",
};
}
}
match crate::vhash::brute_force(version_hash) {
Some((fold, (a, b, c, d))) => Identified {
node: "unknown (no matching Node release)",
v8: leaked(&format!("{a}.{b}.{c}.{d}")),
confidence: if fold == crate::vhash::Fold::LeftFold {
"brute (left-fold, V8 ≥ 12)"
} else {
"brute (right-fold, V8 ≤ 11)"
},
},
None => Identified {
node: "unknown",
v8: "unknown",
confidence: "unknown",
},
}
}
pub fn table_for(v8: &str) -> Option<VersionTable> {
let v8_minor = v8.rsplit_once('.').map(|(a, _)| a).unwrap_or(v8);
let m = manifest().as_ref()?;
let entry = m
.versions
.iter()
.find(|v| v.v8.rsplit_once('.').map(|(a, _)| a) == Some(v8_minor))?;
let raw = table_map().get(entry.table.as_str())?;
serde_json::from_str(raw).ok()
}
fn leaked(s: &str) -> &'static str {
Box::leak(s.to_string().into_boxed_str())
}
#[allow(non_upper_case_globals)]
mod embedded_tables_shim {
pub use super::embed::{EMBEDDED_TABLE_FILES, MANIFEST_JSON};
}
pub use embedded_tables_shim::{EMBEDDED_TABLE_FILES, MANIFEST_JSON};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_embedded_table_deserializes() {
let raw = embed::EMBEDDED_TABLE_FILES;
assert!(!raw.is_empty());
for (name, text) in raw.iter() {
let t: Result<VersionTable, _> = serde_json::from_str(text);
assert!(t.is_ok(), "{name} 反序列化失败: {:?}", t.err());
}
}
#[test]
fn identify_known_hash_is_exact() {
let id = identify(0);
assert!(matches!(id.confidence, "exact" | "brute" | "unknown"));
}
}
#[cfg(test)]
mod table_debug {
use super::*;
#[test]
fn debug_deserialize_v94() {
let raw = EMBEDDED_TABLE_FILES
.iter()
.find(|(n, _)| *n == "v9_4.json")
.map(|(_, s)| *s)
.expect("v9_4 embedded");
match serde_json::from_str::<VersionTable>(raw) {
Ok(t) => println!("ok: v8={} tagged={} bc={} roots={}", t.v8, t.tagged_size, t.bytecodes.len(), t.roots.len()),
Err(e) => panic!("deserialize failed: {e}"),
}
}
}