Skip to main content

exg/fiff/
tree.rs

1//! FIF directory tree construction.
2//!
3//! Mirrors `mne/_fiff/tree.py` but uses owned Rust types throughout.
4//!
5//! The tree is built by scanning all tag headers sequentially and grouping
6//! them into blocks delimited by `FIFF_BLOCK_START` / `FIFF_BLOCK_END` tags.
7use std::io::{Read, Seek};
8use anyhow::Result;
9
10use super::constants::*;
11use super::tag::{read_i32, read_tag_header, TagHeader};
12
13// ── Node ─────────────────────────────────────────────────────────────────
14
15/// One node in the FIF tree, analogous to MNE's `dict` node.
16#[derive(Debug, Default, Clone)]
17pub struct Node {
18    /// Block kind (e.g. `FIFFB_MEAS`, `FIFFB_RAW_DATA`, …).
19    /// 0 = root / unknown.
20    pub block:    i32,
21    /// All non-structural tag headers in this node (not including BLOCK_START/END).
22    pub entries:  Vec<TagHeader>,
23    /// Child nodes.
24    pub children: Vec<Node>,
25}
26
27impl Node {
28    /// Recursively find the first child node with the given block kind.
29    pub fn find_block(&self, kind: i32) -> Option<&Node> {
30        if self.block == kind {
31            return Some(self);
32        }
33        for child in &self.children {
34            if let Some(found) = child.find_block(kind) {
35                return Some(found);
36            }
37        }
38        None
39    }
40
41    /// Recursively collect all nodes with the given block kind.
42    pub fn find_blocks(&self, kind: i32) -> Vec<&Node> {
43        let mut out = Vec::new();
44        self.collect_blocks(kind, &mut out);
45        out
46    }
47
48    fn collect_blocks<'a>(&'a self, kind: i32, out: &mut Vec<&'a Node>) {
49        if self.block == kind {
50            out.push(self);
51        }
52        for child in &self.children {
53            child.collect_blocks(kind, out);
54        }
55    }
56
57    /// Find the first tag header with the given kind in this node's entries.
58    /// Does NOT recurse into children.
59    pub fn find_tag(&self, kind: i32) -> Option<&TagHeader> {
60        self.entries.iter().find(|e| e.kind == kind)
61    }
62}
63
64// ── Tree builder ─────────────────────────────────────────────────────────
65
66/// Build the tag tree by walking the flat directory sequentially.
67///
68/// `directory` is the ordered list of all tag headers in the file.
69/// This matches `mne._fiff.tree.make_dir_tree()`.
70pub fn build_tree(directory: &[TagHeader]) -> Node {
71    let mut stack: Vec<Node> = vec![Node::default()]; // root
72    for &tag in directory {
73        match tag.kind {
74            FIFF_BLOCK_START => {
75                // Push a new child node. Block kind is not resolved here
76                // (no file access); use `read_tree` if you need resolved kinds.
77                stack.push(Node::default());
78            }
79            FIFF_BLOCK_END => {
80                let finished = stack.pop().unwrap_or_default();
81                if let Some(parent) = stack.last_mut() {
82                    parent.children.push(finished);
83                }
84            }
85            _ => {
86                if let Some(node) = stack.last_mut() {
87                    node.entries.push(tag);
88                }
89            }
90        }
91    }
92    // Anything left on the stack belongs to root.
93    while stack.len() > 1 {
94        let orphan = stack.pop().unwrap();
95        stack[0].children.push(orphan);
96    }
97    stack.pop().unwrap_or_default()
98}
99
100/// Walk a flat directory and build the tree, resolving block kinds from the file.
101pub fn read_tree<R: Read + Seek>(reader: &mut R, directory: &[TagHeader]) -> Result<Node> {
102    let mut root = Node::default();
103    build_tree_resolved(reader, directory, &mut root)?;
104    Ok(root)
105}
106
107fn build_tree_resolved<R: Read + Seek>(
108    reader: &mut R,
109    directory: &[TagHeader],
110    root: &mut Node,
111) -> Result<()> {
112    let mut stack: Vec<Node> = vec![Node {
113        block:    0, // root
114        entries:  Vec::new(),
115        children: Vec::new(),
116    }];
117
118    for &tag in directory {
119        match tag.kind {
120            FIFF_BLOCK_START => {
121                let block_kind = read_i32(reader, &tag).unwrap_or(0);
122                stack.push(Node {
123                    block:    block_kind,
124                    entries:  Vec::new(),
125                    children: Vec::new(),
126                });
127            }
128            FIFF_BLOCK_END => {
129                let finished = stack.pop().unwrap_or_default();
130                if let Some(parent) = stack.last_mut() {
131                    parent.children.push(finished);
132                }
133            }
134            _ => {
135                if let Some(node) = stack.last_mut() {
136                    node.entries.push(tag);
137                }
138            }
139        }
140    }
141
142    while stack.len() > 1 {
143        let orphan = stack.pop().unwrap();
144        if let Some(parent) = stack.last_mut() {
145            parent.children.push(orphan);
146        }
147    }
148
149    *root = stack.pop().unwrap_or_default();
150    Ok(())
151}
152
153// ── Directory scanner ─────────────────────────────────────────────────────
154
155/// Read every tag header by following the `next` pointer chain.
156/// This is MNE's "slow path" — used when there is no pre-built directory,
157/// or when we want to build a fresh one.
158pub fn scan_directory<R: Read + Seek>(reader: &mut R) -> Result<Vec<TagHeader>> {
159    let mut directory = Vec::new();
160    let mut pos: Option<u64> = Some(0);
161    while let Some(p) = pos {
162        let tag = read_tag_header(reader, p)?;
163        pos = tag.next_pos();
164        directory.push(tag);
165    }
166    Ok(directory)
167}
168
169// ── Fast directory from embedded dir tag ─────────────────────────────────
170
171/// Try to load the pre-built tag directory embedded at the end of the file.
172///
173/// MNE checks `FIFF_DIR_POINTER` (tag kind 101) early in the file; if its
174/// payload is > 0 it points to a `FIFFT_DIR_ENTRY_STRUCT` tag containing
175/// all headers.  Returns `None` if missing or corrupt.
176pub fn try_load_directory<R: Read + Seek>(reader: &mut R) -> Result<Option<Vec<TagHeader>>> {
177    // First tag must be FIFF_FILE_ID.
178    let id_tag = read_tag_header(reader, 0)?;
179    if id_tag.kind != FIFF_FILE_ID {
180        return Ok(None);
181    }
182    // Second tag must be FIFF_DIR_POINTER.
183    let next = match id_tag.next_pos() {
184        Some(p) => p,
185        None => return Ok(None),
186    };
187    let dir_ptr_tag = read_tag_header(reader, next)?;
188    if dir_ptr_tag.kind != FIFF_DIR_POINTER {
189        return Ok(None);
190    }
191    let dirpos = read_i32(reader, &dir_ptr_tag)? as i64;
192    if dirpos <= 0 {
193        return Ok(None);
194    }
195    let dir_tag = read_tag_header(reader, dirpos as u64)?;
196    if dir_tag.ftype != super::constants::FIFFT_DIR_ENTRY_STRUCT {
197        return Ok(None);
198    }
199    let entries = super::tag::read_directory(reader, &dir_tag)?;
200    Ok(Some(entries))
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn make_flat_dir(spec: &[(i32, u32, i32, i32)]) -> Vec<TagHeader> {
208        spec.iter()
209            .enumerate()
210            .map(|(i, &(kind, ftype, size, next))| TagHeader {
211                kind, ftype, size, next, pos: (i as u64) * 100,
212            })
213            .collect()
214    }
215
216    #[test]
217    fn flat_directory_no_blocks() {
218        // No BLOCK_START/END → everything in root entries
219        let dir = make_flat_dir(&[
220            (FIFF_NCHAN, FIFFT_INT, 4, 0),
221            (FIFF_SFREQ, FIFFT_FLOAT, 4, 0),
222        ]);
223        let root = build_tree(&dir);
224        assert_eq!(root.entries.len(), 2);
225        assert!(root.children.is_empty());
226    }
227
228    #[test]
229    fn single_block() {
230        let dir = make_flat_dir(&[
231            (FIFF_BLOCK_START, FIFFT_INT, 4, 0), // FIFFB_MEAS
232            (FIFF_NCHAN,       FIFFT_INT, 4, 0),
233            (FIFF_BLOCK_END,   FIFFT_INT, 4, 0),
234        ]);
235        let root = build_tree(&dir);
236        assert_eq!(root.children.len(), 1);
237        assert_eq!(root.children[0].entries.len(), 1);
238    }
239
240    #[test]
241    fn nested_blocks() {
242        let dir = make_flat_dir(&[
243            (FIFF_BLOCK_START, FIFFT_INT, 4, 0), // outer
244            (FIFF_BLOCK_START, FIFFT_INT, 4, 0), // inner
245            (FIFF_NCHAN,       FIFFT_INT, 4, 0),
246            (FIFF_BLOCK_END,   FIFFT_INT, 4, 0), // close inner
247            (FIFF_SFREQ,       FIFFT_FLOAT, 4, 0),
248            (FIFF_BLOCK_END,   FIFFT_INT, 4, 0), // close outer
249        ]);
250        let root = build_tree(&dir);
251        assert_eq!(root.children.len(), 1);           // outer
252        let outer = &root.children[0];
253        assert_eq!(outer.children.len(), 1);          // inner
254        assert_eq!(outer.entries.len(), 1);           // SFREQ
255        assert_eq!(outer.children[0].entries.len(), 1); // NCHAN
256    }
257
258    #[test]
259    fn find_block_depth_first() {
260        let dir = make_flat_dir(&[
261            (FIFF_BLOCK_START, FIFFT_INT, 4, 0),
262            (FIFF_BLOCK_START, FIFFT_INT, 4, 0),
263            (FIFF_NCHAN,       FIFFT_INT, 4, 0),
264            (FIFF_BLOCK_END,   FIFFT_INT, 4, 0),
265            (FIFF_BLOCK_END,   FIFFT_INT, 4, 0),
266        ]);
267        let root = build_tree(&dir);
268        // block kinds not set (no file reads in pure-flat test), but
269        // find_block(0) should find root.
270        assert!(root.find_block(0).is_some());
271    }
272}