зеркало из
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
Этот коммит содержится в:
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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);
|
||||
});
|
||||
Ссылка в новой задаче
Block a user