fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
// <FILE>crates/fast-fs/src/reader/cls_stream_state.rs</FILE> - <DESC>Internal state management for directory streaming</DESC>
// <VERS>VERSION: 1.0.0</VERS>
// <WCTX>Implementing streaming directory reading for fast-fs</WCTX>
// <CLOG>Extracted from fnc_read_dir_stream to meet LOC limits</CLOG>

use crate::filter::cls_gitignore_matcher::GitignoreMatcher;
use crate::filter::fnc_apply_filters::apply_filters;
use crate::reader::fnc_read_dir::read_dir;
use crate::{FileEntry, Result, TraversalOptions};
use std::collections::HashSet;
use std::path::PathBuf;

/// Internal state for the streaming traversal
pub(super) struct StreamState {
    /// Stack of directories to visit (path, current_depth)
    stack: Vec<(PathBuf, usize)>,
    /// Maximum depth to traverse
    max_depth: usize,
    /// Traversal options
    options: TraversalOptions,
    /// Gitignore matcher (if enabled)
    gitignore: Option<GitignoreMatcher>,
    /// Visited paths for symlink loop detection
    visited: HashSet<PathBuf>,
    /// Current directory entries being processed
    current_entries: Vec<FileEntry>,
    /// Index in current_entries
    current_index: usize,
    /// Current depth we're processing at
    current_depth: usize,
}

impl StreamState {
    pub(super) fn new(
        root: PathBuf,
        max_depth: usize,
        options: TraversalOptions,
        gitignore: Option<GitignoreMatcher>,
    ) -> Self {
        let mut visited = HashSet::new();

        // Canonicalize root to ensure stable tracking
        if let Ok(canon) = root.canonicalize() {
            visited.insert(canon);
        }

        Self {
            stack: vec![(root, 0)],
            max_depth,
            options,
            gitignore,
            visited,
            current_entries: Vec::new(),
            current_index: 0,
            current_depth: 0,
        }
    }

    /// Get the next entry from the stream
    pub(super) async fn next_entry(&mut self) -> Option<(Result<FileEntry>, StreamState)> {
        loop {
            // First, try to yield from current_entries
            if self.current_index < self.current_entries.len() {
                let entry = self.current_entries[self.current_index].clone();
                self.current_index += 1;

                // If this is a directory and we haven't reached max depth, push to stack
                if entry.is_dir && self.current_depth < self.max_depth {
                    let next_depth = self.current_depth + 1;

                    // Check for symlink loops
                    let should_visit = if let Ok(canon) = entry.path.canonicalize() {
                        if self.visited.contains(&canon) {
                            false
                        } else {
                            self.visited.insert(canon);
                            true
                        }
                    } else {
                        true
                    };

                    if should_visit {
                        self.stack.push((entry.path.clone(), next_depth));
                    }
                }

                // Return this entry and continue the stream
                return Some((
                    Ok(entry),
                    StreamState {
                        stack: std::mem::take(&mut self.stack),
                        max_depth: self.max_depth,
                        options: self.options.clone(),
                        gitignore: self.gitignore.clone(),
                        visited: std::mem::take(&mut self.visited),
                        current_entries: std::mem::take(&mut self.current_entries),
                        current_index: self.current_index,
                        current_depth: self.current_depth,
                    },
                ));
            }

            // Current entries exhausted, pop next directory from stack
            let (current_dir, depth) = match self.stack.pop() {
                Some(dir) => dir,
                None => return None, // All done
            };

            self.current_depth = depth;

            // Read directory entries
            let entries = match read_dir(&current_dir).await {
                Ok(entries) => entries,
                Err(_) => {
                    // Skip directories we can't read
                    continue;
                }
            };

            // Filter entries
            let filtered: Vec<FileEntry> = entries
                .into_iter()
                .filter(|entry| {
                    apply_filters(entry, &self.options, self.gitignore.as_ref()).is_pass()
                })
                .collect();

            // Store filtered entries and reset index
            self.current_entries = filtered;
            self.current_index = 0;

            // Continue loop to try yielding from current_entries
        }
    }
}

// <FILE>crates/fast-fs/src/reader/cls_stream_state.rs</FILE>
// <VERS>END OF VERSION: 1.0.0</VERS>