1use std::fmt;
15
16use concept_graph::ordinal::{Ordinal, to_usize};
17
18const WIDTH: usize = 4;
20
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
23pub enum ColumnError {
24 #[error("the column is {found} bytes, shorter than the {needed} its header declares")]
26 Truncated {
27 found: usize,
29 needed: usize,
31 },
32 #[error(
34 "offset {at} is {offset}, outside the {len} bytes of records or behind its predecessor"
35 )]
36 Offset {
37 at: usize,
39 offset: usize,
41 len: usize,
43 },
44}
45
46#[derive(Clone, Default, PartialEq, Eq)]
48pub struct Column {
49 offsets: Vec<u32>,
51 records: Vec<u8>,
52}
53
54impl fmt::Debug for Column {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.debug_struct("Column")
57 .field("len", &self.len())
58 .field("bytes", &self.records.len())
59 .finish_non_exhaustive()
60 }
61}
62
63impl Column {
64 #[must_use]
69 pub fn pack<'a>(count: u32, records: impl IntoIterator<Item = (Ordinal, &'a [u8])>) -> Vec<u8> {
70 let count = to_usize(count);
71 let mut placed: Vec<&[u8]> = vec![&[]; count];
72 for (ordinal, bytes) in records {
73 let at = to_usize(ordinal.index());
74 if let Some(slot) = placed.get_mut(at) {
75 *slot = bytes;
76 }
77 }
78 let total: usize = placed.iter().map(|r| r.len()).sum();
79 let mut out = Vec::with_capacity(WIDTH * (count + 2) + total);
80 out.extend_from_slice(&u32::try_from(count).unwrap_or(u32::MAX).to_le_bytes());
81 let mut at = 0_u32;
82 for record in &placed {
83 out.extend_from_slice(&at.to_le_bytes());
84 at = at.saturating_add(u32::try_from(record.len()).unwrap_or(u32::MAX));
85 }
86 out.extend_from_slice(&at.to_le_bytes());
87 for record in placed {
88 out.extend_from_slice(record);
89 }
90 out
91 }
92
93 pub fn read(bytes: &[u8]) -> Result<Self, ColumnError> {
100 let short = |needed: usize| ColumnError::Truncated {
101 found: bytes.len(),
102 needed,
103 };
104 let head = bytes.get(..WIDTH).ok_or_else(|| short(WIDTH))?;
105 let count = to_usize(u32::from_le_bytes(head.try_into().unwrap_or([0, 0, 0, 0])));
106 let end = WIDTH.saturating_mul(count.saturating_add(2));
107 let table = bytes.get(WIDTH..end).ok_or_else(|| short(end))?;
108 let offsets: Vec<u32> = table
109 .as_chunks::<WIDTH>()
110 .0
111 .iter()
112 .map(|c| u32::from_le_bytes(*c))
113 .collect();
114 let records = bytes.get(end..).unwrap_or_default().to_vec();
115 let mut previous = 0_u32;
116 for (at, offset) in offsets.iter().enumerate() {
117 if *offset < previous || to_usize(*offset) > records.len() {
118 return Err(ColumnError::Offset {
119 at,
120 offset: to_usize(*offset),
121 len: records.len(),
122 });
123 }
124 previous = *offset;
125 }
126 Ok(Self { offsets, records })
127 }
128
129 #[must_use]
131 pub fn len(&self) -> usize {
132 self.offsets.len().saturating_sub(1)
133 }
134
135 #[must_use]
137 pub fn is_empty(&self) -> bool {
138 self.len() == 0
139 }
140
141 #[must_use]
147 pub fn get(&self, ordinal: Ordinal) -> Option<&[u8]> {
148 let at = to_usize(ordinal.index());
149 let start = to_usize(*self.offsets.get(at)?);
150 let end = to_usize(*self.offsets.get(at.checked_add(1)?)?);
151 if start == end {
152 return None;
153 }
154 self.records.get(start..end)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use concept_graph::ordinal::Ordinal;
161
162 use super::{Column, ColumnError};
163
164 #[test]
165 fn a_packed_column_reads_back_what_was_placed() {
166 let packed = Column::pack(
167 4,
168 [
169 (Ordinal::new(0), b"first".as_slice()),
170 (Ordinal::new(3), b"fourth".as_slice()),
171 ],
172 );
173 let column = Column::read(&packed).expect("reads");
174 assert_eq!(column.len(), 4);
175 assert!(!column.is_empty());
176 assert_eq!(column.get(Ordinal::new(0)), Some(b"first".as_slice()));
177 assert_eq!(column.get(Ordinal::new(1)), None, "no record was placed");
178 assert_eq!(column.get(Ordinal::new(3)), Some(b"fourth".as_slice()));
179 assert_eq!(column.get(Ordinal::new(4)), None, "beyond the column");
180 }
181
182 #[test]
183 fn an_empty_column_holds_nothing() {
184 let column = Column::read(&Column::pack(0, [])).expect("reads");
185 assert!(column.is_empty());
186 assert_eq!(column.get(Ordinal::new(0)), None);
187 }
188
189 #[test]
190 fn a_short_or_crossed_column_is_refused() {
191 assert!(matches!(
192 Column::read(&[0, 0]),
193 Err(ColumnError::Truncated { .. })
194 ));
195 let packed = Column::pack(2, [(Ordinal::new(0), b"ab".as_slice())]);
198 assert!(matches!(
199 Column::read(packed.get(..packed.len() - 1).expect("shortened")),
200 Err(ColumnError::Offset { .. })
201 ));
202 }
203}