diff --git a/interp/builtins/tests/cat/cat_fuzz_test.go b/interp/builtins/tests/cat/cat_fuzz_test.go new file mode 100644 index 00000000..86a02379 --- /dev/null +++ b/interp/builtins/tests/cat/cat_fuzz_test.go @@ -0,0 +1,123 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package cat_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/DataDog/rshell/interp" + "github.com/DataDog/rshell/interp/builtins/testutil" +) + +func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// FuzzCat fuzzes cat with arbitrary file content and verifies output equals input. +func FuzzCat(f *testing.F) { + f.Add([]byte("hello\nworld\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + f.Add(bytes.Repeat([]byte("y"), 4096)) + f.Add([]byte{0xff, 0xfe, 0x00, 0x01}) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, "cat input.txt", dir) + if code != 0 && code != 1 { + t.Errorf("unexpected exit code %d", code) + } + + // cat must output exactly the file contents + if code == 0 && stdout != string(input) { + t.Errorf("cat output differs from input: got %d bytes, want %d bytes", len(stdout), len(input)) + } + }) +} + +// FuzzCatNumberLines fuzzes cat -n with arbitrary file content. +func FuzzCatNumberLines(f *testing.F) { + f.Add([]byte("line1\nline2\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\nc\n")) + f.Add([]byte("\n\n\n")) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "cat -n input.txt", dir) + if code != 0 && code != 1 { + t.Errorf("cat -n unexpected exit code %d", code) + } + }) +} + +// FuzzCatStdin fuzzes cat reading from stdin via shell redirection. +func FuzzCatStdin(f *testing.F) { + f.Add([]byte("hello\nworld\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, "cat < stdin.txt", dir) + if code != 0 && code != 1 { + t.Errorf("cat stdin unexpected exit code %d", code) + } + + if code == 0 && stdout != string(input) { + t.Errorf("cat stdin output differs from input: got %d bytes, want %d bytes", len(stdout), len(input)) + } + }) +} diff --git a/interp/builtins/tests/grep/grep_fuzz_test.go b/interp/builtins/tests/grep/grep_fuzz_test.go new file mode 100644 index 00000000..ff2de18e --- /dev/null +++ b/interp/builtins/tests/grep/grep_fuzz_test.go @@ -0,0 +1,147 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package grep_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + "time" + + "unicode/utf8" + + "github.com/DataDog/rshell/interp" + "github.com/DataDog/rshell/interp/builtins/testutil" +) + +func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// FuzzGrepFileContent fuzzes grep with a fixed pattern and arbitrary file content. +func FuzzGrepFileContent(f *testing.F) { + f.Add([]byte("apple\nbanana\ncherry\n"), "banana") + f.Add([]byte{}, "anything") + f.Add([]byte("no newline"), "new") + f.Add([]byte("a\x00b\nc\n"), "a") + f.Add(bytes.Repeat([]byte("x"), 4097), "x") + f.Add([]byte("\n\n\n"), ".") + f.Add([]byte("hello world\nfoo bar\n"), "foo") + f.Add([]byte{0xff, 0xfe}, "a") + + f.Fuzz(func(t *testing.T, input []byte, pattern string) { + if len(input) > 1<<20 { + return + } + // Skip patterns containing non-UTF-8 sequences: the shell parser's + // tokenizer rejects them before grep runs, so they exercise the parser + // error path rather than the grep builtin. + if !utf8.ValidString(pattern) { + return + } + // Skip patterns that would be problematic in shell quoting or cause the + // shell parser to fail before grep runs. + for _, c := range pattern { + if c == '\'' || c == '\x00' || c == '\n' { + return + } + } + if len(pattern) == 0 { + return + } + if len(pattern) > 100 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Use single-quoted pattern to avoid shell interpretation + script := "grep '" + pattern + "' input.txt" + _, _, code := cmdRunCtx(ctx, t, script, dir) + // grep exits 0 (match found), 1 (no match), or 2 (error/invalid regex) + if code != 0 && code != 1 && code != 2 { + t.Errorf("grep unexpected exit code %d", code) + } + }) +} + +// FuzzGrepStdin fuzzes grep reading from stdin with arbitrary content. +func FuzzGrepStdin(f *testing.F) { + f.Add([]byte("apple\nbanana\ncherry\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\nc\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "grep '.' < stdin.txt", dir) + if code != 0 && code != 1 && code != 2 { + t.Errorf("grep stdin unexpected exit code %d", code) + } + }) +} + +// FuzzGrepFlags fuzzes grep with various flags and arbitrary file content. +func FuzzGrepFlags(f *testing.F) { + f.Add([]byte("Hello\nworld\nHELLO\n"), true, false) + f.Add([]byte("line1\nline2\n"), false, true) + f.Add([]byte{}, true, true) + f.Add([]byte("no newline"), false, false) + f.Add(bytes.Repeat([]byte("abc\n"), 100), true, false) + + f.Fuzz(func(t *testing.T, input []byte, caseInsensitive bool, invertMatch bool) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + flags := "" + if caseInsensitive { + flags += " -i" + } + if invertMatch { + flags += " -v" + } + + script := "grep" + flags + " 'a' input.txt" + _, _, code := cmdRunCtx(ctx, t, script, dir) + if code != 0 && code != 1 && code != 2 { + t.Errorf("grep%s unexpected exit code %d", flags, code) + } + }) +} diff --git a/interp/builtins/tests/head/head_fuzz_test.go b/interp/builtins/tests/head/head_fuzz_test.go new file mode 100644 index 00000000..167c9ede --- /dev/null +++ b/interp/builtins/tests/head/head_fuzz_test.go @@ -0,0 +1,151 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package head_test + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/DataDog/rshell/interp" + "github.com/DataDog/rshell/interp/builtins/testutil" +) + +func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// FuzzHeadLines fuzzes head -n N with arbitrary file content. +func FuzzHeadLines(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(2)) + f.Add([]byte{}, int64(0)) + f.Add([]byte("no newline"), int64(1)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(1)) + f.Add([]byte("\n\n\n"), int64(5)) + f.Add(bytes.Repeat([]byte("y"), 4096), int64(1)) + f.Add([]byte("hello\nworld\n"), int64(10)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("head -n %d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("unexpected exit code %d", code) + } + + // If successful, output line count must be <= n + if code == 0 && n >= 0 { + lineCount := strings.Count(stdout, "\n") + if int64(lineCount) > n { + t.Errorf("head -n %d produced %d newlines in output", n, lineCount) + } + } + }) +} + +// FuzzHeadBytes fuzzes head -c N with arbitrary file content. +func FuzzHeadBytes(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(5)) + f.Add([]byte{}, int64(0)) + f.Add([]byte("no newline"), int64(3)) + f.Add([]byte("a\x00b\nc\n"), int64(4)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(4096)) + f.Add([]byte("\n\n\n"), int64(2)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("head -c %d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("unexpected exit code %d", code) + } + + // If successful, output byte count must be <= n + if code == 0 { + outLen := int64(len(stdout)) + if outLen > n { + t.Errorf("head -c %d produced %d bytes of output", n, outLen) + } + } + }) +} + +// FuzzHeadStdin fuzzes head -n N reading from stdin via shell redirection. +func FuzzHeadStdin(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(2)) + f.Add([]byte{}, int64(1)) + f.Add([]byte("no newline"), int64(1)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(1)) + f.Add([]byte("\n\n\n"), int64(3)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("head -n %d < stdin.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("unexpected exit code %d (stdin mode)", code) + } + }) +} diff --git a/interp/builtins/tests/tail/tail_fuzz_test.go b/interp/builtins/tests/tail/tail_fuzz_test.go new file mode 100644 index 00000000..9e5b7c43 --- /dev/null +++ b/interp/builtins/tests/tail/tail_fuzz_test.go @@ -0,0 +1,228 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package tail_test + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/DataDog/rshell/interp" + "github.com/DataDog/rshell/interp/builtins/testutil" +) + +func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// FuzzTailLines fuzzes tail -n N with arbitrary file content. +func FuzzTailLines(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(2)) + f.Add([]byte{}, int64(0)) + f.Add([]byte("no newline"), int64(1)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(1)) + f.Add([]byte("\n\n\n"), int64(5)) + f.Add(bytes.Repeat([]byte("y"), 4096), int64(1)) + f.Add([]byte("hello\nworld\n"), int64(10)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n %d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("tail -n %d unexpected exit code %d", n, code) + } + + // If successful, output line count must be <= n + if code == 0 && n >= 0 { + lineCount := strings.Count(stdout, "\n") + if int64(lineCount) > n { + t.Errorf("tail -n %d produced %d newlines in output", n, lineCount) + } + } + }) +} + +// FuzzTailBytes fuzzes tail -c N with arbitrary file content. +func FuzzTailBytes(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(5)) + f.Add([]byte{}, int64(0)) + f.Add([]byte("no newline"), int64(3)) + f.Add([]byte("a\x00b\nc\n"), int64(4)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(4096)) + f.Add([]byte("\n\n\n"), int64(2)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -c %d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("tail -c %d unexpected exit code %d", n, code) + } + + // If successful, output byte count must be <= n + if code == 0 { + outLen := int64(len(stdout)) + if outLen > n { + t.Errorf("tail -c %d produced %d bytes of output", n, outLen) + } + } + }) +} + +// FuzzTailStdin fuzzes tail -n N reading from stdin via shell redirection. +func FuzzTailStdin(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(2)) + f.Add([]byte{}, int64(1)) + f.Add([]byte("no newline"), int64(1)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(1)) + f.Add([]byte("\n\n\n"), int64(3)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 0 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n %d < stdin.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("tail stdin unexpected exit code %d", code) + } + }) +} + +// FuzzTailLinesOffset fuzzes tail -n +N (skip-first-N-lines offset mode). +func FuzzTailLinesOffset(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n"), int64(1)) + f.Add([]byte("line1\nline2\nline3\n"), int64(2)) + f.Add([]byte{}, int64(1)) + f.Add([]byte("no newline"), int64(1)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(1)) + f.Add([]byte("\n\n\n"), int64(5)) + f.Add([]byte("hello\nworld\n"), int64(100)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 1 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n +%d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("tail -n +%d unexpected exit code %d", n, code) + } + }) +} + +// FuzzTailBytesOffset fuzzes tail -c +N (skip-first-N-bytes offset mode). +func FuzzTailBytesOffset(f *testing.F) { + f.Add([]byte("hello\nworld\n"), int64(1)) + f.Add([]byte("hello\nworld\n"), int64(6)) + f.Add([]byte{}, int64(1)) + f.Add([]byte("no newline"), int64(3)) + f.Add([]byte("a\x00b\nc\n"), int64(2)) + f.Add(bytes.Repeat([]byte("x"), 4097), int64(4096)) + f.Add([]byte("\n\n\n"), int64(2)) + f.Add([]byte("hello\nworld\n"), int64(100)) + + f.Fuzz(func(t *testing.T, input []byte, n int64) { + if len(input) > 1<<20 { + return + } + if n < 1 { + return + } + if n > 10000 { + n = 10000 + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -c +%d input.txt", n), dir) + if code != 0 && code != 1 { + t.Errorf("tail -c +%d unexpected exit code %d", n, code) + } + }) +} + diff --git a/interp/builtins/tests/wc/wc_fuzz_test.go b/interp/builtins/tests/wc/wc_fuzz_test.go new file mode 100644 index 00000000..98dc739c --- /dev/null +++ b/interp/builtins/tests/wc/wc_fuzz_test.go @@ -0,0 +1,143 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package wc_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/DataDog/rshell/interp" + "github.com/DataDog/rshell/interp/builtins/testutil" +) + +func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// FuzzWc fuzzes wc (default mode: lines, words, bytes) with arbitrary file content. +func FuzzWc(f *testing.F) { + f.Add([]byte("hello world\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\nc\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + f.Add(bytes.Repeat([]byte("word "), 100)) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "wc input.txt", dir) + if code != 0 && code != 1 { + t.Errorf("wc unexpected exit code %d", code) + } + }) +} + +// FuzzWcLines fuzzes wc -l with arbitrary file content. +func FuzzWcLines(f *testing.F) { + f.Add([]byte("line1\nline2\nline3\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\nc\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "wc -l input.txt", dir) + if code != 0 && code != 1 { + t.Errorf("wc -l unexpected exit code %d", code) + } + }) +} + +// FuzzWcBytes fuzzes wc -c with arbitrary file content. +func FuzzWcBytes(f *testing.F) { + f.Add([]byte("hello\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\nc\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "wc -c input.txt", dir) + if code != 0 && code != 1 { + t.Errorf("wc -c unexpected exit code %d", code) + } + }) +} + +// FuzzWcStdin fuzzes wc reading from stdin via shell redirection. +func FuzzWcStdin(f *testing.F) { + f.Add([]byte("hello world\n")) + f.Add([]byte{}) + f.Add([]byte("no newline")) + f.Add([]byte("a\x00b\n")) + f.Add(bytes.Repeat([]byte("x"), 4097)) + f.Add([]byte("\n\n\n")) + + f.Fuzz(func(t *testing.T, input []byte) { + if len(input) > 1<<20 { + return + } + + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, code := cmdRunCtx(ctx, t, "wc < stdin.txt", dir) + if code != 0 && code != 1 { + t.Errorf("wc stdin unexpected exit code %d", code) + } + }) +}