///|
fn field(value : Json, key : String) -> Json raise HeapError {
match value {
Object(m) =>
match m.get(key) {
Some(v) => v
None => raise Invalid("missing field: \{key}")
}
_ => raise Invalid("expected object containing \{key}")
}
}
///|
fn array(value : Json, context : String) -> Array[Json] raise HeapError {
match value {
Array(a) => a
_ => raise Invalid("\{context}: expected array")
}
}
///|
fn text(value : Json, context : String) -> String raise HeapError {
match value {
String(s) => s
_ => raise Invalid("\{context}: expected string")
}
}
///|
fn unsigned(value : Json, context : String) -> UInt64 raise HeapError {
guard value is Number(_, ..) else {
raise Invalid("\{context}: expected integer")
}
let raw = value.stringify()
guard raw.length() > 0 else { raise Invalid(context) }
for c in raw.iter() {
guard c >= '0' && c <= '9' else {
raise Invalid("\{context}: expected nonnegative decimal integer")
}
}
@string.parse_uint64(raw) catch {
_ => raise Invalid("\{context}: integer overflow")
}
}
///|
fn integer(value : Json, context : String) -> Int32 raise HeapError {
let n = unsigned(value, context)
guard n <= 2147483647UL else {
raise Limit("\{context}: exceeds index range")
}
n.to_int()
}
///|
fn string_array(
value : Json,
context : String,
) -> Array[String] raise HeapError {
array(value, context).map(v => text(v, context))
}
///|
fn offset(fields : Array[String], name : String) -> Int32 raise HeapError {
for i in 0.. Unit raise HeapError {
let seen : Map[String, Bool] = Map([])
for f in fields {
guard !seen.contains(f) else {
raise Invalid("duplicate metadata field: \{f}")
}
seen[f] = true
}
}
///|
fn indexed(
strings : Array[String],
index : Int32,
context : String,
) -> String raise HeapError {
guard index >= 0 && index < strings.length() else {
raise Invalid("\{context}: index out of range")
}
strings[index]
}
///|
/// Read a V8 snapshot. All arrays and metadata are validated before graph analysis.
pub fn parse(
input : String,
limits~ : Limits = Limits::default(),
) -> Snapshot raise HeapError {
let decoder = Decoder::new(limits~)
decoder.feed(input)
decoder.finish()
}
///|
fn index_value(n : UInt64, context : String) -> Int32 raise HeapError {
guard n <= 2147483647UL else {
raise Limit("\{context}: exceeds index range")
}
n.to_int()
}
///|
fn parse_parts(
metadata : Map[String, Json],
raw_nodes : Array[UInt64],
raw_edges : Array[UInt64],
strings : Array[String],
limits : Limits,
) -> Snapshot raise HeapError {
let header = metadata.get("snapshot").unwrap()
let meta = field(header, "meta")
let nf = string_array(field(meta, "node_fields"), "node_fields")
let ef = string_array(field(meta, "edge_fields"), "edge_fields")
unique_fields(nf)
unique_fields(ef)
let nt = offset(nf, "type")
let nn = offset(nf, "name")
let ni = offset(nf, "id")
let ns = offset(nf, "self_size")
let nc = offset(nf, "edge_count")
let et = offset(ef, "type")
let en = offset(ef, "name_or_index")
let ed = offset(ef, "to_node")
let node_meta = array(field(meta, "node_types"), "node_types")
let edge_meta = array(field(meta, "edge_types"), "edge_types")
// V8 13.x can include a legacy trailing node_types descriptor without a
// corresponding node field. Strides come exclusively from *_fields.
guard node_meta.length() >= nf.length() && edge_meta.length() >= ef.length() else {
raise Invalid("field/type metadata is incomplete")
}
let node_types = string_array(node_meta[nt], "node type names")
let edge_types = string_array(edge_meta[et], "edge type names")
let count = integer(field(header, "node_count"), "node_count")
let ecount = integer(field(header, "edge_count"), "edge_count")
guard count > 0 else { raise Invalid("snapshot must contain root") }
guard count <= limits.max_nodes && ecount <= limits.max_edges else {
raise Limit("node/edge budget exceeded")
}
guard raw_nodes.length().to_int64() ==
count.to_int64() * nf.length().to_int64() &&
raw_edges.length().to_int64() == ecount.to_int64() * ef.length().to_int64() else {
raise Invalid("flat array length/count mismatch")
}
let ids : Map[String, Int32] = Map([])
let nodes : Array[Node] = []
let mut edge_cursor = 0
let mut total = 0L
for i in 0.. m.get("root_index")
_ => None
}
let root_offset = match root_raw {
Some(v) => integer(v, "root_index")
None => 0
}
guard root_offset % nf.length() == 0 && root_offset < raw_nodes.length() else {
raise Invalid("invalid root offset")
}
let root = root_offset / nf.length()
guard nodes[root].kind == "synthetic" else {
raise Invalid("root must be synthetic")
}
let edges : Array[Edge] = []
for node in nodes {
for index in node.edge_start..<(node.edge_start + node.edge_count) {
let b = index * ef.length()
let kind = indexed(
edge_types,
index_value(raw_edges[b + et], "edge type"),
"edge type",
)
guard kind == "context" ||
kind == "element" ||
kind == "property" ||
kind == "internal" ||
kind == "hidden" ||
kind == "shortcut" ||
kind == "weak" else {
raise Invalid("unsupported edge type: \{kind}")
}
let dest = index_value(raw_edges[b + ed], "to_node")
guard dest % nf.length() == 0 && dest < raw_nodes.length() else {
raise Invalid("edge target must be a node offset")
}
let name = if kind == "element" || kind == "hidden" {
raw_edges[b + en].to_string()
} else {
indexed(
strings,
index_value(raw_edges[b + en], "edge name"),
"edge name",
)
}
edges.push({
index,
source: node.index,
target: dest / nf.length(),
kind,
name,
})
}
}
let source = parse_sources(metadata, meta, nf, raw_nodes, strings, count)
{ nodes, edges, ids, root, total_size: total, limits, source, }
}