Skip to main content

code_moniker_core/core/moniker/
view.rs

1// code-moniker: ignore-file[smell-feature-envy-local, smell-balanced-method-fanout, smell-harmonious-method-size]
2// TODO(smell): split MonikerView parsing, segment traversal, and display/query helpers before enabling these guardrails here.
3use super::encoding::{EncodingError, HEADER_FIXED_LEN, VERSION, is_uri_kind, read_u16};
4
5#[derive(Copy, Clone, Eq, PartialEq, Debug)]
6pub struct Segment<'a> {
7	pub kind: &'a [u8],
8	pub name: &'a [u8],
9}
10
11#[derive(Copy, Clone, Debug)]
12pub struct MonikerView<'a> {
13	bytes: &'a [u8],
14	project_off: usize,
15	project_len: usize,
16	segs_off: usize,
17}
18
19impl<'a> MonikerView<'a> {
20	pub fn from_encoded(bytes: &'a [u8]) -> Result<Self, EncodingError> {
21		if bytes.len() < HEADER_FIXED_LEN {
22			return Err(EncodingError::Truncated);
23		}
24		let version = bytes[0];
25		if version != VERSION {
26			return Err(EncodingError::UnknownVersion(version));
27		}
28		let project_len = read_u16(bytes, 1) as usize;
29		let project_off = 3;
30		let segs_off = project_off + project_len;
31		if bytes.len() < segs_off {
32			return Err(EncodingError::ProjectOverflow);
33		}
34		let project = &bytes[project_off..project_off + project_len];
35		if project.is_empty() {
36			return Err(EncodingError::EmptyProject);
37		}
38		std::str::from_utf8(project).map_err(|_| EncodingError::NonUtf8Project)?;
39		let mut cursor = segs_off;
40		while cursor < bytes.len() {
41			if bytes.len() < cursor + 2 {
42				return Err(EncodingError::SegmentOverflow);
43			}
44			let kind_len = read_u16(bytes, cursor) as usize;
45			let kind_start = cursor + 2;
46			cursor = kind_start + kind_len;
47			if bytes.len() < cursor {
48				return Err(EncodingError::SegmentOverflow);
49			}
50			let kind = &bytes[kind_start..cursor];
51			std::str::from_utf8(kind).map_err(|_| EncodingError::NonUtf8SegmentKind)?;
52			if !is_uri_kind(kind) {
53				return Err(EncodingError::InvalidSegmentKind);
54			}
55			if bytes.len() < cursor + 2 {
56				return Err(EncodingError::SegmentOverflow);
57			}
58			let name_len = read_u16(bytes, cursor) as usize;
59			let name_start = cursor + 2;
60			cursor = name_start + name_len;
61			if bytes.len() < cursor {
62				return Err(EncodingError::SegmentOverflow);
63			}
64			let name = &bytes[name_start..cursor];
65			std::str::from_utf8(name).map_err(|_| EncodingError::NonUtf8SegmentName)?;
66		}
67		Ok(Self {
68			bytes,
69			project_off,
70			project_len,
71			segs_off,
72		})
73	}
74
75	#[allow(clippy::missing_safety_doc)]
76	pub(crate) unsafe fn from_encoded_unchecked(bytes: &'a [u8]) -> Self {
77		debug_assert!(bytes.len() >= HEADER_FIXED_LEN && bytes[0] == VERSION);
78		let project_len = read_u16(bytes, 1) as usize;
79		let project_off = 3;
80		let segs_off = project_off + project_len;
81		Self {
82			bytes,
83			project_off,
84			project_len,
85			segs_off,
86		}
87	}
88
89	pub fn project(&self) -> &'a [u8] {
90		&self.bytes[self.project_off..self.project_off + self.project_len]
91	}
92
93	pub fn segment_count(&self) -> u16 {
94		self.segments().count() as u16
95	}
96
97	pub fn segments(&self) -> SegmentIter<'a> {
98		SegmentIter {
99			bytes: self.bytes,
100			cursor: self.segs_off,
101		}
102	}
103
104	pub fn as_encoded(&self) -> &'a [u8] {
105		self.bytes
106	}
107
108	pub fn is_ancestor_of(&self, other: &MonikerView<'_>) -> bool {
109		if self.project() != other.project() {
110			return false;
111		}
112		other.bytes.starts_with(self.bytes)
113	}
114}
115
116#[derive(Clone, Debug)]
117pub struct SegmentIter<'a> {
118	bytes: &'a [u8],
119	cursor: usize,
120}
121
122impl<'a> Iterator for SegmentIter<'a> {
123	type Item = Segment<'a>;
124
125	fn next(&mut self) -> Option<Self::Item> {
126		if self.cursor >= self.bytes.len() {
127			return None;
128		}
129		let kind_len = read_u16(self.bytes, self.cursor) as usize;
130		let kind_start = self.cursor + 2;
131		let kind = &self.bytes[kind_start..kind_start + kind_len];
132		let name_len_off = kind_start + kind_len;
133		let name_len = read_u16(self.bytes, name_len_off) as usize;
134		let name_start = name_len_off + 2;
135		let name = &self.bytes[name_start..name_start + name_len];
136		self.cursor = name_start + name_len;
137		Some(Segment { kind, name })
138	}
139}
140
141#[cfg(test)]
142mod tests {
143	use super::*;
144
145	#[test]
146	fn view_rejects_truncated_buffer() {
147		assert_eq!(
148			MonikerView::from_encoded(&[2, 0]).unwrap_err(),
149			EncodingError::Truncated
150		);
151	}
152
153	#[test]
154	fn view_rejects_unknown_version() {
155		let buf = [99, 0, 0];
156		assert_eq!(
157			MonikerView::from_encoded(&buf).unwrap_err(),
158			EncodingError::UnknownVersion(99)
159		);
160	}
161
162	#[test]
163	fn view_rejects_project_overflow() {
164		let buf: Vec<u8> = vec![2, 10, 0, 0];
165		assert_eq!(
166			MonikerView::from_encoded(&buf).unwrap_err(),
167			EncodingError::ProjectOverflow
168		);
169	}
170
171	#[test]
172	fn view_rejects_empty_project() {
173		let buf: Vec<u8> = vec![2, 0, 0];
174		assert_eq!(
175			MonikerView::from_encoded(&buf).unwrap_err(),
176			EncodingError::EmptyProject
177		);
178	}
179
180	#[test]
181	fn view_rejects_segment_overflow() {
182		let buf: Vec<u8> = vec![2, 3, 0, b'a', b'p', b'p', 5, 0];
183		assert_eq!(
184			MonikerView::from_encoded(&buf).unwrap_err(),
185			EncodingError::SegmentOverflow
186		);
187	}
188
189	#[test]
190	fn view_rejects_invalid_segment_kind() {
191		let buf: Vec<u8> = vec![2, 3, 0, b'a', b'p', b'p', 1, 0, b'1', 1, 0, b'x'];
192		assert_eq!(
193			MonikerView::from_encoded(&buf).unwrap_err(),
194			EncodingError::InvalidSegmentKind
195		);
196	}
197
198	#[test]
199	fn view_accepts_project_only() {
200		let buf: Vec<u8> = vec![2, 3, 0, b'a', b'p', b'p'];
201		let v = MonikerView::from_encoded(&buf).unwrap();
202		assert_eq!(v.project(), b"app");
203		assert_eq!(v.segment_count(), 0);
204	}
205}