Skip to main content

fmf_core\engine/
results.rs

1use std::sync::Arc;
2
3use crate::index::{EntryId, VolumeIndex, flags};
4
5use super::EngineError;
6use super::volume::VolumeSlot;
7
8/// One row handed across the FFI: everything the UI list needs.
9pub struct Row {
10    /// Stable handle: volume index in the high 32 bits, `EntryId` in the low.
11    pub entry_ref: u64,
12    /// NTFS File Reference Number for this entry.
13    pub frn: u64,
14    /// File size in bytes (0 for directories).
15    pub size: u64,
16    /// Last-modified time, Windows FILETIME (100 ns ticks since 1601-01-01 UTC).
17    pub mtime: i64,
18    /// Bitflags: bit0 = directory, bit1 = deleted-since-query (UI marker).
19    pub flags: u32,
20    /// File name, WTF-8 bytes (no path separators).
21    pub name: Vec<u8>,
22    /// Parent directory path, WTF-8 bytes.
23    pub parent_path: Vec<u8>,
24}
25
26/// Materialized, sort-ordered result. Pages are O(1) slices; reads stay
27/// valid across content mutations and fail with `Stale` only after a
28/// structural change (compaction/rescan).
29pub struct ResultSet {
30    pub(super) slots: Vec<Arc<VolumeSlot>>,
31    pub(super) structural: Vec<u64>,
32    pub(super) rows: Vec<(u32, EntryId)>,
33}
34
35impl ResultSet {
36    /// Number of rows in the materialized result.
37    #[must_use]
38    pub const fn len(&self) -> usize {
39        self.rows.len()
40    }
41
42    /// True when the result contains no rows.
43    #[must_use]
44    pub const fn is_empty(&self) -> bool {
45        self.rows.is_empty()
46    }
47
48    /// Builds the shared page representation — 48-byte contract rows plus
49    /// one string blob, offsets blob-relative — the single implementation
50    /// behind both the FFI `FmfPage` and the pipe `ResultPage` payload
51    /// (ADR-0018). Blob layout: per row, name bytes then parent bytes, in
52    /// row order (the canonical layout the golden corpus pins).
53    ///
54    /// # Errors
55    ///
56    /// Returns [`EngineError::Stale`] if the underlying index changed since
57    /// this result set was produced (the handle is stale).
58    pub fn fill_page(
59        &self,
60        offset: usize,
61        count: usize,
62    ) -> Result<(Vec<fmf_contract::pod::FmfRow>, Vec<u8>), EngineError> {
63        let rows_data = self.page(offset, count)?;
64        let mut blob = Vec::new();
65        let mut rows = Vec::with_capacity(rows_data.len());
66        for row in &rows_data {
67            let name_off = blob.len() as u32;
68            blob.extend_from_slice(&row.name);
69            let parent_off = blob.len() as u32;
70            blob.extend_from_slice(&row.parent_path);
71            rows.push(fmf_contract::pod::FmfRow {
72                entry_ref: row.entry_ref,
73                frn: row.frn,
74                size: row.size,
75                mtime: row.mtime,
76                name_off,
77                parent_path_off: parent_off,
78                flags: row.flags,
79                name_len: row.name.len() as u16,
80                parent_path_len: row.parent_path.len() as u16,
81            });
82        }
83        Ok((rows, blob))
84    }
85
86    /// Materialize `[offset, offset + count)` of the result into owned rows.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`EngineError::Stale`] if any backing volume index changed
91    /// since this result set was produced (the handle is stale).
92    pub fn page(&self, offset: usize, count: usize) -> Result<Vec<Row>, EngineError> {
93        let end = (offset.saturating_add(count)).min(self.rows.len());
94        let start = offset.min(end);
95        let mut out = Vec::with_capacity(end - start);
96
97        let guards: Vec<_> = self.slots.iter().map(|s| s.index.read()).collect();
98        for (v, guard) in guards.iter().enumerate() {
99            let idx = guard.as_ref().ok_or(EngineError::Stale)?;
100            if idx.structural_generation() != self.structural[v] {
101                return Err(EngineError::Stale);
102            }
103        }
104        for &(v, id) in &self.rows[start..end] {
105            let idx = guards[v as usize].as_ref().ok_or(EngineError::Stale)?;
106            let mut parent_path = Vec::new();
107            idx.append_parent_path(id, &mut parent_path);
108            out.push(Row {
109                entry_ref: ((v as u64) << 32) | id as u64,
110                frn: idx.frn(id).0,
111                size: idx.size(id),
112                mtime: idx.mtime(id),
113                flags: idx_flags(idx, id),
114                name: idx.name(id).to_vec(),
115                parent_path,
116            });
117        }
118        Ok(out)
119    }
120}
121
122fn idx_flags(idx: &VolumeIndex, id: EntryId) -> u32 {
123    let mut f = 0u32;
124    if idx.is_dir(id) {
125        f |= 1;
126    }
127    if !idx.is_live(id) {
128        f |= 2; // deleted-since-query marker for the UI
129    }
130    f
131}
132
133// Reuse the flags module so the constant meanings stay in one place.
134const _: () = {
135    assert!(flags::IS_DIR == 1);
136};