Skip to main content

rete_core/v06/
parts.rs

1//! The logical parts of a file, independent of its generation: what the `0x06`
2//! assembler ([`super::write`]) lays out. [`from_v05`] lifts them from a `0x05`
3//! file by reading its framing and directories; payload blocks are referenced
4//! by their byte range in the input and copied verbatim.
5
6use super::{ChunkEnt, QTileEnt, TileEnt, TokenEnt};
7use crate::file::{ByteRange, FileError};
8use crate::header::Header;
9use crate::reader::RangeReader;
10
11/// A dictionary section: its metadata and chunks (with their input offsets).
12#[derive(Debug, Clone, Default)]
13pub struct DictPart {
14    /// Terms in the section.
15    pub term_count: u32,
16    /// Restart interval.
17    pub restart_interval: u32,
18    /// Chunks, each with the absolute input offset of its stored bytes.
19    pub chunks: Vec<(ChunkEnt, u64)>,
20    /// The largest chunk, decompressed.
21    pub max_raw: u64,
22}
23
24/// One permutation's tiles (with their input offsets).
25#[derive(Debug, Clone, Default)]
26pub struct PermPart {
27    /// Section index of the permutation.
28    pub si: usize,
29    /// Tiles, each with the absolute input offset of its stored bytes.
30    pub tiles: Vec<(TileEnt, u64)>,
31}
32
33/// The quad index.
34#[derive(Debug, Clone, Default)]
35pub struct QuadPart {
36    /// Named graphs.
37    pub graph_count: u64,
38    /// Named-graph quads.
39    pub quad_count: u64,
40    /// Graphs per directory block.
41    pub block_graphs: u64,
42    /// The 0x05 `P0` block index (offsets relative to the blocks' start).
43    pub p0: Vec<u8>,
44    /// Input range of the graph-directory blocks.
45    pub gdir: ByteRange,
46    /// Per order: `(section index, tiles)`, `QTileEnt::off` = absolute input offset.
47    pub orders: Vec<(usize, Vec<QTileEnt>)>,
48}
49
50/// The label index.
51#[derive(Debug, Clone, Default)]
52pub struct LabelPart {
53    /// The 32-byte preamble (digest zeroed) and the root node.
54    pub head: Vec<u8>,
55    /// Bytes of data blocks at the start of the body.
56    pub data_len: u64,
57    /// Input range of the body (data blocks, then directory nodes).
58    pub body: ByteRange,
59}
60
61/// The text index.
62#[derive(Debug, Clone, Default)]
63pub struct TextPart {
64    /// The scope block (empty: default scope).
65    pub scope: Vec<u8>,
66    /// Tokens, posting offsets relative to the postings blob.
67    pub tokens: Vec<TokenEnt>,
68    /// Input range of the postings blob.
69    pub postings: ByteRange,
70    /// The index leaves some predicates out.
71    pub partial: bool,
72    /// The index covers the named graphs.
73    pub named: bool,
74}
75
76/// Everything a `0x06` file is assembled from.
77#[derive(Debug, Clone, Default)]
78pub struct Parts {
79    /// Statements.
80    pub quad_count: u64,
81    /// Terms.
82    pub term_count: u64,
83    /// Permutation mask (explicit bits).
84    pub perm_mask: u8,
85    /// Dictionary chunk codec.
86    pub dict_codec: u8,
87    /// Tile codec.
88    pub block_codec: u8,
89    /// Community-pyramid rounds.
90    pub pyramid_levels: u16,
91    /// The file holds named graphs.
92    pub named: bool,
93    /// Some terms are quoted triples.
94    pub quoted: bool,
95    /// The Dataset Card (raw JSON).
96    pub card: Option<Vec<u8>>,
97    /// The build record (raw JSON, possibly space-padded).
98    pub build: Option<Vec<u8>>,
99    /// Stats + schema: the 0x05 SCHEMA payload after its digest.
100    pub schema: Option<Vec<u8>>,
101    /// The four dictionary sections (shared, subject-only, object-only, predicates).
102    pub dict: [DictPart; 4],
103    /// The stored permutations, in section-index order.
104    pub index: Vec<PermPart>,
105    /// The quad index.
106    pub quad: Option<QuadPart>,
107    /// The label index.
108    pub labels: Option<LabelPart>,
109    /// The text index.
110    pub text: Option<TextPart>,
111    /// Input range of the community pyramid.
112    pub pyramid: Option<ByteRange>,
113}
114
115fn read(r: &dyn RangeReader, range: ByteRange) -> Result<Vec<u8>, FileError> {
116    if range.len == 0 {
117        return Ok(Vec::new());
118    }
119    Ok(r.read_at(range.offset, range.len)?)
120}
121
122/// Decode a front-coded chunk body to its first and last terms.
123fn first_last(body: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
124    let mut pos = 0usize;
125    let mut buf: Vec<u8> = Vec::new();
126    let mut first = None;
127    while pos < body.len() {
128        let (shared, n1) = crate::varint::read_uvarint(body.get(pos..)?)?;
129        let p = pos + n1;
130        let (suf, n2) = crate::varint::read_uvarint(body.get(p..)?)?;
131        let start = p + n2;
132        let end = start.checked_add(suf as usize)?;
133        let s = body.get(start..end)?;
134        if shared as usize > buf.len() {
135            return None;
136        }
137        buf.truncate(shared as usize);
138        buf.extend_from_slice(s);
139        if first.is_none() {
140            first = Some(buf.clone());
141        }
142        pos = end;
143    }
144    Some((first?, buf))
145}
146
147/// Read and decompress the chunks of a dictionary section in batches, and
148/// re-derive every routing key as the shortest separator (SPEC.md ยง6.2).
149fn rekey(r: &dyn RangeReader, codec: u8, chunks: &mut [(ChunkEnt, u64)]) -> Result<u64, FileError> {
150    const BATCH: u64 = 8 << 20;
151    let mut max_raw = 0u64;
152    let mut prev_last: Option<Vec<u8>> = None;
153    let mut i = 0usize;
154    while i < chunks.len() {
155        // A run of byte-adjacent chunks up to BATCH bytes, read at once.
156        let start = chunks[i].1;
157        let mut j = i;
158        let mut end = start;
159        while j < chunks.len() && chunks[j].1 == end && (end - start < BATCH || j == i) {
160            end += chunks[j].0.clen;
161            j += 1;
162        }
163        let span = r.read_at(start, end - start)?;
164        for c in chunks.iter_mut().take(j).skip(i) {
165            let lo = (c.1 - start) as usize;
166            let stored = span
167                .get(lo..lo + c.0.clen as usize)
168                .ok_or(FileError::Container("dictionary chunk overruns"))?;
169            let body = crate::file::decompress(codec, stored)?;
170            max_raw = max_raw.max(body.len() as u64);
171            let (first, last) =
172                first_last(&body).ok_or(FileError::Container("malformed dictionary chunk"))?;
173            c.0.key = match &prev_last {
174                None => Vec::new(),
175                Some(pl) => crate::dict::shortest_separator(pl, &first),
176            };
177            prev_last = Some(last);
178        }
179        i = j;
180    }
181    Ok(max_raw)
182}
183
184/// The [`LabelPart`] of a `0x05` LABEL_INDEX section at `offset..+length`
185/// of `r`: its preamble and root kept, its body split into data blocks and
186/// directory nodes.
187pub(crate) fn label_part<R: RangeReader + ?Sized>(
188    r: &R,
189    offset: u64,
190    length: u64,
191    codec: u8,
192) -> Result<LabelPart, FileError> {
193    let head_len = crate::label_index::HEAD_READ.min(length);
194    let head = r.read_at(offset, head_len)?;
195    let pre = crate::label_index::LabelIndexPreamble::parse(&head)?;
196    let root_end = 32u64 + pre.root_len as u64;
197    let mut lhead = if root_end as usize <= head.len() {
198        head[..root_end as usize].to_vec()
199    } else {
200        r.read_at(offset, root_end)?
201    };
202    lhead[..16].fill(0);
203    let rr = |o: u64, l: u64| r.read_at(offset + o, l).ok();
204    let li = crate::label_index::LabelIndex::from_head(
205        &lhead,
206        length,
207        codec,
208        Box::new(move |_o: u64, _l: u64| None),
209    )?;
210    let data_len = li
211        .v05_data_len(&rr)
212        .ok_or(FileError::Container("label index: unreadable directory"))?;
213    Ok(LabelPart {
214        head: lhead,
215        data_len,
216        body: ByteRange {
217            offset: offset + root_end,
218            len: length - root_end,
219        },
220    })
221}
222
223/// Lift a `0x05` file into [`Parts`]: header, framing and directories are
224/// read; payload is referenced by range. Dictionary chunk keys are re-derived
225/// (so a file written before separator keys gets them). A file with
226/// per-graph named graphs (kind 5) is refused: the quad index is the only
227/// named-graph layout of `0x06`, and getting there re-encodes the graphs.
228pub fn from_v05(r: &dyn RangeReader) -> Result<Parts, FileError> {
229    let head = r.read_at(0, crate::header::HEADER_LEN as u64)?;
230    let h = Header::from_bytes(&head)?;
231    if h.version != 0x05 {
232        return Err(FileError::Container("not a 0x05 file"));
233    }
234    if h.named_graphs_len > 0 {
235        return Err(FileError::Container(
236            "per-graph named graphs (kind 5): convert re-encodes them as a quad index",
237        ));
238    }
239    let mut p = Parts {
240        quad_count: h.quad_count,
241        term_count: h.term_count,
242        perm_mask: h.perms.bits(),
243        dict_codec: h.dict_codec,
244        block_codec: h.block_codec,
245        pyramid_levels: h.pyramid_levels,
246        named: h.has_quads(),
247        quoted: h.flags & crate::header::FLAG_HAS_QUOTED_TRIPLES != 0,
248        ..Default::default()
249    };
250    if h.metadata_len > 0 {
251        p.card = Some(read(
252            r,
253            ByteRange {
254                offset: h.metadata_offset,
255                len: h.metadata_len,
256            },
257        )?);
258    }
259    if h.build_info_len > 0 {
260        p.build = Some(read(
261            r,
262            ByteRange {
263                offset: h.build_info_offset,
264                len: h.build_info_len,
265            },
266        )?);
267    }
268    if let Some(s) = h.schema_section() {
269        let b = read(
270            r,
271            ByteRange {
272                offset: s.offset,
273                len: s.length,
274            },
275        )?;
276        if b.len() > 16 {
277            p.schema = Some(b[16..].to_vec());
278        }
279    }
280    // Dictionary: four chunked sections in a container.
281    for si in 0..4 {
282        let sec = crate::file::locate_container_section_ranged(
283            r,
284            h.dictionary_offset,
285            h.dictionary_len,
286            si,
287            4,
288        )?;
289        let (meta, entries) = crate::file::read_dict_dir_ranged(r, sec)?;
290        let mut chunks: Vec<(ChunkEnt, u64)> = entries
291            .into_iter()
292            .map(|e| {
293                (
294                    ChunkEnt {
295                        first_run: e.first_run,
296                        key: e.key,
297                        clen: e.end - e.start,
298                    },
299                    sec.offset + e.start,
300                )
301            })
302            .collect();
303        let max_raw = rekey(r, h.dict_codec, &mut chunks)?;
304        p.dict[si] = DictPart {
305            term_count: meta.term_count,
306            restart_interval: meta.restart_interval,
307            chunks,
308            max_raw,
309        };
310    }
311    // Index: one tiled section per stored permutation (plus a quad seal).
312    let sections = h.index_container_sections();
313    for perm in h.perms.iter() {
314        let pos = h.perms.position(perm).unwrap_or(0);
315        let sec = crate::file::locate_container_section_ranged(
316            r,
317            h.root_dir_offset,
318            h.root_dir_len,
319            pos,
320            sections as u64,
321        )?;
322        let dir = crate::file::read_tile_directory_ranged(r, sec)?;
323        let syn = if h.has_tile_synopsis() {
324            crate::file::read_tile_synopsis_ranged(r, sec, &dir)
325        } else {
326            vec![None; dir.len()]
327        };
328        let mut tiles = Vec::with_capacity(dir.len());
329        for (e, s) in dir.iter().zip(syn) {
330            let range = ByteRange {
331                offset: sec.offset + e.start,
332                len: e.end - e.start,
333            };
334            let syn = match s {
335                Some(s) => s,
336                None => {
337                    // Files without a synopsis trailer: take it from the tile's
338                    // own zone map.
339                    let stored = r.read_at(range.offset, range.len)?;
340                    let body = crate::file::decompress(h.block_codec, &stored)?;
341                    let blk = crate::triples::TripleBlock::parse(&body)
342                        .map_err(|_| FileError::Container("malformed tile"))?;
343                    let z = blk.zone();
344                    (z.min_b, z.max_b, z.min_c, z.max_c)
345                }
346            };
347            tiles.push((
348                TileEnt {
349                    min_a: e.min_a,
350                    max_a: e.max_a,
351                    clen: range.len,
352                    syn,
353                },
354                range.offset,
355            ));
356        }
357        p.index.push(PermPart {
358            si: perm.section_index(),
359            tiles,
360        });
361    }
362    // Quad index.
363    if let Some(q) = h.quad_index_section() {
364        p.quad = Some(crate::quad::v05_parts(
365            r,
366            ByteRange {
367                offset: q.offset,
368                len: q.length,
369            },
370            h.perms,
371        )?);
372    }
373    // Label index.
374    if let Some(s) = h.label_index_section() {
375        p.labels = Some(label_part(r, s.offset, s.length, h.block_codec)?);
376    }
377    // Text index.
378    let (toff, tlen) = h.text_index_payload();
379    if tlen > 0 {
380        let probe = r.read_at(toff, 10.min(tlen))?;
381        let (ttl, n) = crate::varint::read_uvarint(&probe)
382            .ok_or(FileError::Container("truncated text-index len"))?;
383        let prefix_len = (n as u64 + ttl).min(tlen);
384        let prefix = r.read_at(toff, prefix_len)?;
385        let (tokens, scope, scope_raw, pstart) =
386            crate::text_index::parse_token_table_raw(&prefix, h.block_codec)?;
387        p.text = Some(TextPart {
388            scope: scope_raw,
389            tokens: tokens
390                .into_iter()
391                .map(|(t, off, len)| TokenEnt {
392                    token: t.into_bytes(),
393                    off,
394                    len,
395                })
396                .collect(),
397            postings: ByteRange {
398                offset: toff + pstart as u64,
399                len: tlen - pstart as u64,
400            },
401            partial: scope.restricts_predicates(),
402            named: scope.covers_named_graphs(),
403        });
404    }
405    if h.pyramid_meta_len > 0 {
406        p.pyramid = Some(ByteRange {
407            offset: h.pyramid_meta_offset,
408            len: h.pyramid_meta_len,
409        });
410    }
411    Ok(p)
412}