1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-09-06 07:35:51 +03:00

chore(fuzz): upgrade targets, fix false positives, and add invariant assertions (#11)

- Fix insert_delete target using .unwrap() causing false-positive crashes
  on arbitrary::Error (input exhaustion)
- Fix fuzz Cargo.toml referencing wrong package name (tui-textarea vs
  tui-textarea-2)
- Add invariant assertions (cursor bounds, lines non-empty, selection
  validity) to all targets
- Add 4 new fuzz targets: undo_redo, selection, wrap_render, search
- Expand existing targets with full delete/insert variant coverage,
  input_without_shortcuts, set_yank_text, set_max_histories, and clear
- Add DummyBackend::resize() for variable-width wrap rendering tests
Этот коммит содержится в:
srothgan
2026-02-19 08:44:58 +01:00
коммит произвёл GitHub
родитель 19151a4076
Коммит f169a4b18c
9 изменённых файлов: 827 добавлений и 228 удалений

Просмотреть файл

@@ -9,6 +9,7 @@ use tui_textarea_bench::{dummy_terminal, TerminalExt};
#[derive(Arbitrary)]
enum RandomInput {
Input(Input),
InputWithoutShortcuts(Input),
Cursor(CursorMove),
}
@@ -18,11 +19,30 @@ impl RandomInput {
Self::Input(input) => {
t.input(input);
}
Self::InputWithoutShortcuts(input) => {
t.input_without_shortcuts(input);
}
Self::Cursor(m) => t.move_cursor(m),
}
}
}
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut data = Unstructured::new(data);
@@ -32,6 +52,7 @@ fn fuzz(data: &[u8]) -> Result<()> {
let input = RandomInput::arbitrary(&mut data)?;
input.apply(&mut textarea);
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
Ok(())
}

Просмотреть файл

@@ -1,26 +1,93 @@
#![no_main]
use arbitrary::{Arbitrary as _, Result, Unstructured};
use arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt};
#[derive(Arbitrary)]
enum Op {
Move(CursorMove),
InsertStr(String),
InsertChar(char),
InsertTab,
InsertNewline,
DeleteStr(usize),
DeleteChar,
DeleteNextChar,
DeleteWord,
DeleteNextWord,
DeleteLineByEnd,
DeleteLineByHead,
DeleteNewline,
Clear,
}
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut textarea = TextArea::default();
let mut data = Unstructured::new(data);
for i in 0..100 {
textarea.move_cursor(CursorMove::arbitrary(&mut data)?);
if i % 2 == 0 {
textarea.insert_str(String::arbitrary(&mut data)?);
} else {
textarea.delete_str(usize::arbitrary(&mut data)?);
for _ in 0..100 {
match Op::arbitrary(&mut data)? {
Op::Move(m) => textarea.move_cursor(m),
Op::InsertStr(s) => {
textarea.insert_str(s);
}
Op::InsertChar(c) => textarea.insert_char(c),
Op::InsertTab => {
textarea.insert_tab();
}
Op::InsertNewline => textarea.insert_newline(),
Op::DeleteStr(n) => {
textarea.delete_str(n);
}
Op::DeleteChar => {
textarea.delete_char();
}
Op::DeleteNextChar => {
textarea.delete_next_char();
}
Op::DeleteWord => {
textarea.delete_word();
}
Op::DeleteNextWord => {
textarea.delete_next_word();
}
Op::DeleteLineByEnd => {
textarea.delete_line_by_end();
}
Op::DeleteLineByHead => {
textarea.delete_line_by_head();
}
Op::DeleteNewline => {
textarea.delete_newline();
}
Op::Clear => {
textarea.clear();
}
}
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
Ok(())
}
fuzz_target!(|data: &[u8]| {
fuzz(data).unwrap();
let _ = fuzz(data);
});

67
fuzz/fuzz_targets/search.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,67 @@
#![no_main]
use arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt};
#[derive(Arbitrary)]
enum Op {
SetPattern(String),
SearchForward(bool),
SearchBack(bool),
Move(CursorMove),
InsertStr(String),
DeleteStr(usize),
}
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut data = Unstructured::new(data);
let text = <&str>::arbitrary(&mut data)?;
let mut textarea = TextArea::from(text.lines());
for _ in 0..100 {
match Op::arbitrary(&mut data)? {
Op::SetPattern(pat) => {
let _ = textarea.set_search_pattern(&pat);
}
Op::SearchForward(match_cursor) => {
textarea.search_forward(match_cursor);
}
Op::SearchBack(match_cursor) => {
textarea.search_back(match_cursor);
}
Op::Move(m) => textarea.move_cursor(m),
Op::InsertStr(s) => {
textarea.insert_str(s);
}
Op::DeleteStr(n) => {
textarea.delete_str(n);
}
}
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
Ok(())
}
fuzz_target!(|data: &[u8]| {
let _ = fuzz(data);
});

102
fuzz/fuzz_targets/selection.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,102 @@
#![no_main]
use arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt};
#[derive(Arbitrary)]
enum Op {
InsertStr(String),
DeleteStr(usize),
DeleteChar,
Move(CursorMove),
StartSelection,
CancelSelection,
SelectAll,
Copy,
Cut,
Paste,
SetYankText(String),
Undo,
Redo,
}
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
if let Some(((sr, sc), (er, ec))) = textarea.selection_range() {
assert!(sr < lines.len(), "selection start row {sr} out of bounds");
assert!(
sc <= lines[sr].chars().count(),
"selection start col {sc} out of bounds (line {sr} chars: {})",
lines[sr].chars().count(),
);
assert!(er < lines.len(), "selection end row {er} out of bounds");
assert!(
ec <= lines[er].chars().count(),
"selection end col {ec} out of bounds (line {er} chars: {})",
lines[er].chars().count(),
);
assert!(
(sr, sc) <= (er, ec),
"selection start ({sr},{sc}) > end ({er},{ec})",
);
}
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut data = Unstructured::new(data);
let text = <&str>::arbitrary(&mut data)?;
let mut textarea = TextArea::from(text.lines());
for _ in 0..100 {
match Op::arbitrary(&mut data)? {
Op::InsertStr(s) => {
textarea.insert_str(s);
}
Op::DeleteStr(n) => {
textarea.delete_str(n);
}
Op::DeleteChar => {
textarea.delete_char();
}
Op::Move(m) => textarea.move_cursor(m),
Op::StartSelection => textarea.start_selection(),
Op::CancelSelection => textarea.cancel_selection(),
Op::SelectAll => textarea.select_all(),
Op::Copy => textarea.copy(),
Op::Cut => {
textarea.cut();
}
Op::Paste => {
textarea.paste();
}
Op::SetYankText(s) => textarea.set_yank_text(s),
Op::Undo => {
textarea.undo();
}
Op::Redo => {
textarea.redo();
}
}
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
Ok(())
}
fuzz_target!(|data: &[u8]| {
let _ = fuzz(data);
});

80
fuzz/fuzz_targets/undo_redo.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,80 @@
#![no_main]
use arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use tui_textarea::{CursorMove, TextArea};
use tui_textarea_bench::{dummy_terminal, TerminalExt};
#[derive(Arbitrary)]
enum Op {
InsertStr(String),
InsertChar(char),
InsertNewline,
DeleteStr(usize),
DeleteChar,
DeleteNextChar,
Clear,
Move(CursorMove),
Undo,
Redo,
SetMaxHistories(u8),
}
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut data = Unstructured::new(data);
let text = <&str>::arbitrary(&mut data)?;
let mut textarea = TextArea::from(text.lines());
for _ in 0..100 {
match Op::arbitrary(&mut data)? {
Op::InsertStr(s) => {
textarea.insert_str(s);
}
Op::InsertChar(c) => textarea.insert_char(c),
Op::InsertNewline => textarea.insert_newline(),
Op::DeleteStr(n) => {
textarea.delete_str(n);
}
Op::DeleteChar => {
textarea.delete_char();
}
Op::DeleteNextChar => {
textarea.delete_next_char();
}
Op::Clear => {
textarea.clear();
}
Op::Move(m) => textarea.move_cursor(m),
Op::Undo => {
textarea.undo();
}
Op::Redo => {
textarea.redo();
}
Op::SetMaxHistories(n) => textarea.set_max_histories(n as usize),
}
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
Ok(())
}
fuzz_target!(|data: &[u8]| {
let _ = fuzz(data);
});

55
fuzz/fuzz_targets/wrap_render.rs Обычный файл
Просмотреть файл

@@ -0,0 +1,55 @@
#![no_main]
use arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use tui_textarea::{CursorMove, TextArea, WrapMode};
use tui_textarea_bench::{dummy_terminal, TerminalExt};
fn assert_invariants(textarea: &TextArea<'_>) {
let lines = textarea.lines();
assert!(!lines.is_empty(), "lines must never be empty");
let (row, col) = textarea.cursor();
assert!(
row < lines.len(),
"cursor row {row} out of bounds (lines: {})",
lines.len()
);
assert!(
col <= lines[row].chars().count(),
"cursor col {col} out of bounds (line {row} chars: {})",
lines[row].chars().count(),
);
}
fn fuzz(data: &[u8]) -> Result<()> {
let mut term = dummy_terminal();
let mut data = Unstructured::new(data);
let text = <&str>::arbitrary(&mut data)?;
let wrap_mode = WrapMode::arbitrary(&mut data)?;
let width: u16 = *data.choose(&[1, 4, 10, 40, 80, 200])?;
term.backend_mut().resize(width, 12);
let mut textarea = TextArea::from(text.lines());
textarea.set_wrap_mode(wrap_mode);
for _ in 0..100 {
let m = CursorMove::arbitrary(&mut data)?;
textarea.move_cursor(m);
term.draw_textarea(&textarea);
assert_invariants(&textarea);
if bool::arbitrary(&mut data)? {
let s = <&str>::arbitrary(&mut data)?;
textarea.insert_str(s);
term.draw_textarea(&textarea);
assert_invariants(&textarea);
}
}
Ok(())
}
fuzz_target!(|data: &[u8]| {
let _ = fuzz(data);
});