Skip to content

Commit 30d000e

Browse files
author
Paulo Remoli
committed
fix out of bounds panic when regex filter is applied after file grows
Indices beyond the continuation map have no continuation data and are treated as standalone lines.
1 parent 61356d3 commit 30d000e

3 files changed

Lines changed: 51 additions & 9 deletions

File tree

ARCHITECTURE.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ logana is structured around a strict separation between domain logic and the UI
1212

1313
**Filter Pipeline** (`filters/`) — `FilterManager` compiles filter definitions into Aho-Corasick automata or regexes and evaluates them against every line to produce a visibility bitmap. The pipeline runs in a background thread. Filter definitions are persisted to SQLite and reloaded on startup.
1414

15-
**Mode System** (`mode/`) — Modal UI where each mode owns keyboard input and returns a `KeyResult` for effects that cross mode boundaries. Modes: Normal, Command, Search, Filter, Visual, Comment, Select Fields, DLT Select, Docker Select, Merge Select, Keybindings Help.
15+
**Mode System** (`mode/`) — Modal UI where each mode owns keyboard input and returns a `KeyResult` for effects that cross mode boundaries. Example of modes: Normal, Command, Search, Filter, Visual, Comment. Each window is also treated as a mode.
1616

1717
**UI & Rendering** (`ui/`) — The renderer reads tab state and produces widgets each frame; it never mutates state. The event loop dispatches key events to the active mode. Session state is persisted to SQLite and restored on reopen.
1818

19-
**Persistence** (`db/`) — `Database` owns the SQLite connection and schema migrations. `LogManager` builds `FilterManager` instances from persisted filter definitions and manages session state.
19+
**Persistence** (`db/`) — `Database` owns the SQLite connection and schema migrations. Storage is accessed through four traits: `FilterStore` (filter definitions), `FileContextStore` (per-file scroll/search/display context), `SessionStore` (open file list), and `AppSettingsStore` (runtime toggles keyed by `SettingsKey`). `LogManager` builds `FilterManager` instances from persisted filter definitions. `MarkManager` and `CommentManager` are in-memory managers owned by `TabState`; their state is flushed to SQLite via `FileContext` on tab close/switch. Session save/restore is coordinated by `SessionManager` in `ui/session.rs`.
2020

2121
## Component Diagram
2222

2323
```mermaid
2424
graph TD
2525
CLI[CLI / main.rs] -->|creates| App[App]
2626
App -->|owns| Tab[TabState ×N]
27+
App -->|owns| Session[SessionManager]
2728
App -->|renders via| Renderer[Renderer]
2829
Tab -->|owns| Scroll[ScrollState]
2930
Tab -->|owns| Filter[FilterState]
@@ -32,11 +33,14 @@ graph TD
3233
Tab -->|owns| Stream[StreamState]
3334
Tab -->|owns| Display[DisplayConfig]
3435
Tab -->|owns| Interaction[InteractionState]
36+
Tab -->|owns| Marks[MarkManager]
37+
Tab -->|owns| Comments[CommentManager]
3538
Tab -->|owns| FileReader[FileReader]
3639
Tab -->|owns| LogManager[LogManager]
3740
Filter -->|holds| FM[FilterManager]
3841
Display -->|holds| Parser[LogFormatParser]
39-
LogManager -->|queries| DB[(SQLite DB)]
42+
LogManager -->|FilterStore| DB[(SQLite DB)]
43+
Session -->|FileContextStore / SessionStore / AppSettingsStore| DB
4044
FileReader -->|reads| Files[(Log files / stdin / streams)]
4145
Renderer -->|reads| Tab
4246
App -->|snapshot| MCP[MCP Server]
@@ -67,7 +71,7 @@ Live updates are driven by `advance_merged_tabs`, which compares per-source line
6771

6872
## Commands
6973

70-
`src/commands/` contains clap-derived command definitions shared across layers. `src/ui/commands/` contains the handlers that execute `:` commands, split into `filter.rs`, `io.rs`, `display.rs`, `stream.rs`, and `merge.rs`.
74+
`src/commands/` contains clap-derived command definitions shared across layers. `src/ui/commands/` contains the handlers that execute `:` commands.
7175

7276
## MCP Server
7377

@@ -170,18 +174,22 @@ graph TD
170174
| **ratatui** | TUI rendering |
171175
| **crossterm** | Terminal I/O, key events |
172176
| **tokio** | Async runtime |
177+
| **async-trait** | Async trait methods for the mode system |
173178
| **memchr** | SIMD byte scanning for line indexing |
174179
| **aho-corasick** | Literal substring filter matching |
175180
| **regex** | Regex filter matching |
176181
| **rayon** | Parallel line indexing and visibility scan |
177182
| **sqlx** | SQLite async driver |
178183
| **clap** | CLI argument parsing |
179-
| **serde / serde_json** | Config and theme serialisation |
184+
| **serde / serde_json / serde_with** | Config and theme serialisation |
185+
| **schemars** | JSON Schema generation for config |
186+
| **strum** | Enum-to-string and string-to-enum derives |
180187
| **time** | Timestamp parsing for date-range filters |
181188
| **unicode-width** | Unicode display width for cursor/truncation |
182189
| **arboard** | Clipboard |
183-
| **dirs** | XDG data directory for SQLite path |
190+
| **dirs** | XDG config and data directories |
184191
| **anyhow** | Error handling |
192+
| **libc** | Low-level OS interfaces |
185193
| **tempfile** | Temporary files for archive extraction and stdin streaming |
186194
| **flate2** | Gzip / deflate decompression |
187195
| **zip** | ZIP archive parsing |
@@ -190,5 +198,6 @@ graph TD
190198
| **tar** | Tar archive iteration |
191199
| **rmcp** | MCP server implementation |
192200
| **axum** | HTTP transport for MCP |
201+
| **tonic** | gRPC transport for OTLP receiver |
193202
| **opentelemetry-proto** | OTLP protobuf types |
194203
| **prost** | Protobuf decoding |

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ All notable changes to logana will be documented in this file.
66
## [Unreleased]
77

88
### Added
9-
- Export window: `:export` now opens an interactive window when the template footer contains `{{placeholder}}` variables. The window renders one editable field per placeholder, so custom templates with any field names work automatically. Both bundled templates (`markdown` and `jira`) ship with `{{conclusion}}` and `{{next_steps}}` in their footer.
9+
- Export windows allows filling the placeholders from the template (e.g. Context, next steps and conclusion).
10+
- Merge view: merge different source into a single view sorted by timestamp.
11+
- Generate JSON schema for config file validation.
12+
13+
### Fixed
14+
- Panic (index out of bounds) when applying a regex filter after the log file grew past the size it had when the continuation map was built.
1015

1116
## [0.5.1] - 2026-04-02
1217

src/ui/tab_state/mod.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,21 +514,32 @@ pub fn apply_continuation_correction(
514514

515515
let n = cmap.len();
516516

517+
// Indices beyond the map belong to lines appended after the map was built
518+
// (e.g. file grew while the map was stale). Preserve them unchanged.
517519
let mut filter_visible = vec![0u8; n];
518520
for &idx in indices.iter() {
519-
filter_visible[idx] = 1;
521+
if idx < n {
522+
filter_visible[idx] = 1;
523+
}
520524
}
521525

522526
// Each line is visible iff its parent is visible AND
523527
// (the line itself was not explicitly excluded OR include filters exist).
524528
// For parent lines (i == parent) this reduces to filter_visible[i] unchanged.
525-
let new_indices: Vec<usize> = (0..n)
529+
let mut new_indices: Vec<usize> = (0..n)
526530
.into_par_iter()
527531
.filter(|&i| {
528532
filter_visible[cmap[i]] != 0 && (filter_visible[i] != 0 || has_include_filters)
529533
})
530534
.collect();
531535

536+
for &idx in indices.iter() {
537+
if idx >= n {
538+
new_indices.push(idx);
539+
}
540+
}
541+
new_indices.sort_unstable();
542+
532543
*indices = new_indices;
533544
}
534545

@@ -4160,6 +4171,23 @@ mod tests {
41604171
assert!(visible.contains(&3), "line 3 must be visible");
41614172
}
41624173

4174+
/// Regression test: `apply_continuation_correction` must not panic when the
4175+
/// visible-index list contains indices that lie beyond the continuation map
4176+
/// (i.e. the file grew after the map was built).
4177+
#[test]
4178+
fn test_continuation_correction_indices_beyond_cmap() {
4179+
// cmap covers 3 lines (indices 0-2); all are their own parent.
4180+
let cmap = vec![0usize, 1, 2];
4181+
// visible contains index 3, which is beyond cmap — simulates a file that
4182+
// gained a new line after the map was built.
4183+
let mut visible = VisibleLines::Filtered(vec![0, 1, 2, 3]);
4184+
// Must not panic.
4185+
apply_continuation_correction(&mut visible, &cmap, false);
4186+
let result: Vec<usize> = visible.iter().collect();
4187+
// All four lines should be visible and in order.
4188+
assert_eq!(result, vec![0, 1, 2, 3]);
4189+
}
4190+
41634191
#[tokio::test]
41644192
async fn test_filter_match_counts_updated_via_advance() {
41654193
let mut tab = make_tab(&["ERROR line", "INFO line", "ERROR again"]).await;

0 commit comments

Comments
 (0)