Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f73a022
Revert "remove resources"
AlexandreYang Mar 10, 2026
ab1acae
Implement tr builtin command
datadog-prod-us1-3[bot] Mar 10, 2026
ee26051
Reapply "remove resources"
AlexandreYang Mar 10, 2026
0892434
Merge main into dd/implement-tr-builtin
AlexandreYang Mar 10, 2026
b51e13b
Address review comments on tr builtin
AlexandreYang Mar 10, 2026
5adf341
Hide spurious --COMPLEMENT long flag to match GNU tr
AlexandreYang Mar 10, 2026
7d76133
Fix equivalence class backslash escapes
datadog-official[bot] Mar 10, 2026
d4ef7d7
Fix skill name in fix-ci-tests SKILL.md
AlexandreYang Mar 10, 2026
852cbea
Fix Windows CI failures in ls long_format and sandbox tests
AlexandreYang Mar 10, 2026
45c36ee
Address review comments: STRING2 validation and empty operand handling
AlexandreYang Mar 10, 2026
a9ae0a8
Add skip_assert_against_bash to YAML scenarios
datadog-prod-us1-6[bot] Mar 10, 2026
1a51818
Add bounds guard to rptErrMsg to prevent potential out-of-bounds panic
AlexandreYang Mar 10, 2026
6c35f36
Validate case class alignment and escaped equiv class length
AlexandreYang Mar 10, 2026
faa1fd2
Use errors.Is for EOF checks per codebase convention
datadog-prod-us1-6[bot] Mar 10, 2026
fabbfe7
Reject STRING2 ending with character class when STRING1 is longer
AlexandreYang Mar 10, 2026
bceed6f
Merge branch 'main' into dd/implement-tr-builtin
AlexandreYang Mar 10, 2026
da8720d
update README.md
AlexandreYang Mar 10, 2026
9025943
Add ambiguous octal escape warning and remove unnecessary skip_assert…
AlexandreYang Mar 11, 2026
262187b
Fix YAML scenario: replace banned echo -n
datadog-prod-us1-5[bot] Mar 11, 2026
00c2b30
Skip case-class alignment check when complement flag is used
AlexandreYang Mar 11, 2026
68ce3af
Restore exact stderr assertion and remove unnecessary skip_assert_aga…
AlexandreYang Mar 11, 2026
0187a3a
Address review comments: bounds comment and missing stderr assertion
AlexandreYang Mar 11, 2026
1547f4e
Merge branch 'main' into dd/implement-tr-builtin
AlexandreYang Mar 11, 2026
8034c23
Merge branch 'main' into dd/implement-tr-builtin
AlexandreYang Mar 11, 2026
d05beb9
Address PR review comments on tr builtin
AlexandreYang Mar 11, 2026
98f3b03
Fix linter diagnostics in tr builtin
AlexandreYang Mar 11, 2026
7207a7d
Merge branch 'main' into dd/implement-tr-builtin
AlexandreYang Mar 11, 2026
0f57ea5
Address review comments: refactor I/O loop, defensive copy, simplify …
AlexandreYang Mar 11, 2026
4bfd0ca
Remove dead `name` field from caseClassPos struct
AlexandreYang Mar 11, 2026
a768c30
Fix tr repeat parsing edge cases
AlexandreYang Mar 11, 2026
10ae9d3
Address review comments: skip_assert_against_bash, negative-zero repe…
AlexandreYang Mar 11, 2026
86eff2e
Remove unnecessary skip_assert_against_bash from 8 tr error scenarios…
AlexandreYang Mar 11, 2026
c4eafa4
Address remaining review comments: clamp fill-repeat needed count and…
AlexandreYang Mar 11, 2026
73d7bf6
Simplify needed clamp using max() builtin
AlexandreYang Mar 11, 2026
f520198
Address review: allow [c*n] in STRING1, repeats in -ds STRING2, rejec…
AlexandreYang Mar 12, 2026
049aafb
Fix misindented comment on complemented-class rejection
AlexandreYang Mar 12, 2026
12e4d8a
revert .claude/skills/code-review/SKILL.md
AlexandreYang Mar 12, 2026
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
1 change: 1 addition & 0 deletions SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Blocked features are rejected before execution with exit code 2.
- ✅ `ls [-1aAdFhlpRrSt] [FILE]...` — list directory contents
- ✅ `strings [-a] [-n MIN] [-t o|d|x] [-o] [-f] [-s SEP] [FILE]...` — print printable character sequences in files (default min length 4); offsets via `-t`/`-o`; filename prefix via `-f`; custom separator via `-s`
- ✅ `tail [-n N|-c N] [-q|-v] [-z] [FILE]...` — output the last part of files (default: last 10 lines); supports `+N` offset mode; `-f`/`--follow` is rejected
- ✅ `tr [-cdsCt] SET1 [SET2]` — translate, squeeze, and/or delete characters from stdin
- ✅ `true` — return exit code 0
- ✅ `uniq [OPTION]... [INPUT]` — report or omit repeated lines
- ✅ `wc [-l] [-w] [-c] [-m] [FILE]...` — count lines, words, bytes, or characters in files
Expand Down
194 changes: 194 additions & 0 deletions interp/builtins/tr/builtin_tr_pentest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// 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.

// Exploratory pentest for the tr builtin.
//
// These tests probe edge cases in flag/argument handling, special input
// patterns, memory safety boundaries, and behaviour-matching with GNU tr.

package tr_test

import (
"context"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/DataDog/rshell/interp"
)

const pentestTimeout = 10 * time.Second

func mustNotHang(t *testing.T, f func()) {
t.Helper()
done := make(chan struct{})
go func() {
defer close(done)
f()
}()
select {
case <-done:
case <-time.After(pentestTimeout):
t.Fatalf("operation did not complete within %s", pentestTimeout)
}
}

// --- Flag and argument injection ---

func TestTrPentestUnknownLongFlag(t *testing.T) {
_, stderr, code := runScript(t, "tr --no-such-flag a b", t.TempDir())
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "tr:")
}

func TestTrPentestDoubleDashOperands(t *testing.T) {
stdout, _, code := trRun(t, "aha", "-- ah -H")
assert.Equal(t, 0, code)
assert.Equal(t, "-H-", stdout)
}

func TestTrPentestBasicTranslation(t *testing.T) {
stdout, _, code := trRun(t, "abc", "a X")
assert.Equal(t, 0, code)
assert.Equal(t, "Xbc", stdout)
}

// --- Empty/whitespace operands ---

func TestTrPentestEmptySet1(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "in.txt", "abc")
_, _, code := runScript(t, `cat in.txt | tr '' '[.*]'`, dir, interp.AllowedPaths([]string{dir}))
assert.Equal(t, 0, code)
}

func TestTrPentestEmptySet2WithTruncate(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "in.txt", "abc")
_, _, code := runScript(t, `cat in.txt | tr -t abc ''`, dir, interp.AllowedPaths([]string{dir}))
assert.Equal(t, 0, code)
}

// --- Context timeout on infinite source ---

func TestTrPentestContextTimeout(t *testing.T) {
mustNotHang(t, func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, _, _ = runScriptCtx(ctx, t, "tr a b", t.TempDir())
})
}

// --- Large repetitive input ---

func TestTrPentestLargeInput(t *testing.T) {
dir := t.TempDir()
large := strings.Repeat("a", 100000)
writeFile(t, dir, "in.txt", large)
stdout, _, code := runScript(t, "cat in.txt | tr a b", dir, interp.AllowedPaths([]string{dir}))
assert.Equal(t, 0, code)
expected := strings.Repeat("b", 100000)
assert.Equal(t, expected, stdout)
}

// --- Repeat construct edge cases ---

func TestTrPentestRepeatFillEmptyCount(t *testing.T) {
stdout, _, code := trRun(t, "abcdef", "abcdef '1[x*]2'")
assert.Equal(t, 0, code)
assert.Equal(t, "1xxxx2", stdout)
}

func TestTrPentestRepeatOctal(t *testing.T) {
stdout, _, code := trRun(t, "abc", "abc '[b*3]'")
assert.Equal(t, 0, code)
assert.Equal(t, "bbb", stdout)
}

func TestTrPentestInvalidRepeatCount(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "in.txt", "")
_, stderr, code := runScript(t, "cat in.txt | tr 'a' '[b*c]'", dir, interp.AllowedPaths([]string{dir}))
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "invalid repeat count")
}

// --- Character class edge cases ---

func TestTrPentestInvalidClassName(t *testing.T) {
_, stderr, code := trRun(t, "", "-d '[:bogus:]'")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "invalid character class")
}

func TestTrPentestDeleteAlnumDotsRemain(t *testing.T) {
stdout, _, code := trRun(t, ".abc123.", "-d '[:alnum:]'")
assert.Equal(t, 0, code)
assert.Equal(t, "..", stdout)
}

// --- Backslash escape edge cases ---

func TestTrPentestOctalThreeDigit(t *testing.T) {
stdout, _, code := trRun(t, "XYZ", `XYZ '\0\11\77'`)
assert.Equal(t, 0, code)
assert.Equal(t, "\000\t?", stdout)
}

func TestTrPentestUnrecognizedEscape(t *testing.T) {
stdout, _, code := trRun(t, "q", `q '\q'`)
assert.Equal(t, 0, code)
assert.Equal(t, "q", stdout)
}

// --- Combined flags ---

func TestTrPentestDeleteSqueezeCombined(t *testing.T) {
stdout, _, code := trRun(t, "a.b.c $$$$code\\", `-ds a-z '$.'`)
assert.Equal(t, 0, code)
assert.Equal(t, ". $\\", stdout)
}

func TestTrPentestComplementSqueeze(t *testing.T) {
stdout, _, code := trRun(t, "aaBBcDcc", "-sc a-z")
assert.Equal(t, 0, code)
assert.Equal(t, "aaBcDcc", stdout)
}

// --- Equivalence class edge cases ---

func TestTrPentestMultiCharEquiv(t *testing.T) {
_, stderr, code := trRun(t, "", "-d '[=aa=]'")
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "equivalence class operand must be a single character")
}

// --- Pipe integration ---

func TestTrPentestPipeChain(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "in.txt", "Hello World 123")
stdout, _, code := runScript(t, "cat in.txt | tr A-Z a-z | tr -d '[:digit:]'", dir, interp.AllowedPaths([]string{dir}))
assert.Equal(t, 0, code)
assert.Equal(t, "hello world ", stdout)
}

// --- Complement with delete ---

func TestTrPentestComplementDeleteKeepAlpha(t *testing.T) {
stdout, _, code := trRun(t, "abc123!@#", "-dc '[:alpha:]'")
assert.Equal(t, 0, code)
assert.Equal(t, "abc", stdout)
}

// --- Big C flag ---

func TestTrPentestBigCFlag(t *testing.T) {
stdout, _, code := trRun(t, "Phone: 01234 567890", "-d -C 0-9")
assert.Equal(t, 0, code)
assert.Equal(t, "01234567890", stdout)
}
Loading
Loading