pub const NGG1_MAGIC: u32 = 0x3147_474E;
pub const NGG1_VERSION: u16 = 1;
pub const NGG1_HEADER_SIZE: usize = 32;
pub const NGG1_NODE_STRIDE: usize = 24;
const N_ID: usize = 0;
const N_X: usize = 4;
const N_Y: usize = 8;
const N_DOMAIN: usize = 12;
const N_CATEGORY: usize = 14;
const N_FLAGS: usize = 16;
const N_DEGREE: usize = 20;
pub const EDGE_SUBCLASS: u8 = 0;
pub const EDGE_RELATION: u8 = 1;
pub const FLAG_DOMAIN_ROOT: u8 = 0x01;
pub const FLAG_CATEGORY_ROOT: u8 = 0x02;
pub const FLAG_HAS_PAGE: u8 = 0x04;
pub const FLAG_BRIDGE: u8 = 0x08;
pub const CATEGORY_NONE: u16 = 0xFFFF;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NodeRecord {
pub id: u32,
pub x: f32,
pub y: f32,
pub domain: u16,
pub category: u16,
pub flags: u8,
pub degree: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ngg1Error(pub String);
impl core::fmt::Display for Ngg1Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "NGG1: {}", self.0)
}
}
impl std::error::Error for Ngg1Error {}
#[inline]
fn rd_u32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
#[inline]
fn rd_u16(b: &[u8], off: usize) -> u16 {
u16::from_le_bytes([b[off], b[off + 1]])
}
#[inline]
fn rd_f32(b: &[u8], off: usize) -> f32 {
f32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
pub struct Ngg1<'a> {
bytes: &'a [u8],
node_count: usize,
edge_count: usize,
off_nodes: usize,
off_adjacency: usize,
off_col_idx: usize,
off_edge_types: usize,
str_count: usize,
blob_len: usize,
off_str_offsets: usize,
off_blob: usize,
}
impl<'a> Ngg1<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Ngg1Error> {
if bytes.len() < NGG1_HEADER_SIZE {
return Err(Ngg1Error(format!(
"buffer too small ({} < {})",
bytes.len(),
NGG1_HEADER_SIZE
)));
}
let magic = rd_u32(bytes, 0);
if magic != NGG1_MAGIC {
return Err(Ngg1Error(format!(
"bad magic 0x{magic:08X} (expected 0x{NGG1_MAGIC:08X})"
)));
}
let version = rd_u16(bytes, 4);
if version != NGG1_VERSION {
return Err(Ngg1Error(format!(
"unsupported version {version} (reader supports {NGG1_VERSION})"
)));
}
let node_count = rd_u32(bytes, 8) as usize;
let edge_count = rd_u32(bytes, 12) as usize;
let off_nodes = rd_u32(bytes, 16) as usize;
let off_adjacency = rd_u32(bytes, 20) as usize;
let off_edge_types = rd_u32(bytes, 24) as usize;
let off_strings = rd_u32(bytes, 28) as usize;
if (off_nodes | off_adjacency | off_edge_types | off_strings) & 3 != 0 {
return Err(Ngg1Error("a section offset is not 4-byte aligned".into()));
}
let len = bytes.len();
let nodes_end = off_nodes
.checked_add(node_count.checked_mul(NGG1_NODE_STRIDE).ok_or_ov()?)
.ok_or_ov()?;
let row_ptr_len = node_count + 1;
let off_col_idx = off_adjacency
.checked_add(row_ptr_len.checked_mul(4).ok_or_ov()?)
.ok_or_ov()?;
let adj_end = off_col_idx
.checked_add(edge_count.checked_mul(4).ok_or_ov()?)
.ok_or_ov()?;
let edge_types_end = off_edge_types.checked_add(edge_count).ok_or_ov()?;
if off_strings.checked_add(8).ok_or_ov()? > len {
return Err(Ngg1Error("string table header exceeds buffer".into()));
}
if nodes_end > len || adj_end > len || edge_types_end > len {
return Err(Ngg1Error(
"section bounds exceed buffer length (truncated artifact)".into(),
));
}
let str_count = rd_u32(bytes, off_strings) as usize;
let blob_len = rd_u32(bytes, off_strings + 4) as usize;
if str_count != node_count * 2 {
return Err(Ngg1Error(format!(
"string count {str_count} != 2 * node_count {}",
node_count * 2
)));
}
let off_str_offsets = off_strings + 8;
let off_blob = off_str_offsets
.checked_add(str_count.checked_mul(4).ok_or_ov()?)
.ok_or_ov()?;
if off_blob.checked_add(blob_len).ok_or_ov()? > len {
return Err(Ngg1Error("string blob exceeds buffer length".into()));
}
Ok(Self {
bytes,
node_count,
edge_count,
off_nodes,
off_adjacency,
off_col_idx,
off_edge_types,
str_count,
blob_len,
off_str_offsets,
off_blob,
})
}
#[inline]
pub fn node_count(&self) -> usize {
self.node_count
}
#[inline]
pub fn edge_count(&self) -> usize {
self.edge_count
}
#[inline]
pub fn node(&self, i: usize) -> NodeRecord {
assert!(i < self.node_count, "NGG1: node index {i} out of range");
let o = self.off_nodes + i * NGG1_NODE_STRIDE;
NodeRecord {
id: rd_u32(self.bytes, o + N_ID),
x: rd_f32(self.bytes, o + N_X),
y: rd_f32(self.bytes, o + N_Y),
domain: rd_u16(self.bytes, o + N_DOMAIN),
category: rd_u16(self.bytes, o + N_CATEGORY),
flags: self.bytes[o + N_FLAGS],
degree: rd_u32(self.bytes, o + N_DEGREE),
}
}
#[inline]
pub fn row_ptr(&self, i: usize) -> u32 {
debug_assert!(i <= self.node_count);
rd_u32(self.bytes, self.off_adjacency + i * 4)
}
#[inline]
pub fn col_idx(&self, e: usize) -> u32 {
debug_assert!(e < self.edge_count);
rd_u32(self.bytes, self.off_col_idx + e * 4)
}
#[inline]
pub fn edge_type(&self, e: usize) -> u8 {
debug_assert!(e < self.edge_count);
self.bytes[self.off_edge_types + e]
}
pub fn string(&self, s: usize) -> String {
assert!(s < self.str_count, "NGG1: string index {s} out of range");
let start = rd_u32(self.bytes, self.off_str_offsets + s * 4) as usize;
let end = if s + 1 < self.str_count {
rd_u32(self.bytes, self.off_str_offsets + (s + 1) * 4) as usize
} else {
self.blob_len
};
let a = self.off_blob + start.min(self.blob_len);
let b = self.off_blob + end.min(self.blob_len);
String::from_utf8_lossy(&self.bytes[a..b.max(a)]).into_owned()
}
#[inline]
pub fn label(&self, i: usize) -> String {
self.string(i * 2)
}
#[inline]
pub fn iri(&self, i: usize) -> String {
self.string(i * 2 + 1)
}
pub fn seed_positions(&self) -> Vec<f32> {
let mut out = vec![0.0f32; self.node_count * 2];
for i in 0..self.node_count {
let o = self.off_nodes + i * NGG1_NODE_STRIDE;
out[i * 2] = rd_f32(self.bytes, o + N_X);
out[i * 2 + 1] = rd_f32(self.bytes, o + N_Y);
}
out
}
pub fn edges(&self) -> (Vec<u32>, Vec<u32>, Vec<u8>) {
let e = self.edge_count;
let mut src = Vec::with_capacity(e);
let mut tgt = Vec::with_capacity(e);
let mut ty = Vec::with_capacity(e);
for i in 0..self.node_count {
let lo = self.row_ptr(i) as usize;
let hi = self.row_ptr(i + 1) as usize;
for slot in lo..hi.min(e) {
src.push(i as u32);
tgt.push(self.col_idx(slot));
ty.push(self.edge_type(slot));
}
}
(src, tgt, ty)
}
pub fn degrees(&self) -> Vec<u32> {
(0..self.node_count).map(|i| self.node(i).degree).collect()
}
}
trait OkOrOv {
fn ok_or_ov(self) -> Result<usize, Ngg1Error>;
}
impl OkOrOv for Option<usize> {
#[inline]
fn ok_or_ov(self) -> Result<usize, Ngg1Error> {
self.ok_or_else(|| Ngg1Error("section arithmetic overflow".into()))
}
}
#[cfg(test)]
pub(crate) fn worked_example() -> Vec<u8> {
let bytes: Vec<u8> = vec![
0x4E, 0x47, 0x47, 0x31, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x41, 0x49, 0x6E, 0x67, 0x3A, 0x30, 0x4D, 0x4C, 0x6E, 0x67, 0x3A, 0x31, 0x4E, 0x4C, 0x50, 0x6E, 0x67, 0x3A, 0x32, ];
assert_eq!(bytes.len(), 183, "worked example must be 183 bytes");
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_worked_example_header() {
let b = worked_example();
let g = Ngg1::parse(&b).unwrap();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 2);
}
#[test]
fn node_stride_24_fields() {
let b = worked_example();
let g = Ngg1::parse(&b).unwrap();
assert_eq!(g.node(1).x, 1.0);
assert_eq!(g.node(2).y, 1.0);
assert_eq!(g.node(1).degree, 2);
assert_eq!(g.node(0).flags, FLAG_DOMAIN_ROOT);
assert_eq!(g.node(0).id, 0);
assert_eq!(g.node(1).category, 1);
}
#[test]
fn strings_pair_label_iri() {
let b = worked_example();
let g = Ngg1::parse(&b).unwrap();
assert_eq!(g.label(0), "AI");
assert_eq!(g.iri(0), "ng:0");
assert_eq!(g.label(1), "ML");
assert_eq!(g.iri(1), "ng:1");
assert_eq!(g.label(2), "NLP");
assert_eq!(g.iri(2), "ng:2");
}
#[test]
fn csr_topology() {
let b = worked_example();
let g = Ngg1::parse(&b).unwrap();
assert_eq!(g.row_ptr(0), 0);
assert_eq!(g.row_ptr(1), 0);
assert_eq!(g.row_ptr(2), 2);
assert_eq!(g.row_ptr(3), 2); let (src, tgt, ty) = g.edges();
assert_eq!(src, vec![1, 1]); assert_eq!(tgt, vec![0, 2]); assert_eq!(ty, vec![EDGE_SUBCLASS, EDGE_RELATION]);
}
#[test]
fn seed_positions_packed() {
let b = worked_example();
let g = Ngg1::parse(&b).unwrap();
assert_eq!(g.seed_positions(), vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn rejects_bad_magic() {
let mut b = worked_example();
b[0] = 0x00;
assert!(Ngg1::parse(&b).is_err());
}
#[test]
fn rejects_bad_version() {
let mut b = worked_example();
b[4] = 0x02;
assert!(Ngg1::parse(&b).is_err());
}
#[test]
fn rejects_truncated() {
let b = worked_example();
let short = &b[..100];
assert!(Ngg1::parse(short).is_err());
}
#[test]
fn rejects_too_small() {
assert!(Ngg1::parse(&[0u8; 4]).is_err());
}
}