code-moniker-core 0.2.0

Core symbol-graph types and per-language extractors for code-moniker (pure Rust, no pgrx). Consumed by the CLI and the PostgreSQL extension.
Documentation
use super::encoding::{EncodingError, HEADER_FIXED_LEN, VERSION, read_u16};

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Segment<'a> {
	pub kind: &'a [u8],
	pub name: &'a [u8],
}

#[derive(Copy, Clone, Debug)]
pub struct MonikerView<'a> {
	bytes: &'a [u8],
	project_off: usize,
	project_len: usize,
	segs_off: usize,
}

impl<'a> MonikerView<'a> {
	pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, EncodingError> {
		if bytes.len() < HEADER_FIXED_LEN {
			return Err(EncodingError::Truncated);
		}
		let version = bytes[0];
		if version != VERSION {
			return Err(EncodingError::UnknownVersion(version));
		}
		let project_len = read_u16(bytes, 1) as usize;
		let project_off = 3;
		let segs_off = project_off + project_len;
		if bytes.len() < segs_off {
			return Err(EncodingError::ProjectOverflow);
		}
		let mut cursor = segs_off;
		while cursor < bytes.len() {
			if bytes.len() < cursor + 2 {
				return Err(EncodingError::SegmentOverflow);
			}
			let kind_len = read_u16(bytes, cursor) as usize;
			cursor += 2 + kind_len;
			if bytes.len() < cursor + 2 {
				return Err(EncodingError::SegmentOverflow);
			}
			let name_len = read_u16(bytes, cursor) as usize;
			cursor += 2 + name_len;
			if bytes.len() < cursor {
				return Err(EncodingError::SegmentOverflow);
			}
		}
		Ok(Self {
			bytes,
			project_off,
			project_len,
			segs_off,
		})
	}

	#[allow(clippy::missing_safety_doc)]
	pub unsafe fn from_canonical_bytes(bytes: &'a [u8]) -> Self {
		debug_assert!(bytes.len() >= HEADER_FIXED_LEN && bytes[0] == VERSION);
		let project_len = read_u16(bytes, 1) as usize;
		let project_off = 3;
		let segs_off = project_off + project_len;
		Self {
			bytes,
			project_off,
			project_len,
			segs_off,
		}
	}

	pub fn project(&self) -> &'a [u8] {
		&self.bytes[self.project_off..self.project_off + self.project_len]
	}

	pub fn segment_count(&self) -> u16 {
		self.segments().count() as u16
	}

	pub fn segments(&self) -> SegmentIter<'a> {
		SegmentIter {
			bytes: self.bytes,
			cursor: self.segs_off,
		}
	}

	pub fn as_bytes(&self) -> &'a [u8] {
		self.bytes
	}

	pub fn is_ancestor_of(&self, other: &MonikerView<'_>) -> bool {
		if self.project() != other.project() {
			return false;
		}
		other.bytes.starts_with(self.bytes)
	}
}

#[derive(Clone, Debug)]
pub struct SegmentIter<'a> {
	bytes: &'a [u8],
	cursor: usize,
}

impl<'a> Iterator for SegmentIter<'a> {
	type Item = Segment<'a>;

	fn next(&mut self) -> Option<Self::Item> {
		if self.cursor >= self.bytes.len() {
			return None;
		}
		let kind_len = read_u16(self.bytes, self.cursor) as usize;
		let kind_start = self.cursor + 2;
		let kind = &self.bytes[kind_start..kind_start + kind_len];
		let name_len_off = kind_start + kind_len;
		let name_len = read_u16(self.bytes, name_len_off) as usize;
		let name_start = name_len_off + 2;
		let name = &self.bytes[name_start..name_start + name_len];
		self.cursor = name_start + name_len;
		Some(Segment { kind, name })
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn view_rejects_truncated_buffer() {
		assert_eq!(
			MonikerView::from_bytes(&[2, 0]).unwrap_err(),
			EncodingError::Truncated
		);
	}

	#[test]
	fn view_rejects_unknown_version() {
		let buf = [99, 0, 0];
		assert_eq!(
			MonikerView::from_bytes(&buf).unwrap_err(),
			EncodingError::UnknownVersion(99)
		);
	}

	#[test]
	fn view_rejects_project_overflow() {
		let buf: Vec<u8> = vec![2, 10, 0, 0];
		assert_eq!(
			MonikerView::from_bytes(&buf).unwrap_err(),
			EncodingError::ProjectOverflow
		);
	}

	#[test]
	fn view_rejects_segment_overflow() {
		let buf: Vec<u8> = vec![2, 0, 0, 5, 0];
		assert_eq!(
			MonikerView::from_bytes(&buf).unwrap_err(),
			EncodingError::SegmentOverflow
		);
	}

	#[test]
	fn view_accepts_project_only() {
		let buf: Vec<u8> = vec![2, 3, 0, b'a', b'p', b'p'];
		let v = MonikerView::from_bytes(&buf).unwrap();
		assert_eq!(v.project(), b"app");
		assert_eq!(v.segment_count(), 0);
	}
}