1use std::fmt;
20use std::path::{Path, PathBuf};
21
22use concept_graph::ordinal::{Ordinal, to_usize};
23
24const WIDTH: usize = 4;
26
27const CHUNK: usize = 1024;
29
30const EXTENSION: &str = "col";
32
33#[derive(Debug, thiserror::Error)]
35pub enum ColumnError {
36 #[error("the column ends inside its {part}")]
38 Truncated {
39 part: &'static str,
41 #[source]
43 source: std::io::Error,
44 },
45 #[error("offset {at} is {offset}, behind its predecessor {previous}")]
47 Offset {
48 at: usize,
50 offset: usize,
52 previous: usize,
54 },
55 #[error("cannot read the column")]
57 Io(#[source] std::io::Error),
58}
59
60fn read_failed(part: &'static str) -> impl FnOnce(std::io::Error) -> ColumnError {
62 move |source| match source.kind() {
63 std::io::ErrorKind::UnexpectedEof => ColumnError::Truncated { part, source },
64 _ => ColumnError::Io(source),
65 }
66}
67
68fn push_offset(
70 offsets: &mut Vec<u32>,
71 previous: &mut u32,
72 word: [u8; WIDTH],
73) -> Result<(), ColumnError> {
74 let offset = u32::from_le_bytes(word);
75 if offset < *previous {
76 return Err(ColumnError::Offset {
77 at: offsets.len(),
78 offset: to_usize(offset),
79 previous: to_usize(*previous),
80 });
81 }
82 *previous = offset;
83 offsets.push(offset);
84 Ok(())
85}
86
87#[derive(Clone, Default, PartialEq, Eq)]
89pub struct Column {
90 offsets: Vec<u32>,
92 records: Vec<u8>,
93}
94
95impl fmt::Debug for Column {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.debug_struct("Column")
98 .field("len", &self.len())
99 .field("bytes", &self.records.len())
100 .finish_non_exhaustive()
101 }
102}
103
104impl Column {
105 #[must_use]
110 pub fn file(store: &Path, name: &str) -> PathBuf {
111 let stem = store.file_stem().unwrap_or_default().to_string_lossy();
112 store.with_file_name(format!("{stem}.{name}.{EXTENSION}"))
113 }
114
115 pub fn write_to<'a>(
126 count: u32,
127 records: impl IntoIterator<Item = (Ordinal, &'a [u8])>,
128 out: &mut impl std::io::Write,
129 ) -> std::io::Result<()> {
130 let count = to_usize(count);
131 let mut placed: Vec<&[u8]> = vec![&[]; count];
132 for (ordinal, bytes) in records {
133 let at = to_usize(ordinal.index());
134 if let Some(slot) = placed.get_mut(at) {
135 *slot = bytes;
136 }
137 }
138 out.write_all(&u32::try_from(count).unwrap_or(u32::MAX).to_le_bytes())?;
139 let mut at = 0_u32;
140 for record in &placed {
141 out.write_all(&at.to_le_bytes())?;
142 at = at.saturating_add(u32::try_from(record.len()).unwrap_or(u32::MAX));
143 }
144 out.write_all(&at.to_le_bytes())?;
145 for record in placed {
146 out.write_all(record)?;
147 }
148 Ok(())
149 }
150
151 pub fn read_from(reader: &mut impl std::io::Read) -> Result<Self, ColumnError> {
161 let mut word = [0_u8; WIDTH];
162 reader
163 .read_exact(&mut word)
164 .map_err(read_failed("header"))?;
165 let count = to_usize(u32::from_le_bytes(word));
166 let mut offsets: Vec<u32> = Vec::with_capacity(count.saturating_add(1));
167 let mut buffer = [0_u8; WIDTH * CHUNK];
168 let mut previous = 0_u32;
169 let mut remaining = count.saturating_add(1);
170 while remaining >= CHUNK {
171 reader
172 .read_exact(&mut buffer)
173 .map_err(read_failed("offsets"))?;
174 for chunk in buffer.as_chunks::<WIDTH>().0 {
175 push_offset(&mut offsets, &mut previous, *chunk)?;
176 }
177 remaining = remaining.saturating_sub(CHUNK);
178 }
179 for _ in 0..remaining {
180 reader
181 .read_exact(&mut word)
182 .map_err(read_failed("offsets"))?;
183 push_offset(&mut offsets, &mut previous, word)?;
184 }
185 let mut records = vec![0_u8; to_usize(previous)];
186 reader
187 .read_exact(&mut records)
188 .map_err(read_failed("records"))?;
189 Ok(Self { offsets, records })
190 }
191
192 pub fn read(bytes: &[u8]) -> Result<Self, ColumnError> {
198 Self::read_from(&mut std::io::Cursor::new(bytes))
199 }
200
201 #[must_use]
204 pub fn size_in_bytes(&self) -> usize {
205 concept_graph::footprint::vector(&self.offsets)
206 .saturating_add(concept_graph::footprint::vector(&self.records))
207 }
208
209 #[must_use]
211 pub fn len(&self) -> usize {
212 self.offsets.len().saturating_sub(1)
213 }
214
215 #[must_use]
217 pub fn is_empty(&self) -> bool {
218 self.len() == 0
219 }
220
221 #[must_use]
227 pub fn get(&self, ordinal: Ordinal) -> Option<&[u8]> {
228 let at = to_usize(ordinal.index());
229 let start = to_usize(*self.offsets.get(at)?);
230 let end = to_usize(*self.offsets.get(at.checked_add(1)?)?);
231 if start == end {
232 return None;
233 }
234 self.records.get(start..end)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use std::path::Path;
241
242 use concept_graph::ordinal::Ordinal;
243
244 use super::{Column, ColumnError};
245
246 fn pack<'a>(count: u32, records: impl IntoIterator<Item = (Ordinal, &'a [u8])>) -> Vec<u8> {
247 let mut bytes = Vec::new();
248 Column::write_to(count, records, &mut bytes).expect("a vector takes every write");
249 bytes
250 }
251
252 #[test]
253 fn a_packed_column_reads_back_what_was_placed() {
254 let packed = pack(
255 4,
256 [
257 (Ordinal::new(0), b"first".as_slice()),
258 (Ordinal::new(3), b"fourth".as_slice()),
259 ],
260 );
261 let column = Column::read(&packed).expect("reads");
262 assert_eq!(column.len(), 4);
263 assert!(!column.is_empty());
264 assert_eq!(column.get(Ordinal::new(0)), Some(b"first".as_slice()));
265 assert_eq!(column.get(Ordinal::new(1)), None, "no record was placed");
266 assert_eq!(column.get(Ordinal::new(3)), Some(b"fourth".as_slice()));
267 assert_eq!(column.get(Ordinal::new(4)), None, "beyond the column");
268 }
269
270 #[test]
271 fn a_column_reads_the_same_from_a_reader_as_from_bytes() {
272 let packed = pack(
275 2048,
276 (0..2048).map(|at| (Ordinal::new(at), b"x".as_slice())),
277 );
278 let from_bytes = Column::read(&packed).expect("reads");
279 let from_reader =
280 Column::read_from(&mut std::io::Cursor::new(packed.clone())).expect("reads");
281 assert_eq!(
282 from_bytes, from_reader,
283 "more offsets than one read converts"
284 );
285 }
286
287 #[test]
288 fn an_empty_column_holds_nothing() {
289 let column = Column::read(&pack(0, [])).expect("reads");
290 assert!(column.is_empty());
291 assert_eq!(column.get(Ordinal::new(0)), None);
292 }
293
294 #[test]
295 fn a_short_or_crossed_column_is_refused() {
296 assert!(matches!(
297 Column::read(&[0, 0]),
298 Err(ColumnError::Truncated { part: "header", .. })
299 ));
300 let packed = pack(2, [(Ordinal::new(0), b"ab".as_slice())]);
301 assert!(
302 matches!(
303 Column::read(packed.get(..packed.len() - 1).expect("shortened")),
304 Err(ColumnError::Truncated {
305 part: "records",
306 ..
307 })
308 ),
309 "the records end before the offsets say they do"
310 );
311 let crossed = [1_u32, 2, 0]
313 .iter()
314 .flat_map(|word| word.to_le_bytes())
315 .chain(*b"ab")
316 .collect::<Vec<u8>>();
317 assert!(matches!(
318 Column::read(&crossed),
319 Err(ColumnError::Offset {
320 at: 1,
321 offset: 0,
322 previous: 2
323 })
324 ));
325 }
326
327 #[test]
328 fn a_column_file_carries_the_store_it_belongs_to() {
329 assert_eq!(
330 Column::file(Path::new("/srv/nl/store.redb"), "concepts"),
331 Path::new("/srv/nl/store.concepts.col")
332 );
333 assert_eq!(
334 Column::file(Path::new("other.redb"), "displays"),
335 Path::new("other.displays.col"),
336 "two stores in one directory keep their columns apart"
337 );
338 }
339}