Skip to main content

tightbeam/
matrix.rs

1#[cfg(not(feature = "std"))]
2use alloc::vec::Vec;
3
4use crate::Asn1Matrix;
5
6#[cfg(feature = "derive")]
7use crate::Errorizable;
8
9pub type MatrixResult<T> = core::result::Result<T, MatrixError>;
10
11#[cfg_attr(feature = "derive", derive(Errorizable))]
12#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub enum MatrixError {
14	#[cfg_attr(feature = "derive", error("Asn1Matrix: n MUST be in 1..=255 (got {0})"))]
15	InvalidN(u8),
16	#[cfg_attr(
17		feature = "derive",
18		error("Asn1Matrix: data length MUST equal n*n (n={n}, len={len})")
19	)]
20	LengthMismatch { n: u8, len: usize },
21}
22
23crate::impl_error_display!(MatrixError {
24	InvalidN(n) => "Asn1Matrix: n MUST be in 1..=255 (got {n})",
25	LengthMismatch { n, len } => "Asn1Matrix: data length MUST equal n*n (n={n}, len={len})",
26});
27
28/// A common interface for NxN flag matrices (u8 cells), row-major.
29pub trait MatrixLike {
30	/// Dimension N (matrix is N×N).
31	fn n(&self) -> u8;
32
33	/// Get cell (row r, col c). Out-of-bounds returns 0.
34	fn get(&self, r: u8, c: u8) -> u8;
35
36	/// Set cell (row r, col c) to value. Out-of-bounds is a no-op.
37	fn set(&mut self, r: u8, c: u8, value: u8);
38
39	/// Fill all cells with value.
40	fn fill(&mut self, value: u8);
41
42	/// Clear all cells to zero.
43	fn clear(&mut self) {
44		self.fill(0);
45	}
46}
47
48/// Trait for converting matrix types to MatrixDyn.
49/// This avoids the Infallible error type when MatrixDyn is converted to itself.
50pub trait IntoMatrixDyn {
51	fn into_matrix_dyn(self) -> Result<MatrixDyn, MatrixError>;
52}
53
54// Identity conversion for MatrixDyn (no error possible)
55impl IntoMatrixDyn for MatrixDyn {
56	fn into_matrix_dyn(self) -> Result<MatrixDyn, MatrixError> {
57		Ok(self)
58	}
59}
60
61// Conversion for other MatrixLike types via TryFrom
62impl<M> IntoMatrixDyn for M
63where
64	M: MatrixLike,
65	MatrixDyn: TryFrom<M, Error = MatrixError>,
66{
67	fn into_matrix_dyn(self) -> Result<MatrixDyn, MatrixError> {
68		MatrixDyn::try_from(self)
69	}
70}
71
72/// Runtime-sized N×N matrix of u8 flags (row-major).
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct MatrixDyn {
75	n: u8,
76	data: Vec<u8>,
77}
78
79impl Default for MatrixDyn {
80	/// Smallest valid matrix (1×1, zeroed) so the `data.len() == n*n`
81	/// invariant holds for every reachable value.
82	fn default() -> Self {
83		Self { n: 1, data: vec![0u8; 1] }
84	}
85}
86
87impl MatrixDyn {
88	/// Construct from row-major n-bytes. Length MUST be n*n.
89	pub fn from_row_major(n: u8, bytes: Vec<u8>) -> Option<Self> {
90		if n == 0 {
91			return None;
92		}
93
94		let n_usize = n as usize;
95		if bytes.len() == n_usize * n_usize {
96			Some(Self { n, data: bytes })
97		} else {
98			None
99		}
100	}
101
102	/// Defense in depth: bounds by the actual buffer as well as `n`, so a
103	/// value constructed in violation of the `data.len() == n*n` invariant
104	/// cannot produce an out-of-bounds index.
105	#[inline]
106	fn idx(&self, r: u8, c: u8) -> Option<usize> {
107		let n = self.n as usize;
108		let (ru, cu) = (r as usize, c as usize);
109		(ru < n && cu < n).then(|| ru * n + cu).filter(|i| *i < self.data.len())
110	}
111
112	/// Borrow the underlying row-major bytes.
113	pub fn as_bytes(&self) -> &[u8] {
114		&self.data
115	}
116
117	/// Mutable borrow of row-major bytes.
118	pub fn as_bytes_mut(&mut self) -> &mut [u8] {
119		&mut self.data
120	}
121
122	/// Borrow a single row as a slice.
123	pub fn row(&self, r: u8) -> Option<&[u8]> {
124		if r >= self.n {
125			return None;
126		}
127
128		let n = self.n as usize;
129		let start = r as usize * n;
130		self.data.get(start..start + n)
131	}
132}
133
134impl MatrixLike for MatrixDyn {
135	fn n(&self) -> u8 {
136		self.n
137	}
138
139	fn get(&self, r: u8, c: u8) -> u8 {
140		self.idx(r, c).map(|i| self.data[i]).unwrap_or(0)
141	}
142
143	fn set(&mut self, r: u8, c: u8, value: u8) {
144		if let Some(i) = self.idx(r, c) {
145			self.data[i] = value;
146		}
147	}
148
149	fn fill(&mut self, value: u8) {
150		for b in &mut self.data {
151			*b = value;
152		}
153	}
154}
155
156/// Compile-time N×N matrix of u8 flags (row-major).
157///
158/// The wire format bounds the dimension to 1..=255; constructing a matrix
159/// with `N` outside that range is rejected at compile time:
160///
161/// ```compile_fail
162/// use tightbeam::matrix::Matrix;
163///
164/// let rejected = Matrix::<256>::new();
165/// ```
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct Matrix<const N: usize> {
168	data: [[u8; N]; N],
169}
170
171impl<const N: usize> Default for Matrix<N> {
172	fn default() -> Self {
173		const { Self::VALID_N };
174		Self { data: [[0u8; N]; N] }
175	}
176}
177
178impl<const N: usize> Matrix<N> {
179	/// Compile-time guard: the wire format bounds the dimension to 1..=255
180	/// (`MatrixLike::n` returns `u8`). Evaluated by every constructor so an
181	/// out-of-range `N` is rejected at monomorphization.
182	const VALID_N: () = assert!(N >= 1 && N <= 255, "Matrix dimension must be 1..=255");
183
184	/// Create a zero-initialized matrix.
185	pub fn new() -> Self {
186		Self::default()
187	}
188
189	/// Construct from row-major bytes; extra bytes are ignored, missing are
190	/// zeroed.
191	pub fn from_row_major(bytes: &[u8]) -> Self {
192		let mut m = Self::default();
193		let mut i = 0usize;
194		for r in 0..N {
195			for c in 0..N {
196				if i < bytes.len() {
197					m.data[r][c] = bytes[i];
198				}
199				i += 1;
200			}
201		}
202		m
203	}
204
205	/// Borrow a row by index.
206	pub fn row(&self, r: u8) -> Option<&[u8; N]> {
207		if (r as usize) < N {
208			Some(&self.data[r as usize])
209		} else {
210			None
211		}
212	}
213}
214
215impl<const N: usize> MatrixLike for Matrix<N> {
216	fn n(&self) -> u8 {
217		N as u8
218	}
219
220	fn get(&self, r: u8, c: u8) -> u8 {
221		if (r as usize) < N && (c as usize) < N {
222			self.data[r as usize][c as usize]
223		} else {
224			0
225		}
226	}
227
228	fn set(&mut self, r: u8, c: u8, value: u8) {
229		if (r as usize) < N && (c as usize) < N {
230			self.data[r as usize][c as usize] = value;
231		}
232	}
233
234	fn fill(&mut self, value: u8) {
235		for r in 0..N {
236			for c in 0..N {
237				self.data[r][c] = value;
238			}
239		}
240	}
241}
242
243macro_rules! validate_n {
244	($n:expr) => {
245		if $n == 0 {
246			return Err(MatrixError::InvalidN($n));
247		}
248	};
249}
250
251impl TryFrom<u8> for MatrixDyn {
252	type Error = MatrixError;
253	fn try_from(n: u8) -> Result<Self, Self::Error> {
254		validate_n!(n);
255
256		let n_usize = n as usize;
257		let data = vec![0u8; n_usize * n_usize];
258		Ok(Self { n, data })
259	}
260}
261
262impl TryFrom<MatrixDyn> for Asn1Matrix {
263	type Error = crate::matrix::MatrixError;
264
265	fn try_from(mut matrix: MatrixDyn) -> Result<Self, Self::Error> {
266		let n = matrix.n();
267		validate_n!(n);
268
269		// MatrixDyn stores row-major n*n bytes, same as Asn1Matrix
270		let expected_len = (n as usize) * (n as usize);
271		if matrix.data.len() != expected_len {
272			return Err(crate::matrix::MatrixError::LengthMismatch { n, len: matrix.data.len() });
273		}
274
275		let data = core::mem::take(&mut matrix.data);
276		Ok(Self { n, data })
277	}
278}
279
280macro_rules! asn1_to_matrix_dyn_impl {
281	($matrix:expr) => {{
282		let n = $matrix.n;
283		validate_n!(n);
284
285		let n_u8 = n as u8;
286		let n2 = n_u8 as usize * n_u8 as usize;
287		if $matrix.data.len() != n2 {
288			return Err(MatrixError::LengthMismatch { n, len: $matrix.data.len() });
289		}
290	}};
291}
292
293impl TryFrom<Asn1Matrix> for MatrixDyn {
294	type Error = MatrixError;
295	fn try_from(m: Asn1Matrix) -> Result<Self, Self::Error> {
296		asn1_to_matrix_dyn_impl!(m);
297
298		let mut m = m;
299		let data = core::mem::take(&mut m.data);
300		let len = data.len();
301
302		MatrixDyn::from_row_major(m.n, data).ok_or(MatrixError::LengthMismatch { n: m.n, len })
303	}
304}
305
306impl TryFrom<&Asn1Matrix> for MatrixDyn {
307	type Error = MatrixError;
308	fn try_from(m: &Asn1Matrix) -> Result<Self, Self::Error> {
309		asn1_to_matrix_dyn_impl!(m);
310
311		MatrixDyn::from_row_major(m.n, m.data.clone()).ok_or(MatrixError::LengthMismatch { n: m.n, len: m.data.len() })
312	}
313}
314
315impl TryFrom<Option<Asn1Matrix>> for MatrixDyn {
316	type Error = MatrixError;
317	fn try_from(m: Option<Asn1Matrix>) -> Result<Self, Self::Error> {
318		match m {
319			Some(m) => Self::try_from(m),
320			None => Ok(Default::default()),
321		}
322	}
323}
324
325#[cfg(test)]
326mod tests {
327	use super::*;
328
329	#[test]
330	#[cfg(feature = "std")]
331	fn default_matrix_dyn_upholds_invariant() {
332		let matrix = MatrixDyn::default();
333		assert_eq!(matrix.n(), 1);
334		assert_eq!(matrix.as_bytes(), &[0u8]);
335		assert_eq!(matrix.get(0, 0), 0);
336		assert!(matrix.row(0).is_some());
337	}
338
339	#[test]
340	#[cfg(feature = "std")]
341	fn absent_asn1_matrix_converts_to_valid_default() -> crate::error::Result<()> {
342		let matrix = MatrixDyn::try_from(None::<Asn1Matrix>)?;
343		assert_eq!(matrix.n(), 1);
344		assert_eq!(matrix.get(0, 0), 0);
345		assert!(matrix.row(0).is_some());
346
347		Ok(())
348	}
349
350	#[test]
351	#[cfg(feature = "std")]
352	fn test_matrix_like_reality_end_to_end() -> crate::error::Result<()> {
353		// Build a 3×3 from row-major bytes 0..9
354		let bytes: Vec<u8> = (0u8..9u8).collect();
355		let mut dyn_m = MatrixDyn::from_row_major(3, bytes.clone()).expect("n*n bytes");
356		let mut stat_m: Matrix<3> = Matrix::<3>::from_row_major(&bytes);
357
358		// Paint identity for any MatrixLike without recursion.
359		fn paint_diag<M: MatrixLike>(m: &mut M) {
360			let n = m.n();
361			// Ensure non-diagonal cells are zeroed
362			m.clear();
363			for i in 0..n {
364				m.set(i, i, 1);
365			}
366		}
367
368		// Dimensions
369		assert_eq!(dyn_m.n(), 3);
370		assert_eq!(stat_m.n(), 3);
371
372		// Row views match source bytes
373		assert_eq!(
374			dyn_m.row(1).ok_or(crate::testing::error::TestingError::InvariantViolated)?,
375			&[3, 4, 5]
376		);
377		assert_eq!(
378			stat_m
379				.row(1)
380				.ok_or(crate::testing::error::TestingError::InvariantViolated)?
381				.as_slice(),
382			&[3, 4, 5]
383		);
384
385		// Indexing
386		assert_eq!(dyn_m.get(2, 2), 8);
387		assert_eq!(stat_m.get(0, 2), 2);
388
389		// Paint identity on both
390		paint_diag(&mut dyn_m);
391		paint_diag(&mut stat_m);
392
393		// Validate diagonal = 1, others = 0
394		for r in 0..3 {
395			for c in 0..3 {
396				let dv = dyn_m.get(r, c);
397				let sv = stat_m.get(r, c);
398				if r == c {
399					assert_eq!(dv, 1);
400					assert_eq!(sv, 1);
401				} else {
402					assert_eq!(dv, 0);
403					assert_eq!(sv, 0);
404				}
405			}
406		}
407
408		// Fill and clear
409		dyn_m.fill(7);
410		for r in 0..3 {
411			for c in 0..3 {
412				assert_eq!(dyn_m.get(r, c), 7);
413			}
414		}
415		dyn_m.clear();
416		for r in 0..3 {
417			for c in 0..3 {
418				assert_eq!(dyn_m.get(r, c), 0);
419			}
420		}
421
422		// Byte view length and static reconstruction
423		assert_eq!(dyn_m.as_bytes().len(), 9);
424
425		let stat_bytes = Matrix::<3>::from_row_major(&bytes);
426		assert_eq!(
427			stat_bytes
428				.row(0)
429				.ok_or(crate::testing::error::TestingError::InvariantViolated)?,
430			&[0, 1, 2]
431		);
432
433		// Invalid constructor length rejected
434		assert!(MatrixDyn::from_row_major(3, vec![0u8; 8]).is_none());
435
436		Ok(())
437	}
438
439	#[test]
440	#[cfg(feature = "std")]
441	fn test_matrix_specification_compliance() -> crate::error::Result<()> {
442		// Test data for various matrix sizes
443		let test_matrices = vec![
444			(1u8, vec![42u8]),
445			(2u8, vec![1, 2, 3, 4]),
446			(3u8, (0u8..9u8).collect()),
447			(255u8, vec![0u8; 255 * 255]),
448		];
449
450		// Test 1: Wire format compliance (n ∈ [1, 255], data.len == n*n)
451		for (n, data) in &test_matrices {
452			// Valid construction
453			let asn1_matrix = Asn1Matrix { n: *n, data: data.clone() };
454			// Validate n bounds
455			assert!(*n >= 1);
456
457			// Validate data length equals n*n
458			let expected_len = (*n as usize) * (*n as usize);
459			assert_eq!(data.len(), expected_len);
460
461			// Test conversion to MatrixDyn preserves row-major ordering
462			let matrix_dyn = MatrixDyn::try_from(&asn1_matrix)?;
463			assert_eq!(matrix_dyn.n(), *n);
464			assert_eq!(matrix_dyn.as_bytes(), data.as_slice());
465		}
466
467		// Test 2: Error handling - invalid n (n == 0)
468		let invalid_n = MatrixDyn::try_from(0u8);
469		assert!(invalid_n.is_err());
470
471		// Test 3: Error handling - length mismatch
472		let invalid_asn1 = Asn1Matrix { n: 2, data: vec![1, 2, 3] }; // 3 != 2*2
473		let invalid_conversion = MatrixDyn::try_from(invalid_asn1);
474		assert!(invalid_conversion.is_err());
475
476		// Test 4: Row-major ordering preservation
477		// Fill with values: row 0 = [10, 11, 12], row 1 = [20, 21, 22], row 2 = [30, 31, 32]
478		let matrix_3x3 = MatrixDyn::try_from(3u8)?;
479		let mut test_matrix = matrix_3x3;
480		for r in 0..3 {
481			for c in 0..3 {
482				let value = (r + 1) * 10 + c;
483				test_matrix.set(r, c, value);
484			}
485		}
486
487		// Verify row-major layout
488		let expected_bytes = vec![10, 11, 12, 20, 21, 22, 30, 31, 32];
489		assert_eq!(test_matrix.as_bytes(), expected_bytes.as_slice());
490
491		// Test 5: MatrixLike trait compliance
492		let mut matrix = MatrixDyn::try_from(2u8)?;
493
494		// Test get/set operations
495		matrix.set(0, 1, 99);
496		assert_eq!(matrix.get(0, 1), 99);
497
498		// Test out-of-bounds behavior (returns 0, no-op for set)
499		assert_eq!(matrix.get(5, 5), 0);
500		matrix.set(5, 5, 123); // Should be no-op
501
502		// Test fill operation
503		matrix.fill(77);
504		for r in 0..2 {
505			for c in 0..2 {
506				assert_eq!(matrix.get(r, c), 77);
507			}
508		}
509
510		// Test clear operation
511		matrix.clear();
512		for r in 0..2 {
513			for c in 0..2 {
514				assert_eq!(matrix.get(r, c), 0);
515			}
516		}
517
518		// Test 6: Conversion consistency between Matrix<N> and MatrixDyn
519		let static_matrix: Matrix<3> = Matrix::from_row_major(&[1, 2, 3, 4, 5, 6, 7, 8, 9]);
520		let dynamic_matrix = MatrixDyn::from_row_major(3, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
521		for r in 0..3 {
522			for c in 0..3 {
523				assert_eq!(static_matrix.get(r, c), dynamic_matrix.get(r, c));
524			}
525		}
526
527		Ok(())
528	}
529}