Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 33 additions & 28 deletions src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use lru::LruCache;
use packet_dissector::registry::DissectorRegistry;

use super::completion::CompletionEngine;
use super::filter_bitmap::FilterBitmap;
use super::live::StdinCopier;
use super::loader;
use super::state::{
Expand All @@ -27,8 +28,8 @@ pub struct App {
pub capture: CaptureMap,
/// Minimal index of all packets (32 bytes each).
pub indices: Vec<PacketIndex>,
/// Indices into `indices` matching the current filter.
pub filtered_indices: Vec<usize>,
/// Bitmap of packets matching the current filter (one bit per packet).
pub filtered: FilterBitmap,
/// Dissector registry (for on-demand dissection).
pub registry: DissectorRegistry,
/// Fuzzy completion engine for filter input.
Expand Down Expand Up @@ -105,13 +106,13 @@ impl App {
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| file_path.display().to_string());
let completion_engine = CompletionEngine::from_registry(&registry);
let filtered_indices: Vec<usize> = (0..indices.len()).collect();
let filtered = FilterBitmap::all_set(indices.len());
let loaded_history = super::state::load_history();
let mut app = Self {
file_name,
capture,
indices,
filtered_indices,
filtered,
registry,
completion_engine,
summary_cache: LruCache::new(SUMMARY_CACHE_CAPACITY),
Expand Down Expand Up @@ -160,13 +161,13 @@ impl App {
// Start at 0 so the first live_tick() ingests any data that was
// already in the mmap (e.g. written before CaptureMap::new_live).
let indexed_bytes = 0;
let filtered_indices: Vec<usize> = (0..indices.len()).collect();
let filtered = FilterBitmap::all_set(indices.len());
let loaded_history = super::state::load_history();
let mut app = Self {
file_name: "<stdin>".to_string(),
capture,
indices,
filtered_indices,
filtered,
registry,
completion_engine,
summary_cache: LruCache::new(SUMMARY_CACHE_CAPACITY),
Expand Down Expand Up @@ -205,7 +206,7 @@ impl App {

/// Number of displayed (filtered) packets.
pub fn displayed_count(&self) -> usize {
self.filtered_indices.len()
self.filtered.count_ones()
}

/// Total number of packets in the capture.
Expand All @@ -215,9 +216,9 @@ impl App {

/// Get the selected packet number (1-based), or 0 if nothing selected.
pub fn selected_number(&self) -> u64 {
self.filtered_indices
.get(self.packet_list.selected)
.map(|&idx| idx as u64 + 1)
self.filtered
.select(self.packet_list.selected)
.map(|idx| idx as u64 + 1)
.unwrap_or(0)
}

Expand Down Expand Up @@ -262,7 +263,7 @@ impl App {
self.detail_tree.selected = 0;
self.detail_tree.scroll_offset = 0;

if let Some(&pkt_idx) = self.filtered_indices.get(self.packet_list.selected) {
if let Some(pkt_idx) = self.filtered.select(self.packet_list.selected) {
if let Some(index) = self.indices.get(pkt_idx) {
if let Some(data) = self.capture.packet_data(index) {
self.selected = Some(loader::dissect_selected(
Expand Down Expand Up @@ -327,14 +328,16 @@ impl App {
if self.indices.is_empty() {
let estimate = (bg.total_bytes / 80).min(Self::MAX_PREALLOC);
self.indices.reserve(estimate);
self.filtered_indices.reserve(estimate);
self.filtered.reserve(estimate);
}

let old_count = self.indices.len();
let new_count = new_records.len();
self.indices.extend(new_records);
self.filtered_indices
.extend(old_count..old_count + new_count);
// No filter is active during initial indexing: every new packet is
// displayed, so set the corresponding bits (identity growth).
self.filtered
.push_set_range(old_count..old_count + new_count);

// Select the first packet as soon as we have data.
if old_count == 0 && !self.indices.is_empty() && self.selected.is_none() {
Expand All @@ -359,7 +362,7 @@ impl App {
if self.indices.is_empty() {
let estimate = (progress.total_bytes / 80).min(Self::MAX_PREALLOC);
self.indices.reserve(estimate);
self.filtered_indices.reserve(estimate);
self.filtered.reserve(estimate);
}

let deadline = std::time::Instant::now() + Self::INDEX_TIME_BUDGET;
Expand Down Expand Up @@ -388,9 +391,10 @@ impl App {
let new_count = new_records.len();
self.indices.extend(new_records);

// Extend filtered_indices (no filter is active during initial indexing).
self.filtered_indices
.extend(old_count..old_count + new_count);
// Set bits for the new packets (no filter is active during initial
// indexing).
self.filtered
.push_set_range(old_count..old_count + new_count);

// Read `done` before releasing the mutable borrow on `index_progress`
// so that `self.load_selected()` can borrow `self` mutably.
Expand Down Expand Up @@ -473,30 +477,31 @@ impl App {
};

let old_count = self.indices.len();
let old_displayed = self.filtered_indices.len();
let old_displayed = self.filtered.count_ones();
let was_at_bottom =
old_displayed == 0 || self.packet_list.selected >= old_displayed.saturating_sub(1);

if all_indices.len() > old_count {
let new_packets = &all_indices[old_count..];
self.indices.extend_from_slice(new_packets);

// If no filter is active, extend filtered_indices with the new ones.
let start = old_count;
let end = self.indices.len();
if self.filter.applied.is_empty() {
let start = old_count;
let end = self.indices.len();
self.filtered_indices.extend(start..end);
// No filter active: display the new packets (set their bits).
self.filtered.push_set_range(start..end);
} else {
// A filter is active: new packets are not in the view, but the
// universe must still grow so rank/select stay consistent.
self.filtered.extend_universe(end);
}
}

self.indexed_bytes = data.len();

// Auto-scroll: if user was at the bottom, follow new packets.
if was_at_bottom
&& self.live_mode == Some(LiveMode::Live)
&& !self.filtered_indices.is_empty()
{
self.packet_list.selected = self.filtered_indices.len() - 1;
if was_at_bottom && self.live_mode == Some(LiveMode::Live) && !self.filtered.is_empty() {
self.packet_list.selected = self.filtered.count_ones() - 1;
self.load_selected();
}
}
Expand Down
40 changes: 31 additions & 9 deletions src/tui/filter_apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::sync::Arc;
use packet_dissector_core::packet::{DissectBuffer, Packet};

use super::app::App;
use super::filter_bitmap::FilterBitmap;
use super::parallel_scan::{ParallelFilterScan, ScanPoll};
use super::state::FilterProgress;
use crate::filter_expr::FilterExpr;
Expand All @@ -24,8 +25,8 @@ impl App {
self.filter.applied = self.filter.buf.input.clone();

if expr.is_none() {
// Empty filter ��� show all packets immediately.
self.filtered_indices = (0..self.indices.len()).collect();
// Empty filter show all packets immediately.
self.filtered = FilterBitmap::all_set(self.indices.len());
self.summary_cache.clear();
self.packet_list.selected = 0;
self.packet_list.scroll_offset = 0;
Expand All @@ -43,7 +44,7 @@ impl App {
self.filter_progress = Some(FilterProgress {
expr,
cursor: 0,
results: Vec::new(),
results: FilterBitmap::new(),
});
}
}
Expand Down Expand Up @@ -157,7 +158,7 @@ impl App {
self.filter_progress = Some(FilterProgress {
expr,
cursor: 0,
results: Vec::new(),
results: FilterBitmap::new(),
});
true
}
Expand Down Expand Up @@ -216,19 +217,22 @@ impl App {

if progress.cursor >= total {
// Scan complete — take results and finalize.
let results = match std::mem::take(&mut self.filter_progress) {
let mut results = match std::mem::take(&mut self.filter_progress) {
Some(fp) => fp.results,
None => Vec::new(),
None => FilterBitmap::new(),
};
// Cover every scanned packet, including trailing non-matches, so
// rank/select stay consistent over the full universe.
results.extend_universe(total);
self.finalize_filter(results);
return false;
}
true
}

/// Apply completed filter results and update the UI state.
fn finalize_filter(&mut self, results: Vec<usize>) {
self.filtered_indices = results;
fn finalize_filter(&mut self, results: FilterBitmap) {
self.filtered = results;
self.summary_cache.clear();
self.packet_list.selected = 0;
self.packet_list.scroll_offset = 0;
Expand Down Expand Up @@ -270,7 +274,7 @@ mod tests {
app.filter.buf.cursor = 0;
app.apply_filter();
assert!(app.filter_progress.is_none());
assert_eq!(app.filtered_indices.len(), app.indices.len());
assert_eq!(app.filtered.count_ones(), app.indices.len());
assert_eq!(app.displayed_count(), 3);
}

Expand All @@ -297,6 +301,24 @@ mod tests {
assert_eq!(app.displayed_count(), 3);
}

#[test]
fn sequential_scan_bitmap_matches_expected_indices() {
// make_test_app's capture_path ("test.pcap") cannot be reopened by
// workers, so the scan finalizes via the sequential fallback.
let mut app = make_test_app(10);
app.filter.buf.input = "udp".into();
app.filter.buf.cursor = 3;
app.apply_filter();
drive_filter_to_completion(&mut app);

// All 10 fixture packets are UDP → every bit set.
let collected: Vec<usize> = app.filtered.iter().collect();
assert_eq!(collected, (0..10).collect::<Vec<_>>());
// The bitmap universe must cover every scanned packet, not just matches.
assert_eq!(app.filtered.universe(), 10);
assert_eq!(app.filtered.rank(10), 10);
}

#[test]
fn filter_tick_returns_false_when_idle() {
let mut app = make_test_app(1);
Expand Down
Loading
Loading