use crate::bytes::{le_u32, le_u64};
use crate::error::UrnaError;
use crate::layout::{SECTION_SPACE_TABLE, SPACE_BAND_LEN};
pub const SPACE_TABLE_PAYLOAD_VERSION: u32 = 1;
pub const SPACE_DTYPE_F32: u8 = 0;
pub const SPACE_DTYPE_F16: u8 = 1;
pub const SPACE_DTYPE_I8: u8 = 2;
pub const SPACE_DTYPE_I4: u8 = 3;
const MIN_ENTRY_SIZE: usize = 1 + 4 + 4 + 1 + 4 + 8;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpaceEntry {
pub space_index: u8,
pub name: String,
pub dim: u32,
pub dtype: u8,
pub model_hash: String,
pub n_vectors: u64,
}
impl SpaceEntry {
pub fn dtype_str(&self) -> &'static str {
match self.dtype {
SPACE_DTYPE_F32 => "float32",
SPACE_DTYPE_F16 => "float16",
SPACE_DTYPE_I8 => "int8",
SPACE_DTYPE_I4 => "int4",
_ => "unknown",
}
}
}
fn malformed(reason: impl Into<String>) -> UrnaError {
UrnaError::MalformedSectionPayload {
section_id: SECTION_SPACE_TABLE,
reason: reason.into(),
}
}
fn check_entry(e: &SpaceEntry) -> Result<(), UrnaError> {
if e.space_index == 0 || e.space_index >= SPACE_BAND_LEN as u8 {
return Err(malformed(format!(
"space_table: space_index {} outside 1..{}",
e.space_index,
SPACE_BAND_LEN - 1
)));
}
if e.dtype > SPACE_DTYPE_I4 {
return Err(malformed(format!(
"space_table: unknown dtype code {}",
e.dtype
)));
}
if e.name.is_empty() {
return Err(malformed("space_table: empty space name"));
}
if !e.model_hash.starts_with("sha256:") {
return Err(malformed("space_table: model_hash must be sha256:<hex>"));
}
Ok(())
}
pub fn encode_space_table(entries: &[SpaceEntry]) -> Result<Vec<u8>, UrnaError> {
for (i, e) in entries.iter().enumerate() {
check_entry(e)?;
if entries[..i].iter().any(|p| p.space_index == e.space_index) {
return Err(malformed(format!(
"space_table: duplicate space_index {}",
e.space_index
)));
}
if entries[..i].iter().any(|p| p.name == e.name) {
return Err(malformed(format!("space_table: duplicate name {}", e.name)));
}
}
let mut out = Vec::new();
out.extend_from_slice(&SPACE_TABLE_PAYLOAD_VERSION.to_le_bytes());
out.extend_from_slice(&(entries.len() as u64).to_le_bytes());
for e in entries {
out.push(e.space_index);
out.extend_from_slice(&(e.name.len() as u32).to_le_bytes());
out.extend_from_slice(e.name.as_bytes());
out.extend_from_slice(&e.dim.to_le_bytes());
out.push(e.dtype);
out.extend_from_slice(&(e.model_hash.len() as u32).to_le_bytes());
out.extend_from_slice(e.model_hash.as_bytes());
out.extend_from_slice(&e.n_vectors.to_le_bytes());
}
Ok(out)
}
pub fn decode_space_table(bytes: &[u8]) -> Result<Vec<SpaceEntry>, UrnaError> {
let mut cur = Cursor::new(bytes);
let version = cur.u32()?;
if version != SPACE_TABLE_PAYLOAD_VERSION {
return Err(UrnaError::UnsupportedSectionVersion {
section_id: SECTION_SPACE_TABLE,
version,
});
}
let n = cur.u64()? as usize;
if n > cur.remaining() / MIN_ENTRY_SIZE {
return Err(malformed("space_table: entry count exceeds payload"));
}
let mut entries = Vec::with_capacity(n);
for _ in 0..n {
let space_index = cur.u8()?;
let name = cur.utf8()?;
let dim = cur.u32()?;
let dtype = cur.u8()?;
let model_hash = cur.utf8()?;
let n_vectors = cur.u64()?;
let e = SpaceEntry {
space_index,
name,
dim,
dtype,
model_hash,
n_vectors,
};
check_entry(&e)?;
if entries
.iter()
.any(|p: &SpaceEntry| p.space_index == e.space_index || p.name == e.name)
{
return Err(malformed("space_table: duplicate index or name"));
}
entries.push(e);
}
if cur.pos != bytes.len() {
return Err(malformed("trailing bytes after entries"));
}
Ok(entries)
}
struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn remaining(&self) -> usize {
self.buf.len() - self.pos
}
fn take(&mut self, n: usize) -> Result<&'a [u8], UrnaError> {
if n > self.remaining() {
return Err(malformed("unexpected EOF"));
}
let s = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, UrnaError> {
Ok(self.take(1)?[0])
}
fn u32(&mut self) -> Result<u32, UrnaError> {
le_u32(self.take(4)?)
}
fn u64(&mut self) -> Result<u64, UrnaError> {
le_u64(self.take(8)?)
}
fn utf8(&mut self) -> Result<String, UrnaError> {
let len = self.u32()? as usize;
if len > self.remaining() {
return Err(malformed("space_table: string length exceeds payload"));
}
let s = std::str::from_utf8(self.take(len)?)
.map_err(|_| malformed("space_table: string is not utf-8"))?;
Ok(s.to_string())
}
}