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