Skip to main content

concept_graph/
persist.rs

1//! A versioned binary layout for the graph, for the offline build to store.
2//!
3//! No spec governs this: our own design. Little-endian, a magic and version
4//! prefix, then the is-a adjacency (offsets and targets), then the two
5//! closure bitmap lists in roaring's portable serialization. The store crate
6//! places these bytes in its artifact; the server reads them back at startup.
7
8use std::io::{self, Read, Write};
9
10use roaring::RoaringBitmap;
11
12use crate::closure::Closure;
13use crate::csr::{Csr, CsrError};
14use crate::ordinal::to_usize;
15
16const MAGIC: &[u8; 8] = b"FTGRAPH\0";
17const VERSION: u32 = 1;
18
19/// A failure while reading or writing the layout.
20#[derive(Debug, thiserror::Error)]
21pub enum PersistError {
22    /// An I/O failure.
23    #[error("graph I/O failed")]
24    Io(#[from] io::Error),
25    /// The bytes do not start with the graph magic.
26    #[error("not a graph artifact")]
27    Magic,
28    /// The layout version is not the one this build reads.
29    #[error("graph layout version {found}, expected {expected}")]
30    Version {
31        /// The version found.
32        found: u32,
33        /// The version this build reads.
34        expected: u32,
35    },
36    /// The adjacency arrays are inconsistent.
37    #[error(transparent)]
38    Csr(#[from] CsrError),
39    /// The bitmap lists do not match the node count.
40    #[error("{found} closure sets for {nodes} nodes")]
41    Count {
42        /// The number of sets found.
43        found: usize,
44        /// The node count.
45        nodes: u32,
46    },
47}
48
49/// The is-a adjacency and its closure, as one artifact.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Hierarchy {
52    /// Child-to-parent adjacency.
53    pub is_a: Csr,
54    /// The transitive closure of `is_a`.
55    pub closure: Closure,
56}
57
58impl Hierarchy {
59    /// The heap bytes the adjacency and the closure hold
60    /// ([`crate::footprint`]).
61    #[must_use]
62    pub fn size_in_bytes(&self) -> usize {
63        self.is_a
64            .size_in_bytes()
65            .saturating_add(self.closure.size_in_bytes())
66    }
67
68    /// Writes the layout.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`PersistError::Io`] when writing fails.
73    pub fn write_to(&self, out: &mut impl Write) -> Result<(), PersistError> {
74        out.write_all(MAGIC)?;
75        out.write_all(&VERSION.to_le_bytes())?;
76        write_u32s(out, self.is_a.offsets())?;
77        write_u32s(out, self.is_a.targets())?;
78        write_bitmaps(out, self.closure.ancestor_sets())?;
79        write_bitmaps(out, self.closure.descendant_sets())?;
80        Ok(())
81    }
82
83    /// Reads the layout.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`PersistError`] for a truncated, foreign, or inconsistent artifact.
88    pub fn read_from(input: &mut impl Read) -> Result<Self, PersistError> {
89        let mut magic = [0_u8; 8];
90        input.read_exact(&mut magic)?;
91        if &magic != MAGIC {
92            return Err(PersistError::Magic);
93        }
94        let version = read_u32(input)?;
95        if version != VERSION {
96            return Err(PersistError::Version {
97                found: version,
98                expected: VERSION,
99            });
100        }
101        let offsets = read_u32s(input)?;
102        let targets = read_u32s(input)?;
103        let is_a = Csr::from_parts(offsets, targets)?;
104        let nodes = is_a.nodes();
105        let ancestors = read_bitmaps(input)?;
106        let descendants = read_bitmaps(input)?;
107        for list in [&ancestors, &descendants] {
108            if list.len() != to_usize(nodes) {
109                return Err(PersistError::Count {
110                    found: list.len(),
111                    nodes,
112                });
113            }
114        }
115        Ok(Self {
116            is_a,
117            closure: Closure::from_parts(ancestors, descendants),
118        })
119    }
120}
121
122/// A length or size past `u32::MAX`, which the artifact layout cannot store.
123#[derive(Debug, thiserror::Error)]
124#[error("{what} exceeds the u32 the artifact layout stores")]
125pub(crate) struct TooLong {
126    what: &'static str,
127    #[source]
128    source: std::num::TryFromIntError,
129}
130
131/// `len` as the `u32` the layout stores, or the I/O error naming `what` overflowed.
132pub(crate) fn u32_len(len: usize, what: &'static str) -> io::Result<u32> {
133    u32::try_from(len).map_err(|source| io::Error::other(TooLong { what, source }))
134}
135
136fn write_u32s(out: &mut impl Write, values: &[u32]) -> io::Result<()> {
137    let len = u32_len(values.len(), "an array length")?;
138    out.write_all(&len.to_le_bytes())?;
139    for value in values {
140        out.write_all(&value.to_le_bytes())?;
141    }
142    Ok(())
143}
144
145fn read_u32(input: &mut impl Read) -> io::Result<u32> {
146    let mut buffer = [0_u8; 4];
147    input.read_exact(&mut buffer)?;
148    Ok(u32::from_le_bytes(buffer))
149}
150
151fn read_u32s(input: &mut impl Read) -> io::Result<Vec<u32>> {
152    let len = read_u32(input)?;
153    let mut values = Vec::with_capacity(to_usize(len));
154    for _ in 0..len {
155        values.push(read_u32(input)?);
156    }
157    Ok(values)
158}
159
160fn write_bitmaps(out: &mut impl Write, sets: &[RoaringBitmap]) -> io::Result<()> {
161    let len = u32_len(sets.len(), "the set count")?;
162    out.write_all(&len.to_le_bytes())?;
163    for set in sets {
164        let size = u32_len(set.serialized_size(), "a set size")?;
165        out.write_all(&size.to_le_bytes())?;
166        set.serialize_into(&mut *out)?;
167    }
168    Ok(())
169}
170
171fn read_bitmaps(input: &mut impl Read) -> io::Result<Vec<RoaringBitmap>> {
172    let len = read_u32(input)?;
173    let mut sets = Vec::with_capacity(to_usize(len));
174    for _ in 0..len {
175        let size = read_u32(input)?;
176        let mut bytes = vec![0_u8; to_usize(size)];
177        input.read_exact(&mut bytes)?;
178        sets.push(RoaringBitmap::deserialize_from(bytes.as_slice())?);
179    }
180    Ok(sets)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::{Hierarchy, PersistError};
186    use crate::closure::Closure;
187    use crate::csr::Csr;
188    use crate::ordinal::Ordinal;
189
190    #[test]
191    fn the_layout_round_trips_and_rejects_foreign_bytes() {
192        let o = Ordinal::new;
193        let is_a = Csr::build(4, [(o(1), o(0)), (o(2), o(0)), (o(3), o(1)), (o(3), o(2))])
194            .expect("builds");
195        let closure = Closure::compute(&is_a).expect("acyclic");
196        let hierarchy = Hierarchy { is_a, closure };
197        let mut bytes = Vec::new();
198        hierarchy.write_to(&mut bytes).expect("writes");
199        let back = Hierarchy::read_from(&mut bytes.as_slice()).expect("reads");
200        assert_eq!(back, hierarchy);
201        assert!(matches!(
202            Hierarchy::read_from(&mut b"nope".as_slice()),
203            Err(PersistError::Io(_))
204        ));
205        assert!(matches!(
206            Hierarchy::read_from(&mut b"XXXXXXXX\0\0\0\0".as_slice()),
207            Err(PersistError::Magic)
208        ));
209    }
210}