use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use memmap2::Mmap;
use crate::storage::cas::blob::{self, BLOB_HEADER_LEN, BadBlob, extract_payload};
pub use crate::storage::cas::CasError as ReadIoError;
pub const NEURAL_MAGIC: &[u8; 9] = b"LIDX-NRL1";
pub const TFIDF_MAGIC: &[u8; 9] = b"LIDX-TFD1";
pub const PDG_MAGIC: &[u8; 9] = b"LIDX-PDG1";
pub const SYMBOLS_MAGIC: &[u8; 9] = b"LIDX-SYM1";
pub const NEURAL_HEADER_LEN: usize = 9 + 1 + 3 + 4 + 4 + 4 + 4 + 4 + 32 + 1;
pub const NEURAL_VERSION_POSITIONAL: u8 = 1;
pub const NEURAL_VERSION_WITH_IDS: u8 = 2;
pub const NEURAL_VERSION_IDENTITY: u8 = 3;
pub const TFIDF_HEADER_LEN: usize = 9 + 1 + 3 + 4 + 4 + 4 + 32;
pub const PDG_HEADER_LEN: usize = 9 + 1 + 3 + 4 + 4 + 4 + 4 + 32;
pub const SYMBOLS_HEADER_LEN: usize = 9 + 1 + 3 + 4 + 4 + 4 + 32;
pub const TFIDF_ENTRY_LEN: usize = 4 + 4 + 4;
pub const PDG_NODE_LEN: usize = 4 * 6;
pub const PDG_NODE_V2_LEN: usize = 4 * 9;
pub const PDG_EDGE_META_LEN: usize = 4 * 5;
pub const PDG_V2_NONE: u32 = u32::MAX;
pub const PDG_EDGE_LEN: usize = 4 * 3;
pub const PDG_STRING_OFFSET_LEN: usize = 4 + 4;
pub const SYMBOL_ENTRY_LEN: usize = 4 * 5 + 4;
#[derive(Debug, thiserror::Error)]
pub enum ReaderError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("blob validation failed: {0}")]
BadBlob(#[from] BadBlob),
#[error("layer header invalid: {0}")]
BadHeader(String),
#[error("index out of range: {0}")]
OutOfRange(String),
}
struct BlobMmap {
_file: File,
mmap: Arc<Mmap>,
payload_offset: usize,
payload_len: usize,
}
impl std::fmt::Debug for BlobMmap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlobMmap")
.field("payload_offset", &self.payload_offset)
.field("payload_len", &self.payload_len)
.finish()
}
}
impl BlobMmap {
fn open(path: &Path) -> Result<Self, ReaderError> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
blob::validate_blob(&mmap[..])?;
let (payload_slice, _hash) = extract_payload(&mmap[..])?;
let payload_offset = BLOB_HEADER_LEN;
let payload_len = payload_slice.len();
let mmap_arc = Arc::new(mmap);
Ok(BlobMmap {
_file: file,
mmap: mmap_arc,
payload_offset,
payload_len,
})
}
fn frame_ptr(&self) -> *const u8 {
self.mmap.as_ptr()
}
fn frame_len(&self) -> usize {
self.mmap.len()
}
fn payload(&self) -> &[u8] {
let end = self
.payload_offset
.checked_add(self.payload_len)
.expect("bounds checked at open");
unsafe {
let start = self.mmap.as_ptr().add(self.payload_offset);
std::slice::from_raw_parts(start, end - self.payload_offset)
}
}
}
fn read_u32_le(bytes: &[u8], offset: usize) -> Result<u32, ReaderError> {
if offset + 4 > bytes.len() {
return Err(ReaderError::BadHeader(format!(
"u32 read at offset {} exceeds payload of {} bytes",
offset,
bytes.len()
)));
}
let mut buf = [0u8; 4];
buf.copy_from_slice(&bytes[offset..offset + 4]);
Ok(u32::from_le_bytes(buf))
}
fn read_f32_le(bytes: &[u8], offset: usize) -> Result<f32, ReaderError> {
let raw = read_u32_le(bytes, offset)?;
Ok(f32::from_bits(raw))
}
fn read_magic(bytes: &[u8]) -> Result<[u8; 9], ReaderError> {
if bytes.len() < 9 {
return Err(ReaderError::BadHeader(format!(
"payload too short for magic: {} bytes",
bytes.len()
)));
}
let mut magic = [0u8; 9];
magic.copy_from_slice(&bytes[..9]);
Ok(magic)
}
fn require_magic(bytes: &[u8], expected: &[u8; 9]) -> Result<(), ReaderError> {
let got = read_magic(bytes)?;
if &got != expected {
return Err(ReaderError::BadHeader(format!(
"bad layer magic: expected {:?} got {:?}",
expected, got
)));
}
Ok(())
}
fn validate_neural_data_region(payload: &[u8], header: &NeuralHeader) -> Result<(), ReaderError> {
let data_offset_in_payload = header.data_offset();
let element_size = header.dtype.element_size();
let expected = header
.count
.checked_mul(header.dim)
.and_then(|n| n.checked_mul(element_size))
.ok_or_else(|| ReaderError::BadHeader("count * dim * element_size overflow".to_string()))?;
let available = payload
.len()
.checked_sub(data_offset_in_payload)
.ok_or_else(|| ReaderError::BadHeader("data offset exceeds payload".to_string()))?;
if available < expected {
return Err(ReaderError::BadHeader(format!(
"neural payload short: have {} bytes of data, need {}",
available, expected
)));
}
let mut stored_hash = [0u8; 32];
stored_hash.copy_from_slice(&payload[33..65]);
let computed = blob::blob_hash(&payload[NEURAL_HEADER_LEN..data_offset_in_payload + expected]);
if stored_hash != computed {
return Err(ReaderError::BadHeader(
"neural content_hash does not match data".to_string(),
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NeuralDtype {
F32,
Int8,
Q4,
}
impl NeuralDtype {
fn from_u32(raw: u32) -> Option<Self> {
match raw {
0 => Some(Self::F32),
1 => Some(Self::Int8),
2 => Some(Self::Q4),
_ => None,
}
}
pub fn element_size(self) -> usize {
match self {
Self::F32 => 4,
Self::Int8 => 1,
Self::Q4 => 0,
}
}
}
#[derive(Clone, Copy)]
pub enum VectorView<'a> {
F32(&'a [f32]),
Int8(&'a [i8]),
}
impl<'a> VectorView<'a> {
pub fn dim(&self) -> usize {
match self {
Self::F32(s) => s.len(),
Self::Int8(s) => s.len(),
}
}
pub fn as_f32_slice(&self) -> &'a [f32] {
match self {
Self::F32(s) => s,
Self::Int8(_) => panic!("VectorView::as_f32_slice on INT8 view"),
}
}
}
struct NeuralHeader {
version: u8,
count: usize,
dim: usize,
dtype: NeuralDtype,
scale: f32,
zero_point: f32,
}
impl NeuralHeader {
fn parse(payload: &[u8]) -> Result<Self, ReaderError> {
if payload.len() < NEURAL_HEADER_LEN {
return Err(ReaderError::BadHeader(format!(
"neural payload truncated: {} < {}",
payload.len(),
NEURAL_HEADER_LEN
)));
}
let version = payload[9];
if !matches!(
version,
NEURAL_VERSION_POSITIONAL | NEURAL_VERSION_WITH_IDS | NEURAL_VERSION_IDENTITY
) {
return Err(ReaderError::BadHeader(format!(
"unsupported neural version {}",
version
)));
}
let count = read_u32_le(payload, 13)? as usize;
let dim = read_u32_le(payload, 17)? as usize;
let dtype_raw = read_u32_le(payload, 21)?;
let dtype = NeuralDtype::from_u32(dtype_raw)
.ok_or_else(|| ReaderError::BadHeader(format!("unknown dtype {}", dtype_raw)))?;
if matches!(dtype, NeuralDtype::Q4) {
return Err(ReaderError::BadHeader(
"Q4 dtype deferred to WS11; refuse to open".to_string(),
));
}
let scale = read_f32_le(payload, 25)?;
let zero_point = read_f32_le(payload, 29)?;
Ok(NeuralHeader {
version,
count,
dim,
dtype,
scale,
zero_point,
})
}
fn has_ids(&self) -> bool {
self.version >= NEURAL_VERSION_WITH_IDS
}
fn data_offset(&self) -> usize {
let ids_len = if self.has_ids() { self.count * 4 } else { 0 };
NEURAL_HEADER_LEN + ids_len
}
fn identity_len_offset(&self) -> Option<usize> {
(self.version == NEURAL_VERSION_IDENTITY)
.then(|| self.data_offset() + self.count * self.dim * self.dtype.element_size())
}
}
#[derive(Debug)]
pub struct NeuralReader {
blob: BlobMmap,
count: usize,
dim: usize,
dtype: NeuralDtype,
scale: f32,
zero_point: f32,
has_ids: bool,
data_offset_in_payload: usize,
identity_len_offset: Option<usize>,
}
impl NeuralReader {
pub fn open(path: &Path) -> Result<Self, ReaderError> {
let blob = BlobMmap::open(path)?;
let payload = blob.payload();
require_magic(payload, NEURAL_MAGIC)?;
let header = NeuralHeader::parse(payload)?;
validate_neural_data_region(payload, &header)?;
Ok(NeuralReader {
blob,
count: header.count,
dim: header.dim,
dtype: header.dtype,
scale: header.scale,
zero_point: header.zero_point,
has_ids: header.has_ids(),
data_offset_in_payload: header.data_offset(),
identity_len_offset: header.identity_len_offset(),
})
}
pub fn node_id(&self, i: usize) -> Option<u32> {
if !self.has_ids || i >= self.count {
return None;
}
read_u32_le(self.blob.payload(), NEURAL_HEADER_LEN + i * 4).ok()
}
pub fn count(&self) -> usize {
self.count
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn identity(&self) -> Option<&str> {
let offset = self.identity_len_offset?;
let payload = self.payload_byte();
let len_bytes = payload.get(offset..offset + 4)?;
let len = u32::from_le_bytes(len_bytes.try_into().ok()?) as usize;
let identity = payload.get(offset + 4..offset + 4 + len)?;
std::str::from_utf8(identity).ok()
}
pub fn dtype(&self) -> NeuralDtype {
self.dtype
}
pub fn scale(&self) -> f32 {
self.scale
}
pub fn zero_point(&self) -> f32 {
self.zero_point
}
pub fn mmap_data_ptr(&self) -> *const u8 {
self.blob.frame_ptr()
}
pub fn mmap_data_len(&self) -> usize {
self.blob.frame_len()
}
fn payload_byte(&self) -> &[u8] {
self.blob.payload()
}
pub fn vector(&self, i: usize) -> VectorView<'_> {
assert!(i < self.count, "vector index {} out of range", i);
let data_offset = self.data_offset_in_payload;
let payload = self.payload_byte();
match self.dtype {
NeuralDtype::F32 => {
let byte_off = data_offset + i * self.dim * 4;
let byte_len = self.dim * 4;
let raw = &payload[byte_off..byte_off + byte_len];
let f32_slice: &[f32] =
unsafe { std::slice::from_raw_parts(raw.as_ptr() as *const f32, self.dim) };
VectorView::F32(f32_slice)
}
NeuralDtype::Int8 => {
let byte_off = data_offset + i * self.dim;
let raw = &payload[byte_off..byte_off + self.dim];
let i8_slice: &[i8] =
unsafe { std::slice::from_raw_parts(raw.as_ptr() as *const i8, self.dim) };
VectorView::Int8(i8_slice)
}
NeuralDtype::Q4 => unreachable!("Q4 guarded at open()"),
}
}
pub fn dot(&self, i: usize, query: &[f32]) -> f32 {
assert_eq!(query.len(), self.dim, "query length mismatch");
match self.dtype {
NeuralDtype::F32 => {
let v = self.vector(i);
let f32s = v.as_f32_slice();
let query_sum: f32 = query.iter().sum();
let _ = query_sum;
dot_f32(query, f32s)
}
NeuralDtype::Int8 => {
let v = self.vector(i);
let qs: &[i8] = match v {
VectorView::Int8(s) => s,
_ => unreachable!("dtype path"),
};
dot_int8(query, qs, self.scale, self.zero_point)
}
NeuralDtype::Q4 => unreachable!("Q4 guarded at open()"),
}
}
}
fn dot_f32(query: &[f32], stored: &[f32]) -> f32 {
assert_eq!(query.len(), stored.len());
let mut sum = 0.0f32;
for (q, s) in query.iter().zip(stored.iter()) {
sum += q * s;
}
sum
}
fn dot_int8(query: &[f32], stored: &[i8], scale: f32, zero_point: f32) -> f32 {
assert_eq!(query.len(), stored.len());
let n = query.len();
let mut acc = 0.0f32;
let mut query_sum = 0.0f32;
let mut i = 0;
while i + 8 <= n {
let q = wide::f32x8::from([
query[i],
query[i + 1],
query[i + 2],
query[i + 3],
query[i + 4],
query[i + 5],
query[i + 6],
query[i + 7],
]);
let s = wide::f32x8::from([
stored[i] as f32,
stored[i + 1] as f32,
stored[i + 2] as f32,
stored[i + 3] as f32,
stored[i + 4] as f32,
stored[i + 5] as f32,
stored[i + 6] as f32,
stored[i + 7] as f32,
]);
acc += (q * s).reduce_add();
query_sum += q.reduce_add();
i += 8;
}
while i < n {
acc += query[i] * stored[i] as f32;
query_sum += query[i];
i += 1;
}
scale * acc + zero_point * query_sum
}
#[cfg(target_arch = "x86_64")]
#[allow(dead_code)]
mod int8_avx2 {
use std::arch::x86_64::*;
#[target_feature(enable = "avx2")]
pub unsafe fn dot_int8_avx2(query: &[f32], stored: &[i8], scale: f32, zero_point: f32) -> f32 {
unsafe {
assert_eq!(query.len(), stored.len());
let n = query.len();
let mut acc0 = _mm256_setzero_ps();
let mut acc1 = _mm256_setzero_ps();
let mut acc2 = _mm256_setzero_ps();
let mut acc3 = _mm256_setzero_ps();
let mut query_acc0 = _mm256_setzero_ps();
let mut query_acc1 = _mm256_setzero_ps();
let mut i = 0;
while i + 32 <= n {
let i8_ptr_a = stored.as_ptr().add(i) as *const __m128i;
let i8_vec_a = _mm_loadu_si128(i8_ptr_a);
let i16_a = _mm256_cvtepi8_epi16(i8_vec_a);
let low_a = _mm256_cvtepi16_epi32(_mm256_extracti128_si256(i16_a, 0));
let high_a = _mm256_cvtepi16_epi32(_mm256_extracti128_si256(i16_a, 1));
let lf_a = _mm256_cvtepi32_ps(low_a);
let hf_a = _mm256_cvtepi32_ps(high_a);
let q_ptr_a = query.as_ptr().add(i);
let q_low_a = _mm256_loadu_ps(q_ptr_a);
let q_high_a = _mm256_loadu_ps(q_ptr_a.add(8));
acc0 = _mm256_fmadd_ps(q_low_a, lf_a, acc0);
acc1 = _mm256_fmadd_ps(q_high_a, hf_a, acc1);
query_acc0 = _mm256_add_ps(query_acc0, q_low_a);
query_acc0 = _mm256_add_ps(query_acc0, q_high_a);
let i8_ptr_b = stored.as_ptr().add(i + 16) as *const __m128i;
let i8_vec_b = _mm_loadu_si128(i8_ptr_b);
let i16_b = _mm256_cvtepi8_epi16(i8_vec_b);
let low_b = _mm256_cvtepi16_epi32(_mm256_extracti128_si256(i16_b, 0));
let high_b = _mm256_cvtepi16_epi32(_mm256_extracti128_si256(i16_b, 1));
let lf_b = _mm256_cvtepi32_ps(low_b);
let hf_b = _mm256_cvtepi32_ps(high_b);
let q_ptr_b = query.as_ptr().add(i + 16);
let q_low_b = _mm256_loadu_ps(q_ptr_b);
let q_high_b = _mm256_loadu_ps(q_ptr_b.add(8));
acc2 = _mm256_fmadd_ps(q_low_b, lf_b, acc2);
acc3 = _mm256_fmadd_ps(q_high_b, hf_b, acc3);
query_acc1 = _mm256_add_ps(query_acc1, q_low_b);
query_acc1 = _mm256_add_ps(query_acc1, q_high_b);
i += 32;
}
let acc_a = _mm256_add_ps(acc0, acc1);
let acc_b = _mm256_add_ps(acc2, acc3);
let acc = _mm256_add_ps(acc_a, acc_b);
let qacc = _mm256_add_ps(query_acc0, query_acc1);
let mut acc_scalar = 0.0f32;
let mut query_scalar = 0.0f32;
while i < n {
acc_scalar += query[i] * stored[i] as f32;
query_scalar += query[i];
i += 1;
}
let acc_total = horizontal_sum(acc) + acc_scalar;
let query_total = horizontal_sum(qacc) + query_scalar;
scale * acc_total + zero_point * query_total
}
}
#[target_feature(enable = "avx2")]
unsafe fn horizontal_sum(v: __m256) -> f32 {
let mut hi128 = _mm256_extractf128_ps(v, 1);
let lo128 = _mm256_castps256_ps128(v);
hi128 = _mm_add_ps(hi128, lo128);
let mut shuf = _mm_movehdup_ps(hi128);
shuf = _mm_add_ps(hi128, shuf);
shuf = _mm_movehl_ps(shuf, hi128);
shuf = _mm_add_ss(shuf, hi128);
_mm_cvtss_f32(shuf)
}
}
pub(crate) fn dot_int8_dispatch(query: &[f32], stored: &[i8], scale: f32, zero_point: f32) -> f32 {
#[cfg(target_arch = "x86_64")]
{
if std::is_x86_feature_detected!("avx2") {
return unsafe { int8_avx2::dot_int8_avx2(query, stored, scale, zero_point) };
}
}
dot_int8(query, stored, scale, zero_point)
}
impl NeuralReader {
pub fn dot_simd(&self, i: usize, query: &[f32]) -> f32 {
assert_eq!(query.len(), self.dim, "query length mismatch");
match self.dtype {
NeuralDtype::F32 => {
let v = self.vector(i);
dot_f32(query, v.as_f32_slice())
}
NeuralDtype::Int8 => {
let v = self.vector(i);
let qs: &[i8] = match v {
VectorView::Int8(s) => s,
_ => unreachable!("dtype path"),
};
dot_int8_dispatch(query, qs, self.scale, self.zero_point)
}
NeuralDtype::Q4 => unreachable!("Q4 guarded at open()"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TfidfEntry {
pub doc_id: u32,
pub term_id: u32,
pub value: f32,
}
#[derive(Debug)]
pub struct TfidfReader {
blob: BlobMmap,
num_docs: usize,
num_terms: usize,
num_entries: usize,
data_offset_in_payload: usize,
}
impl TfidfReader {
pub fn open(path: &Path) -> Result<Self, ReaderError> {
let blob = BlobMmap::open(path)?;
let payload = blob.payload();
require_magic(payload, TFIDF_MAGIC)?;
if payload.len() < TFIDF_HEADER_LEN {
return Err(ReaderError::BadHeader(format!(
"tfidf payload truncated: {} < {}",
payload.len(),
TFIDF_HEADER_LEN
)));
}
let version = payload[9];
if version != 1 {
return Err(ReaderError::BadHeader(format!(
"unsupported tfidf version {}",
version
)));
}
let num_docs = read_u32_le(payload, 13)? as usize;
let num_terms = read_u32_le(payload, 17)? as usize;
let num_entries = read_u32_le(payload, 21)? as usize;
let data_offset_in_payload = TFIDF_HEADER_LEN;
let expected = num_entries
.checked_mul(TFIDF_ENTRY_LEN)
.ok_or_else(|| ReaderError::BadHeader("tfidf entries length overflow".to_string()))?;
let available = payload
.len()
.checked_sub(data_offset_in_payload)
.ok_or_else(|| {
ReaderError::BadHeader("tfidf data offset exceeds payload".to_string())
})?;
if available < expected {
return Err(ReaderError::BadHeader(format!(
"tfidf payload short: have {} bytes, need {}",
available, expected
)));
}
let mut stored_hash = [0u8; 32];
stored_hash.copy_from_slice(&payload[25..57]);
let computed =
blob::blob_hash(&payload[data_offset_in_payload..data_offset_in_payload + expected]);
if stored_hash != computed {
return Err(ReaderError::BadHeader(
"tfidf content_hash does not match data".to_string(),
));
}
Ok(TfidfReader {
blob,
num_docs,
num_terms,
num_entries,
data_offset_in_payload,
})
}
pub fn num_docs(&self) -> usize {
self.num_docs
}
pub fn num_terms(&self) -> usize {
self.num_terms
}
pub fn num_entries(&self) -> usize {
self.num_entries
}
fn entry_at(&self, idx: usize) -> TfidfEntry {
let base = self.data_offset_in_payload + idx * TFIDF_ENTRY_LEN;
let payload = self.blob.payload();
let doc_id = read_u32_le(payload, base).expect("bounds checked at open");
let term_id = read_u32_le(payload, base + 4).expect("bounds checked at open");
let value = read_f32_le(payload, base + 8).expect("bounds checked at open");
TfidfEntry {
doc_id,
term_id,
value,
}
}
pub fn entries(&self) -> impl Iterator<Item = TfidfEntry> + '_ {
(0..self.num_entries).map(move |i| self.entry_at(i))
}
pub fn tf_idf(&self, doc_id: u32, term_id: u32) -> Option<f32> {
self.entries().find_map(|e| {
if e.doc_id == doc_id && e.term_id == term_id {
Some(e.value)
} else {
None
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdgNode {
pub node_id: u32,
pub node_type: u32,
pub file_path_id: u32,
pub start_line: u32,
pub end_line: u32,
pub sym_name_id: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdgEdge {
pub src: u32,
pub dst: u32,
pub edge_type: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdgNodeFull {
pub node_id: u32,
pub symbol_name: u32,
pub file_path: u32,
pub language: u32,
pub node_type: u32,
pub complexity: u32,
pub byte_start: u32,
pub byte_end: u32,
pub precision: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PdgEdgeMeta {
pub call_count: Option<u32>,
pub variable_name: Option<u32>,
pub confidence: Option<f32>,
pub channel: Option<u32>,
pub position: Option<u32>,
}
#[derive(Debug)]
pub struct PdgReader {
blob: BlobMmap,
version: u8,
num_nodes: usize,
num_edges: usize,
num_strings: usize,
strings_bytes_len: usize,
nodes_offset: usize,
edges_offset: usize,
edge_meta_offset: usize,
string_offsets_offset: usize,
string_bytes_offset: usize,
}
impl PdgReader {
pub fn open(path: &Path) -> Result<Self, ReaderError> {
let blob = BlobMmap::open(path)?;
let payload = blob.payload();
require_magic(payload, PDG_MAGIC)?;
if payload.len() < PDG_HEADER_LEN {
return Err(ReaderError::BadHeader(format!(
"pdg payload truncated: {} < {}",
payload.len(),
PDG_HEADER_LEN
)));
}
let version = payload[9];
if !matches!(version, 1 | 2) {
return Err(ReaderError::BadHeader(format!(
"unsupported pdg version {}",
version
)));
}
let num_nodes = read_u32_le(payload, 13)? as usize;
let num_edges = read_u32_le(payload, 17)? as usize;
let num_strings = read_u32_le(payload, 21)? as usize;
let strings_bytes_len = read_u32_le(payload, 25)? as usize;
let nodes_offset = PDG_HEADER_LEN;
let node_len = if version == 2 {
PDG_NODE_V2_LEN
} else {
PDG_NODE_LEN
};
let edges_offset = nodes_offset + num_nodes * node_len;
let (edge_meta_offset, string_offsets_offset) = if version == 2 {
let meta = edges_offset + num_edges * PDG_EDGE_LEN;
(meta, meta + num_edges * PDG_EDGE_META_LEN)
} else {
(
edges_offset + num_edges * PDG_EDGE_LEN,
edges_offset + num_edges * PDG_EDGE_LEN,
)
};
let string_bytes_offset = string_offsets_offset + num_strings * PDG_STRING_OFFSET_LEN;
let end = string_bytes_offset + strings_bytes_len;
if end > payload.len() {
return Err(ReaderError::BadHeader(format!(
"pdg payload overflow: need {} have {}",
end,
payload.len()
)));
}
let mut stored_hash = [0u8; 32];
stored_hash.copy_from_slice(&payload[29..61]);
let computed = blob::blob_hash(&payload[PDG_HEADER_LEN..end]);
if stored_hash != computed {
return Err(ReaderError::BadHeader(
"pdg content_hash does not match data".to_string(),
));
}
Ok(PdgReader {
blob,
version,
num_nodes,
num_edges,
num_strings,
strings_bytes_len,
nodes_offset,
edges_offset,
edge_meta_offset,
string_offsets_offset,
string_bytes_offset,
})
}
pub fn version(&self) -> u8 {
self.version
}
pub fn num_nodes(&self) -> usize {
self.num_nodes
}
pub fn num_edges(&self) -> usize {
self.num_edges
}
pub fn strings_bytes_len(&self) -> usize {
self.strings_bytes_len
}
pub fn node(&self, idx: usize) -> Result<PdgNode, ReaderError> {
if self.version != 1 {
return Err(ReaderError::BadHeader(
"node() requires a version-1 pdg payload; use node_full() for version 2"
.to_string(),
));
}
if idx >= self.num_nodes {
return Err(ReaderError::OutOfRange(format!(
"pdg node idx {} out of range {}",
idx, self.num_nodes
)));
}
let base = self.nodes_offset + idx * PDG_NODE_LEN;
let payload = self.blob.payload();
Ok(PdgNode {
node_id: read_u32_le(payload, base)?,
node_type: read_u32_le(payload, base + 4)?,
file_path_id: read_u32_le(payload, base + 8)?,
start_line: read_u32_le(payload, base + 12)?,
end_line: read_u32_le(payload, base + 16)?,
sym_name_id: read_u32_le(payload, base + 20)?,
})
}
pub fn edge(&self, idx: usize) -> Result<PdgEdge, ReaderError> {
if idx >= self.num_edges {
return Err(ReaderError::OutOfRange(format!(
"pdg edge idx {} out of range {}",
idx, self.num_edges
)));
}
let base = self.edges_offset + idx * PDG_EDGE_LEN;
let payload = self.blob.payload();
Ok(PdgEdge {
src: read_u32_le(payload, base)?,
dst: read_u32_le(payload, base + 4)?,
edge_type: read_u32_le(payload, base + 8)?,
})
}
pub fn node_full(&self, idx: usize) -> Result<PdgNodeFull, ReaderError> {
if self.version != 2 {
return Err(ReaderError::BadHeader(
"node_full requires a version-2 pdg payload".to_string(),
));
}
if idx >= self.num_nodes {
return Err(ReaderError::OutOfRange(format!(
"pdg node idx {} out of range {}",
idx, self.num_nodes
)));
}
let base = self.nodes_offset + idx * PDG_NODE_V2_LEN;
let payload = self.blob.payload();
let flags = read_u32_le(payload, base + 32)?;
Ok(PdgNodeFull {
node_id: read_u32_le(payload, base)?,
symbol_name: read_u32_le(payload, base + 4)?,
file_path: read_u32_le(payload, base + 8)?,
language: read_u32_le(payload, base + 12)?,
node_type: read_u32_le(payload, base + 16)?,
complexity: read_u32_le(payload, base + 20)?,
byte_start: read_u32_le(payload, base + 24)?,
byte_end: read_u32_le(payload, base + 28)?,
precision: flags & 1 == 1,
})
}
pub fn edge_meta(&self, idx: usize) -> Result<PdgEdgeMeta, ReaderError> {
if self.version != 2 {
return Err(ReaderError::BadHeader(
"edge_meta requires a version-2 pdg payload".to_string(),
));
}
if idx >= self.num_edges {
return Err(ReaderError::OutOfRange(format!(
"pdg edge idx {} out of range {}",
idx, self.num_edges
)));
}
let base = self.edge_meta_offset + idx * PDG_EDGE_META_LEN;
let payload = self.blob.payload();
let opt = |raw: u32| (raw != PDG_V2_NONE).then_some(raw);
let confidence_raw = read_u32_le(payload, base + 8)?;
let confidence = f32::from_bits(confidence_raw);
Ok(PdgEdgeMeta {
call_count: opt(read_u32_le(payload, base)?),
variable_name: opt(read_u32_le(payload, base + 4)?),
confidence: (!confidence.is_nan()).then_some(confidence),
channel: opt(read_u32_le(payload, base + 12)?),
position: opt(read_u32_le(payload, base + 16)?),
})
}
pub fn resolve_string(&self, id: u32) -> Option<&str> {
resolve_string_helper(
self.blob.payload(),
self.string_offsets_offset,
self.string_bytes_offset,
self.num_strings,
id,
)
}
pub fn callers(&self, node_id: u32) -> impl Iterator<Item = u32> + '_ {
let collect: Vec<u32> = (0..self.num_edges)
.filter_map(move |i| {
let e = self.edge(i).ok()?;
if e.dst == node_id { Some(e.src) } else { None }
})
.collect();
collect.into_iter()
}
pub fn callees(&self, node_id: u32) -> impl Iterator<Item = u32> + '_ {
let collect: Vec<u32> = (0..self.num_edges)
.filter_map(move |i| {
let e = self.edge(i).ok()?;
if e.src == node_id { Some(e.dst) } else { None }
})
.collect();
collect.into_iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SymbolEntry {
pub name_id: u32,
pub sym_type: u32,
pub file_path_id: u32,
pub start_line: u32,
pub end_line: u32,
pub complexity: f32,
}
#[derive(Debug)]
pub struct SymbolReader {
blob: BlobMmap,
num_symbols: usize,
num_strings: usize,
strings_bytes_len: usize,
symbols_offset: usize,
string_offsets_offset: usize,
string_bytes_offset: usize,
}
impl SymbolReader {
pub fn open(path: &Path) -> Result<Self, ReaderError> {
let blob = BlobMmap::open(path)?;
let payload = blob.payload();
require_magic(payload, SYMBOLS_MAGIC)?;
if payload.len() < SYMBOLS_HEADER_LEN {
return Err(ReaderError::BadHeader(format!(
"symbols payload truncated: {} < {}",
payload.len(),
SYMBOLS_HEADER_LEN
)));
}
let version = payload[9];
if version != 1 {
return Err(ReaderError::BadHeader(format!(
"unsupported symbols version {}",
version
)));
}
let num_symbols = read_u32_le(payload, 13)? as usize;
let num_strings = read_u32_le(payload, 17)? as usize;
let strings_bytes_len = read_u32_le(payload, 21)? as usize;
let symbols_offset = SYMBOLS_HEADER_LEN;
let string_offsets_offset = symbols_offset + num_symbols * SYMBOL_ENTRY_LEN;
let string_bytes_offset = string_offsets_offset + num_strings * PDG_STRING_OFFSET_LEN;
let end = string_bytes_offset + strings_bytes_len;
if end > payload.len() {
return Err(ReaderError::BadHeader(format!(
"symbols payload overflow: need {} have {}",
end,
payload.len()
)));
}
let mut stored_hash = [0u8; 32];
stored_hash.copy_from_slice(&payload[25..57]);
let computed = blob::blob_hash(&payload[SYMBOLS_HEADER_LEN..end]);
if stored_hash != computed {
return Err(ReaderError::BadHeader(
"symbols content_hash does not match data".to_string(),
));
}
Ok(SymbolReader {
blob,
num_symbols,
num_strings,
strings_bytes_len,
symbols_offset,
string_offsets_offset,
string_bytes_offset,
})
}
pub fn num_symbols(&self) -> usize {
self.num_symbols
}
pub fn strings_bytes_len(&self) -> usize {
self.strings_bytes_len
}
pub fn symbol(&self, idx: usize) -> Result<SymbolEntry, ReaderError> {
if idx >= self.num_symbols {
return Err(ReaderError::OutOfRange(format!(
"symbol idx {} out of range {}",
idx, self.num_symbols
)));
}
let base = self.symbols_offset + idx * SYMBOL_ENTRY_LEN;
let payload = self.blob.payload();
Ok(SymbolEntry {
name_id: read_u32_le(payload, base)?,
sym_type: read_u32_le(payload, base + 4)?,
file_path_id: read_u32_le(payload, base + 8)?,
start_line: read_u32_le(payload, base + 12)?,
end_line: read_u32_le(payload, base + 16)?,
complexity: read_f32_le(payload, base + 20)?,
})
}
pub fn resolve_string(&self, id: u32) -> Option<&str> {
resolve_string_helper(
self.blob.payload(),
self.string_offsets_offset,
self.string_bytes_offset,
self.num_strings,
id,
)
}
pub fn lookup(&self, name: &str) -> Option<SymbolEntry> {
(0..self.num_symbols).find_map(|i| {
let s = self.symbol(i).ok()?;
let actual = self.resolve_string(s.name_id)?;
if actual == name { Some(s) } else { None }
})
}
}
fn resolve_string_helper(
payload: &[u8],
string_offsets_offset: usize,
string_bytes_offset: usize,
num_strings: usize,
id: u32,
) -> Option<&str> {
let id_usize = id as usize;
if id_usize >= num_strings {
return None;
}
let off_base = string_offsets_offset + id_usize * PDG_STRING_OFFSET_LEN;
let mut off_buf = [0u8; 4];
off_buf.copy_from_slice(&payload[off_base..off_base + 4]);
let str_offset = u32::from_le_bytes(off_buf) as usize;
let mut len_buf = [0u8; 4];
len_buf.copy_from_slice(&payload[off_base + 4..off_base + 8]);
let str_len = u32::from_le_bytes(len_buf) as usize;
let bytes_start = string_bytes_offset + str_offset;
if bytes_start + str_len > payload.len() {
return None;
}
let raw = &payload[bytes_start..bytes_start + str_len];
std::str::from_utf8(raw).ok()
}
#[cfg(test)]
#[path = "reader_test.rs"]
mod tests;